diff --git a/changelog/55_UNRELEASED_2019-xx-xx.md b/changelog/55_UNRELEASED_2019-xx-xx.md index c4cc49ab..06d9f6e3 100644 --- a/changelog/55_UNRELEASED_2019-xx-xx.md +++ b/changelog/55_UNRELEASED_2019-xx-xx.md @@ -1,10 +1,26 @@ +### New feature: Transfer products between locations and edit stock entries +- New menu entry in the sidebar to transfer products (or as a shortcut in the more/context menu per line on the stock overview page) +- New menu entry in the more/context menu of stock overview page lines to show the detail stock entries behind the corresponding product + - From there you can also edit the stock entries +- A huge THANK YOU goes to @kriddles for the work on this feature + +## Recipe improvements +- When adding or editing a recipe ingredient, a dialog is now used instead of switching between pages (thanks @kriddles) + +### Meal plan fixes +- Fixed that when `FEATURE_FLAG_STOCK_PRICE_TRACKING` was set to `false`, prices were still shown (thanks @kriddles) + ### Calendar improvements - Improved that meal plan events in the iCal calendar export now contain a link to the appropriate meal plan week in the body of the event (thanks @kriddles) +### API improvements/fixes +- Fixed that the route `/stock/barcodes/external-lookup/{barcode}` did not work, because the `barcode` argument was expected as a route argument but the route was missing it (thanks @Mikhail5555 and @beetle442002) +- New endpoints for the stock transfer & stock entry edit capabilities mentioned above + ### General & other improvements/fixes - Fixed that the meal plan menu entry (sidebar) was not visible when the calendar was disabled (`FEATURE_FLAG_CALENDAR`) (thanks @lwis) - Slightly optimized table loading & search performance (thanks @lwis) - For integration: If a `GET` parameter `closeAfterCreation` is passed to the product edit page, the window will be closed on save (due to Browser restrictions, this only works when the window was opened from JavaScript) (thanks @Forceu) - The `update.sh` file had wrong line endings (DOS instead of Unix) - New translations: (thanks all the translators) - - Portuguese (demo available at https://pt.demo.grocy.info) + - Portuguese (Brazil) (demo available at https://pt-br.demo.grocy.info) diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 57972329..06649965 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -121,6 +121,108 @@ class StockApiController extends BaseApiController } } + public function EditStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $requestBody = $request->getParsedBody(); + + try + { + if ($requestBody === null) + { + throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)'); + } + + if (!array_key_exists('stock_row_id', $requestBody)) + { + throw new \Exception('A stock row id is required'); + } + + if (!array_key_exists('amount', $requestBody)) + { + throw new \Exception('An amount is required'); + } + + $bestBeforeDate = null; + if (array_key_exists('best_before_date', $requestBody) && IsIsoDate($requestBody['best_before_date'])) + { + $bestBeforeDate = $requestBody['best_before_date']; + } + + $price = null; + if (array_key_exists('price', $requestBody) && is_numeric($requestBody['price'])) + { + $price = $requestBody['price']; + } + + $locationId = null; + if (array_key_exists('location_id', $requestBody) && is_numeric($requestBody['location_id'])) + { + $locationId = $requestBody['location_id']; + } + + $bookingId = $this->StockService->EditStock($requestBody['stock_row_id'], $requestBody['amount'], $bestBeforeDate, $locationId, $price); + return $this->ApiResponse($this->Database->stock_log($bookingId)); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } + + public function TransferProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $requestBody = $request->getParsedBody(); + + try + { + if ($requestBody === null) + { + throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)'); + } + + if (!array_key_exists('amount', $requestBody)) + { + throw new \Exception('An amount is required'); + } + + if (!array_key_exists('location_id_from', $requestBody)) + { + throw new \Exception('A transfer from location is required'); + } + + if (!array_key_exists('location_id_to', $requestBody)) + { + throw new \Exception('A transfer to location is required'); + } + + $specificStockEntryId = 'default'; + if (array_key_exists('stock_entry_id', $requestBody) && !empty($requestBody['stock_entry_id'])) + { + $specificStockEntryId = $requestBody['stock_entry_id']; + } + + $bookingId = $this->StockService->TransferProduct($args['productId'], $requestBody['amount'], $requestBody['location_id_from'], $requestBody['location_id_to'], $specificStockEntryId); + return $this->ApiResponse($this->Database->stock_log($bookingId)); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } + + public function TransferProductByBarcode(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + $args['productId'] = $this->StockService->GetProductIdFromBarcode($args['barcode']); + return $this->TransferProduct($request, $response, $args); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } + public function ConsumeProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { $requestBody = $request->getParsedBody(); @@ -157,14 +259,20 @@ class StockApiController extends BaseApiController $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']; } - $bookingId = $this->getStockService()->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId); - $result = $this->ApiResponse($this->getDatabase()->stock_log($bookingId)); + $bookingId = $this->getStockService()->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId, $locationId); + return $this->ApiResponse($this->getDatabase()->stock_log($bookingId)); } catch (\Exception $ex) { @@ -460,11 +568,29 @@ class StockApiController extends BaseApiController } } + public function UndoTransaction(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + $this->ApiResponse($this->StockService->UndoTransaction($args['transactionId'])); + return $this->EmptyApiResponse($response); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } + public function ProductStockEntries(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { return $this->ApiResponse($this->getStockService()->GetProductStockEntries($args['productId'])); } + public function ProductStockLocations(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + return $this->ApiResponse($this->StockService->GetProductStockLocations($args['productId'])); + } + public function StockBooking(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { try @@ -483,4 +609,23 @@ class StockApiController extends BaseApiController return $this->GenericErrorResponse($response, $ex->getMessage()); } } + + public function StockTransactions(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + $transactionRows = $this->Database->stock_log()->where('transaction_id = :1', $args['transactionId'])->fetchAll(); + + if (count($transactionRows) === 0) + { + throw new \Exception('No transaction was found by the given transaction id'); + } + + return $this->ApiResponse($transactionRows); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } } diff --git a/controllers/StockController.php b/controllers/StockController.php index 385c317e..88831eed 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -43,6 +43,25 @@ class StockController extends BaseController ]); } + public function Detail(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $usersService = new UsersService(); + $nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_expring_soon_days']; + + return $this->AppContainer->view->render($response, 'stockdetail', [ + 'products' => $this->Database->products()->orderBy('name'), + 'quantityunits' => $this->Database->quantity_units()->orderBy('name'), + 'locations' => $this->Database->locations()->orderBy('name'), + 'currentStockDetail' => $this->Database->stock()->orderBy('product_id'), + 'currentStockLocations' => $this->StockService->GetCurrentStockLocations(), + 'missingProducts' => $this->StockService->GetMissingProducts(), + 'nextXDays' => $nextXDays, + 'productGroups' => $this->Database->product_groups()->orderBy('name'), + 'userfields' => $this->UserfieldsService->GetFields('products'), + 'userfieldValues' => $this->UserfieldsService->GetAllValues('products') + ]); + } + public function Purchase(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { return $this->renderPage($response, 'purchase', [ @@ -53,11 +72,20 @@ class StockController extends BaseController public function Consume(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { - $result = $this->renderPage($response, 'consume', [ + return $this->renderPage($response, 'consume', [ 'products' => $this->getDatabase()->products()->orderBy('name'), 'recipes' => $this->getDatabase()->recipes()->orderBy('name') + 'locations' => $this->getDatabase()->locations()->orderBy('name') + ]); + } + + public function Transfer(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + return $this->AppContainer->view->render($response, 'transfer', [ + 'products' => $this->getDatabase()->products()->orderBy('name'), + 'recipes' => $this->getDatabase()->recipes()->orderBy('name'), + 'locations' => $this->getDatabase()->locations()->orderBy('name') ]); - return $result; } public function Inventory(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) @@ -68,6 +96,14 @@ class StockController extends BaseController ]); } + public function StockEdit(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + return $this->AppContainer->view->render($response, 'stockedit', [ + 'products' => $this->Database->products()->orderBy('name'), + 'locations' => $this->Database->locations()->orderBy('name') + ]); + } + public function ShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { $listId = 1; @@ -277,6 +313,7 @@ class StockController extends BaseController { return $this->renderPage($response, 'stockjournal', [ 'stockLog' => $this->getDatabase()->stock_log()->orderBy('row_created_timestamp', 'DESC'), + 'locations' => $this->getDatabase()->locations()->orderBy('name'), 'products' => $this->getDatabase()->products()->orderBy('name'), 'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name') ]); diff --git a/grocy.openapi.json b/grocy.openapi.json index 7f1af783..4fd92a4d 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1050,6 +1050,78 @@ } } } + }, + "put": { + "summary": "Edits the stock entry", + "tags": [ + "Stock" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "stock_row_id": { + "type": "number", + "format": "number", + "description": "The Stock Row Id" + }, + "amount": { + "type": "number", + "format": "number", + "description": "The amount to add - please note that when tare weight handling for the product is enabled, this needs to be the amount including the container weight (gross), the amount to be posted will be automatically calculated based on what is in stock and the defined tare weight" + }, + "best_before_date": { + "type": "string", + "format": "date", + "description": "The best before date of the product to add, when omitted, the current date is used" + }, + "location_id": { + "type": "number", + "format": "integer", + "description": "If omitted, the default location of the product is used" + }, + "price": { + "type": "number", + "format": "number", + "description": "The price per purchase quantity unit in configured currency" + } + }, + "example": { + "stock_row_id": 2, + "amount": 1, + "best_before_date": "2019-01-19", + "location_id": 2, + "price": "1.99" + } + } + } + } + }, + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StockLogEntry" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing product, invalid transaction type)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } } }, "/stock/volatile": { @@ -1128,6 +1200,50 @@ } } }, + "/stock/products/{productId}/locations": { + "get": { + "summary": "Returns all locations where the given product currently has stock", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "description": "A valid product id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "An array of StockLocation objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StockLocation" + } + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/stock/products/{productId}/entries": { "get": { "summary": "Returns all stock entries of the given product in order of next use (first expiring first, then first in first out)", @@ -1341,6 +1457,11 @@ "type": "number", "format": "integer", "description": "A valid recipe id for which this product was used (for statistical purposes only)" + }, + "location_id": { + "type": "number", + "format": "integer", + "description": "A valid location id (if supplied, only stock at the given location is considered, if ommitted, stock of any location is considered)" } }, "example": { @@ -1376,6 +1497,82 @@ } } }, + "/stock/products/{productId}/transfer": { + "post": { + "summary": "Transfers the given amount of the given product from one location to another (this is currently not supported for tare weight handling enabled products)", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "description": "A valid product id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "description": "The amount to transfer - please note that when tare weight handling for the product is enabled, this needs to be the amount including the container weight (gross), the amount to be posted will be automatically calculated based on what is in stock and the defined tare weight" + }, + "location_id_from": { + "type": "number", + "format": "integer", + "description": "A valid location id, the location from where the product should be transfered" + }, + "location_id_to": { + "type": "number", + "format": "integer", + "description": "A valid location id, the location to where the product should be transfered" + }, + "stock_entry_id": { + "type": "string", + "description": "A specific stock entry id to transfer, if used, the amount has to be 1" + } + }, + "example": { + "amount": 1, + "location_id_from": 1, + "location_id_to": 2 + } + } + } + } + }, + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StockLogEntry" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing product, no existing from or to location, given amount > current stock amount at the source location)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/stock/products/{productId}/inventory": { "post": { "summary": "Inventories the given product (adds/removes based on the given new amount)", @@ -1678,6 +1875,11 @@ "type": "number", "format": "integer", "description": "A valid recipe id for which this product was used (for statistical purposes only)" + }, + "location_id": { + "type": "number", + "format": "integer", + "description": "A valid location id (if supplied, only stock at the given location is considered, if ommitted, stock of any location is considered)" } }, "example": { @@ -1713,6 +1915,82 @@ } } }, + "/stock/products/by-barcode/{barcode}/transfer": { + "post": { + "summary": "Transfers the given amount of the by its barcode given product from one location to another (this is currently not supported for tare weight handling enabled products)", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "barcode", + "required": true, + "description": "Barcode", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "description": "The amount to transfer - please note that when tare weight handling for the product is enabled, this needs to be the amount including the container weight (gross), the amount to be posted will be automatically calculated based on what is in stock and the defined tare weight" + }, + "location_id_from": { + "type": "number", + "format": "integer", + "description": "A valid location id, the location from where the product should be transfered" + }, + "location_id_to": { + "type": "number", + "format": "integer", + "description": "A valid location id, the location to where the product should be transfered" + }, + "stock_entry_id": { + "type": "string", + "description": "A specific stock entry id to transfer, if used, the amount has to be 1" + } + }, + "example": { + "amount": 1, + "location_id_from": 1, + "location_id_to": 2 + } + } + } + } + }, + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StockLogEntry" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing product, no existing from or to location, given amount > current stock amount at the source location)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/stock/products/by-barcode/{barcode}/inventory": { "post": { "summary": "Inventories the by its barcode given product (adds/removes based on the given new amount)", @@ -2119,7 +2397,85 @@ } } }, - "/stock/barcodes/external-lookup": { + "/stock/transactions/{transactionId}": { + "get": { + "summary": "Returns all stock bookings of the given transaction id", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "transactionId", + "required": true, + "description": "A valid stock transaction id", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "An array of StockLogEntry objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StockLogEntry" + } + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing transaction)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/transactions/{transactionId}/undo": { + "post": { + "summary": "Undoes a transaction", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "transactionId", + "required": true, + "description": "A valid stock transaction id", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing transaction)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/barcodes/external-lookup/{barcode}": { "get": { "summary": "Executes an external barcode lookoup via the configured plugin with the given barcode", "tags": [ @@ -2127,7 +2483,7 @@ ], "parameters": [ { - "in": "query", + "in": "path", "name": "barcode", "required": true, "description": "The barcode to lookup up", @@ -2847,6 +3203,7 @@ "quantity_unit_conversions", "shopping_list", "shopping_lists", + "stock", "recipes", "recipes_pos", "recipes_nestings", @@ -3023,6 +3380,29 @@ "row_created_timestamp": "2019-05-02 20:12:25" } }, + "StockLocation": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "product_id": { + "type": "integer" + }, + "location_id": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "example": { + "id": "1", + "product_id": "3", + "location_id": "1", + "name": "Fridge" + } + }, "StockEntry": { "type": "object", "properties": { diff --git a/localization/demo_data.pot b/localization/demo_data.pot index 8001155a..3ffc43a6 100644 --- a/localization/demo_data.pot +++ b/localization/demo_data.pot @@ -325,5 +325,5 @@ msgstr "" msgid "not yet released" msgstr "" -msgid "Portuguese" +msgid "Portuguese (Brazil)" msgstr "" diff --git a/localization/en/stock_transaction_types.po b/localization/en/stock_transaction_types.po index 7242c79e..d8b65ef1 100644 --- a/localization/en/stock_transaction_types.po +++ b/localization/en/stock_transaction_types.po @@ -15,6 +15,12 @@ msgstr "" msgid "purchase" msgstr "Purchase" +msgid "transfer_to" +msgstr "Transfer To" + +msgid "transfer_from" +msgstr "Transfer From" + msgid "consume" msgstr "Consume" @@ -23,3 +29,9 @@ msgstr "Inventory correction" msgid "product-opened" msgstr "Product opened" + +msgid "stock-edit-old" +msgstr "Stock entry edited (old values)" + +msgid "stock-edit-new" +msgstr "Stock entry edited (new values)" diff --git a/localization/stock_transaction_types.pot b/localization/stock_transaction_types.pot index 9f89fc48..48930d28 100644 --- a/localization/stock_transaction_types.pot +++ b/localization/stock_transaction_types.pot @@ -15,6 +15,12 @@ msgstr "" msgid "purchase" msgstr "" +msgid "transfer_from" +msgstr "" + +msgid "transfer_to" +msgstr "" + msgid "consume" msgstr "" @@ -23,3 +29,9 @@ msgstr "" msgid "product-opened" msgstr "" + +msgid "stock-edit-old" +msgstr "" + +msgid "stock-edit-new" +msgstr "" diff --git a/localization/strings.pot b/localization/strings.pot index ac91e926..f77077e4 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -929,9 +929,6 @@ msgstr "" msgid "Mark as opened" msgstr "" -msgid "Expires on %1$s; Bought on %2$s" -msgstr "" - msgid "Not opened" msgstr "" @@ -1567,3 +1564,45 @@ msgstr "" msgid "This means the next execution of this chore should only be scheduled every %s years" msgstr "" + +msgid "Transfer" +msgstr "" + +msgid "From location" +msgstr "" + +msgid "To location" +msgstr "" + +msgid "There are no units available at this location" +msgstr "" + +msgid "Amount: %1$s; Expires on %2$s; Bought on %3$s" +msgstr "" + +msgid "Transfered %1$s of %2$s from %3$s to %4$s" +msgstr "" + +msgid "Show stock entries" +msgstr "" + +msgid "Stock entries" +msgstr "" + +msgid "Best before date" +msgstr "" + +msgid "Purchased date" +msgstr "" + +msgid "Consume all %s for this stock entry" +msgstr "" + +msgid "The amount cannot be lower than %1$s" +msgstr "" + +msgid "Stock entry successfully updated" +msgstr "" + +msgid "Edit stock entry" +msgstr "" diff --git a/migrations/0095.sql b/migrations/0095.sql new file mode 100644 index 00000000..dfd01779 --- /dev/null +++ b/migrations/0095.sql @@ -0,0 +1,37 @@ +CREATE TRIGGER set_products_default_location_if_empty_stock AFTER INSERT ON stock +BEGIN + UPDATE stock + SET location_id = (SELECT location_id FROM products where id = product_id) + WHERE id = NEW.id + AND location_id IS NULL; +END; + +CREATE TRIGGER set_products_default_location_if_empty_stock_log AFTER INSERT ON stock_log +BEGIN + UPDATE stock_log + SET location_id = (SELECT location_id FROM products where id = product_id) + WHERE id = NEW.id + AND location_id IS NULL; +END; + +ALTER TABLE stock_log +ADD correlation_id TEXT; + +ALTER TABLE stock_log +ADD transaction_id TEXT; + +ALTER TABLE stock_log +ADD stock_row_id INTEGER; + +DROP VIEW stock_current_locations; +CREATE VIEW stock_current_locations +AS +SELECT + 1 AS id, -- Dummy, LessQL needs an id column + s.product_id, + s.location_id AS location_id, + l.name AS location_name +FROM stock s +JOIN locations l + ON s.location_id = l.id +GROUP BY s.product_id, s.location_id, l.name; diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 1a7bffb4..2b37f23c 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -1,15 +1,30 @@ -$('#save-consume-button').on('click', function(e) +$(document).ready(function() { + + if (GetUriParam("embedded") !== undefined) + { + var locationId = GetUriParam('locationId'); + + if (typeof locationId === 'undefined') + { + Grocy.Components.ProductPicker.GetPicker().trigger('change'); + Grocy.Components.ProductPicker.GetInputElement().focus(); + } else { + + $("#location_id").val(locationId); + $("#location_id").trigger('change'); + $("#use_specific_stock_entry").click(); + $("#use_specific_stock_entry").trigger('change'); + } + } +}); + +$('#save-consume-button').on('click', function(e) { e.preventDefault(); var jsonForm = $('#consume-form').serializeJSON(); Grocy.FrontendHelpers.BeginUiBusy("consume-form"); - if ($("#use_specific_stock_entry").is(":checked")) - { - jsonForm.amount = 1; - } - var apiUrl = 'stock/products/' + jsonForm.product_id + '/consume'; var jsonData = {}; @@ -21,6 +36,15 @@ jsonData.stock_entry_id = jsonForm.specific_stock_entry; } + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) + { + jsonData.location_id = $("#location_id").val(); + } + else + { + jsonData.location_id = 1; + } + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES && Grocy.Components.RecipePicker.GetValue().toString().length > 0) { jsonData.recipe_id = Grocy.Components.RecipePicker.GetValue(); @@ -71,11 +95,11 @@ if (productDetails.product.enable_tare_weight_handling == 1) { - var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount - parseFloat(productDetails.product.tare_weight)) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; + var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount - parseFloat(productDetails.product.tare_weight)) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; } else { - var successMessage =__t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; + var successMessage = __t('Removed %1$s of %2$s from stock', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; } if (GetUriParam("embedded") !== undefined) @@ -86,7 +110,6 @@ } else { - Grocy.FrontendHelpers.EndUiBusy("consume-form"); toastr.success(successMessage); @@ -102,6 +125,10 @@ { Grocy.Components.RecipePicker.Clear(); } + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) + { + $("#location_id").find("option").remove().end().append(""); + } Grocy.Components.ProductPicker.GetInputElement().focus(); Grocy.FrontendHelpers.ValidateForm('consume-form'); } @@ -128,11 +155,6 @@ $('#save-mark-as-open-button').on('click', function(e) var jsonForm = $('#consume-form').serializeJSON(); Grocy.FrontendHelpers.BeginUiBusy("consume-form"); - if ($("#use_specific_stock_entry").is(":checked")) - { - jsonForm.amount = 1; - } - var apiUrl = 'stock/products/' + jsonForm.product_id + '/open'; jsonData = { }; @@ -156,7 +178,7 @@ $('#save-mark-as-open-button').on('click', function(e) } Grocy.FrontendHelpers.EndUiBusy("consume-form"); - toastr.success(__t('Marked %1$s of %2$s as opened', jsonForm.amount + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''); + toastr.success(__t('Marked %1$s of %2$s as opened', jsonForm.amount + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''); $('#amount').val(Grocy.UserSettings.stock_default_consume_amount); Grocy.Components.ProductPicker.Clear(); @@ -178,6 +200,69 @@ $('#save-mark-as-open-button').on('click', function(e) ); }); +$("#location_id").on('change', function(e) +{ + var locationId = $(e.target).val(); + var sumValue = 0; + var stockId = null; + + $("#specific_stock_entry").find("option").remove().end().append(""); + if ($("#use_specific_stock_entry").is(":checked")) + { + $("#use_specific_stock_entry").click(); + } + + if (GetUriParam("embedded") !== undefined) + { + stockId = GetUriParam('stockId'); + } + + if (locationId) + { + Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries', + function(stockEntries) + { + stockEntries.forEach(stockEntry => + { + var openTxt = __t("Not opened"); + if (stockEntry.open == 1) + { + openTxt = __t("Opened"); + } + + if (stockEntry.location_id == locationId) + { + $("#specific_stock_entry").append($(""); @@ -185,6 +270,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) { $("#use_specific_stock_entry").click(); } + $("#location_id").val(""); var productId = $(e.target).val(); @@ -195,9 +281,45 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) Grocy.Api.Get('stock/products/' + productId, function(productDetails) { - $('#amount').attr('max', productDetails.stock_amount); $('#amount_qu_unit').text(productDetails.quantity_unit_stock.name); + $("#location_id").find("option").remove().end().append(""); + Grocy.Api.Get("stock/products/" + productId + '/locations', + function(stockLocations) + { + var setDefault = 0; + stockLocations.forEach(stockLocation => + { + if (productDetails.location.id == stockLocation.location_id) { + $("#location_id").append($("