mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 17:45:39 +00:00
Make it possible to pick a specific stock item on consume (closes #62)
This commit is contained in:
parent
b6d60c4e34
commit
816ca6460f
@ -83,9 +83,15 @@ class StockApiController extends BaseApiController
|
|||||||
$transactionType = $request->getQueryParams()['transactiontype'];
|
$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
|
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));
|
return $this->ApiResponse(array('booking_id' => $bookingId));
|
||||||
}
|
}
|
||||||
catch (\Exception $ex)
|
catch (\Exception $ex)
|
||||||
@ -178,4 +184,9 @@ class StockApiController extends BaseApiController
|
|||||||
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
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']));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -968,6 +968,15 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/internalSchemas/StockTransactionType"
|
"$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": {
|
"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": {
|
"/stock/get-current-stock": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns all products which are currently in stock incl. the next expiring date per product",
|
"description": "Returns all products which are currently in stock incl. the next expiring date per product",
|
||||||
|
@ -305,5 +305,8 @@ return array(
|
|||||||
'Disable stock fulfillment checking for this ingredient' => 'Disable stock fulfillment checking for this ingredient',
|
'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 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',
|
'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"'
|
||||||
);
|
);
|
||||||
|
@ -4,18 +4,36 @@
|
|||||||
|
|
||||||
var jsonForm = $('#consume-form').serializeJSON();
|
var jsonForm = $('#consume-form').serializeJSON();
|
||||||
|
|
||||||
|
if ($("#use_specific_stock_entry").is(":checked"))
|
||||||
|
{
|
||||||
|
jsonForm.amount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
var spoiled = 0;
|
var spoiled = 0;
|
||||||
if ($('#spoiled').is(':checked'))
|
if ($('#spoiled').is(':checked'))
|
||||||
{
|
{
|
||||||
spoiled = 1;
|
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,
|
Grocy.Api.Get('stock/get-product-details/' + jsonForm.product_id,
|
||||||
function (productDetails)
|
function (productDetails)
|
||||||
{
|
{
|
||||||
Grocy.Api.Get('stock/consume-product/' + jsonForm.product_id + '/' + jsonForm.amount + '?spoiled=' + spoiled,
|
Grocy.Api.Get(apiUrl,
|
||||||
function(result)
|
function(result)
|
||||||
{
|
{
|
||||||
|
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||||
|
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.booking_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
|
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.booking_id + ')"><i class="fas fa-undo"></i> ' + L("Undo") + '</a>');
|
||||||
|
|
||||||
$('#amount').val(1);
|
$('#amount').val(1);
|
||||||
@ -38,6 +56,12 @@
|
|||||||
|
|
||||||
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||||
{
|
{
|
||||||
|
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||||
|
if ($("#use_specific_stock_entry").is(":checked"))
|
||||||
|
{
|
||||||
|
$("#use_specific_stock_entry").click();
|
||||||
|
}
|
||||||
|
|
||||||
var productId = $(e.target).val();
|
var productId = $(e.target).val();
|
||||||
|
|
||||||
if (productId)
|
if (productId)
|
||||||
@ -69,6 +93,26 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
|||||||
console.error(xhr);
|
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($("<option>", {
|
||||||
|
value: stockEntry.stock_id,
|
||||||
|
text: L("Expires on #1; bought on #2", moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD"))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(xhr)
|
||||||
|
{
|
||||||
|
console.error(xhr);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -81,7 +125,12 @@ $('#amount').on('focus', function(e)
|
|||||||
$(this).select();
|
$(this).select();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#consume-form input').keyup(function (event)
|
$('#consume-form input').keyup(function(event)
|
||||||
|
{
|
||||||
|
Grocy.FrontendHelpers.ValidateForm('consume-form');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#consume-form select').change(function(event)
|
||||||
{
|
{
|
||||||
Grocy.FrontendHelpers.ValidateForm('consume-form');
|
Grocy.FrontendHelpers.ValidateForm('consume-form');
|
||||||
});
|
});
|
||||||
@ -103,6 +152,27 @@ $('#consume-form input').keydown(function(event)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#use_specific_stock_entry").on("change", function()
|
||||||
|
{
|
||||||
|
var value = $(this).is(":checked");
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
$("#specific_stock_entry").removeAttr("disabled");
|
||||||
|
$("#amount").attr("disabled", "");
|
||||||
|
$("#amount").removeAttr("required");
|
||||||
|
$("#specific_stock_entry").attr("required", "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$("#specific_stock_entry").attr("disabled", "");
|
||||||
|
$("#amount").removeAttr("disabled");
|
||||||
|
$("#amount").attr("required", "");
|
||||||
|
$("#specific_stock_entry").removeAttr("required");
|
||||||
|
}
|
||||||
|
|
||||||
|
Grocy.FrontendHelpers.ValidateForm("consume-form");
|
||||||
|
});
|
||||||
|
|
||||||
function UndoStockBooking(bookingId)
|
function UndoStockBooking(bookingId)
|
||||||
{
|
{
|
||||||
Grocy.Api.Get('stock/undo-booking/' + bookingId.toString(),
|
Grocy.Api.Get('stock/undo-booking/' + bookingId.toString(),
|
||||||
|
@ -111,6 +111,7 @@ $app->group('/api', function()
|
|||||||
$this->get('/stock/inventory-product/{productId}/{newAmount}', '\Grocy\Controllers\StockApiController:InventoryProduct');
|
$this->get('/stock/inventory-product/{productId}/{newAmount}', '\Grocy\Controllers\StockApiController:InventoryProduct');
|
||||||
$this->get('/stock/get-product-details/{productId}', '\Grocy\Controllers\StockApiController:ProductDetails');
|
$this->get('/stock/get-product-details/{productId}', '\Grocy\Controllers\StockApiController:ProductDetails');
|
||||||
$this->get('/stock/get-product-price-history/{productId}', '\Grocy\Controllers\StockApiController:ProductPriceHistory');
|
$this->get('/stock/get-product-price-history/{productId}', '\Grocy\Controllers\StockApiController:ProductPriceHistory');
|
||||||
|
$this->get('/stock/get-product-stock-entries/{productId}', '\Grocy\Controllers\StockApiController:ProductStockEntries');
|
||||||
$this->get('/stock/get-current-stock', '\Grocy\Controllers\StockApiController:CurrentStock');
|
$this->get('/stock/get-current-stock', '\Grocy\Controllers\StockApiController:CurrentStock');
|
||||||
$this->get('/stock/get-current-volatil-stock', '\Grocy\Controllers\StockApiController:CurrentVolatilStock');
|
$this->get('/stock/get-current-volatil-stock', '\Grocy\Controllers\StockApiController:CurrentVolatilStock');
|
||||||
$this->get('/stock/add-missing-products-to-shoppinglist', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
|
$this->get('/stock/add-missing-products-to-shoppinglist', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
|
||||||
|
@ -91,6 +91,13 @@ class StockService extends BaseService
|
|||||||
return $returnData;
|
return $returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function GetProductStockEntries($productId)
|
||||||
|
{
|
||||||
|
// In order of next use:
|
||||||
|
// First expiring first, then first in first out
|
||||||
|
return $this->Database->stock()->where('product_id', $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
public function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType, $purchasedDate, $price)
|
public function AddProduct(int $productId, int $amount, string $bestBeforeDate, $transactionType, $purchasedDate, $price)
|
||||||
{
|
{
|
||||||
if (!$this->ProductExists($productId))
|
if (!$this->ProductExists($productId))
|
||||||
@ -133,7 +140,7 @@ class StockService extends BaseService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ConsumeProduct(int $productId, int $amount, bool $spoiled, $transactionType)
|
public function ConsumeProduct(int $productId, int $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default')
|
||||||
{
|
{
|
||||||
if (!$this->ProductExists($productId))
|
if (!$this->ProductExists($productId))
|
||||||
{
|
{
|
||||||
@ -143,13 +150,18 @@ class StockService extends BaseService
|
|||||||
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
|
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
|
||||||
{
|
{
|
||||||
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
|
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
|
||||||
$potentialStockEntries = $this->Database->stock()->where('product_id', $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll(); //First expiring first, then first in first out
|
$potentialStockEntries = $this->GetProductStockEntries($productId);
|
||||||
|
|
||||||
if ($amount > $productStockAmount)
|
if ($amount > $productStockAmount)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($specificStockEntryId !== 'default')
|
||||||
|
{
|
||||||
|
$potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId);
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($potentialStockEntries as $stockEntry)
|
foreach ($potentialStockEntries as $stockEntry)
|
||||||
{
|
{
|
||||||
if ($amount == 0)
|
if ($amount == 0)
|
||||||
|
@ -26,6 +26,16 @@
|
|||||||
'invalidFeedback' => $L('The amount cannot be lower than #1', '1')
|
'invalidFeedback' => $L('The amount cannot be lower than #1', '1')
|
||||||
))
|
))
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="use_specific_stock_entry">
|
||||||
|
<input type="checkbox" id="use_specific_stock_entry" name="use_specific_stock_entry"> {{ $L('Use a specific stock item') }}
|
||||||
|
</label>
|
||||||
|
<select disabled class="form-control" id="specific_stock_entry" name="specific_stock_entry">
|
||||||
|
<option></option>
|
||||||
|
</select>
|
||||||
|
<span class="small text-muted">{{ $L('The first item in this list would be picked by the default rule which is "First expiring first, then first in first out"') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label for="spoiled">
|
<label for="spoiled">
|
||||||
<input type="checkbox" id="spoiled" name="spoiled"> {{ $L('Spoiled') }}
|
<input type="checkbox" id="spoiled" name="spoiled"> {{ $L('Spoiled') }}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user