Make it possible to pick a specific stock item on consume (closes #62)

This commit is contained in:
Bernd Bestel 2018-11-17 17:51:35 +01:00
parent b6d60c4e34
commit 816ca6460f
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
7 changed files with 167 additions and 7 deletions

View File

@ -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']));
}
}

View File

@ -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",

View File

@ -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"'
);

View File

@ -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,8 +56,14 @@
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
{
var productId = $(e.target).val();
$("#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)
{
Grocy.Components.ProductCard.Refresh(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(),

View File

@ -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');

View File

@ -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)

View File

@ -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') }}