mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Make it possible to mark a product as opened (closes #86)
This commit is contained in:
parent
816ca6460f
commit
10ea9c44fd
@ -119,6 +119,25 @@ class StockApiController extends BaseApiController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function OpenProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||||
|
{
|
||||||
|
$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->OpenProduct($args['productId'], $args['amount'], $specificStockEntryId);
|
||||||
|
return $this->ApiResponse(array('booking_id' => $bookingId));
|
||||||
|
}
|
||||||
|
catch (\Exception $ex)
|
||||||
|
{
|
||||||
|
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function CurrentStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
public function CurrentStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||||
{
|
{
|
||||||
return $this->ApiResponse($this->StockService->GetCurrentStock());
|
return $this->ApiResponse($this->StockService->GetCurrentStock());
|
||||||
|
@ -1003,6 +1003,66 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/stock/open-product/{productId}/{amount}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Marks the the given amount of the given product as opened",
|
||||||
|
"tags": [
|
||||||
|
"Stock"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "productId",
|
||||||
|
"required": true,
|
||||||
|
"description": "A valid product id",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "amount",
|
||||||
|
"required": false,
|
||||||
|
"description": "The amount to remove",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "stock_entry_id",
|
||||||
|
"required": false,
|
||||||
|
"description": "A specific stock entry id to open, if used, the amount has to be 1",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A VoidApiActionResponse object",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/VoidApiActionResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "A VoidApiActionResponse object (possible errors are: Not existing product)",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/stock/inventory-product/{productId}/{newAmount}": {
|
"/stock/inventory-product/{productId}/{newAmount}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Inventories the the given product (adds/removes based on the given new current amount)",
|
"description": "Inventories the the given product (adds/removes based on the given new current amount)",
|
||||||
|
@ -3,5 +3,6 @@
|
|||||||
return array(
|
return array(
|
||||||
'purchase' => 'Purchase',
|
'purchase' => 'Purchase',
|
||||||
'consume' => 'Consume',
|
'consume' => 'Consume',
|
||||||
'inventory-correction' => 'Inventory correction'
|
'inventory-correction' => 'Inventory correction',
|
||||||
|
'product-opened' => 'Product opened'
|
||||||
);
|
);
|
||||||
|
@ -308,5 +308,13 @@ return array(
|
|||||||
'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',
|
'Use a specific stock item' => 'Use a specific stock item',
|
||||||
'Expires on #1; bought on #2' => 'Expires on #1; bought on #2',
|
'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"'
|
'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"',
|
||||||
|
'Mark #3 #1 of #2 as open' => 'Mark #3 #1 of #2 as open',
|
||||||
|
'When a product was marked as opened, the best before date will be replaced by today + this amount of days (a value of 0 disables this)' => 'When a product was marked as opened, the best before date will be replaced by today + this amount of days (a value of 0 disables this)',
|
||||||
|
'Default best before days after opened' => 'Default best before days after opened',
|
||||||
|
'Marked #1 #2 of #3 as opened' => 'Marked #1 #2 of #3 as opened',
|
||||||
|
'Mark as opened' => 'Mark as opened',
|
||||||
|
'Expires on #1; Bought on #2' => 'Expires on #1; Bought on #2',
|
||||||
|
'Not opened' => 'Not opened',
|
||||||
|
'Opened' => 'Opened'
|
||||||
);
|
);
|
||||||
|
17
migrations/0046.sql
Normal file
17
migrations/0046.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
ALTER TABLE stock
|
||||||
|
ADD opened_date DATETIME;
|
||||||
|
|
||||||
|
ALTER TABLE stock_log
|
||||||
|
ADD opened_date DATETIME;
|
||||||
|
|
||||||
|
ALTER TABLE stock
|
||||||
|
ADD open TINYINT NOT NULL DEFAULT 0 CHECK(open IN (0, 1));
|
||||||
|
|
||||||
|
UPDATE stock
|
||||||
|
SET open = 0;
|
||||||
|
|
||||||
|
ALTER TABLE products
|
||||||
|
ADD default_best_before_days_after_open INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE products
|
||||||
|
SET default_best_before_days_after_open = 0;
|
@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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(apiUrl,
|
Grocy.Api.Get(apiUrl,
|
||||||
function(result)
|
function(result)
|
||||||
@ -54,6 +54,56 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#save-mark-as-open-button').on('click', function(e)
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var jsonForm = $('#consume-form').serializeJSON();
|
||||||
|
|
||||||
|
if ($("#use_specific_stock_entry").is(":checked"))
|
||||||
|
{
|
||||||
|
jsonForm.amount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiUrl = 'stock/open-product/' + jsonForm.product_id + '/' + jsonForm.amount;
|
||||||
|
|
||||||
|
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(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('Marked #1 #2 of #3 as opened', 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);
|
||||||
|
Grocy.Components.ProductPicker.SetValue('');
|
||||||
|
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||||
|
Grocy.FrontendHelpers.ValidateForm('consume-form');
|
||||||
|
},
|
||||||
|
function(xhr)
|
||||||
|
{
|
||||||
|
console.error(xhr);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function(xhr)
|
||||||
|
{
|
||||||
|
console.error(xhr);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
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>");
|
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||||
@ -99,11 +149,17 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
|||||||
{
|
{
|
||||||
stockEntries.forEach(stockEntry =>
|
stockEntries.forEach(stockEntry =>
|
||||||
{
|
{
|
||||||
|
var openTxt = L("Not opened");
|
||||||
|
if (stockEntry.open == 1)
|
||||||
|
{
|
||||||
|
openTxt = L("Opened");
|
||||||
|
}
|
||||||
|
|
||||||
for (i = 0; i < stockEntry.amount; i++)
|
for (i = 0; i < stockEntry.amount; i++)
|
||||||
{
|
{
|
||||||
$("#specific_stock_entry").append($("<option>", {
|
$("#specific_stock_entry").append($("<option>", {
|
||||||
value: stockEntry.stock_id,
|
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"))
|
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")) + "; " + openTxt
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -152,6 +152,64 @@ $(document).on('click', '.product-consume-button', function(e)
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.product-open-button', function(e)
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Remove the focus from the current button
|
||||||
|
// to prevent that the tooltip stays until clicked anywhere else
|
||||||
|
document.activeElement.blur();
|
||||||
|
|
||||||
|
var productId = $(e.currentTarget).attr('data-product-id');
|
||||||
|
var productName = $(e.currentTarget).attr('data-product-name');
|
||||||
|
var productQuName = $(e.currentTarget).attr('data-product-qu-name');
|
||||||
|
|
||||||
|
Grocy.Api.Get('stock/open-product/' + productId + '/1',
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
Grocy.Api.Get('stock/get-product-details/' + productId,
|
||||||
|
function(result)
|
||||||
|
{
|
||||||
|
var productRow = $('#product-' + productId + '-row');
|
||||||
|
var expiringThreshold = moment().add("-" + $("#info-expiring-products").data("next-x-days"), "days");
|
||||||
|
var now = moment();
|
||||||
|
var nextBestBeforeDate = moment(result.next_best_before_date);
|
||||||
|
|
||||||
|
productRow.removeClass("table-warning");
|
||||||
|
productRow.removeClass("table-danger");
|
||||||
|
if (now.isAfter(nextBestBeforeDate))
|
||||||
|
{
|
||||||
|
productRow.addClass("table-danger");
|
||||||
|
}
|
||||||
|
if (expiringThreshold.isAfter(nextBestBeforeDate))
|
||||||
|
{
|
||||||
|
productRow.addClass("table-warning");
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#product-' + productId + '-next-best-before-date').parent().effect('highlight', {}, 500);
|
||||||
|
$('#product-' + productId + '-next-best-before-date').fadeOut(500, function ()
|
||||||
|
{
|
||||||
|
$(this).text(result.next_best_before_date).fadeIn(500);
|
||||||
|
});
|
||||||
|
$('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date);
|
||||||
|
|
||||||
|
toastr.success(L('Marked #1 #2 of #3 as opened', 1, productQuName, productName));
|
||||||
|
RefreshContextualTimeago();
|
||||||
|
RefreshStatistics();
|
||||||
|
},
|
||||||
|
function(xhr)
|
||||||
|
{
|
||||||
|
console.error(xhr);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
function(xhr)
|
||||||
|
{
|
||||||
|
console.error(xhr);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on("click", ".product-name-cell", function(e)
|
$(document).on("click", ".product-name-cell", function(e)
|
||||||
{
|
{
|
||||||
Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-product-id"));
|
Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-product-id"));
|
||||||
|
@ -108,6 +108,7 @@ $app->group('/api', function()
|
|||||||
// Stock
|
// Stock
|
||||||
$this->get('/stock/add-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:AddProduct');
|
$this->get('/stock/add-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:AddProduct');
|
||||||
$this->get('/stock/consume-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:ConsumeProduct');
|
$this->get('/stock/consume-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:ConsumeProduct');
|
||||||
|
$this->get('/stock/open-product/{productId}/{amount}', '\Grocy\Controllers\StockApiController:OpenProduct');
|
||||||
$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');
|
||||||
|
@ -7,6 +7,7 @@ class StockService extends BaseService
|
|||||||
const TRANSACTION_TYPE_PURCHASE = 'purchase';
|
const TRANSACTION_TYPE_PURCHASE = 'purchase';
|
||||||
const TRANSACTION_TYPE_CONSUME = 'consume';
|
const TRANSACTION_TYPE_CONSUME = 'consume';
|
||||||
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
|
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
|
||||||
|
const TRANSACTION_TYPE_PRODUCT_OPENED = 'product-opened';
|
||||||
|
|
||||||
public function GetCurrentStock($includeNotInStockButMissingProducts = false)
|
public function GetCurrentStock($includeNotInStockButMissingProducts = false)
|
||||||
{
|
{
|
||||||
@ -91,11 +92,19 @@ class StockService extends BaseService
|
|||||||
return $returnData;
|
return $returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function GetProductStockEntries($productId)
|
public function GetProductStockEntries($productId, $excludeOpened = false)
|
||||||
{
|
{
|
||||||
// In order of next use:
|
// In order of next use:
|
||||||
// First expiring first, then first in first out
|
// 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();
|
|
||||||
|
if ($excludeOpened)
|
||||||
|
{
|
||||||
|
return $this->Database->stock()->where('product_id = :1 AND open = 0', $productId)->orderBy('best_before_date', 'ASC')->orderBy('purchased_date', 'ASC')->fetchAll();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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)
|
||||||
@ -180,7 +189,8 @@ class StockService extends BaseService
|
|||||||
'spoiled' => $spoiled,
|
'spoiled' => $spoiled,
|
||||||
'stock_id' => $stockEntry->stock_id,
|
'stock_id' => $stockEntry->stock_id,
|
||||||
'transaction_type' => $transactionType,
|
'transaction_type' => $transactionType,
|
||||||
'price' => $stockEntry->price
|
'price' => $stockEntry->price,
|
||||||
|
'opened_date' => $stockEntry->opened_date
|
||||||
));
|
));
|
||||||
$logRow->save();
|
$logRow->save();
|
||||||
|
|
||||||
@ -198,7 +208,8 @@ class StockService extends BaseService
|
|||||||
'spoiled' => $spoiled,
|
'spoiled' => $spoiled,
|
||||||
'stock_id' => $stockEntry->stock_id,
|
'stock_id' => $stockEntry->stock_id,
|
||||||
'transaction_type' => $transactionType,
|
'transaction_type' => $transactionType,
|
||||||
'price' => $stockEntry->price
|
'price' => $stockEntry->price,
|
||||||
|
'opened_date' => $stockEntry->opened_date
|
||||||
));
|
));
|
||||||
$logRow->save();
|
$logRow->save();
|
||||||
|
|
||||||
@ -243,6 +254,91 @@ class StockService extends BaseService
|
|||||||
return $this->Database->lastInsertId();
|
return $this->Database->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function OpenProduct(int $productId, int $amount, $specificStockEntryId = 'default')
|
||||||
|
{
|
||||||
|
if (!$this->ProductExists($productId))
|
||||||
|
{
|
||||||
|
throw new \Exception('Product does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
$productStockAmount = $this->Database->stock()->where('product_id', $productId)->sum('amount');
|
||||||
|
$potentialStockEntries = $this->GetProductStockEntries($productId, true);
|
||||||
|
$product = $this->Database->products($productId);
|
||||||
|
|
||||||
|
if ($amount > $productStockAmount)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($specificStockEntryId !== 'default')
|
||||||
|
{
|
||||||
|
$potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($potentialStockEntries as $stockEntry)
|
||||||
|
{
|
||||||
|
if ($amount == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$newBestBeforeDate = $stockEntry->best_before_date;
|
||||||
|
if ($product->default_best_before_days_after_open > 0)
|
||||||
|
{
|
||||||
|
$newBestBeforeDate = date("Y-m-d", (strtotime('+' . $product->default_best_before_days_after_open . ' days')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($amount >= $stockEntry->amount) //Take the whole stock entry
|
||||||
|
{
|
||||||
|
$logRow = $this->Database->stock_log()->createRow(array(
|
||||||
|
'product_id' => $stockEntry->product_id,
|
||||||
|
'amount' => $stockEntry->amount,
|
||||||
|
'best_before_date' => $stockEntry->best_before_date,
|
||||||
|
'purchased_date' => $stockEntry->purchased_date,
|
||||||
|
'stock_id' => $stockEntry->stock_id,
|
||||||
|
'transaction_type' => self::TRANSACTION_TYPE_PRODUCT_OPENED,
|
||||||
|
'price' => $stockEntry->price,
|
||||||
|
'opened_date' => date('Y-m-d')
|
||||||
|
));
|
||||||
|
$logRow->save();
|
||||||
|
|
||||||
|
$amount -= $stockEntry->amount;
|
||||||
|
|
||||||
|
$stockEntry->update(array(
|
||||||
|
'open' => 1,
|
||||||
|
'opened_date' => date('Y-m-d'),
|
||||||
|
'best_before_date' => $newBestBeforeDate
|
||||||
|
));
|
||||||
|
}
|
||||||
|
else //Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
|
||||||
|
{
|
||||||
|
$logRow = $this->Database->stock_log()->createRow(array(
|
||||||
|
'product_id' => $stockEntry->product_id,
|
||||||
|
'amount' => $amount,
|
||||||
|
'best_before_date' => $stockEntry->best_before_date,
|
||||||
|
'purchased_date' => $stockEntry->purchased_date,
|
||||||
|
'stock_id' => $stockEntry->stock_id,
|
||||||
|
'transaction_type' => self::TRANSACTION_TYPE_PRODUCT_OPENED,
|
||||||
|
'price' => $stockEntry->price,
|
||||||
|
'opened_date' => date('Y-m-d')
|
||||||
|
));
|
||||||
|
$logRow->save();
|
||||||
|
|
||||||
|
$restStockAmount = $stockEntry->amount - $amount;
|
||||||
|
$amount = 0;
|
||||||
|
|
||||||
|
$stockEntry->update(array(
|
||||||
|
'amount' => $restStockAmount,
|
||||||
|
'open' => 1,
|
||||||
|
'opened_date' => date('Y-m-d'),
|
||||||
|
'best_before_date' => $newBestBeforeDate
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->Database->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
public function AddMissingProductsToShoppingList()
|
public function AddMissingProductsToShoppingList()
|
||||||
{
|
{
|
||||||
$missingProducts = $this->GetMissingProducts();
|
$missingProducts = $this->GetMissingProducts();
|
||||||
@ -365,6 +461,21 @@ class StockService extends BaseService
|
|||||||
'undone_timestamp' => date('Y-m-d H:i:s')
|
'undone_timestamp' => date('Y-m-d H:i:s')
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_PRODUCT_OPENED)
|
||||||
|
{
|
||||||
|
// Remove opened flag from corresponding log entry
|
||||||
|
$stockRows = $this->Database->stock()->where('stock_id = :1 AND amount = :2', $logRow->stock_id, $logRow->amount);
|
||||||
|
$stockRows->update(array(
|
||||||
|
'open' => 0,
|
||||||
|
'opened_date' => null
|
||||||
|
));
|
||||||
|
|
||||||
|
// Update log entry
|
||||||
|
$logRow->update(array(
|
||||||
|
'undone' => 1,
|
||||||
|
'undone_timestamp' => date('Y-m-d H:i:s')
|
||||||
|
));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
throw new \Exception('This booking cannot be undone');
|
throw new \Exception('This booking cannot be undone');
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="save-consume-button" class="btn btn-success">{{ $L('OK') }}</button>
|
<button id="save-consume-button" class="btn btn-success">{{ $L('OK') }}</button>
|
||||||
|
<button id="save-mark-as-open-button" class="btn btn-secondary">{{ $L('Mark as opened') }}</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,6 +73,16 @@
|
|||||||
'hint' => $L('For purchases this amount of days will be added to today for the best before date suggestion') . ' (' . $L('-1 means that this product never expires') . ')'
|
'hint' => $L('For purchases this amount of days will be added to today for the best before date suggestion') . ' (' . $L('-1 means that this product never expires') . ')'
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@php if($mode == 'edit') { $value = $product->default_best_before_days_after_open; } else { $value = 0; } @endphp
|
||||||
|
@include('components.numberpicker', array(
|
||||||
|
'id' => 'default_best_before_days_after_open',
|
||||||
|
'label' => 'Default best before days after opened',
|
||||||
|
'min' => 0,
|
||||||
|
'value' => $value,
|
||||||
|
'invalidFeedback' => $L('The amount cannot be lower than #1', '-1'),
|
||||||
|
'hint' => $L('When a product was marked as opened, the best before date will be replaced by today + this amount of days (a value of 0 disables this)')
|
||||||
|
))
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="product_group_id">{{ $L('Product group') }}</label>
|
<label for="product_group_id">{{ $L('Product group') }}</label>
|
||||||
<select class="form-control" id="product_group_id" name="product_group_id">
|
<select class="form-control" id="product_group_id" name="product_group_id">
|
||||||
|
@ -97,6 +97,12 @@
|
|||||||
data-consume-amount="{{ $currentStockEntry->amount }}">
|
data-consume-amount="{{ $currentStockEntry->amount }}">
|
||||||
<i class="fas fa-utensils"></i> {{ $L('All') }}
|
<i class="fas fa-utensils"></i> {{ $L('All') }}
|
||||||
</a>
|
</a>
|
||||||
|
<a class="btn btn-success btn-sm product-open-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $L('Mark #3 #1 of #2 as open', FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name, 1) }}"
|
||||||
|
data-product-id="{{ $currentStockEntry->product_id }}"
|
||||||
|
data-product-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}"
|
||||||
|
data-product-qu-name="{{ FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name }}">
|
||||||
|
<i class="fas fa-box-open"></i> 1
|
||||||
|
</a>
|
||||||
<a class="btn btn-info btn-sm" href="{{ $U('/stockjournal?product=') }}{{ $currentStockEntry->product_id }}">
|
<a class="btn btn-info btn-sm" href="{{ $U('/stockjournal?product=') }}{{ $currentStockEntry->product_id }}">
|
||||||
<i class="fas fa-file-alt"></i>
|
<i class="fas fa-file-alt"></i>
|
||||||
</a>
|
</a>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user