diff --git a/changelog/60_UNRELEASED_2020-xx-xx.md b/changelog/60_UNRELEASED_2020-xx-xx.md index 5583d016..0dc984a8 100644 --- a/changelog/60_UNRELEASED_2020-xx-xx.md +++ b/changelog/60_UNRELEASED_2020-xx-xx.md @@ -30,6 +30,7 @@ - Added a "Clear filter"-button on the stock overview page to quickly reset applied filters - 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) +- For products with tare weight handling enabled, it's now optionally possible to consume a fixed/exact amount (just like for "normal" products) in case you don't want to weigh the whole container this time (new checkbox on the consume page) (thanks @fipwmaqzufheoxq92ebc) - Fixed that it was not possible to leave the "Barcode(s)" on the product edit page by `TAB` - Fixed that when adding products through a product picker workflow and when the created products contains special characters, the product was not preselected on the previous page (thanks @Forceu) - Fixed that when editing a product the default store was not visible / always empty regardless if the product had one set (thanks @kriddles) @@ -47,6 +48,7 @@ - Decimal amounts are now allowed (for any product, rounded by two decimal places) - "Add products that are below defined min. stock amount" always rounded up the missing amount to an integral number, this now allows decimal numbers - Added a button to add all currently in-stock but expired products to the shopping list (thanks @m-byte) +- Improved that when `FEATURE_FLAG_STOCK` is disabled, all product/stock related inputs and buttons are now hidden on the shopping list page (thanks @fipwmaqzufheoxq92ebc) ### Recipe improvements/fixes - It's now possible to print recipes (button next to the recipe title) (thanks @zsarnett) @@ -58,12 +60,16 @@ - 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 when using the "+"-symbol (which appears on small screens only to expand columns which don't fit the screen) the page reloaded - 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) ### Chores improvements/fixes - Changed that not assigned chores on the chores overview page display now just a dash instead of an ellipsis in the "Assigned to" column to make this more clear (thanks @Germs2004) - Fixed (again) that weekly chores, where the next execution should be in the same week, were scheduled (not) always (but sometimes) for the next week only (thanks @shadow7412) - Fixed that the assignment type "In alphabetic order" did not work correctly (the last person in the list was always assigned next once reached) (thanks @fipwmaqzufheoxq92ebc) +### Equipment improvements +- The equipment page now will be never automatically reloaded, even when `Auto reload on external changes` is on and a change was detected (because you most probably have that page open longer to read the manual) (thanks @fipwmaqzufheoxq92ebc) + ### Calendar improvements/fixes - Events are now links to the corresponding page (thanks @zsarnett) - Fixed a PHP warning when using the "Share/Integrate calendar (iCal)" button (thanks @tsia) @@ -109,6 +115,7 @@ - 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`) +- Fixed that the endpoint `/stock/consume` returned the response code `200` and an empty response body when `stock_entry_id` was set (consuming a specific stock entry) but invalid (now returns the response code `400`) (thanks @fipwmaqzufheoxq92ebc) - Endpoint `/calendar/ical`: Fixed that "Track date only"-chores were always set to happen at 12am (are treated as all-day events now) - Fixed (again) that CORS was broken @@ -129,6 +136,7 @@ - Replaced (again, added before in v2.7.0, then reverted in v2.7.1 due to some problems) [QuaggaJS](https://github.com/serratus/quaggaJS) (seems to be unmaintained) by [Quagga2](https://github.com/ericblade/quagga2) - More `config.php` settings (see the section `Component configuration for Quagga2`) to tweak Quagga2 (this is the component used for device camera for barcode scanning) (thanks @andrelam) - Some localization string fixes (thanks @duckfullstop) +- Fixed that XSS / HTML injection was possible through some user input fields (low severity / not really a problem as this could not be abused unauthenticated) - New translations: (thanks all the translators) - Greek (demo available at https://el.demo.grocy.info) - Korean (demo available at https://ko.demo.grocy.info) diff --git a/composer.json b/composer.json index 53bd8e34..e907617a 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "gettext/gettext": "^4.8", "eluceo/ical": "^0.16.0", "erusev/parsedown": "^1.7", - "gumlet/php-image-resize": "^1.9" + "gumlet/php-image-resize": "^1.9", + "ezyang/htmlpurifier": "^4.13" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 51e6bda0..3d7c1f61 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "70c5b65f78f4eb43dac8df8dc144e56c", + "content-hash": "651fcabf083befffe196b08c8f17506b", "packages": [ { "name": "doctrine/inflector", @@ -195,6 +195,56 @@ ], "time": "2019-12-30T22:54:17+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.13.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "require-dev": { + "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ], + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2020-06-29T00:56:53+00:00" + }, { "name": "fig/http-message-util", "version": "1.1.4", diff --git a/controllers/BaseApiController.php b/controllers/BaseApiController.php index 74fc49bb..ed3557b0 100644 --- a/controllers/BaseApiController.php +++ b/controllers/BaseApiController.php @@ -115,4 +115,22 @@ class BaseApiController extends BaseController return $this->OpenApiSpec; } + + private static $htmlPurifierInstance = null; + + protected function GetParsedAndFilteredRequestBody($request) + { + if (self::$htmlPurifierInstance == null) + { + self::$htmlPurifierInstance = new \HTMLPurifier(\HTMLPurifier_Config::createDefault()); + } + + $requestBody = $request->getParsedBody(); + foreach ($requestBody as $key => &$value) + { + $value = self::$htmlPurifierInstance->purify($value); + } + + return $requestBody; + } } diff --git a/controllers/BatteriesApiController.php b/controllers/BatteriesApiController.php index 9f06a3ed..163b45b3 100644 --- a/controllers/BatteriesApiController.php +++ b/controllers/BatteriesApiController.php @@ -27,7 +27,7 @@ class BatteriesApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_BATTERIES_TRACK_CHARGE_CYCLE); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/ChoresApiController.php b/controllers/ChoresApiController.php index 8220ad16..c70d4d0b 100644 --- a/controllers/ChoresApiController.php +++ b/controllers/ChoresApiController.php @@ -10,7 +10,7 @@ class ChoresApiController extends BaseApiController { try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $choreId = null; @@ -60,7 +60,7 @@ class ChoresApiController extends BaseApiController public function TrackChoreExecution(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/GenericEntityApiController.php b/controllers/GenericEntityApiController.php index 320e537d..4f416b88 100644 --- a/controllers/GenericEntityApiController.php +++ b/controllers/GenericEntityApiController.php @@ -18,7 +18,7 @@ class GenericEntityApiController extends BaseApiController User::checkPermission($request, User::PERMISSION_ADMIN); } - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -78,7 +78,8 @@ class GenericEntityApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_ADMIN); } - $requestBody = $request->getParsedBody(); + + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -202,7 +203,7 @@ class GenericEntityApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_MASTER_DATA_EDIT); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/LoginController.php b/controllers/LoginController.php index c80a541e..5e61535e 100644 --- a/controllers/LoginController.php +++ b/controllers/LoginController.php @@ -24,7 +24,7 @@ class LoginController extends BaseController public function ProcessLogin(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { - $postParams = $request->getParsedBody(); + $postParams = $this->GetParsedAndFilteredRequestBody($request); if (isset($postParams['username']) && isset($postParams['password'])) { diff --git a/controllers/RecipesApiController.php b/controllers/RecipesApiController.php index 5e19350b..502bb1b3 100644 --- a/controllers/RecipesApiController.php +++ b/controllers/RecipesApiController.php @@ -10,7 +10,7 @@ class RecipesApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_ADD); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $excludedProductIds = null; if ($requestBody !== null && array_key_exists('excludedProductIds', $requestBody)) diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 8e54aae2..4e4774a4 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -13,7 +13,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; @@ -37,7 +37,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; @@ -59,7 +59,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_PURCHASE); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -143,7 +143,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; $amount = 1; @@ -190,7 +190,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; @@ -212,7 +212,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_CONSUME); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $result = null; @@ -263,7 +263,15 @@ class StockApiController extends BaseApiController $recipeId = $requestBody['recipe_id']; } - $bookingId = $this->getStockService()->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId, $locationId); + $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); return $this->ApiResponse($response, $this->getDatabase()->stock_log($bookingId)); } catch (\Exception $ex) @@ -315,7 +323,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_EDIT); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -391,7 +399,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_INVENTORY); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -459,7 +467,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_OPEN); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -574,7 +582,7 @@ class StockApiController extends BaseApiController try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $listId = 1; $amount = 1; @@ -656,7 +664,7 @@ class StockApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_STOCK_TRANSFER); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/SystemApiController.php b/controllers/SystemApiController.php index 380e37d3..debadc9f 100644 --- a/controllers/SystemApiController.php +++ b/controllers/SystemApiController.php @@ -49,7 +49,7 @@ class SystemApiController extends BaseApiController { try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $this->getLocalizationService()->CheckAndAddMissingTranslationToPot($requestBody['text']); return $this->EmptyApiResponse($response); diff --git a/controllers/TasksApiController.php b/controllers/TasksApiController.php index 7518c4a8..eba0f4ee 100644 --- a/controllers/TasksApiController.php +++ b/controllers/TasksApiController.php @@ -15,7 +15,7 @@ class TasksApiController extends BaseApiController { User::checkPermission($request, User::PERMISSION_TASKS_MARK_COMPLETED); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { diff --git a/controllers/UsersApiController.php b/controllers/UsersApiController.php index 94446e76..2e88c27d 100644 --- a/controllers/UsersApiController.php +++ b/controllers/UsersApiController.php @@ -11,7 +11,7 @@ class UsersApiController extends BaseApiController try { User::checkPermission($request, User::PERMISSION_ADMIN); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $this->getDatabase()->user_permissions()->createRow([ 'user_id' => $args['userId'], @@ -32,7 +32,7 @@ class UsersApiController extends BaseApiController public function CreateUser(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) { User::checkPermission($request, User::PERMISSION_USERS_CREATE); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -75,7 +75,7 @@ class UsersApiController extends BaseApiController User::checkPermission($request, User::PERMISSION_USERS_EDIT); } - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); try { @@ -152,7 +152,7 @@ class UsersApiController extends BaseApiController try { User::checkPermission($request, User::PERMISSION_ADMIN); - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $db = $this->getDatabase(); $db->user_permissions() ->where('user_id', $args['userId']) @@ -186,7 +186,7 @@ class UsersApiController extends BaseApiController { try { - $requestBody = $request->getParsedBody(); + $requestBody = $this->GetParsedAndFilteredRequestBody($request); $value = $this->getUsersService()->SetUserSetting(GROCY_USER_ID, $args['settingKey'], $requestBody['value']); return $this->EmptyApiResponse($response); diff --git a/public/js/extensions.js b/public/js/extensions.js index 9ed9e1f8..df3e80d6 100644 --- a/public/js/extensions.js +++ b/public/js/extensions.js @@ -195,8 +195,3 @@ function getQRCodeForAPIKey(apikey_type, apikey_key) } return getQRCodeForContent(content); } - -function SanitizeHtml(input) -{ - return $("
").text(input).html(); -} diff --git a/public/js/grocy_dbchangedhandling.js b/public/js/grocy_dbchangedhandling.js index 77ee6c0c..b92e7b3e 100644 --- a/public/js/grocy_dbchangedhandling.js +++ b/public/js/grocy_dbchangedhandling.js @@ -15,31 +15,34 @@ // Check if the database has changed once a minute // If a change is detected, reload the current page, but only if already idling for at least 50 seconds, // when there is no unsaved form data and when the user enabled auto reloading -setInterval(function() +if (Grocy.DbChangedHandlingEnabledForPage) { - Grocy.Api.Get('system/db-changed-time', - function(result) - { - var newDbChangedTime = moment(result.changed_time); - if (newDbChangedTime.isAfter(Grocy.DatabaseChangedTime)) + setInterval(function() + { + Grocy.Api.Get('system/db-changed-time', + function(result) { - if (Grocy.IdleTime >= 50) + var newDbChangedTime = moment(result.changed_time); + if (newDbChangedTime.isAfter(Grocy.DatabaseChangedTime)) { - if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0 && !$("body").hasClass("fullscreen-card")) + if (Grocy.IdleTime >= 50) { - window.location.reload(); + if (BoolVal(Grocy.UserSettings.auto_reload_on_db_change) && $("form.is-dirty").length === 0 && !$("body").hasClass("fullscreen-card")) + { + window.location.reload(); + } } - } - Grocy.DatabaseChangedTime = newDbChangedTime; + Grocy.DatabaseChangedTime = newDbChangedTime; + } + }, + function(xhr) + { + console.error(xhr); } - }, - function(xhr) - { - console.error(xhr); - } - ); -}, 60000); + ); + }, 60000); +} Grocy.IdleTime = 0; Grocy.ResetIdleTime = function() diff --git a/public/viewjs/batteries.js b/public/viewjs/batteries.js index 71e1a216..0829ebe4 100644 --- a/public/viewjs/batteries.js +++ b/public/viewjs/batteries.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.battery-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-battery-name')); + var objectName = $(e.currentTarget).attr('data-battery-name'); var objectId = $(e.currentTarget).attr('data-battery-id'); bootbox.confirm({ diff --git a/public/viewjs/chores.js b/public/viewjs/chores.js index a120bd6f..7d3ca85a 100644 --- a/public/viewjs/chores.js +++ b/public/viewjs/chores.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.chore-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-chore-name')); + var objectName = $(e.currentTarget).attr('data-chore-name'); var objectId = $(e.currentTarget).attr('data-chore-id'); bootbox.confirm({ diff --git a/public/viewjs/components/productpicker.js b/public/viewjs/components/productpicker.js index f90623c2..16d4bd5f 100644 --- a/public/viewjs/components/productpicker.js +++ b/public/viewjs/components/productpicker.js @@ -162,7 +162,7 @@ $('#product_id_text_input').on('blur', function(e) Grocy.Components.ProductPicker.PopupOpen = true; bootbox.dialog({ - message: __t('"%s" could not be resolved to a product, how do you want to proceed?', SanitizeHtml(input)), + message: __t('"%s" could not be resolved to a product, how do you want to proceed?', input), title: __t('Create or assign product'), onEscape: function() { diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index e007dc55..69e772be 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -9,6 +9,7 @@ var jsonData = {}; jsonData.amount = jsonForm.amount; + jsonData.exact_amount = (jsonForm.exact_amount == "on"); jsonData.spoiled = $('#spoiled').is(':checked'); if ($("#use_specific_stock_entry").is(":checked")) @@ -70,7 +71,7 @@ $("#use_specific_stock_entry").click(); } - if (productDetails.product.enable_tare_weight_handling == 1) + if (productDetails.product.enable_tare_weight_handling == 1 && !jsonData.exact_amount) { var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount - (parseFloat(productDetails.product.tare_weight) + parseFloat(productDetails.stock_amount))) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; } @@ -177,11 +178,11 @@ $('#save-mark-as-open-button').on('click', function(e) } ); }); - +var sumValue = 0; $("#location_id").on('change', function(e) { var locationId = $(e.target).val(); - var sumValue = 0; + sumValue = 0; var stockId = null; $("#specific_stock_entry").find("option").remove().end().append(""); @@ -228,37 +229,8 @@ $("#location_id").on('change', function(e) Grocy.Api.Get('stock/products/' + Grocy.Components.ProductPicker.GetValue(), function(productDetails) { - if (productDetails.product.enable_tare_weight_handling == 1) - { - $("#amount").attr("min", productDetails.product.tare_weight); - $('#amount').attr('max', sumValue + parseFloat(productDetails.product.tare_weight)); - $("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', parseFloat(productDetails.product.tare_weight).toLocaleString(), (parseFloat(productDetails.stock_amount) + parseFloat(productDetails.product.tare_weight)).toLocaleString())); - $("#tare-weight-handling-info").removeClass("d-none"); - } - else - { - $("#tare-weight-handling-info").addClass("d-none"); - - if (productDetails.product.allow_partial_units_in_stock == 1) - { - $("#amount").attr("min", "0.01"); - $("#amount").attr("step", "0.01"); - $("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', 0.01.toLocaleString(), parseFloat(productDetails.stock_amount).toLocaleString())); - } - else - { - $("#amount").attr("min", "1"); - $("#amount").attr("step", "1"); - $("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', "1", parseFloat(productDetails.stock_amount).toLocaleString())); - } - - $('#amount').attr('max', sumValue); - - if (sumValue == 0) - { - $("#amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location')); - } - } + current_productDetails = productDetails; + RefreshForm(); }, function(xhr) { @@ -447,7 +419,7 @@ $("#specific_stock_entry").on("change", function(e) { if ($(e.target).val() == "") { - var sumValue = 0; + sumValue = 0; Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries', function(stockEntries) { @@ -572,3 +544,49 @@ $("#scan-mode-button").on("click", function(e) $("#scan-mode-status").text(__t("off")); } }); + +$('#consume-exact-amount').on('change', RefreshForm); +var current_productDetails; +function RefreshForm() +{ + var productDetails = current_productDetails; + if (productDetails.product.enable_tare_weight_handling == 1) + { + $("#consume-exact-amount").parent().removeClass("d-none"); + } + else + { + $("#consume-exact-amount").parent().addClass("d-none"); + } + if (productDetails.product.enable_tare_weight_handling == 1 && !$('#consume-exact-amount').is(':checked')) + { + $("#amount").attr("min", productDetails.product.tare_weight); + $('#amount').attr('max', sumValue + parseFloat(productDetails.product.tare_weight)); + $("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', parseFloat(productDetails.product.tare_weight).toLocaleString(), (parseFloat(productDetails.stock_amount) + parseFloat(productDetails.product.tare_weight)).toLocaleString())); + $("#tare-weight-handling-info").removeClass("d-none"); + } + else + { + $("#tare-weight-handling-info").addClass("d-none"); + + if (productDetails.product.allow_partial_units_in_stock == 1) + { + $("#amount").attr("min", "0.01"); + $("#amount").attr("step", "0.01"); + $("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', 0.01.toLocaleString(), parseFloat(productDetails.stock_amount).toLocaleString())); + } + else + { + $("#amount").attr("min", "1"); + $("#amount").attr("step", "1"); + $("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', "1", parseFloat(productDetails.stock_amount).toLocaleString())); + } + + $('#amount').attr('max', sumValue); + + if (sumValue == 0) + { + $("#amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location')); + } + } +} diff --git a/public/viewjs/equipment.js b/public/viewjs/equipment.js index ebfb003c..0bdb87e5 100644 --- a/public/viewjs/equipment.js +++ b/public/viewjs/equipment.js @@ -68,7 +68,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.equipment-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-equipment-name')); + var objectName = $(e.currentTarget).attr('data-equipment-name'); var objectId = $(e.currentTarget).attr('data-equipment-id'); bootbox.confirm({ diff --git a/public/viewjs/locations.js b/public/viewjs/locations.js index 6bab2dcf..28947d3a 100644 --- a/public/viewjs/locations.js +++ b/public/viewjs/locations.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.location-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-location-name')); + var objectName = $(e.currentTarget).attr('data-location-name'); var objectId = $(e.currentTarget).attr('data-location-id'); bootbox.confirm({ diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 13c4c00e..002ffee5 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -563,7 +563,7 @@ $(document).on('click', '.recipe-order-missing-button', function(e) // to prevent that the tooltip stays until clicked anywhere else document.activeElement.blur(); - var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name')); + var objectName = $(e.currentTarget).attr('data-recipe-name'); var objectId = $(e.currentTarget).attr('data-recipe-id'); var button = $(this); var servings = $(e.currentTarget).attr('data-mealplan-servings'); @@ -667,7 +667,7 @@ $(document).on('click', '.recipe-consume-button', function(e) // to prevent that the tooltip stays until clicked anywhere else document.activeElement.blur(); - var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name')); + var objectName = $(e.currentTarget).attr('data-recipe-name'); var objectId = $(e.currentTarget).attr('data-recipe-id'); var servings = $(e.currentTarget).attr('data-mealplan-servings'); diff --git a/public/viewjs/productgroups.js b/public/viewjs/productgroups.js index 40587899..af64c63d 100644 --- a/public/viewjs/productgroups.js +++ b/public/viewjs/productgroups.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.product-group-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-group-name')); + var objectName = $(e.currentTarget).attr('data-group-name'); var objectId = $(e.currentTarget).attr('data-group-id'); bootbox.confirm({ diff --git a/public/viewjs/products.js b/public/viewjs/products.js index 77d1da70..5934c4b0 100644 --- a/public/viewjs/products.js +++ b/public/viewjs/products.js @@ -38,7 +38,7 @@ if (typeof GetUriParam("product-group") !== "undefined") $(document).on('click', '.product-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-product-name')); + var objectName = $(e.currentTarget).attr('data-product-name'); var objectId = $(e.currentTarget).attr('data-product-id'); Grocy.Api.Get('stock/products/' + objectId, diff --git a/public/viewjs/quantityunits.js b/public/viewjs/quantityunits.js index 8b5cc35f..9d5766a1 100644 --- a/public/viewjs/quantityunits.js +++ b/public/viewjs/quantityunits.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.quantityunit-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-quantityunit-name')); + var objectName = $(e.currentTarget).attr('data-quantityunit-name'); var objectId = $(e.currentTarget).attr('data-quantityunit-id'); bootbox.confirm({ diff --git a/public/viewjs/recipeform.js b/public/viewjs/recipeform.js index 09501c47..3c1db7bd 100644 --- a/public/viewjs/recipeform.js +++ b/public/viewjs/recipeform.js @@ -1,4 +1,4 @@ -function saveRecipePicture(result, location) +function saveRecipePicture(result, location, jsonData) { $recipeId = Grocy.EditObjectId || result.created_object_id; Grocy.Components.UserfieldsForm.Save(() => @@ -43,7 +43,7 @@ $('.save-recipe').on('click', function(e) { console.log(jsonData); Grocy.Api.Post('objects/recipes', jsonData, - (result) => saveRecipePicture(result, location)); + (result) => saveRecipePicture(result, location, jsonData)); return; } @@ -65,7 +65,7 @@ $('.save-recipe').on('click', function(e) } Grocy.Api.Put('objects/recipes/' + Grocy.EditObjectId, jsonData, - (result) => saveRecipePicture(result, location), + (result) => saveRecipePicture(result, location, jsonData), function(xhr) { Grocy.FrontendHelpers.EndUiBusy("recipe-form"); @@ -126,7 +126,7 @@ $('#recipe-form input').keydown(function(event) $(document).on('click', '.recipe-pos-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-pos-name')); + var objectName = $(e.currentTarget).attr('data-recipe-pos-name'); var objectId = $(e.currentTarget).attr('data-recipe-pos-id'); bootbox.confirm({ @@ -163,7 +163,7 @@ $(document).on('click', '.recipe-pos-delete-button', function(e) $(document).on('click', '.recipe-include-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-include-name')); + var objectName = $(e.currentTarget).attr('data-recipe-include-name'); var objectId = $(e.currentTarget).attr('data-recipe-include-id'); bootbox.confirm({ @@ -200,7 +200,7 @@ $(document).on('click', '.recipe-include-delete-button', function(e) $(document).on('click', '.recipe-pos-show-note-button', function(e) { - var note = SanitizeHtml($(e.currentTarget).attr('data-recipe-pos-note')); + var note = $(e.currentTarget).attr('data-recipe-pos-note'); bootbox.alert(note); }); diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js index e2fe7e49..1e4d835b 100644 --- a/public/viewjs/recipes.js +++ b/public/viewjs/recipes.js @@ -98,7 +98,7 @@ $(".recipe-delete").on('click', function(e) { e.preventDefault(); - var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name')); + var objectName = $(e.currentTarget).attr('data-recipe-name'); var objectId = $(e.currentTarget).attr('data-recipe-id'); bootbox.confirm({ @@ -135,7 +135,7 @@ $(".recipe-delete").on('click', function(e) $(document).on('click', '.recipe-shopping-list', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name')); + var objectName = $(e.currentTarget).attr('data-recipe-name'); var objectId = $(e.currentTarget).attr('data-recipe-id'); bootbox.confirm({ @@ -181,7 +181,7 @@ $(document).on('click', '.recipe-shopping-list', function(e) $(".recipe-consume").on('click', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-recipe-name')); + var objectName = $(e.currentTarget).attr('data-recipe-name'); var objectId = $(e.currentTarget).attr('data-recipe-id'); bootbox.confirm({ diff --git a/public/viewjs/shoppinglist.js b/public/viewjs/shoppinglist.js index 27802080..7bdf28d6 100644 --- a/public/viewjs/shoppinglist.js +++ b/public/viewjs/shoppinglist.js @@ -77,7 +77,7 @@ $(".status-filter-message").on("click", function() $("#delete-selected-shopping-list").on("click", function() { - var objectName = SanitizeHtml($("#selected-shopping-list option:selected").text()); + var objectName = $("#selected-shopping-list option:selected").text(); var objectId = $("#selected-shopping-list").val(); bootbox.confirm({ @@ -172,7 +172,7 @@ $(document).on('click', '#add-expired-products', function(e) $(document).on('click', '#clear-shopping-list', function(e) { bootbox.confirm({ - message: __t('Are you sure to empty shopping list "%s"?', SanitizeHtml($("#selected-shopping-list option:selected").text())), + message: __t('Are you sure to empty shopping list "%s"?', $("#selected-shopping-list option:selected").text()), closeButton: false, buttons: { confirm: { diff --git a/public/viewjs/shoppinglistitemform.js b/public/viewjs/shoppinglistitemform.js index db706114..f08dae77 100644 --- a/public/viewjs/shoppinglistitemform.js +++ b/public/viewjs/shoppinglistitemform.js @@ -139,7 +139,7 @@ if (Grocy.EditMode === "edit") $('#amount').on('focus', function(e) { - if (Grocy.Components.ProductPicker.GetValue().length === 0) + if (Grocy.Components.ProductPicker.GetValue().length === 0 && Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK) { Grocy.Components.ProductPicker.GetInputElement().focus(); } diff --git a/public/viewjs/shoppinglocations.js b/public/viewjs/shoppinglocations.js index c5bdb6ee..5843ba00 100644 --- a/public/viewjs/shoppinglocations.js +++ b/public/viewjs/shoppinglocations.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.shoppinglocation-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-shoppinglocation-name')); + var objectName = $(e.currentTarget).attr('data-shoppinglocation-name'); var objectId = $(e.currentTarget).attr('data-shoppinglocation-id'); bootbox.confirm({ diff --git a/public/viewjs/taskcategories.js b/public/viewjs/taskcategories.js index 025bfc64..d3953b43 100644 --- a/public/viewjs/taskcategories.js +++ b/public/viewjs/taskcategories.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.task-category-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-category-name')); + var objectName = $(e.currentTarget).attr('data-category-name'); var objectId = $(e.currentTarget).attr('data-category-id'); bootbox.confirm({ diff --git a/public/viewjs/tasks.js b/public/viewjs/tasks.js index e8a84d92..7e93c03d 100644 --- a/public/viewjs/tasks.js +++ b/public/viewjs/tasks.js @@ -119,7 +119,7 @@ $(document).on('click', '.delete-task-button', function(e) { e.preventDefault(); - var objectName = SanitizeHtml($(e.currentTarget).attr('data-task-name')); + var objectName = $(e.currentTarget).attr('data-task-name'); var objectId = $(e.currentTarget).attr('data-task-id'); bootbox.confirm({ diff --git a/public/viewjs/userentities.js b/public/viewjs/userentities.js index e6c5e85a..74bbaf98 100644 --- a/public/viewjs/userentities.js +++ b/public/viewjs/userentities.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.userentity-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-userentity-name')); + var objectName = $(e.currentTarget).attr('data-userentity-name'); var objectId = $(e.currentTarget).attr('data-userentity-id'); bootbox.confirm({ diff --git a/public/viewjs/userfields.js b/public/viewjs/userfields.js index b9b69b28..5d5c2711 100644 --- a/public/viewjs/userfields.js +++ b/public/viewjs/userfields.js @@ -33,7 +33,7 @@ $("#entity-filter").on("change", function() $(document).on('click', '.userfield-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-userfield-name')); + var objectName = $(e.currentTarget).attr('data-userfield-name'); var objectId = $(e.currentTarget).attr('data-userfield-id'); bootbox.confirm({ diff --git a/public/viewjs/users.js b/public/viewjs/users.js index bc17bf8d..d0d4c675 100644 --- a/public/viewjs/users.js +++ b/public/viewjs/users.js @@ -21,7 +21,7 @@ $("#search").on("keyup", Delay(function() $(document).on('click', '.user-delete-button', function(e) { - var objectName = SanitizeHtml($(e.currentTarget).attr('data-user-username')); + var objectName = $(e.currentTarget).attr('data-user-username'); var objectId = $(e.currentTarget).attr('data-user-id'); bootbox.confirm({ diff --git a/services/RecipesService.php b/services/RecipesService.php index 697ecde9..5e5e53bb 100644 --- a/services/RecipesService.php +++ b/services/RecipesService.php @@ -58,7 +58,7 @@ class RecipesService extends BaseService { if ($recipePosition->only_check_single_unit_in_stock == 0) { - $this->getStockService()->ConsumeProduct($recipePosition->product_id, $recipePosition->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId, null, $transactionId, true); + $this->getStockService()->ConsumeProduct($recipePosition->product_id, $recipePosition->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId, null, $transactionId, true, true); } } diff --git a/services/StockService.php b/services/StockService.php index 959f8554..b5a0ea49 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -210,7 +210,7 @@ class StockService extends BaseService $this->getDatabase()->shopping_list()->where('shopping_list_id = :1', $listId)->delete(); } - public function ConsumeProduct(int $productId, float $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default', $recipeId = null, $locationId = null, &$transactionId = null, $allowSubproductSubstitution = false) + public function ConsumeProduct(int $productId, float $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default', $recipeId = null, $locationId = null, &$transactionId = null, $allowSubproductSubstitution = false, $consumeExactAmount = false) { if (!$this->ProductExists($productId)) { @@ -230,6 +230,10 @@ class StockService extends BaseService if ($productDetails->product->enable_tare_weight_handling == 1) { + if($consumeExactAmount) + { + $amount = floatval($productDetails->stock_amount) + floatval($productDetails->product->tare_weight) - $amount; + } if ($amount < floatval($productDetails->product->tare_weight)) { throw new \Exception('The amount cannot be lower than the defined tare weight'); @@ -249,6 +253,11 @@ class StockService extends BaseService $potentialStockEntries = $this->GetProductStockEntriesForLocation($productId, $locationId, false, $allowSubproductSubstitution); } + if ($specificStockEntryId !== 'default') + { + $potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId); + } + $productStockAmount = SumArrayValue($potentialStockEntries, 'amount'); if ($amount > $productStockAmount) @@ -256,11 +265,6 @@ class StockService extends BaseService throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)'); } - if ($specificStockEntryId !== 'default') - { - $potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId); - } - if ($transactionId === null) { $transactionId = uniqid(); diff --git a/views/components/locationpicker.blade.php b/views/components/locationpicker.blade.php index 36e5b678..bbe54cd7 100644 --- a/views/components/locationpicker.blade.php +++ b/views/components/locationpicker.blade.php @@ -6,9 +6,10 @@ @php if(empty($prefillById)) { $prefillById = ''; } @endphp @php if(!isset($isRequired)) { $isRequired = true; } @endphp @php if(empty($hint)) { $hint = ''; } @endphp +@php if(empty($nextInputSelector)) { $nextInputSelector = ''; } @endphp