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);
+ }
+ }
}