diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 39a4f2a7..e1982edc 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -509,6 +509,19 @@ 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->StockService->GetProductStockEntries($args['productId'])); @@ -537,4 +550,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/grocy.openapi.json b/grocy.openapi.json index 4ae93ae4..c3fcd69b 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -2325,6 +2325,84 @@ } } }, + "/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", diff --git a/public/viewjs/consume.js b/public/viewjs/consume.js index 5ee4d4fc..1aef81dc 100644 --- a/public/viewjs/consume.js +++ b/public/viewjs/consume.js @@ -75,11 +75,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) @@ -158,7 +158,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(); @@ -451,3 +451,17 @@ function UndoStockBooking(bookingId) } ); }; + +function UndoStockTransaction(transactionId) +{ + Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', { }, + function (result) + { + toastr.success(__t("Transaction successfully undone")); + }, + function (xhr) + { + console.error(xhr); + } + ); +}; diff --git a/public/viewjs/inventory.js b/public/viewjs/inventory.js index 378085ee..1c044519 100644 --- a/public/viewjs/inventory.js +++ b/public/viewjs/inventory.js @@ -64,7 +64,7 @@ Grocy.Api.Get('stock/products/' + jsonForm.product_id, function(result) { - var successMessage = __t('Stock amount of %1$s is now %2$s', result.product.name, result.stock_amount + " " + __n(result.stock_amount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural)) + '
' + __t("Undo") + ''; + var successMessage = __t('Stock amount of %1$s is now %2$s', result.product.name, result.stock_amount + " " + __n(result.stock_amount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural)) + '
' + __t("Undo") + ''; if (GetUriParam("embedded") !== undefined) { @@ -299,3 +299,17 @@ function UndoStockBooking(bookingId) } ); }; + +function UndoStockTransaction(transactionId) +{ + Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', { }, + function (result) + { + toastr.success(__t("Transaction successfully undone")); + }, + function (xhr) + { + console.error(xhr); + } + ); +}; diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index 15f4fe8e..26073c20 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -70,7 +70,7 @@ ); } - var successMessage = __t('Added %1$s of %2$s to stock', result.amount + " " +__n(result.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; + var successMessage = __t('Added %1$s of %2$s to stock', result.amount + " " + __n(result.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; if (GetUriParam("embedded") !== undefined) { @@ -309,3 +309,28 @@ function UndoStockBooking(bookingId) } ); }; + +function UndoStockTransaction(transactionId) +{ + Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', { }, + function(result) + { + toastr.success(__t("Transaction successfully undone")); + + Grocy.Api.Get('stock/transactions/' + transactionId.toString(), + function(result) + { + window.postMessage(WindowMessageBag("ProductChanged", result[0].product_id), Grocy.BaseUrl); + }, + function (xhr) + { + console.error(xhr); + } + ); + }, + function(xhr) + { + console.error(xhr); + } + ); +}; diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index 5b68dbc2..645e2ac9 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -84,7 +84,7 @@ $(document).on('click', '.product-consume-button', function(e) Grocy.Api.Get('stock/products/' + productId, function(result) { - var toastMessage = __t('Removed %1$s of %2$s from stock', consumeAmount.toString() + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural), result.product.name) + '
' + __t("Undo") + ''; + var toastMessage = __t('Removed %1$s of %2$s from stock', consumeAmount.toString() + " " + __n(consumeAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural), result.product.name) + '
' + __t("Undo") + ''; if (wasSpoiled) { toastMessage += " (" + __t("Spoiled") + ")"; @@ -137,7 +137,7 @@ $(document).on('click', '.product-open-button', function(e) } Grocy.FrontendHelpers.EndUiBusy(); - toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '
' + __t("Undo") + ''); + toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '
' + __t("Undo") + ''); RefreshStatistics(); RefreshProductRow(productId); }, diff --git a/routes.php b/routes.php index 6e83a4e6..0fea71f8 100644 --- a/routes.php +++ b/routes.php @@ -177,6 +177,8 @@ $app->group('/api', function() $this->post('/stock/products/by-barcode/{barcode}/open', '\Grocy\Controllers\StockApiController:OpenProductByBarcode'); $this->get('/stock/bookings/{bookingId}', '\Grocy\Controllers\StockApiController:StockBooking'); $this->post('/stock/bookings/{bookingId}/undo', '\Grocy\Controllers\StockApiController:UndoBooking'); + $this->get('/stock/transactions/{transactionId}', '\Grocy\Controllers\StockApiController:StockTransactions'); + $this->post('/stock/transactions/{transactionId}/undo', '\Grocy\Controllers\StockApiController:UndoTransaction'); $this->get('/stock/barcodes/external-lookup/{barcode}', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup'); } diff --git a/services/StockService.php b/services/StockService.php index 51618b8f..a22db698 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -244,7 +244,8 @@ class StockService extends BaseService 'stock_id' => $stockId, 'transaction_type' => $transactionType, 'price' => $price, - 'location_id' => $locationId + 'location_id' => $locationId, + 'transaction_id' => $transactionId )); $logRow->save(); @@ -932,4 +933,19 @@ class StockService extends BaseService throw new \Exception('This booking cannot be undone'); } } + + public function UndoTransaction($transactionId) + { + $transactionBookings = $this->Database->stock_log()->where('undone = 0 AND transaction_id = :1', $transactionId)->orderBy('id', 'DESC')->fetchAll(); + + if (count($transactionBookings) === 0) + { + throw new \Exception('This transaction was not found or already undone'); + } + + foreach ($transactionBookings as $transactionBooking) + { + $this->UndoBooking($transactionBooking->id, true); + } + } }