From 10ea9c44fdb5aa9b054fe944404f08f684d7d013 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 17 Nov 2018 19:39:37 +0100 Subject: [PATCH] Make it possible to mark a product as opened (closes #86) --- controllers/StockApiController.php | 19 ++++ grocy.openapi.json | 60 ++++++++++ localization/en/stock_transaction_types.php | 3 +- localization/en/strings.php | 10 +- migrations/0046.sql | 17 +++ public/viewjs/consume.js | 60 +++++++++- public/viewjs/stockoverview.js | 58 ++++++++++ routes.php | 1 + services/StockService.php | 119 +++++++++++++++++++- views/consume.blade.php | 1 + views/productform.blade.php | 10 ++ views/stockoverview.blade.php | 6 + 12 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 migrations/0046.sql diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 4578f64d..07b91151 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -119,6 +119,25 @@ class StockApiController extends BaseApiController } } + public function OpenProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $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->OpenProduct($args['productId'], $args['amount'], $specificStockEntryId); + return $this->ApiResponse(array('booking_id' => $bookingId)); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } + } + public function CurrentStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { return $this->ApiResponse($this->StockService->GetCurrentStock()); diff --git a/grocy.openapi.json b/grocy.openapi.json index 2438954e..85dc302d 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1003,6 +1003,66 @@ } } }, + "/stock/open-product/{productId}/{amount}": { + "get": { + "description": "Marks the the given amount of the given product as opened", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "description": "A valid product id", + "schema": { + "type": "integer" + } + }, + { + "in": "path", + "name": "amount", + "required": false, + "description": "The amount to remove", + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "stock_entry_id", + "required": false, + "description": "A specific stock entry id to open, if used, the amount has to be 1", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoidApiActionResponse" + } + } + } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } + } + } + } + }, "/stock/inventory-product/{productId}/{newAmount}": { "get": { "description": "Inventories the the given product (adds/removes based on the given new current amount)", diff --git a/localization/en/stock_transaction_types.php b/localization/en/stock_transaction_types.php index 22a57178..24d87eff 100644 --- a/localization/en/stock_transaction_types.php +++ b/localization/en/stock_transaction_types.php @@ -3,5 +3,6 @@ return array( 'purchase' => 'Purchase', 'consume' => 'Consume', - 'inventory-correction' => 'Inventory correction' + 'inventory-correction' => 'Inventory correction', + 'product-opened' => 'Product opened' ); diff --git a/localization/en/strings.php b/localization/en/strings.php index 04895cba..c27df1fd 100644 --- a/localization/en/strings.php +++ b/localization/en/strings.php @@ -308,5 +308,13 @@ return array( '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"' + '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"', + 'Mark #3 #1 of #2 as open' => 'Mark #3 #1 of #2 as open', + 'When a product was marked as opened, the best before date will be replaced by today + this amount of days (a value of 0 disables this)' => 'When a product was marked as opened, the best before date will be replaced by today + this amount of days (a value of 0 disables this)', + 'Default best before days after opened' => 'Default best before days after opened', + 'Marked #1 #2 of #3 as opened' => 'Marked #1 #2 of #3 as opened', + 'Mark as opened' => 'Mark as opened', + 'Expires on #1; Bought on #2' => 'Expires on #1; Bought on #2', + 'Not opened' => 'Not opened', + 'Opened' => 'Opened' ); diff --git a/migrations/0046.sql b/migrations/0046.sql new file mode 100644 index 00000000..262b1d6b --- /dev/null +++ b/migrations/0046.sql @@ -0,0 +1,17 @@ +ALTER TABLE stock +ADD opened_date DATETIME; + +ALTER TABLE stock_log +ADD opened_date DATETIME; + +ALTER TABLE stock +ADD open TINYINT NOT NULL DEFAULT 0 CHECK(open IN (0, 1)); + +UPDATE stock +SET open = 0; + +ALTER TABLE products +ADD default_best_before_days_after_open INTEGER NOT NULL DEFAULT 0; + +UPDATE products +SET default_best_before_days_after_open = 0; diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 1208b85a..3b57b69e 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -23,7 +23,7 @@ } Grocy.Api.Get('stock/get-product-details/' + jsonForm.product_id, - function (productDetails) + function(productDetails) { Grocy.Api.Get(apiUrl, function(result) @@ -54,6 +54,56 @@ ); }); +$('#save-mark-as-open-button').on('click', function(e) +{ + e.preventDefault(); + + var jsonForm = $('#consume-form').serializeJSON(); + + if ($("#use_specific_stock_entry").is(":checked")) + { + jsonForm.amount = 1; + } + + var apiUrl = 'stock/open-product/' + jsonForm.product_id + '/' + jsonForm.amount; + + 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(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('Marked #1 #2 of #3 as opened', jsonForm.amount, Pluralize(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + L("Undo") + ''); + + $('#amount').val(1); + Grocy.Components.ProductPicker.SetValue(''); + Grocy.Components.ProductPicker.GetInputElement().focus(); + Grocy.FrontendHelpers.ValidateForm('consume-form'); + }, + function(xhr) + { + console.error(xhr); + } + ); + }, + function(xhr) + { + console.error(xhr); + } + ); +}); + Grocy.Components.ProductPicker.GetPicker().on('change', function(e) { $("#specific_stock_entry").find("option").remove().end().append(""); @@ -99,11 +149,17 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) { stockEntries.forEach(stockEntry => { + var openTxt = L("Not opened"); + if (stockEntry.open == 1) + { + openTxt = L("Opened"); + } + for (i = 0; i < stockEntry.amount; i++) { $("#specific_stock_entry").append($("