mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +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'];
|
||||
}
|
||||
|
||||
$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']));
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"'
|
||||
);
|
||||
|
@ -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("<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>');
|
||||
|
||||
$('#amount').val(1);
|
||||
@ -38,6 +56,12 @@
|
||||
|
||||
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();
|
||||
|
||||
if (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($("<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();
|
||||
});
|
||||
|
||||
$('#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');
|
||||
});
|
||||
@ -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)
|
||||
{
|
||||
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/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-stock-entries/{productId}', '\Grocy\Controllers\StockApiController:ProductStockEntries');
|
||||
$this->get('/stock/get-current-stock', '\Grocy\Controllers\StockApiController:CurrentStock');
|
||||
$this->get('/stock/get-current-volatil-stock', '\Grocy\Controllers\StockApiController:CurrentVolatilStock');
|
||||
$this->get('/stock/add-missing-products-to-shoppinglist', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
|
||||
|
@ -91,6 +91,13 @@ class StockService extends BaseService
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
@ -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)
|
||||
{
|
||||
$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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($specificStockEntryId !== 'default')
|
||||
{
|
||||
$potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId);
|
||||
}
|
||||
|
||||
foreach ($potentialStockEntries as $stockEntry)
|
||||
{
|
||||
if ($amount == 0)
|
||||
|
@ -26,6 +26,16 @@
|
||||
'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">
|
||||
<label for="spoiled">
|
||||
<input type="checkbox" id="spoiled" name="spoiled"> {{ $L('Spoiled') }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user