diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 6843ee57..4578f64d 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -83,9 +83,15 @@ class StockApiController extends BaseApiController $transactionType = $request->getQueryParams()['transactiontype']; } + $specificStockEntryId = "default"; + if (isset($request->getQueryParams()['stock_entry_id']) && !empty($request->getQueryParams()['stock_entry_id'])) + { + $specificStockEntryId = $request->getQueryParams()['stock_entry_id']; + } + try { - $bookingId = $this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType); + $bookingId = $this->StockService->ConsumeProduct($args['productId'], $args['amount'], $spoiled, $transactionType, $specificStockEntryId); return $this->ApiResponse(array('booking_id' => $bookingId)); } catch (\Exception $ex) @@ -178,4 +184,9 @@ class StockApiController extends BaseApiController return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); } } + + public function ProductStockEntries(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + return $this->ApiResponse($this->StockService->GetProductStockEntries($args['productId'])); + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index 35fbd957..2438954e 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -968,6 +968,15 @@ "schema": { "$ref": "#/components/internalSchemas/StockTransactionType" } + }, + { + "in": "query", + "name": "stock_entry_id", + "required": false, + "description": "A specific stock entry id to consume, if used, the amount has to be 1", + "schema": { + "type": "string" + } } ], "responses": { @@ -1139,6 +1148,50 @@ } } }, + "/stock/get-product-stock-entries/{productId}": { + "get": { + "description": "Returns all stock entries of the given product in order of next use (first expiring first, then first in first out)", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "description": "A valid product id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "An array of StockEntry objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StockEntry" + } + } + } + } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } + } + } + } + }, "/stock/get-current-stock": { "get": { "description": "Returns all products which are currently in stock incl. the next expiring date per product", diff --git a/localization/en/strings.php b/localization/en/strings.php index e6c91823..04895cba 100644 --- a/localization/en/strings.php +++ b/localization/en/strings.php @@ -305,5 +305,8 @@ return array( 'Disable stock fulfillment checking for this ingredient' => 'Disable stock fulfillment checking for this ingredient', 'Add all list items to stock' => 'Add all list items to stock', 'Add #3 #1 of #2 to stock' => 'Add #3 #1 of #2 to stock', - 'Adding shopping list item #1 of #2' => 'Adding shopping list item #1 of #2' + 'Adding shopping list item #1 of #2' => 'Adding shopping list item #1 of #2', + 'Use a specific stock item' => 'Use a specific stock item', + 'Expires on #1; bought on #2' => 'Expires on #1; bought on #2', + 'The first item in this list would be picked by the default rule which is "First expiring first, then first in first out"' => 'The first item in this list would be picked by the default rule which is "First expiring first, then first in first out"' ); diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 2840c65c..1208b85a 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -4,18 +4,36 @@ var jsonForm = $('#consume-form').serializeJSON(); + if ($("#use_specific_stock_entry").is(":checked")) + { + jsonForm.amount = 1; + } + var spoiled = 0; if ($('#spoiled').is(':checked')) { spoiled = 1; } + var apiUrl = 'stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled; + + if ($("#use_specific_stock_entry").is(":checked")) + { + apiUrl += "&stock_entry_id=" + jsonForm.specific_stock_entry; + } + Grocy.Api.Get('stock/get-product-details/' + jsonForm.product_id, function (productDetails) { - Grocy.Api.Get('stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled, + Grocy.Api.Get(apiUrl, function(result) { + $("#specific_stock_entry").find("option").remove().end().append(""); + if ($("#use_specific_stock_entry").is(":checked")) + { + $("#use_specific_stock_entry").click(); + } + toastr.success(L('Removed #1 #2 of #3 from stock', jsonForm.amount, Pluralize(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + L("Undo") + ''); $('#amount').val(1); @@ -38,8 +56,14 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) { - var productId = $(e.target).val(); + $("#specific_stock_entry").find("option").remove().end().append(""); + if ($("#use_specific_stock_entry").is(":checked")) + { + $("#use_specific_stock_entry").click(); + } + var productId = $(e.target).val(); + if (productId) { Grocy.Components.ProductCard.Refresh(productId); @@ -69,6 +93,26 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) console.error(xhr); } ); + + Grocy.Api.Get("stock/get-product-stock-entries/" + productId, + function (stockEntries) + { + stockEntries.forEach(stockEntry => + { + for (i = 0; i < stockEntry.amount; i++) + { + $("#specific_stock_entry").append($("