mirror of
https://github.com/grocy/grocy.git
synced 2026-04-05 20:36:15 +02:00
Merge branch 'master' of https://github.com/grocy/grocy into grocy-448
This commit is contained in:
commit
146c7b6011
|
|
@ -1,10 +1,26 @@
|
|||
### New feature: Transfer products between locations and edit stock entries
|
||||
- New menu entry in the sidebar to transfer products (or as a shortcut in the more/context menu per line on the stock overview page)
|
||||
- New menu entry in the more/context menu of stock overview page lines to show the detail stock entries behind the corresponding product
|
||||
- From there you can also edit the stock entries
|
||||
- A huge THANK YOU goes to @kriddles for the work on this feature
|
||||
|
||||
## Recipe improvements
|
||||
- When adding or editing a recipe ingredient, a dialog is now used instead of switching between pages (thanks @kriddles)
|
||||
|
||||
### Meal plan fixes
|
||||
- Fixed that when `FEATURE_FLAG_STOCK_PRICE_TRACKING` was set to `false`, prices were still shown (thanks @kriddles)
|
||||
|
||||
### Calendar improvements
|
||||
- Improved that meal plan events in the iCal calendar export now contain a link to the appropriate meal plan week in the body of the event (thanks @kriddles)
|
||||
|
||||
### API improvements/fixes
|
||||
- Fixed that the route `/stock/barcodes/external-lookup/{barcode}` did not work, because the `barcode` argument was expected as a route argument but the route was missing it (thanks @Mikhail5555 and @beetle442002)
|
||||
- New endpoints for the stock transfer & stock entry edit capabilities mentioned above
|
||||
|
||||
### General & other improvements/fixes
|
||||
- Fixed that the meal plan menu entry (sidebar) was not visible when the calendar was disabled (`FEATURE_FLAG_CALENDAR`) (thanks @lwis)
|
||||
- Slightly optimized table loading & search performance (thanks @lwis)
|
||||
- For integration: If a `GET` parameter `closeAfterCreation` is passed to the product edit page, the window will be closed on save (due to Browser restrictions, this only works when the window was opened from JavaScript) (thanks @Forceu)
|
||||
- The `update.sh` file had wrong line endings (DOS instead of Unix)
|
||||
- New translations: (thanks all the translators)
|
||||
- Portuguese (demo available at https://pt.demo.grocy.info)
|
||||
- Portuguese (Brazil) (demo available at https://pt-br.demo.grocy.info)
|
||||
|
|
|
|||
|
|
@ -121,6 +121,108 @@ class StockApiController extends BaseApiController
|
|||
}
|
||||
}
|
||||
|
||||
public function EditStock(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$requestBody = $request->getParsedBody();
|
||||
|
||||
try
|
||||
{
|
||||
if ($requestBody === null)
|
||||
{
|
||||
throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)');
|
||||
}
|
||||
|
||||
if (!array_key_exists('stock_row_id', $requestBody))
|
||||
{
|
||||
throw new \Exception('A stock row id is required');
|
||||
}
|
||||
|
||||
if (!array_key_exists('amount', $requestBody))
|
||||
{
|
||||
throw new \Exception('An amount is required');
|
||||
}
|
||||
|
||||
$bestBeforeDate = null;
|
||||
if (array_key_exists('best_before_date', $requestBody) && IsIsoDate($requestBody['best_before_date']))
|
||||
{
|
||||
$bestBeforeDate = $requestBody['best_before_date'];
|
||||
}
|
||||
|
||||
$price = null;
|
||||
if (array_key_exists('price', $requestBody) && is_numeric($requestBody['price']))
|
||||
{
|
||||
$price = $requestBody['price'];
|
||||
}
|
||||
|
||||
$locationId = null;
|
||||
if (array_key_exists('location_id', $requestBody) && is_numeric($requestBody['location_id']))
|
||||
{
|
||||
$locationId = $requestBody['location_id'];
|
||||
}
|
||||
|
||||
$bookingId = $this->StockService->EditStock($requestBody['stock_row_id'], $requestBody['amount'], $bestBeforeDate, $locationId, $price);
|
||||
return $this->ApiResponse($this->Database->stock_log($bookingId));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function TransferProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$requestBody = $request->getParsedBody();
|
||||
|
||||
try
|
||||
{
|
||||
if ($requestBody === null)
|
||||
{
|
||||
throw new \Exception('Request body could not be parsed (probably invalid JSON format or missing/wrong Content-Type header)');
|
||||
}
|
||||
|
||||
if (!array_key_exists('amount', $requestBody))
|
||||
{
|
||||
throw new \Exception('An amount is required');
|
||||
}
|
||||
|
||||
if (!array_key_exists('location_id_from', $requestBody))
|
||||
{
|
||||
throw new \Exception('A transfer from location is required');
|
||||
}
|
||||
|
||||
if (!array_key_exists('location_id_to', $requestBody))
|
||||
{
|
||||
throw new \Exception('A transfer to location is required');
|
||||
}
|
||||
|
||||
$specificStockEntryId = 'default';
|
||||
if (array_key_exists('stock_entry_id', $requestBody) && !empty($requestBody['stock_entry_id']))
|
||||
{
|
||||
$specificStockEntryId = $requestBody['stock_entry_id'];
|
||||
}
|
||||
|
||||
$bookingId = $this->StockService->TransferProduct($args['productId'], $requestBody['amount'], $requestBody['location_id_from'], $requestBody['location_id_to'], $specificStockEntryId);
|
||||
return $this->ApiResponse($this->Database->stock_log($bookingId));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function TransferProductByBarcode(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
{
|
||||
$args['productId'] = $this->StockService->GetProductIdFromBarcode($args['barcode']);
|
||||
return $this->TransferProduct($request, $response, $args);
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function ConsumeProduct(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$requestBody = $request->getParsedBody();
|
||||
|
|
@ -157,14 +259,20 @@ class StockApiController extends BaseApiController
|
|||
$specificStockEntryId = $requestBody['stock_entry_id'];
|
||||
}
|
||||
|
||||
$locationId = null;
|
||||
if (array_key_exists('location_id', $requestBody) && !empty($requestBody['location_id']) && is_numeric($requestBody['location_id']))
|
||||
{
|
||||
$locationId = $requestBody['location_id'];
|
||||
}
|
||||
|
||||
$recipeId = null;
|
||||
if (array_key_exists('recipe_id', $requestBody) && is_numeric($requestBody['recipe_id']))
|
||||
{
|
||||
$recipeId = $requestBody['recipe_id'];
|
||||
}
|
||||
|
||||
$bookingId = $this->getStockService()->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId);
|
||||
$result = $this->ApiResponse($this->getDatabase()->stock_log($bookingId));
|
||||
$bookingId = $this->getStockService()->ConsumeProduct($args['productId'], $requestBody['amount'], $spoiled, $transactionType, $specificStockEntryId, $recipeId, $locationId);
|
||||
return $this->ApiResponse($this->getDatabase()->stock_log($bookingId));
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
|
|
@ -460,11 +568,29 @@ 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->getStockService()->GetProductStockEntries($args['productId']));
|
||||
}
|
||||
|
||||
public function ProductStockLocations(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->ApiResponse($this->StockService->GetProductStockLocations($args['productId']));
|
||||
}
|
||||
|
||||
public function StockBooking(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
try
|
||||
|
|
@ -483,4 +609,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,25 @@ class StockController extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
public function Detail(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$usersService = new UsersService();
|
||||
$nextXDays = $usersService->GetUserSettings(GROCY_USER_ID)['stock_expring_soon_days'];
|
||||
|
||||
return $this->AppContainer->view->render($response, 'stockdetail', [
|
||||
'products' => $this->Database->products()->orderBy('name'),
|
||||
'quantityunits' => $this->Database->quantity_units()->orderBy('name'),
|
||||
'locations' => $this->Database->locations()->orderBy('name'),
|
||||
'currentStockDetail' => $this->Database->stock()->orderBy('product_id'),
|
||||
'currentStockLocations' => $this->StockService->GetCurrentStockLocations(),
|
||||
'missingProducts' => $this->StockService->GetMissingProducts(),
|
||||
'nextXDays' => $nextXDays,
|
||||
'productGroups' => $this->Database->product_groups()->orderBy('name'),
|
||||
'userfields' => $this->UserfieldsService->GetFields('products'),
|
||||
'userfieldValues' => $this->UserfieldsService->GetAllValues('products')
|
||||
]);
|
||||
}
|
||||
|
||||
public function Purchase(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->renderPage($response, 'purchase', [
|
||||
|
|
@ -53,11 +72,20 @@ class StockController extends BaseController
|
|||
|
||||
public function Consume(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$result = $this->renderPage($response, 'consume', [
|
||||
return $this->renderPage($response, 'consume', [
|
||||
'products' => $this->getDatabase()->products()->orderBy('name'),
|
||||
'recipes' => $this->getDatabase()->recipes()->orderBy('name')
|
||||
'locations' => $this->getDatabase()->locations()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function Transfer(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'transfer', [
|
||||
'products' => $this->getDatabase()->products()->orderBy('name'),
|
||||
'recipes' => $this->getDatabase()->recipes()->orderBy('name'),
|
||||
'locations' => $this->getDatabase()->locations()->orderBy('name')
|
||||
]);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function Inventory(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
|
|
@ -68,6 +96,14 @@ class StockController extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
public function StockEdit(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
return $this->AppContainer->view->render($response, 'stockedit', [
|
||||
'products' => $this->Database->products()->orderBy('name'),
|
||||
'locations' => $this->Database->locations()->orderBy('name')
|
||||
]);
|
||||
}
|
||||
|
||||
public function ShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||
{
|
||||
$listId = 1;
|
||||
|
|
@ -277,6 +313,7 @@ class StockController extends BaseController
|
|||
{
|
||||
return $this->renderPage($response, 'stockjournal', [
|
||||
'stockLog' => $this->getDatabase()->stock_log()->orderBy('row_created_timestamp', 'DESC'),
|
||||
'locations' => $this->getDatabase()->locations()->orderBy('name'),
|
||||
'products' => $this->getDatabase()->products()->orderBy('name'),
|
||||
'quantityunits' => $this->getDatabase()->quantity_units()->orderBy('name')
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -1050,6 +1050,78 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"summary": "Edits the stock entry",
|
||||
"tags": [
|
||||
"Stock"
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stock_row_id": {
|
||||
"type": "number",
|
||||
"format": "number",
|
||||
"description": "The Stock Row Id"
|
||||
},
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"format": "number",
|
||||
"description": "The amount to add - please note that when tare weight handling for the product is enabled, this needs to be the amount including the container weight (gross), the amount to be posted will be automatically calculated based on what is in stock and the defined tare weight"
|
||||
},
|
||||
"best_before_date": {
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"description": "The best before date of the product to add, when omitted, the current date is used"
|
||||
},
|
||||
"location_id": {
|
||||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "If omitted, the default location of the product is used"
|
||||
},
|
||||
"price": {
|
||||
"type": "number",
|
||||
"format": "number",
|
||||
"description": "The price per purchase quantity unit in configured currency"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"stock_row_id": 2,
|
||||
"amount": 1,
|
||||
"best_before_date": "2019-01-19",
|
||||
"location_id": 2,
|
||||
"price": "1.99"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The operation was successful",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StockLogEntry"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The operation was not successful (possible errors are: Not existing product, invalid transaction type)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GenericErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stock/volatile": {
|
||||
|
|
@ -1128,6 +1200,50 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/{productId}/locations": {
|
||||
"get": {
|
||||
"summary": "Returns all locations where the given product currently has stock",
|
||||
"tags": [
|
||||
"Stock"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "productId",
|
||||
"required": true,
|
||||
"description": "A valid product id",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "An array of StockLocation objects",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/StockLocation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The operation was not successful (possible errors are: Not existing product)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GenericErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/{productId}/entries": {
|
||||
"get": {
|
||||
"summary": "Returns all stock entries of the given product in order of next use (first expiring first, then first in first out)",
|
||||
|
|
@ -1341,6 +1457,11 @@
|
|||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "A valid recipe id for which this product was used (for statistical purposes only)"
|
||||
},
|
||||
"location_id": {
|
||||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "A valid location id (if supplied, only stock at the given location is considered, if ommitted, stock of any location is considered)"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
|
|
@ -1376,6 +1497,82 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/{productId}/transfer": {
|
||||
"post": {
|
||||
"summary": "Transfers the given amount of the given product from one location to another (this is currently not supported for tare weight handling enabled products)",
|
||||
"tags": [
|
||||
"Stock"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "productId",
|
||||
"required": true,
|
||||
"description": "A valid product id",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"description": "The amount to transfer - please note that when tare weight handling for the product is enabled, this needs to be the amount including the container weight (gross), the amount to be posted will be automatically calculated based on what is in stock and the defined tare weight"
|
||||
},
|
||||
"location_id_from": {
|
||||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "A valid location id, the location from where the product should be transfered"
|
||||
},
|
||||
"location_id_to": {
|
||||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "A valid location id, the location to where the product should be transfered"
|
||||
},
|
||||
"stock_entry_id": {
|
||||
"type": "string",
|
||||
"description": "A specific stock entry id to transfer, if used, the amount has to be 1"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"amount": 1,
|
||||
"location_id_from": 1,
|
||||
"location_id_to": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The operation was successful",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StockLogEntry"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The operation was not successful (possible errors are: Not existing product, no existing from or to location, given amount > current stock amount at the source location)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GenericErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/{productId}/inventory": {
|
||||
"post": {
|
||||
"summary": "Inventories the given product (adds/removes based on the given new amount)",
|
||||
|
|
@ -1678,6 +1875,11 @@
|
|||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "A valid recipe id for which this product was used (for statistical purposes only)"
|
||||
},
|
||||
"location_id": {
|
||||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "A valid location id (if supplied, only stock at the given location is considered, if ommitted, stock of any location is considered)"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
|
|
@ -1713,6 +1915,82 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/by-barcode/{barcode}/transfer": {
|
||||
"post": {
|
||||
"summary": "Transfers the given amount of the by its barcode given product from one location to another (this is currently not supported for tare weight handling enabled products)",
|
||||
"tags": [
|
||||
"Stock"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "barcode",
|
||||
"required": true,
|
||||
"description": "Barcode",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"description": "The amount to transfer - please note that when tare weight handling for the product is enabled, this needs to be the amount including the container weight (gross), the amount to be posted will be automatically calculated based on what is in stock and the defined tare weight"
|
||||
},
|
||||
"location_id_from": {
|
||||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "A valid location id, the location from where the product should be transfered"
|
||||
},
|
||||
"location_id_to": {
|
||||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "A valid location id, the location to where the product should be transfered"
|
||||
},
|
||||
"stock_entry_id": {
|
||||
"type": "string",
|
||||
"description": "A specific stock entry id to transfer, if used, the amount has to be 1"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"amount": 1,
|
||||
"location_id_from": 1,
|
||||
"location_id_to": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The operation was successful",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StockLogEntry"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The operation was not successful (possible errors are: Not existing product, no existing from or to location, given amount > current stock amount at the source location)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GenericErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/stock/products/by-barcode/{barcode}/inventory": {
|
||||
"post": {
|
||||
"summary": "Inventories the by its barcode given product (adds/removes based on the given new amount)",
|
||||
|
|
@ -2119,7 +2397,85 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/stock/barcodes/external-lookup": {
|
||||
"/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",
|
||||
"tags": [
|
||||
|
|
@ -2127,7 +2483,7 @@
|
|||
],
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"in": "path",
|
||||
"name": "barcode",
|
||||
"required": true,
|
||||
"description": "The barcode to lookup up",
|
||||
|
|
@ -2847,6 +3203,7 @@
|
|||
"quantity_unit_conversions",
|
||||
"shopping_list",
|
||||
"shopping_lists",
|
||||
"stock",
|
||||
"recipes",
|
||||
"recipes_pos",
|
||||
"recipes_nestings",
|
||||
|
|
@ -3023,6 +3380,29 @@
|
|||
"row_created_timestamp": "2019-05-02 20:12:25"
|
||||
}
|
||||
},
|
||||
"StockLocation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"product_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"location_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"id": "1",
|
||||
"product_id": "3",
|
||||
"location_id": "1",
|
||||
"name": "Fridge"
|
||||
}
|
||||
},
|
||||
"StockEntry": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -325,5 +325,5 @@ msgstr ""
|
|||
msgid "not yet released"
|
||||
msgstr ""
|
||||
|
||||
msgid "Portuguese"
|
||||
msgid "Portuguese (Brazil)"
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ msgstr ""
|
|||
msgid "purchase"
|
||||
msgstr "Purchase"
|
||||
|
||||
msgid "transfer_to"
|
||||
msgstr "Transfer To"
|
||||
|
||||
msgid "transfer_from"
|
||||
msgstr "Transfer From"
|
||||
|
||||
msgid "consume"
|
||||
msgstr "Consume"
|
||||
|
||||
|
|
@ -23,3 +29,9 @@ msgstr "Inventory correction"
|
|||
|
||||
msgid "product-opened"
|
||||
msgstr "Product opened"
|
||||
|
||||
msgid "stock-edit-old"
|
||||
msgstr "Stock entry edited (old values)"
|
||||
|
||||
msgid "stock-edit-new"
|
||||
msgstr "Stock entry edited (new values)"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,12 @@ msgstr ""
|
|||
msgid "purchase"
|
||||
msgstr ""
|
||||
|
||||
msgid "transfer_from"
|
||||
msgstr ""
|
||||
|
||||
msgid "transfer_to"
|
||||
msgstr ""
|
||||
|
||||
msgid "consume"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -23,3 +29,9 @@ msgstr ""
|
|||
|
||||
msgid "product-opened"
|
||||
msgstr ""
|
||||
|
||||
msgid "stock-edit-old"
|
||||
msgstr ""
|
||||
|
||||
msgid "stock-edit-new"
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -929,9 +929,6 @@ msgstr ""
|
|||
msgid "Mark as opened"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expires on %1$s; Bought on %2$s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not opened"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -1567,3 +1564,45 @@ msgstr ""
|
|||
|
||||
msgid "This means the next execution of this chore should only be scheduled every %s years"
|
||||
msgstr ""
|
||||
|
||||
msgid "Transfer"
|
||||
msgstr ""
|
||||
|
||||
msgid "From location"
|
||||
msgstr ""
|
||||
|
||||
msgid "To location"
|
||||
msgstr ""
|
||||
|
||||
msgid "There are no units available at this location"
|
||||
msgstr ""
|
||||
|
||||
msgid "Amount: %1$s; Expires on %2$s; Bought on %3$s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Transfered %1$s of %2$s from %3$s to %4$s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show stock entries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stock entries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Best before date"
|
||||
msgstr ""
|
||||
|
||||
msgid "Purchased date"
|
||||
msgstr ""
|
||||
|
||||
msgid "Consume all %s for this stock entry"
|
||||
msgstr ""
|
||||
|
||||
msgid "The amount cannot be lower than %1$s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stock entry successfully updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit stock entry"
|
||||
msgstr ""
|
||||
|
|
|
|||
37
migrations/0095.sql
Normal file
37
migrations/0095.sql
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
CREATE TRIGGER set_products_default_location_if_empty_stock AFTER INSERT ON stock
|
||||
BEGIN
|
||||
UPDATE stock
|
||||
SET location_id = (SELECT location_id FROM products where id = product_id)
|
||||
WHERE id = NEW.id
|
||||
AND location_id IS NULL;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER set_products_default_location_if_empty_stock_log AFTER INSERT ON stock_log
|
||||
BEGIN
|
||||
UPDATE stock_log
|
||||
SET location_id = (SELECT location_id FROM products where id = product_id)
|
||||
WHERE id = NEW.id
|
||||
AND location_id IS NULL;
|
||||
END;
|
||||
|
||||
ALTER TABLE stock_log
|
||||
ADD correlation_id TEXT;
|
||||
|
||||
ALTER TABLE stock_log
|
||||
ADD transaction_id TEXT;
|
||||
|
||||
ALTER TABLE stock_log
|
||||
ADD stock_row_id INTEGER;
|
||||
|
||||
DROP VIEW stock_current_locations;
|
||||
CREATE VIEW stock_current_locations
|
||||
AS
|
||||
SELECT
|
||||
1 AS id, -- Dummy, LessQL needs an id column
|
||||
s.product_id,
|
||||
s.location_id AS location_id,
|
||||
l.name AS location_name
|
||||
FROM stock s
|
||||
JOIN locations l
|
||||
ON s.location_id = l.id
|
||||
GROUP BY s.product_id, s.location_id, l.name;
|
||||
|
|
@ -1,15 +1,30 @@
|
|||
$('#save-consume-button').on('click', function(e)
|
||||
$(document).ready(function() {
|
||||
|
||||
if (GetUriParam("embedded") !== undefined)
|
||||
{
|
||||
var locationId = GetUriParam('locationId');
|
||||
|
||||
if (typeof locationId === 'undefined')
|
||||
{
|
||||
Grocy.Components.ProductPicker.GetPicker().trigger('change');
|
||||
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||
} else {
|
||||
|
||||
$("#location_id").val(locationId);
|
||||
$("#location_id").trigger('change');
|
||||
$("#use_specific_stock_entry").click();
|
||||
$("#use_specific_stock_entry").trigger('change');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#save-consume-button').on('click', function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var jsonForm = $('#consume-form').serializeJSON();
|
||||
Grocy.FrontendHelpers.BeginUiBusy("consume-form");
|
||||
|
||||
if ($("#use_specific_stock_entry").is(":checked"))
|
||||
{
|
||||
jsonForm.amount = 1;
|
||||
}
|
||||
|
||||
var apiUrl = 'stock/products/' + jsonForm.product_id + '/consume';
|
||||
|
||||
var jsonData = {};
|
||||
|
|
@ -21,6 +36,15 @@
|
|||
jsonData.stock_entry_id = jsonForm.specific_stock_entry;
|
||||
}
|
||||
|
||||
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||
{
|
||||
jsonData.location_id = $("#location_id").val();
|
||||
}
|
||||
else
|
||||
{
|
||||
jsonData.location_id = 1;
|
||||
}
|
||||
|
||||
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES && Grocy.Components.RecipePicker.GetValue().toString().length > 0)
|
||||
{
|
||||
jsonData.recipe_id = Grocy.Components.RecipePicker.GetValue();
|
||||
|
|
@ -71,11 +95,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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse.transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
}
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse.transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
}
|
||||
|
||||
if (GetUriParam("embedded") !== undefined)
|
||||
|
|
@ -86,7 +110,6 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
|
||||
Grocy.FrontendHelpers.EndUiBusy("consume-form");
|
||||
toastr.success(successMessage);
|
||||
|
||||
|
|
@ -102,6 +125,10 @@
|
|||
{
|
||||
Grocy.Components.RecipePicker.Clear();
|
||||
}
|
||||
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||
{
|
||||
$("#location_id").find("option").remove().end().append("<option></option>");
|
||||
}
|
||||
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||
Grocy.FrontendHelpers.ValidateForm('consume-form');
|
||||
}
|
||||
|
|
@ -128,11 +155,6 @@ $('#save-mark-as-open-button').on('click', function(e)
|
|||
var jsonForm = $('#consume-form').serializeJSON();
|
||||
Grocy.FrontendHelpers.BeginUiBusy("consume-form");
|
||||
|
||||
if ($("#use_specific_stock_entry").is(":checked"))
|
||||
{
|
||||
jsonForm.amount = 1;
|
||||
}
|
||||
|
||||
var apiUrl = 'stock/products/' + jsonForm.product_id + '/open';
|
||||
|
||||
jsonData = { };
|
||||
|
|
@ -156,7 +178,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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + result.transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
|
||||
|
||||
$('#amount').val(Grocy.UserSettings.stock_default_consume_amount);
|
||||
Grocy.Components.ProductPicker.Clear();
|
||||
|
|
@ -178,6 +200,69 @@ $('#save-mark-as-open-button').on('click', function(e)
|
|||
);
|
||||
});
|
||||
|
||||
$("#location_id").on('change', function(e)
|
||||
{
|
||||
var locationId = $(e.target).val();
|
||||
var sumValue = 0;
|
||||
var stockId = null;
|
||||
|
||||
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||
if ($("#use_specific_stock_entry").is(":checked"))
|
||||
{
|
||||
$("#use_specific_stock_entry").click();
|
||||
}
|
||||
|
||||
if (GetUriParam("embedded") !== undefined)
|
||||
{
|
||||
stockId = GetUriParam('stockId');
|
||||
}
|
||||
|
||||
if (locationId)
|
||||
{
|
||||
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries',
|
||||
function(stockEntries)
|
||||
{
|
||||
stockEntries.forEach(stockEntry =>
|
||||
{
|
||||
var openTxt = __t("Not opened");
|
||||
if (stockEntry.open == 1)
|
||||
{
|
||||
openTxt = __t("Opened");
|
||||
}
|
||||
|
||||
if (stockEntry.location_id == locationId)
|
||||
{
|
||||
$("#specific_stock_entry").append($("<option>", {
|
||||
value: stockEntry.stock_id,
|
||||
amount: stockEntry.amount,
|
||||
text: __t("Amount: %1$s; Expires on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt
|
||||
}));
|
||||
sumValue = sumValue + parseFloat(stockEntry.amount);
|
||||
|
||||
if (stockEntry.stock_id == stockId)
|
||||
{
|
||||
$("#specific_stock_entry").val(stockId);
|
||||
}
|
||||
}
|
||||
});
|
||||
$("#amount").attr("max", sumValue);
|
||||
if (sumValue == 0)
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location'));
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', "1", sumValue));
|
||||
}
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||
{
|
||||
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||
|
|
@ -185,6 +270,7 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
|||
{
|
||||
$("#use_specific_stock_entry").click();
|
||||
}
|
||||
$("#location_id").val("");
|
||||
|
||||
var productId = $(e.target).val();
|
||||
|
||||
|
|
@ -195,9 +281,45 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
|||
Grocy.Api.Get('stock/products/' + productId,
|
||||
function(productDetails)
|
||||
{
|
||||
$('#amount').attr('max', productDetails.stock_amount);
|
||||
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
|
||||
|
||||
$("#location_id").find("option").remove().end().append("<option></option>");
|
||||
Grocy.Api.Get("stock/products/" + productId + '/locations',
|
||||
function(stockLocations)
|
||||
{
|
||||
var setDefault = 0;
|
||||
stockLocations.forEach(stockLocation =>
|
||||
{
|
||||
if (productDetails.location.id == stockLocation.location_id) {
|
||||
$("#location_id").append($("<option>", {
|
||||
value: stockLocation.location_id,
|
||||
text: stockLocation.location_name + " (" + __t("Default location") + ")"
|
||||
}));
|
||||
$("#location_id").val(productDetails.location.id);
|
||||
$("#location_id").trigger('change');
|
||||
setDefault = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#location_id").append($("<option>", {
|
||||
value: stockLocation.location_id,
|
||||
text: stockLocation.location_name
|
||||
}));
|
||||
}
|
||||
|
||||
if (setDefault == 0)
|
||||
{
|
||||
$("#location_id").val(stockLocation.location_id);
|
||||
$("#location_id").trigger('change');
|
||||
}
|
||||
});
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
|
||||
if (productDetails.product.allow_partial_units_in_stock == 1)
|
||||
{
|
||||
$("#amount").attr("min", "0.01");
|
||||
|
|
@ -251,44 +373,10 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
|||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
|
||||
Grocy.Api.Get("stock/products/" + productId + '/entries',
|
||||
function(stockEntries)
|
||||
{
|
||||
stockEntries.forEach(stockEntry =>
|
||||
{
|
||||
var openTxt = __t("Not opened");
|
||||
if (stockEntry.open == 1)
|
||||
{
|
||||
openTxt = __t("Opened");
|
||||
}
|
||||
|
||||
for (i = 0; i < stockEntry.amount; i++)
|
||||
{
|
||||
// Do this only for the first 50 entries to prevent a very long loop (is more anytime needed)?
|
||||
if (i > 50)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
$("#specific_stock_entry").append($("<option>", {
|
||||
value: stockEntry.stock_id,
|
||||
text: __t("Expires on %1$s; Bought on %2$s", moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt
|
||||
}));
|
||||
}
|
||||
});
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$('#amount').val(Grocy.UserSettings.stock_default_consume_amount);
|
||||
Grocy.Components.ProductPicker.GetPicker().trigger('change');
|
||||
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||
Grocy.FrontendHelpers.ValidateForm('consume-form');
|
||||
|
||||
$('#amount').on('focus', function(e)
|
||||
|
|
@ -323,23 +411,59 @@ $('#consume-form input').keydown(function(event)
|
|||
}
|
||||
});
|
||||
|
||||
$("#specific_stock_entry").on("change", function(e)
|
||||
{
|
||||
if ($(e.target).val() == "")
|
||||
{
|
||||
var sumValue = 0;
|
||||
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries',
|
||||
function(stockEntries)
|
||||
{
|
||||
stockEntries.forEach(stockEntry =>
|
||||
{
|
||||
if (stockEntry.location_id == $("#location_id").val() || stockEntry.location_id == "")
|
||||
{
|
||||
sumValue = sumValue + parseFloat(stockEntry.amount);
|
||||
}
|
||||
});
|
||||
$("#amount").attr("max", sumValue);
|
||||
if (sumValue == 0)
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location'));
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', "1", sumValue));
|
||||
}
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', "1", $('option:selected', this).attr('amount')));
|
||||
$("#amount").attr("max", $('option:selected', this).attr('amount'));
|
||||
}
|
||||
});
|
||||
|
||||
$("#use_specific_stock_entry").on("change", function()
|
||||
{
|
||||
var value = $(this).is(":checked");
|
||||
|
||||
if (value)
|
||||
{
|
||||
$("#specific_stock_entry").removeAttr("disabled");
|
||||
$("#amount").attr("disabled", "");
|
||||
$("#amount").val(1);
|
||||
$("#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");
|
||||
$("#specific_stock_entry").val("");
|
||||
$("#location_id").trigger('change');
|
||||
}
|
||||
|
||||
Grocy.FrontendHelpers.ValidateForm("consume-form");
|
||||
|
|
@ -358,3 +482,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);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
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)) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse.transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,7 +39,12 @@ var calendar = $("#calendar").fullCalendar({
|
|||
var weekRecipeConsumeButtonHtml = "";
|
||||
if (weekRecipe !== null)
|
||||
{
|
||||
weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs;
|
||||
var weekCostsHtml = "";
|
||||
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
||||
{
|
||||
weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs;
|
||||
weekCostsHtml = __t("Week costs") + ': <span class="locale-number locale-number-currency">' + weekCosts.toString() + "</span> ";
|
||||
}
|
||||
|
||||
var weekRecipeOrderMissingButtonDisabledClasses = "";
|
||||
if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled_with_shopping_list == 1)
|
||||
|
|
@ -54,7 +59,7 @@ var calendar = $("#calendar").fullCalendar({
|
|||
weekRecipeOrderMissingButtonHtml = '<a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + weekRecipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fas fa-cart-plus"></i></a>'
|
||||
weekRecipeConsumeButtonHtml = '<a class="ml-1 btn btn-outline-success btn-xs recipe-consume-button ' + weekRecipeConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this recipe") + '" data-recipe-id="' + weekRecipe.id.toString() + '" data-recipe-name="' + weekRecipe.name + '" data-recipe-type="' + weekRecipe.type + '"><i class="fas fa-utensils"></i></a>'
|
||||
}
|
||||
$(".fc-header-toolbar .fc-center").html("<h4>" + __t("Week costs") + ': <span class="locale-number locale-number-currency">' + weekCosts.toString() + "</span> " + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "</h4>");
|
||||
$(".fc-header-toolbar .fc-center").html("<h4>" + weekCostsHtml + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "</h4>");
|
||||
},
|
||||
"eventRender": function(event, element)
|
||||
{
|
||||
|
|
@ -92,13 +97,18 @@ var calendar = $("#calendar").fullCalendar({
|
|||
fulfillmentInfoHtml = __t('Not enough in stock');
|
||||
var fulfillmentIconHtml = '<i class="fas fa-times text-danger"></i>';
|
||||
}
|
||||
var costPerServing = ""
|
||||
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
||||
{
|
||||
costPerServing = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + resolvedRecipe.costs + '</span> ' + __t('per serving') + '<h5>';
|
||||
}
|
||||
|
||||
element.html(' \
|
||||
<div> \
|
||||
<h5 class="text-truncate">' + recipe.name + '<h5> \
|
||||
<h5 class="small text-truncate">' + __n(mealPlanEntry.servings, "%s serving", "%s servings") + '</h5> \
|
||||
<h5 class="small timeago-contextual text-truncate">' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '</h5> \
|
||||
<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + resolvedRecipe.costs + '</span> ' + __t('per serving') + '<h5> \
|
||||
' + costPerServing + ' \
|
||||
<h5> \
|
||||
<a class="ml-1 btn btn-outline-danger btn-xs remove-recipe-button" href="#"><i class="fas fa-trash"></i></a> \
|
||||
<a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + recipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + recipe.id.toString() + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-cart-plus"></i></a> \
|
||||
|
|
|
|||
|
|
@ -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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + result.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + result.transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -192,20 +192,29 @@ $(document).on('click', '.recipe-pos-show-note-button', function(e)
|
|||
bootbox.alert(note);
|
||||
});
|
||||
|
||||
$(document).on('click', '.recipe-pos-edit-button', function (e)
|
||||
$(document).on('click', '.recipe-pos-edit-button', function(e)
|
||||
{
|
||||
var recipePosId = $(e.currentTarget).attr('data-recipe-pos-id');
|
||||
e.preventDefault();
|
||||
|
||||
Grocy.Api.Put('objects/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(),
|
||||
function(result)
|
||||
{
|
||||
window.location.href = U('/recipe/' + Grocy.EditObjectId + '/pos/' + recipePosId);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
var productId = $(e.currentTarget).attr("data-product-id");
|
||||
var recipePosId = $(e.currentTarget).attr('data-recipe-pos-id');
|
||||
|
||||
bootbox.dialog({
|
||||
message: '<iframe height="650px" class="embed-responsive" src="' + U("/recipe/") + Grocy.EditObjectId.toString() + '/pos/' + recipePosId.toString() + '?embedded&product=' + productId.toString() + '"></iframe>',
|
||||
size: 'large',
|
||||
backdrop: true,
|
||||
closeButton: false,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: __t('Cancel'),
|
||||
className: 'btn-secondary responsive-button',
|
||||
callback: function()
|
||||
{
|
||||
bootbox.hideAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('click', '.recipe-include-edit-button', function (e)
|
||||
|
|
@ -234,16 +243,24 @@ $(document).on('click', '.recipe-include-edit-button', function (e)
|
|||
|
||||
$("#recipe-pos-add-button").on("click", function(e)
|
||||
{
|
||||
Grocy.Api.Put('objects/recipes/' + Grocy.EditObjectId, $('#recipe-form').serializeJSON(),
|
||||
function(result)
|
||||
{
|
||||
window.location.href = U('/recipe/' + Grocy.EditObjectId + '/pos/new');
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
e.preventDefault();
|
||||
|
||||
bootbox.dialog({
|
||||
message: '<iframe height="650px" class="embed-responsive" src="' + U("/recipe/") + Grocy.EditObjectId + '/pos/new?embedded"></iframe>',
|
||||
size: 'large',
|
||||
backdrop: true,
|
||||
closeButton: false,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: __t('Cancel'),
|
||||
className: 'btn-secondary responsive-button',
|
||||
callback: function()
|
||||
{
|
||||
bootbox.hideAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
$("#recipe-include-add-button").on("click", function(e)
|
||||
|
|
@ -319,3 +336,14 @@ $('#delete-current-recipe-picture-button').on('click', function (e)
|
|||
});
|
||||
|
||||
Grocy.Components.UserfieldsForm.Load();
|
||||
|
||||
$(window).on("message", function(e)
|
||||
{
|
||||
var data = e.originalEvent.data;
|
||||
|
||||
if (data.Message === "IngredientsChanged")
|
||||
{
|
||||
window.location.href = U('/recipe/' + Grocy.EditObjectId);
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ $('#save-recipe-pos-button').on('click', function (e)
|
|||
Grocy.Api.Post('objects/recipes_pos', jsonData,
|
||||
function(result)
|
||||
{
|
||||
window.location.href = U('/recipe/' + Grocy.EditObjectParentId);
|
||||
window.parent.postMessage(WindowMessageBag("IngredientsChanged"), Grocy.BaseUrl);
|
||||
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
|
|
@ -29,7 +30,8 @@ $('#save-recipe-pos-button').on('click', function (e)
|
|||
Grocy.Api.Put('objects/recipes_pos/' + Grocy.EditObjectId, jsonData,
|
||||
function(result)
|
||||
{
|
||||
window.location.href = U('/recipe/' + Grocy.EditObjectParentId);
|
||||
window.parent.postMessage(WindowMessageBag("IngredientsChanged"), Grocy.BaseUrl);
|
||||
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
|
|
@ -152,5 +154,5 @@ $("#only_check_single_unit_in_stock").on("click", function()
|
|||
});
|
||||
|
||||
// Click twice to trigger on-click but not change the actual checked state
|
||||
$("#only_check_single_unit_in_stock").click();
|
||||
$("#only_check_single_unit_in_stock").click();
|
||||
//$("#only_check_single_unit_in_stock").click();
|
||||
//$("#only_check_single_unit_in_stock").click();
|
||||
|
|
|
|||
293
public/viewjs/stockdetail.js
Normal file
293
public/viewjs/stockdetail.js
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
var stockDetailTable = $('#stock-detail-table').DataTable({
|
||||
'order': [[2, 'asc']],
|
||||
'columnDefs': [
|
||||
{ 'orderable': false, 'targets': 0 },
|
||||
],
|
||||
});
|
||||
$('#stock-detail-table tbody').removeClass("d-none");
|
||||
|
||||
function bootBoxModal(message) {
|
||||
bootbox.dialog({
|
||||
message: message,
|
||||
size: 'large',
|
||||
backdrop: true,
|
||||
closeButton: false,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: __t('Cancel'),
|
||||
className: 'btn-secondary responsive-button',
|
||||
callback: function()
|
||||
{
|
||||
bootbox.hideAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
$.fn.dataTable.ext.search.push(
|
||||
function( settings, data, dataIndex ) {
|
||||
var productId = Grocy.Components.ProductPicker.GetValue();
|
||||
|
||||
if ( ( isNaN( productId ) ||
|
||||
productId == "" ||
|
||||
//assume productId is in the first column
|
||||
productId == data[1] ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
$(document).ready(function() {
|
||||
Grocy.Components.ProductPicker.GetPicker().trigger('change');
|
||||
} );
|
||||
|
||||
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||
{
|
||||
stockDetailTable.draw();
|
||||
});
|
||||
|
||||
$(document).on('click', '.stock-consume-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();
|
||||
|
||||
Grocy.FrontendHelpers.BeginUiBusy();
|
||||
|
||||
var productId = $(e.currentTarget).attr('data-product-id');
|
||||
var locationId = $(e.currentTarget).attr('data-location-id');
|
||||
var specificStockEntryId = $(e.currentTarget).attr('data-stock-id');
|
||||
var stockRowId = $(e.currentTarget).attr('data-stockrow-id');
|
||||
var consumeAmount = $(e.currentTarget).attr('data-consume-amount');
|
||||
|
||||
var wasSpoiled = $(e.currentTarget).hasClass("product-consume-button-spoiled");
|
||||
|
||||
Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount, 'spoiled': wasSpoiled, 'location_id': locationId, 'stock_entry_id': specificStockEntryId},
|
||||
function(bookingResponse)
|
||||
{
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
if (wasSpoiled)
|
||||
{
|
||||
toastMessage += " (" + __t("Spoiled") + ")";
|
||||
}
|
||||
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
toastr.success(toastMessage);
|
||||
RefreshStockDetailRow(stockRowId);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(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();
|
||||
|
||||
Grocy.FrontendHelpers.BeginUiBusy();
|
||||
|
||||
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');
|
||||
var button = $(e.currentTarget);
|
||||
|
||||
Grocy.Api.Post('stock/products/' + productId + '/open', { 'amount': 1 },
|
||||
function(bookingResponse)
|
||||
{
|
||||
Grocy.Api.Get('stock/products/' + productId,
|
||||
function(result)
|
||||
{
|
||||
if (result.stock_amount == result.stock_amount_opened)
|
||||
{
|
||||
button.addClass("disabled");
|
||||
}
|
||||
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
|
||||
RefreshProductRow(productId);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$(document).on("click", ".stock-name-cell", function(e)
|
||||
{
|
||||
Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr("data-stock-id"));
|
||||
$("#stockdetail-productcard-modal").modal("show");
|
||||
});
|
||||
|
||||
$(document).on("click", ".product-purchase-button", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var productId = $(e.currentTarget).attr("data-product-id");
|
||||
|
||||
bootBoxModal('<iframe height="650px" class="embed-responsive" src="' + U("/purchase?embedded&product=") + productId.toString() + '"></iframe>');
|
||||
});
|
||||
|
||||
$(document).on("click", ".product-transfer-button", function(e)
|
||||
{
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
var productId = $(e.currentTarget).attr("data-product-id");
|
||||
var locationId = $(e.currentTarget).attr('data-location-id');
|
||||
var specificStockEntryId = $(e.currentTarget).attr('data-stock-id');
|
||||
bootBoxModal('<iframe height="650px" class="embed-responsive" src="' + U("/transfer?embedded&product=") + productId.toString() + '&locationId=' + locationId.toString() + '&stockId=' + specificStockEntryId.toString() + '"></iframe>');
|
||||
|
||||
});
|
||||
|
||||
$(document).on("click", ".product-consume-custom-amount-button", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var productId = $(e.currentTarget).attr("data-product-id");
|
||||
var locationId = $(e.currentTarget).attr('data-location-id');
|
||||
var specificStockEntryId = $(e.currentTarget).attr('data-stock-id');
|
||||
|
||||
bootBoxModal('<iframe height="650px" class="embed-responsive" src="' + U("/consume?embedded&product=") + productId.toString() + '&locationId=' + locationId.toString() + '&stockId=' + specificStockEntryId.toString() + '"></iframe>');
|
||||
|
||||
});
|
||||
|
||||
$(document).on("click", ".product-inventory-button", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var productId = $(e.currentTarget).attr("data-product-id");
|
||||
|
||||
bootBoxModal('<iframe height="650px" class="embed-responsive" src="' + U("/inventory?embedded&product=") + productId.toString() + '"></iframe>');
|
||||
});
|
||||
|
||||
$(document).on("click", ".product-stockedit-button", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var productId = $(e.currentTarget).attr("data-product-id");
|
||||
var stockRowId = $(e.currentTarget).attr("data-id");
|
||||
|
||||
bootBoxModal('<iframe height="650px" class="embed-responsive" src="' + U("/stockedit?embedded&product=") + productId.toString() + '&stockRowId=' + stockRowId.toString() + '"></iframe>');
|
||||
});
|
||||
|
||||
$(document).on("click", ".product-add-to-shopping-list-button", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var productId = $(e.currentTarget).attr("data-product-id");
|
||||
|
||||
bootBoxModal('<iframe height="650px" class="embed-responsive" src="' + U("/shoppinglistitem/new?embedded&updateexistingproduct&product=") + productId.toString() + '"></iframe>');
|
||||
});
|
||||
|
||||
function RefreshStockDetailRow(stockRowId)
|
||||
{
|
||||
Grocy.Api.Get("objects/stock/" + stockRowId,
|
||||
function(result)
|
||||
{
|
||||
var stockRow = $('#stock-' + stockRowId + '-row');
|
||||
var now = moment();
|
||||
|
||||
stockRow.removeClass("table-warning");
|
||||
stockRow.removeClass("table-danger");
|
||||
stockRow.removeClass("table-info");
|
||||
stockRow.removeClass("d-none");
|
||||
stockRow.removeAttr("style");
|
||||
|
||||
if (result == null || result.amount == 0)
|
||||
{
|
||||
stockRow.fadeOut(500, function()
|
||||
{
|
||||
//$(this).tooltip("hide");
|
||||
$(this).addClass("d-none");
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
$('#stock-' + stockRowId + '-amount').parent().effect('highlight', { }, 500);
|
||||
$('#stock-' + stockRowId + '-amount').fadeOut(500, function ()
|
||||
{
|
||||
$(this).text(result.amount).fadeIn(500);
|
||||
});
|
||||
|
||||
$('#stock-' + stockRowId + '-best-before-date').parent().effect('highlight', { }, 500);
|
||||
$('#stock-' + stockRowId + '-best-before-date').fadeOut(500, function()
|
||||
{
|
||||
$(this).text(result.best_before_date).fadeIn(500);
|
||||
});
|
||||
|
||||
$('#stock-' + stockRowId + '-location').parent().effect('highlight', { }, 500);
|
||||
$('#stock-' + stockRowId + '-location').fadeOut(500, function()
|
||||
{
|
||||
//TODO grab location name instead of id
|
||||
$(this).text(result.location_id).fadeIn(500);
|
||||
});
|
||||
|
||||
$('#stock-' + stockRowId + '-price').parent().effect('highlight', { }, 500);
|
||||
$('#stock-' + stockRowId + '-price').fadeOut(500, function()
|
||||
{
|
||||
$(this).text(result.price).fadeIn(500);
|
||||
});
|
||||
|
||||
$('#stock-' + stockRowId + '-purchased-date').parent().effect('highlight', { }, 500);
|
||||
$('#stock-' + stockRowId + '-purchased-date').fadeOut(500, function()
|
||||
{
|
||||
$(this).text(result.purchased_date).fadeIn(500);
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(function()
|
||||
{
|
||||
RefreshContextualTimeago();
|
||||
RefreshLocaleNumberDisplay();
|
||||
}, 600);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy();
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$(window).on("message", function(e)
|
||||
{
|
||||
var data = e.originalEvent.data;
|
||||
|
||||
if (data.Message === "StockDetailChanged")
|
||||
{
|
||||
RefreshStockDetailRow(data.Payload);
|
||||
}
|
||||
});
|
||||
151
public/viewjs/stockedit.js
Normal file
151
public/viewjs/stockedit.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
$(document).ready(function() {
|
||||
var stockRowId = GetUriParam('stockRowId');
|
||||
Grocy.Api.Get("objects/stock/" + stockRowId,
|
||||
function(stockEntry)
|
||||
{
|
||||
Grocy.Components.LocationPicker.SetId(stockEntry.location_id);
|
||||
$('#amount').val(stockEntry.amount);
|
||||
$('#price').val(stockEntry.price);
|
||||
Grocy.Components.DateTimePicker.SetValue(stockEntry.best_before_date);
|
||||
|
||||
Grocy.Api.Get('stock/products/' + stockEntry.product_id,
|
||||
function(productDetails)
|
||||
{
|
||||
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
|
||||
|
||||
if (productDetails.product.allow_partial_units_in_stock == 1)
|
||||
{
|
||||
$("#amount").attr("min", "0.01");
|
||||
$("#amount").attr("step", "0.01");
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %1$s', 0.01.toLocaleString()));
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#amount").attr("min", "1");
|
||||
$("#amount").attr("step", "1");
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %1$s', '1'));
|
||||
}
|
||||
|
||||
if (productDetails.product.enable_tare_weight_handling == 1)
|
||||
{
|
||||
$("#amount").attr("min", productDetails.product.tare_weight);
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %1$s', parseFloat(productDetails.product.tare_weight).toLocaleString({ minimumFractionDigits: 0, maximumFractionDigits: 2 })));
|
||||
$("#tare-weight-handling-info").removeClass("d-none");
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#tare-weight-handling-info").addClass("d-none");
|
||||
}
|
||||
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
} );
|
||||
|
||||
$('#save-stockedit-button').on('click', function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var jsonForm = $('#stockedit-form').serializeJSON();
|
||||
Grocy.FrontendHelpers.BeginUiBusy("stockedit-form");
|
||||
|
||||
if (!jsonForm.price.toString().isEmpty())
|
||||
{
|
||||
price = parseFloat(jsonForm.price).toFixed(2);
|
||||
}
|
||||
|
||||
var jsonData = { };
|
||||
jsonData.amount = jsonForm.amount;
|
||||
jsonData.best_before_date = Grocy.Components.DateTimePicker.GetValue();
|
||||
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||
{
|
||||
jsonData.location_id = Grocy.Components.LocationPicker.GetValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
jsonData.location_id = 1;
|
||||
}
|
||||
jsonData.price = price;
|
||||
|
||||
var bookingResponse = null;
|
||||
|
||||
var stockRowId = GetUriParam('stockRowId');
|
||||
jsonData.stock_row_id = stockRowId;
|
||||
|
||||
Grocy.Api.Put("stock", jsonData,
|
||||
function(result)
|
||||
{
|
||||
var successMessage = __t('Stock entry successfully updated') + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(\'' + result.id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
|
||||
window.parent.postMessage(WindowMessageBag("StockDetailChanged", stockRowId), Grocy.BaseUrl);
|
||||
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl);
|
||||
window.parent.postMessage(WindowMessageBag("Ready"), Grocy.BaseUrl);
|
||||
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy("stockedit-form");
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Grocy.FrontendHelpers.ValidateForm('stockedit-form');
|
||||
|
||||
$('#stockedit-form input').keyup(function (event)
|
||||
{
|
||||
Grocy.FrontendHelpers.ValidateForm('stockedit-form');
|
||||
});
|
||||
|
||||
$('#stockedit-form input').keydown(function(event)
|
||||
{
|
||||
if (event.keyCode === 13) //Enter
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
if (document.getElementById('stockedit-form').checkValidity() === false) //There is at least one validation error
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$('#save-stockedit-button').click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Grocy.Components.DateTimePicker)
|
||||
{
|
||||
Grocy.Components.DateTimePicker.GetInputElement().on('change', function(e)
|
||||
{
|
||||
Grocy.FrontendHelpers.ValidateForm('stockedit-form');
|
||||
});
|
||||
|
||||
Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e)
|
||||
{
|
||||
Grocy.FrontendHelpers.ValidateForm('stockedit-form');
|
||||
});
|
||||
}
|
||||
|
||||
function UndoStockBooking(bookingId)
|
||||
{
|
||||
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', { },
|
||||
function(result)
|
||||
{
|
||||
toastr.success(__t("Booking successfully undone"));
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse.transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockBooking(' + bookingResponse.id + ')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
|
||||
toastr.success(__t('Marked %1$s of %2$s as opened', 1 + " " + productQuName, productName) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse.transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>');
|
||||
RefreshStatistics();
|
||||
RefreshProductRow(productId);
|
||||
},
|
||||
|
|
@ -219,6 +219,30 @@ $(document).on("click", ".product-purchase-button", function(e)
|
|||
});
|
||||
});
|
||||
|
||||
$(document).on("click", ".product-transfer-button", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var productId = $(e.currentTarget).attr("data-product-id");
|
||||
|
||||
bootbox.dialog({
|
||||
message: '<iframe height="650px" class="embed-responsive" src="' + U("/transfer?embedded&product=") + productId.toString() + '"></iframe>',
|
||||
size: 'large',
|
||||
backdrop: true,
|
||||
closeButton: false,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: __t('Cancel'),
|
||||
className: 'btn-secondary responsive-button',
|
||||
callback: function()
|
||||
{
|
||||
bootbox.hideAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on("click", ".product-consume-custom-amount-button", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
|
|
|||
441
public/viewjs/transfer.js
Normal file
441
public/viewjs/transfer.js
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
$(document).ready(function() {
|
||||
if (GetUriParam("embedded") !== undefined)
|
||||
{
|
||||
var locationId = GetUriParam('locationId');
|
||||
|
||||
if (typeof locationId === 'undefined')
|
||||
{
|
||||
Grocy.Components.ProductPicker.GetPicker().trigger('change');
|
||||
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||
} else {
|
||||
|
||||
$("#location_id_from").val(locationId);
|
||||
$("#location_id_from").trigger('change');
|
||||
$("#use_specific_stock_entry").click();
|
||||
$("#use_specific_stock_entry").trigger('change');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#save-transfer-button').on('click', function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var jsonForm = $('#transfer-form').serializeJSON();
|
||||
Grocy.FrontendHelpers.BeginUiBusy("transfer-form");
|
||||
|
||||
var apiUrl = 'stock/products/' + jsonForm.product_id + '/transfer';
|
||||
|
||||
var jsonData = {};
|
||||
jsonData.amount = jsonForm.amount;
|
||||
jsonData.location_id_to = $("#location_id_to").val();
|
||||
jsonData.location_id_from = $("#location_id_from").val();
|
||||
|
||||
if ($("#use_specific_stock_entry").is(":checked"))
|
||||
{
|
||||
jsonData.stock_entry_id = jsonForm.specific_stock_entry;
|
||||
}
|
||||
|
||||
var bookingResponse = null;
|
||||
|
||||
Grocy.Api.Get('stock/products/' + jsonForm.product_id,
|
||||
function(productDetails)
|
||||
{
|
||||
Grocy.Api.Post(apiUrl, jsonData,
|
||||
function(result)
|
||||
{
|
||||
var addBarcode = GetUriParam('addbarcodetoselection');
|
||||
bookingResponse = result;
|
||||
|
||||
if (addBarcode !== undefined)
|
||||
{
|
||||
var existingBarcodes = productDetails.product.barcode || '';
|
||||
if (existingBarcodes.length === 0)
|
||||
{
|
||||
productDetails.product.barcode = addBarcode;
|
||||
}
|
||||
else
|
||||
{
|
||||
productDetails.product.barcode += ',' + addBarcode;
|
||||
}
|
||||
|
||||
Grocy.Api.Put('objects/products/' + productDetails.product.id, productDetails.product,
|
||||
function(result)
|
||||
{
|
||||
$("#flow-info-addbarcodetoselection").addClass("d-none");
|
||||
$('#barcode-lookup-disabled-hint').addClass('d-none');
|
||||
window.history.replaceState({ }, document.title, U("/transfer"));
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (productDetails.product.enable_tare_weight_handling == 1)
|
||||
{
|
||||
var successMessage = __t('Transfered %1$s of %2$s from %3$s to %4$s', 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,$('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse.transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
}
|
||||
else
|
||||
{
|
||||
var successMessage =__t('Transfered %1$s of %2$s from %3$s to %4$s', Math.abs(jsonForm.amount) + " " + __n(jsonForm.amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name, $('option:selected', "#location_id_from").text(), $('option:selected', "#location_id_to").text()) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + bookingResponse.transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||
}
|
||||
|
||||
if (GetUriParam("embedded") !== undefined)
|
||||
{
|
||||
window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl);
|
||||
window.parent.postMessage(WindowMessageBag("ShowSuccessMessage", successMessage), Grocy.BaseUrl);
|
||||
window.parent.postMessage(WindowMessageBag("CloseAllModals"), Grocy.BaseUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Grocy.FrontendHelpers.EndUiBusy("transfer-form");
|
||||
toastr.success(successMessage);
|
||||
|
||||
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||
$("#specific_stock_entry").attr("disabled", "");
|
||||
$("#specific_stock_entry").removeAttr("required");
|
||||
if ($("#use_specific_stock_entry").is(":checked"))
|
||||
{
|
||||
$("#use_specific_stock_entry").click();
|
||||
}
|
||||
|
||||
$("#location_id_from").find("option").remove().end().append("<option></option>");
|
||||
$("#amount").attr("min", "1");
|
||||
$("#amount").attr("max", "999999");
|
||||
$("#amount").attr("step", "1");
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount cannot be lower than %s', '1'));
|
||||
$('#amount').val(Grocy.UserSettings.stock_default_transfer_amount);
|
||||
$('#amount_qu_unit').text("");
|
||||
$("#tare-weight-handling-info").addClass("d-none");
|
||||
Grocy.Components.ProductPicker.Clear();
|
||||
$("#location_id_to").val("");
|
||||
$("#location_id_from").val("");
|
||||
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||
Grocy.FrontendHelpers.ValidateForm('transfer-form');
|
||||
}
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy("transfer-form");
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
Grocy.FrontendHelpers.EndUiBusy("transfer-form");
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
$("#location_id_to").val("");
|
||||
$("#location_id_from").val("");
|
||||
|
||||
var productId = $(e.target).val();
|
||||
|
||||
if (productId)
|
||||
{
|
||||
Grocy.Components.ProductCard.Refresh(productId);
|
||||
|
||||
Grocy.Api.Get('stock/products/' + productId,
|
||||
function(productDetails)
|
||||
{
|
||||
if (productDetails.product.enable_tare_weight_handling == 1) {
|
||||
Grocy.Components.ProductPicker.GetPicker().parent().find(".invalid-feedback").text(__t('Products with Tare weight enabled are currently not supported for Transfer. Please select another product.'));
|
||||
Grocy.Components.ProductPicker.Clear();
|
||||
return;
|
||||
}
|
||||
$('#amount_qu_unit').text(productDetails.quantity_unit_stock.name);
|
||||
|
||||
$("#location_id_from").find("option").remove().end().append("<option></option>");
|
||||
Grocy.Api.Get("stock/products/" + productId + '/locations',
|
||||
function(stockLocations)
|
||||
{
|
||||
var setDefault = 0;
|
||||
stockLocations.forEach(stockLocation =>
|
||||
{
|
||||
if (productDetails.location.id == stockLocation.location_id)
|
||||
{
|
||||
$("#location_id_from").append($("<option>", {
|
||||
value: stockLocation.location_id,
|
||||
text: stockLocation.location_name + " (" + __t("Default location") + ")"
|
||||
}));
|
||||
$("#location_id_from").val(productDetails.location.id);
|
||||
$("#location_id_from").trigger('change');
|
||||
setDefault = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#location_id_from").append($("<option>", {
|
||||
value: stockLocation.location_id,
|
||||
text: stockLocation.location_name
|
||||
}));
|
||||
}
|
||||
|
||||
if (setDefault == 0)
|
||||
{
|
||||
$("#location_id_from").val(stockLocation.location_id);
|
||||
$("#location_id_from").trigger('change');
|
||||
}
|
||||
});
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
|
||||
if (productDetails.product.allow_partial_units_in_stock == 1)
|
||||
{
|
||||
$("#amount").attr("min", "0.01");
|
||||
$("#amount").attr("step", "0.01");
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', 0.01.toLocaleString(), parseFloat(productDetails.stock_amount).toLocaleString()));
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#amount").attr("min", "1");
|
||||
$("#amount").attr("step", "1");
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', "1", parseFloat(productDetails.stock_amount).toLocaleString()));
|
||||
}
|
||||
|
||||
if (productDetails.product.enable_tare_weight_handling == 1)
|
||||
{
|
||||
$("#amount").attr("min", productDetails.product.tare_weight);
|
||||
$("#tare-weight-handling-info").removeClass("d-none");
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#tare-weight-handling-info").addClass("d-none");
|
||||
}
|
||||
|
||||
if ((parseFloat(productDetails.stock_amount) || 0) === 0)
|
||||
{
|
||||
Grocy.Components.ProductPicker.Clear();
|
||||
Grocy.FrontendHelpers.ValidateForm('transfer-form');
|
||||
Grocy.Components.ProductPicker.ShowCustomError(__t('This product is not in stock'));
|
||||
Grocy.Components.ProductPicker.GetInputElement().focus();
|
||||
}
|
||||
else
|
||||
{
|
||||
Grocy.Components.ProductPicker.HideCustomError();
|
||||
Grocy.FrontendHelpers.ValidateForm('transfer-form');
|
||||
$('#amount').focus();
|
||||
}
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$('#amount').val(Grocy.UserSettings.stock_default_transfer_amount);
|
||||
Grocy.FrontendHelpers.ValidateForm('transfer-form');
|
||||
|
||||
$("#location_id_from").on('change', function(e)
|
||||
{
|
||||
var locationId = $(e.target).val();
|
||||
var sumValue = 0;
|
||||
var stockId = null;
|
||||
|
||||
if (locationId == $("#location_id_to").val())
|
||||
{
|
||||
$("#location_id_to").val("");
|
||||
}
|
||||
|
||||
if (GetUriParam("embedded") !== undefined)
|
||||
{
|
||||
stockId = GetUriParam('stockId');
|
||||
}
|
||||
|
||||
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||
if ($("#use_specific_stock_entry").is(":checked"))
|
||||
{
|
||||
$("#use_specific_stock_entry").click();
|
||||
}
|
||||
|
||||
if (locationId)
|
||||
{
|
||||
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries',
|
||||
function(stockEntries)
|
||||
{
|
||||
stockEntries.forEach(stockEntry =>
|
||||
{
|
||||
var openTxt = __t("Not opened");
|
||||
if (stockEntry.open == 1)
|
||||
{
|
||||
openTxt = __t("Opened");
|
||||
}
|
||||
|
||||
if (stockEntry.location_id == locationId)
|
||||
{
|
||||
$("#specific_stock_entry").append($("<option>", {
|
||||
value: stockEntry.stock_id,
|
||||
amount: stockEntry.amount,
|
||||
text: __t("Amount: %1$s; Expires on %2$s; Bought on %3$s", stockEntry.amount, moment(stockEntry.best_before_date).format("YYYY-MM-DD"), moment(stockEntry.purchased_date).format("YYYY-MM-DD")) + "; " + openTxt
|
||||
}));
|
||||
if (stockEntry.stock_id == stockId)
|
||||
{
|
||||
$("#specific_stock_entry").val(stockId);
|
||||
}
|
||||
sumValue = sumValue + parseFloat(stockEntry.amount);
|
||||
}
|
||||
});
|
||||
$("#amount").attr("max", sumValue);
|
||||
if (sumValue == 0)
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location'));
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', "1", sumValue));
|
||||
}
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$("#location_id_to").on('change', function(e)
|
||||
{
|
||||
var locationId = $(e.target).val();
|
||||
|
||||
if (locationId == $("#location_id_from").val())
|
||||
{
|
||||
$("#location_id_to").parent().find(".invalid-feedback").text(__t('This cannot be the same as the "From" location'));
|
||||
$("#location_id_to").val("");
|
||||
}
|
||||
});
|
||||
|
||||
$('#amount').on('focus', function(e)
|
||||
{
|
||||
$(this).select();
|
||||
});
|
||||
|
||||
$('#transfer-form input').keyup(function(event)
|
||||
{
|
||||
Grocy.FrontendHelpers.ValidateForm('transfer-form');
|
||||
});
|
||||
|
||||
$('#transfer-form select').change(function(event)
|
||||
{
|
||||
Grocy.FrontendHelpers.ValidateForm('transfer-form');
|
||||
});
|
||||
|
||||
$('#transfer-form input').keydown(function(event)
|
||||
{
|
||||
if (event.keyCode === 13) //Enter
|
||||
{
|
||||
event.preventDefault();
|
||||
|
||||
if (document.getElementById('transfer-form').checkValidity() === false) //There is at least one validation error
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$('#save-transfer-button').click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$("#specific_stock_entry").on("change", function(e)
|
||||
{
|
||||
if ($(e.target).val() == "")
|
||||
{
|
||||
var sumValue = 0;
|
||||
Grocy.Api.Get("stock/products/" + Grocy.Components.ProductPicker.GetValue() + '/entries',
|
||||
function(stockEntries)
|
||||
{
|
||||
stockEntries.forEach(stockEntry =>
|
||||
{
|
||||
if (stockEntry.location_id == $("#location_id_from").val() || stockEntry.location_id == "")
|
||||
{
|
||||
sumValue = sumValue + parseFloat(stockEntry.amount);
|
||||
}
|
||||
});
|
||||
$("#amount").attr("max", sumValue);
|
||||
if (sumValue == 0)
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('There are no units available at this location'));
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', "1", sumValue));
|
||||
}
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#amount").parent().find(".invalid-feedback").text(__t('The amount must be between %1$s and %2$s', "1", $('option:selected', this).attr('amount')));
|
||||
$("#amount").attr("max", $('option:selected', this).attr('amount'));
|
||||
}
|
||||
});
|
||||
|
||||
$("#use_specific_stock_entry").on("change", function()
|
||||
{
|
||||
var value = $(this).is(":checked");
|
||||
|
||||
if (value)
|
||||
{
|
||||
$("#specific_stock_entry").removeAttr("disabled");
|
||||
$("#specific_stock_entry").attr("required", "");
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#specific_stock_entry").attr("disabled", "");
|
||||
$("#specific_stock_entry").removeAttr("required");
|
||||
$("#specific_stock_entry").val("");
|
||||
$("#location_id_from").trigger('change');
|
||||
}
|
||||
|
||||
Grocy.FrontendHelpers.ValidateForm("transfer-form");
|
||||
});
|
||||
|
||||
function UndoStockBooking(bookingId)
|
||||
{
|
||||
Grocy.Api.Post('stock/bookings/' + bookingId.toString() + '/undo', { },
|
||||
function(result)
|
||||
{
|
||||
toastr.success(__t("Booking successfully undone"));
|
||||
},
|
||||
function(xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function UndoStockTransaction(transactionId)
|
||||
{
|
||||
Grocy.Api.Post('stock/transactions/' + transactionId.toString() + '/undo', { },
|
||||
function (result)
|
||||
{
|
||||
toastr.success(__t("Transaction successfully undone"));
|
||||
},
|
||||
function (xhr)
|
||||
{
|
||||
console.error(xhr);
|
||||
}
|
||||
);
|
||||
};
|
||||
11
routes.php
11
routes.php
|
|
@ -40,9 +40,12 @@ $app->group('', function()
|
|||
if (GROCY_FEATURE_FLAG_STOCK)
|
||||
{
|
||||
$this->get('/stockoverview', '\Grocy\Controllers\StockController:Overview');
|
||||
$this->get('/stockdetail', '\Grocy\Controllers\StockController:Detail');
|
||||
$this->get('/purchase', '\Grocy\Controllers\StockController:Purchase');
|
||||
$this->get('/consume', '\Grocy\Controllers\StockController:Consume');
|
||||
$this->get('/transfer', '\Grocy\Controllers\StockController:Transfer');
|
||||
$this->get('/inventory', '\Grocy\Controllers\StockController:Inventory');
|
||||
$this->get('/stockedit', '\Grocy\Controllers\StockController:StockEdit');
|
||||
$this->get('/products', '\Grocy\Controllers\StockController:ProductsList');
|
||||
$this->get('/product/{productId}', '\Grocy\Controllers\StockController:ProductEditForm');
|
||||
$this->get('/stocksettings', '\Grocy\Controllers\StockController:StockSettings');
|
||||
|
|
@ -167,22 +170,28 @@ $app->group('/api', function()
|
|||
if (GROCY_FEATURE_FLAG_STOCK)
|
||||
{
|
||||
$this->get('/stock', '\Grocy\Controllers\StockApiController:CurrentStock');
|
||||
$this->put('/stock', '\Grocy\Controllers\StockApiController:EditStock');
|
||||
$this->get('/stock/volatile', '\Grocy\Controllers\StockApiController:CurrentVolatilStock');
|
||||
$this->get('/stock/products/{productId}', '\Grocy\Controllers\StockApiController:ProductDetails');
|
||||
$this->get('/stock/products/{productId}/entries', '\Grocy\Controllers\StockApiController:ProductStockEntries');
|
||||
$this->get('/stock/products/{productId}/locations', '\Grocy\Controllers\StockApiController:ProductStockLocations');
|
||||
$this->get('/stock/products/{productId}/price-history', '\Grocy\Controllers\StockApiController:ProductPriceHistory');
|
||||
$this->post('/stock/products/{productId}/add', '\Grocy\Controllers\StockApiController:AddProduct');
|
||||
$this->post('/stock/products/{productId}/consume', '\Grocy\Controllers\StockApiController:ConsumeProduct');
|
||||
$this->post('/stock/products/{productId}/transfer', '\Grocy\Controllers\StockApiController:TransferProduct');
|
||||
$this->post('/stock/products/{productId}/inventory', '\Grocy\Controllers\StockApiController:InventoryProduct');
|
||||
$this->post('/stock/products/{productId}/open', '\Grocy\Controllers\StockApiController:OpenProduct');
|
||||
$this->get('/stock/products/by-barcode/{barcode}', '\Grocy\Controllers\StockApiController:ProductDetailsByBarcode');
|
||||
$this->post('/stock/products/by-barcode/{barcode}/add', '\Grocy\Controllers\StockApiController:AddProductByBarcode');
|
||||
$this->post('/stock/products/by-barcode/{barcode}/consume', '\Grocy\Controllers\StockApiController:ConsumeProductByBarcode');
|
||||
$this->post('/stock/products/by-barcode/{barcode}/transfer', '\Grocy\Controllers\StockApiController:TransferProductByBarcode');
|
||||
$this->post('/stock/products/by-barcode/{barcode}/inventory', '\Grocy\Controllers\StockApiController:InventoryProductByBarcode');
|
||||
$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/barcodes/external-lookup', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
|
||||
$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');
|
||||
}
|
||||
|
||||
// Shopping list
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ class StockService extends BaseService
|
|||
{
|
||||
const TRANSACTION_TYPE_PURCHASE = 'purchase';
|
||||
const TRANSACTION_TYPE_CONSUME = 'consume';
|
||||
const TRANSACTION_TYPE_TRANSFER_FROM = 'transfer_from';
|
||||
const TRANSACTION_TYPE_TRANSFER_TO = 'transfer_to';
|
||||
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
|
||||
const TRANSACTION_TYPE_STOCK_EDIT_NEW = 'stock-edit-new';
|
||||
const TRANSACTION_TYPE_STOCK_EDIT_OLD = 'stock-edit-old';
|
||||
const TRANSACTION_TYPE_PRODUCT_OPENED = 'product-opened';
|
||||
|
||||
public function GetCurrentStock($includeNotInStockButMissingProducts = false)
|
||||
|
|
@ -55,6 +59,11 @@ class StockService extends BaseService
|
|||
return $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ);
|
||||
}
|
||||
|
||||
public function GetProductStockLocations($productId)
|
||||
{
|
||||
return $this->Database->stock_current_locations()->where('product_id', $productId)->fetchAll();
|
||||
}
|
||||
|
||||
public function GetProductIdFromBarcode(string $barcode)
|
||||
{
|
||||
$potentialProduct = $this->getDatabase()->products()->where("',' || barcode || ',' LIKE '%,' || :1 || ',%' AND IFNULL(barcode, '') != ''", $barcode)->limit(1)->fetch();
|
||||
|
|
@ -176,7 +185,13 @@ class StockService extends BaseService
|
|||
}
|
||||
}
|
||||
|
||||
public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null)
|
||||
public function GetProductStockEntriesForLocation($productId, $locationId, $excludeOpened = false)
|
||||
{
|
||||
$stockEntries = $this->GetProductStockEntries($productId, $excludeOpened);
|
||||
return FindAllObjectsInArrayByPropertyValue($stockEntries, 'location_id', $locationId);
|
||||
}
|
||||
|
||||
public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, &$transactionId = null)
|
||||
{
|
||||
if (!$this->ProductExists($productId))
|
||||
{
|
||||
|
|
@ -216,6 +231,11 @@ class StockService extends BaseService
|
|||
|
||||
if ($transactionType === self::TRANSACTION_TYPE_PURCHASE || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
|
||||
{
|
||||
if ($transactionId === null)
|
||||
{
|
||||
$transactionId = uniqid();
|
||||
}
|
||||
|
||||
$stockId = uniqid();
|
||||
|
||||
$logRow = $this->getDatabase()->stock_log()->createRow(array(
|
||||
|
|
@ -226,7 +246,8 @@ class StockService extends BaseService
|
|||
'stock_id' => $stockId,
|
||||
'transaction_type' => $transactionType,
|
||||
'price' => $price,
|
||||
'location_id' => $locationId
|
||||
'location_id' => $locationId,
|
||||
'transaction_id' => $transactionId
|
||||
));
|
||||
$logRow->save();
|
||||
|
||||
|
|
@ -251,13 +272,18 @@ class StockService extends BaseService
|
|||
}
|
||||
}
|
||||
|
||||
public function ConsumeProduct(int $productId, float $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default', $recipeId = null)
|
||||
public function ConsumeProduct(int $productId, float $amount, bool $spoiled, $transactionType, $specificStockEntryId = 'default', $recipeId = null, $locationId = null, &$transactionId = null)
|
||||
{
|
||||
if (!$this->ProductExists($productId))
|
||||
{
|
||||
throw new \Exception('Product does not exist');
|
||||
}
|
||||
|
||||
if ($locationId !== null & !$this->LocationExists($locationId))
|
||||
{
|
||||
throw new \Exception('Location does not exist');
|
||||
}
|
||||
|
||||
// Tare weight handling
|
||||
// The given amount is the new total amount including the container weight (gross)
|
||||
// The amount to be posted needs to be the absolute value of the given amount - stock amount - tare weight
|
||||
|
|
@ -274,12 +300,21 @@ class StockService extends BaseService
|
|||
|
||||
if ($transactionType === self::TRANSACTION_TYPE_CONSUME || $transactionType === self::TRANSACTION_TYPE_INVENTORY_CORRECTION)
|
||||
{
|
||||
$productStockAmount = $this->getDatabase()->stock()->where('product_id', $productId)->sum('amount');
|
||||
$potentialStockEntries = $this->GetProductStockEntries($productId);
|
||||
|
||||
if ($locationId === null) // Consume from any location
|
||||
{
|
||||
$productStockAmount = $this->getDatabase()->stock()->where('product_id', $productId)->sum('amount');
|
||||
$potentialStockEntries = $this->GetProductStockEntries($productId);
|
||||
}
|
||||
else // Consume only from the supplied location
|
||||
{
|
||||
$productStockAmount = $this->getDatabase()->stock()->where('product_id = :1 AND location_id = :2', $productId, $locationId)->sum('amount');
|
||||
$potentialStockEntries = $this->GetProductStockEntriesForLocation($productId, $locationId);
|
||||
}
|
||||
|
||||
if ($amount > $productStockAmount)
|
||||
{
|
||||
throw new \Exception('Amount to be consumed cannot be > current stock amount');
|
||||
throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)');
|
||||
}
|
||||
|
||||
if ($specificStockEntryId !== 'default')
|
||||
|
|
@ -287,6 +322,11 @@ class StockService extends BaseService
|
|||
$potentialStockEntries = FindAllObjectsInArrayByPropertyValue($potentialStockEntries, 'stock_id', $specificStockEntryId);
|
||||
}
|
||||
|
||||
if ($transactionId === null)
|
||||
{
|
||||
$transactionId = uniqid();
|
||||
}
|
||||
|
||||
foreach ($potentialStockEntries as $stockEntry)
|
||||
{
|
||||
if ($amount == 0)
|
||||
|
|
@ -307,7 +347,8 @@ class StockService extends BaseService
|
|||
'transaction_type' => $transactionType,
|
||||
'price' => $stockEntry->price,
|
||||
'opened_date' => $stockEntry->opened_date,
|
||||
'recipe_id' => $recipeId
|
||||
'recipe_id' => $recipeId,
|
||||
'transaction_id' => $transactionId
|
||||
));
|
||||
$logRow->save();
|
||||
|
||||
|
|
@ -330,7 +371,8 @@ class StockService extends BaseService
|
|||
'transaction_type' => $transactionType,
|
||||
'price' => $stockEntry->price,
|
||||
'opened_date' => $stockEntry->opened_date,
|
||||
'recipe_id' => $recipeId
|
||||
'recipe_id' => $recipeId,
|
||||
'transaction_id' => $transactionId
|
||||
));
|
||||
$logRow->save();
|
||||
|
||||
|
|
@ -350,6 +392,219 @@ class StockService extends BaseService
|
|||
}
|
||||
}
|
||||
|
||||
public function TransferProduct(int $productId, float $amount, int $locationIdFrom, int $locationIdTo, $specificStockEntryId = 'default', &$transactionId = null)
|
||||
{
|
||||
if (!$this->ProductExists($productId))
|
||||
{
|
||||
throw new \Exception('Product does not exist');
|
||||
}
|
||||
|
||||
if (!$this->LocationExists($locationIdFrom))
|
||||
{
|
||||
throw new \Exception('Source location does not exist');
|
||||
}
|
||||
|
||||
if (!$this->LocationExists($locationIdTo))
|
||||
{
|
||||
throw new \Exception('Destination location does not exist');
|
||||
}
|
||||
|
||||
// Tare weight handling
|
||||
// The given amount is the new total amount including the container weight (gross)
|
||||
// The amount to be posted needs to be the absolute value of the given amount - stock amount - tare weight
|
||||
$productDetails = (object)$this->GetProductDetails($productId);
|
||||
if ($productDetails->product->enable_tare_weight_handling == 1)
|
||||
{
|
||||
// Hard fail for now, as we not yet support transfering tare weight enabled products
|
||||
throw new \Exception('Transfering tare weight enabled products is not yet possible');
|
||||
|
||||
if ($amount < floatval($productDetails->product->tare_weight))
|
||||
{
|
||||
throw new \Exception('The amount cannot be lower than the defined tare weight');
|
||||
}
|
||||
|
||||
$amount = abs($amount - floatval($productDetails->stock_amount) - floatval($productDetails->product->tare_weight));
|
||||
}
|
||||
|
||||
$productStockAmountAtFromLocation = $this->Database->stock()->where('product_id = :1 AND location_id = :2', $productId, $locationIdFrom)->sum('amount');
|
||||
$potentialStockEntriesAtFromLocation = $this->GetProductStockEntriesForLocation($productId, $locationIdFrom);
|
||||
|
||||
if ($amount > $productStockAmountAtFromLocation)
|
||||
{
|
||||
throw new \Exception('Amount to be transfered cannot be > current stock amount at the source location');
|
||||
}
|
||||
|
||||
if ($specificStockEntryId !== 'default')
|
||||
{
|
||||
$potentialStockEntriesAtFromLocation = FindAllObjectsInArrayByPropertyValue($potentialStockEntriesAtFromLocation, 'stock_id', $specificStockEntryId);
|
||||
}
|
||||
|
||||
if ($transactionId === null)
|
||||
{
|
||||
$transactionId = uniqid();
|
||||
}
|
||||
|
||||
foreach ($potentialStockEntriesAtFromLocation as $stockEntry)
|
||||
{
|
||||
if ($amount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
$correlationId = uniqid();
|
||||
if ($amount >= $stockEntry->amount) // Take the whole stock entry
|
||||
{
|
||||
$logRowForLocationFrom = $this->Database->stock_log()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $stockEntry->amount * -1,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'stock_id' => $stockEntry->stock_id,
|
||||
'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_FROM,
|
||||
'price' => $stockEntry->price,
|
||||
'opened_date' => $stockEntry->opened_date,
|
||||
'location_id' => $stockEntry->location_id,
|
||||
'correlation_id' => $correlationId,
|
||||
'transaction_Id' => $transactionId
|
||||
));
|
||||
$logRowForLocationFrom->save();
|
||||
|
||||
$logRowForLocationTo = $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_TRANSFER_TO,
|
||||
'price' => $stockEntry->price,
|
||||
'opened_date' => $stockEntry->opened_date,
|
||||
'location_id' => $locationIdTo,
|
||||
'correlation_id' => $correlationId,
|
||||
'transaction_Id' => $transactionId
|
||||
));
|
||||
$logRowForLocationTo->save();
|
||||
|
||||
$stockEntry->update(array(
|
||||
'location_id' => $locationIdTo
|
||||
));
|
||||
|
||||
$amount -= $stockEntry->amount;
|
||||
}
|
||||
else // Stock entry amount is > than needed amount -> split the stock entry resp. update the amount
|
||||
{
|
||||
$restStockAmount = $stockEntry->amount - $amount;
|
||||
|
||||
$logRowForLocationFrom = $this->Database->stock_log()->createRow(array(
|
||||
'product_id' => $stockEntry->product_id,
|
||||
'amount' => $amount * -1,
|
||||
'best_before_date' => $stockEntry->best_before_date,
|
||||
'purchased_date' => $stockEntry->purchased_date,
|
||||
'stock_id' => $stockEntry->stock_id,
|
||||
'transaction_type' => self::TRANSACTION_TYPE_TRANSFER_FROM,
|
||||
'price' => $stockEntry->price,
|
||||
'opened_date' => $stockEntry->opened_date,
|
||||
'location_id' => $stockEntry->location_id,
|
||||
'correlation_id' => $correlationId,
|
||||
'transaction_Id' => $transactionId
|
||||
));
|
||||
$logRowForLocationFrom->save();
|
||||
|
||||
$logRowForLocationTo = $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_TRANSFER_TO,
|
||||
'price' => $stockEntry->price,
|
||||
'opened_date' => $stockEntry->opened_date,
|
||||
'location_id' => $locationIdTo,
|
||||
'correlation_id' => $correlationId,
|
||||
'transaction_Id' => $transactionId
|
||||
));
|
||||
$logRowForLocationTo->save();
|
||||
|
||||
// This is the existing stock entry -> remains at the source location with the rest amount
|
||||
$stockEntry->update(array(
|
||||
'amount' => $restStockAmount
|
||||
));
|
||||
|
||||
// The transfered amount gets into a new stock entry
|
||||
$stockEntryNew = $this->Database->stock()->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,
|
||||
'price' => $stockEntry->price,
|
||||
'location_id' => $locationIdTo,
|
||||
'open' => $stockEntry->open,
|
||||
'opened_date' => $stockEntry->opened_date
|
||||
));
|
||||
$stockEntryNew->save();
|
||||
|
||||
$amount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->Database->lastInsertId();
|
||||
}
|
||||
|
||||
public function EditStock(int $stockRowId, int $amount, $bestBeforeDate, $locationId, $price)
|
||||
{
|
||||
|
||||
$stockRow = $this->Database->stock()->where('id = :1', $stockRowId)->fetch();
|
||||
|
||||
if ($stockRow === null)
|
||||
{
|
||||
throw new \Exception('Stock does not exist');
|
||||
}
|
||||
|
||||
$correlationId = uniqid();
|
||||
$transactionId = uniqid();
|
||||
$logOldRowForStockUpdate = $this->Database->stock_log()->createRow(array(
|
||||
'product_id' => $stockRow->product_id,
|
||||
'amount' => $stockRow->amount,
|
||||
'best_before_date' => $stockRow->best_before_date,
|
||||
'purchased_date' => $stockRow->purchased_date,
|
||||
'stock_id' => $stockRow->stock_id,
|
||||
'transaction_type' => self::TRANSACTION_TYPE_STOCK_EDIT_OLD,
|
||||
'price' => $stockRow->price,
|
||||
'opened_date' => $stockRow->opened_date,
|
||||
'location_id' => $stockRow->location_id,
|
||||
'correlation_id' => $correlationId,
|
||||
'transaction_id' => $transactionId,
|
||||
'stock_row_id' => $stockRow->id
|
||||
));
|
||||
$logOldRowForStockUpdate->save();
|
||||
|
||||
$stockRow->update(array(
|
||||
'amount' => $amount,
|
||||
'price' => $price,
|
||||
'best_before_date' => $bestBeforeDate,
|
||||
'location_id' => $locationId
|
||||
));
|
||||
|
||||
$logNewRowForStockUpdate = $this->Database->stock_log()->createRow(array(
|
||||
'product_id' => $stockRow->product_id,
|
||||
'amount' => $amount,
|
||||
'best_before_date' => $bestBeforeDate,
|
||||
'purchased_date' => $stockRow->purchased_date,
|
||||
'stock_id' => $stockRow->stock_id,
|
||||
'transaction_type' => self::TRANSACTION_TYPE_STOCK_EDIT_NEW,
|
||||
'price' => $price,
|
||||
'opened_date' => $stockRow->opened_date,
|
||||
'location_id' => $locationId,
|
||||
'correlation_id' => $correlationId,
|
||||
'transaction_id' => $transactionId,
|
||||
'stock_row_id' => $stockRow->id
|
||||
));
|
||||
$logNewRowForStockUpdate->save();
|
||||
|
||||
$returnValue = $this->Database->lastInsertId();
|
||||
|
||||
return $returnValue;
|
||||
}
|
||||
public function InventoryProduct(int $productId, int $newAmount, $bestBeforeDate, $locationId = null, $price = null)
|
||||
{
|
||||
if (!$this->ProductExists($productId))
|
||||
|
|
@ -608,6 +863,12 @@ class StockService extends BaseService
|
|||
return $productRow !== null;
|
||||
}
|
||||
|
||||
private function LocationExists($locationId)
|
||||
{
|
||||
$locationRow = $this->Database->locations()->where('id = :1', $locationId)->fetch();
|
||||
return $locationRow !== null;
|
||||
}
|
||||
|
||||
private function ShoppingListExists($listId)
|
||||
{
|
||||
$shoppingListRow = $this->getDatabase()->shopping_lists()->where('id = :1', $listId)->fetch();
|
||||
|
|
@ -654,7 +915,7 @@ class StockService extends BaseService
|
|||
return $pluginOutput;
|
||||
}
|
||||
|
||||
public function UndoBooking($bookingId)
|
||||
public function UndoBooking($bookingId, $skipCorrelatedBookings = false)
|
||||
{
|
||||
$logRow = $this->getDatabase()->stock_log()->where('id = :1 AND undone = 0', $bookingId)->fetch();
|
||||
if ($logRow == null)
|
||||
|
|
@ -662,7 +923,18 @@ class StockService extends BaseService
|
|||
throw new \Exception('Booking does not exist or was already undone');
|
||||
}
|
||||
|
||||
$hasSubsequentBookings = $this->getDatabase()->stock_log()->where('stock_id = :1 AND id != :2 AND id > :2', $logRow->stock_id, $logRow->id)->count() > 0;
|
||||
// Undo all correlated bookings first, in order from newest first to the oldest
|
||||
if (!$skipCorrelatedBookings && !empty($logRow->correlation_id))
|
||||
{
|
||||
$correlatedBookings = $this->getDatabase()->stock_log()->where('undone = 0 AND correlation_id = :1', $logRow->correlation_id)->orderBy('id', 'DESC')->fetchAll();
|
||||
foreach ($correlatedBookings as $correlatedBooking)
|
||||
{
|
||||
$this->UndoBooking($correlatedBooking->id, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$hasSubsequentBookings = $this->getDatabase()->stock_log()->where('stock_id = :1 AND id != :2 AND (correlation_id is not null OR correlation_id != :3) AND id > :2 AND undone = 0', $logRow->stock_id, $logRow->id, $logRow->correlation_id)->count() > 0;
|
||||
if ($hasSubsequentBookings)
|
||||
{
|
||||
throw new \Exception('Booking has subsequent dependent bookings, undo not possible');
|
||||
|
|
@ -700,6 +972,60 @@ class StockService extends BaseService
|
|||
'undone_timestamp' => date('Y-m-d H:i:s')
|
||||
));
|
||||
}
|
||||
elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_TRANSFER_TO)
|
||||
{
|
||||
$stockRow = $this->Database->stock()->where('stock_id = :1 AND location_id = :2', $logRow->stock_id, $logRow->location_id)->fetch();
|
||||
if ($stockRow === null)
|
||||
{
|
||||
throw new \Exception('Booking does not exist or was already undone');
|
||||
}
|
||||
$newAmount = $stockRow->amount - $logRow->amount;
|
||||
|
||||
if ($newAmount == 0)
|
||||
{
|
||||
$stockRow->delete();
|
||||
} else {
|
||||
// Remove corresponding amount back to stock
|
||||
$stockRow->update(array(
|
||||
'amount' => $newAmount
|
||||
));
|
||||
}
|
||||
|
||||
// Update log entry
|
||||
$logRow->update(array(
|
||||
'undone' => 1,
|
||||
'undone_timestamp' => date('Y-m-d H:i:s')
|
||||
));
|
||||
}
|
||||
elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_TRANSFER_FROM)
|
||||
{
|
||||
// Add corresponding amount back to stock or
|
||||
// create a row if missing
|
||||
$stockRow = $this->Database->stock()->where('stock_id = :1 AND location_id = :2', $logRow->stock_id, $logRow->location_id)->fetch();
|
||||
if ($stockRow === null)
|
||||
{
|
||||
$stockRow = $this->Database->stock()->createRow(array(
|
||||
'product_id' => $logRow->product_id,
|
||||
'amount' => $logRow->amount * -1,
|
||||
'best_before_date' => $logRow->best_before_date,
|
||||
'purchased_date' => $logRow->purchased_date,
|
||||
'stock_id' => $logRow->stock_id,
|
||||
'price' => $logRow->price,
|
||||
'opened_date' => $logRow->opened_date
|
||||
));
|
||||
$stockRow->save();
|
||||
} else {
|
||||
$stockRow->update(array(
|
||||
'amount' => $stockRow->amount - $logRow->amount
|
||||
));
|
||||
}
|
||||
|
||||
// Update log entry
|
||||
$logRow->update(array(
|
||||
'undone' => 1,
|
||||
'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
|
||||
|
|
@ -715,9 +1041,55 @@ class StockService extends BaseService
|
|||
'undone_timestamp' => date('Y-m-d H:i:s')
|
||||
));
|
||||
}
|
||||
elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_STOCK_EDIT_NEW)
|
||||
{
|
||||
// Update log entry, no action needed
|
||||
$logRow->update(array(
|
||||
'undone' => 1,
|
||||
'undone_timestamp' => date('Y-m-d H:i:s')
|
||||
));
|
||||
}
|
||||
elseif ($logRow->transaction_type === self::TRANSACTION_TYPE_STOCK_EDIT_OLD)
|
||||
{
|
||||
// Make sure there is a stock row still
|
||||
$stockRow = $this->Database->stock()->where('id = :1', $logRow->stock_row_id)->fetch();
|
||||
if ($stockRow == null)
|
||||
{
|
||||
throw new \Exception('Booking does not exist or was already undone');
|
||||
}
|
||||
|
||||
$stockRow->update(array(
|
||||
'amount' => $logRow->amount,
|
||||
'best_before_date' => $logRow->best_before_date,
|
||||
'purchased_date' => $logRow->purchased_date,
|
||||
'price' => $logRow->price,
|
||||
'location_id' => $logRow->location_id
|
||||
));
|
||||
|
||||
// Update log entry
|
||||
$logRow->update(array(
|
||||
'undone' => 1,
|
||||
'undone_timestamp' => date('Y-m-d H:i:s')
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
<strong>{{ $__t('Stock amount') . ' / ' . $__t('Quantity unit') }}:</strong> <span id="productcard-product-stock-amount" class="locale-number locale-number-quantity-amount"></span> <span id="productcard-product-stock-qu-name"></span> <span id="productcard-product-stock-opened-amount" class="small font-italic locale-number locale-number-quantity-amount"></span>
|
||||
<span id="productcard-aggregated-amounts" class="pl-2 text-secondary d-none"><i class="fas fa-custom-sigma-sign"></i> <span id="productcard-product-stock-amount-aggregated" class="locale-number locale-number-quantity-amount"></span> <span id="productcard-product-stock-qu-name-aggregated"></span> <span id="productcard-product-stock-opened-amount-aggregated locale-number locale-number-quantity-amount" class="small font-italic"></span></span><br>
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<strong>{{ $__t('Location') }}:</strong> <span id="productcard-product-location"></span><br>@endif
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<strong>{{ $__t('Default location') }}:</strong> <span id="productcard-product-location"></span><br>@endif
|
||||
<strong>{{ $__t('Last purchased') }}:</strong> <span id="productcard-product-last-purchased"></span> <time id="productcard-product-last-purchased-timeago" class="timeago timeago-contextual"></time><br>
|
||||
<strong>{{ $__t('Last used') }}:</strong> <span id="productcard-product-last-used"></span> <time id="productcard-product-last-used-timeago" class="timeago timeago-contextual"></time><br>
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<strong>{{ $__t('Last price') }}:</strong> <span id="productcard-product-last-price"></span><br>@endif
|
||||
|
|
|
|||
|
|
@ -22,11 +22,33 @@
|
|||
'label' => 'Amount',
|
||||
'hintId' => 'amount_qu_unit',
|
||||
'min' => 1,
|
||||
'value' => 1,
|
||||
'value' => 0,
|
||||
'invalidFeedback' => $__t('The amount cannot be lower than %s', '1'),
|
||||
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>'
|
||||
))
|
||||
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||
@php /*@include('components.locationpicker', array(
|
||||
'id' => 'location_id',
|
||||
'locations' => $locations,
|
||||
'isRequired' => true,
|
||||
'label' => 'Location'
|
||||
))*/ @endphp
|
||||
|
||||
<div class="form-group">
|
||||
<label for="location_id">{{ $__t('Location') }}</label>
|
||||
<select required class="form-control location-combobox" id="location_id" name="location_id">
|
||||
<option></option>
|
||||
@foreach($locations as $location)
|
||||
<option value="{{ $location->id }}">{{ $location->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="invalid-feedback">{{ $__t('A location is required') }}</div>
|
||||
</div>
|
||||
@else
|
||||
<input type="hidden" name="location_id" id="location_id" value="1">
|
||||
@endif
|
||||
|
||||
<div class="form-group">
|
||||
<label for="use_specific_stock_entry">
|
||||
<input type="checkbox" id="use_specific_stock_entry" name="use_specific_stock_entry"> {{ $__t('Use a specific stock item') }}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@
|
|||
'id' => 'price',
|
||||
'label' => 'Price',
|
||||
'min' => 0,
|
||||
'step' => 0.0001,
|
||||
'step' => 0.01,
|
||||
'value' => '',
|
||||
'hint' => $__t('in %s per purchase quantity unit', GROCY_CURRENCY),
|
||||
'additionalHtmlContextHelp' => '<br><span class="small text-muted">' . $__t('This will apply to added products') . '</span>',
|
||||
|
|
|
|||
|
|
@ -157,6 +157,14 @@
|
|||
<span class="nav-link-text">{{ $__t('Consume') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||
<li class="nav-item" data-toggle="tooltip" data-placement="right" title="{{ $__t('Transfer') }}" data-nav-for-page="transfer">
|
||||
<a class="nav-link discrete-link" href="{{ $U('/transfer') }}">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
<span class="nav-link-text">{{ $__t('Transfer') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<li class="nav-item" data-toggle="tooltip" data-placement="right" title="{{ $__t('Inventory') }}" data-nav-for-page="inventory">
|
||||
<a class="nav-link discrete-link" href="{{ $U('/inventory') }}">
|
||||
<i class="fas fa-list"></i>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
'id' => 'price',
|
||||
'label' => 'Price',
|
||||
'min' => 0,
|
||||
'step' => 0.0001,
|
||||
'step' => 0.01,
|
||||
'value' => '',
|
||||
'hint' => $__t('in %s and based on the purchase quantity unit', GROCY_CURRENCY),
|
||||
'invalidFeedback' => $__t('The price cannot be lower than %s', '0'),
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@
|
|||
<div class="col">
|
||||
<h2>
|
||||
{{ $__t('Ingredients list') }}
|
||||
<a id="recipe-pos-add-button" class="btn btn-outline-dark" href="#">
|
||||
<a id="recipe-pos-add-button" class="btn btn-outline-dark recipe-pos-add-button" type="button" href="#">
|
||||
<i class="fas fa-plus"></i> {{ $__t('Add') }}
|
||||
</a>
|
||||
</h2>
|
||||
|
|
@ -118,7 +118,7 @@
|
|||
@foreach($recipePositions as $recipePosition)
|
||||
<tr>
|
||||
<td class="fit-content border-right">
|
||||
<a class="btn btn-sm btn-info recipe-pos-edit-button" href="#" data-recipe-pos-id="{{ $recipePosition->id }}">
|
||||
<a class="btn btn-sm btn-info recipe-pos-edit-button" type="button" href="#" data-recipe-pos-id="{{ $recipePosition->id }}" data-product-id="{{ $recipePosition->product_id }}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a class="btn btn-sm btn-danger recipe-pos-delete-button" href="#" data-recipe-pos-id="{{ $recipePosition->id }}" data-recipe-pos-name="{{ FindObjectInArrayByPropertyValue($products, 'id', $recipePosition->product_id)->name }}">
|
||||
|
|
|
|||
|
|
@ -28,11 +28,9 @@
|
|||
|
||||
<form id="recipe-pos-form" novalidate>
|
||||
|
||||
@php $prefillByName = ''; if($mode=='edit') { $prefillByName = FindObjectInArrayByPropertyValue($products, 'id', $recipePos->product_id)->name; } @endphp
|
||||
@include('components.productpicker', array(
|
||||
'products' => $products,
|
||||
'nextInputSelector' => '#amount',
|
||||
'prefillByName' => $prefillByName
|
||||
'nextInputSelector' => '#amount'
|
||||
))
|
||||
|
||||
@php if($mode == 'edit') { $value = $recipePos->amount; } else { $value = 1; } @endphp
|
||||
|
|
@ -80,7 +78,7 @@
|
|||
'id' => 'price_factor',
|
||||
'label' => 'Price factor',
|
||||
'min' => 0,
|
||||
'step' => 0.0001,
|
||||
'step' => 0.01,
|
||||
'value' => '',
|
||||
'hint' => $__t('The resulting price of this ingredient will be multiplied by this factor'),
|
||||
'invalidFeedback' => $__t('This cannot be lower than %s', '0'),
|
||||
|
|
@ -96,7 +94,7 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-xl-4">
|
||||
<div class="col-xs-12 col-md-6 col-xl-4 hide-when-embedded">
|
||||
@include('components.productcard')
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
197
views/stockdetail.blade.php
Normal file
197
views/stockdetail.blade.php
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
@extends('layout.default')
|
||||
|
||||
@section('title', $__t('Stock entries'))
|
||||
@section('activeNav', 'stockdetail')
|
||||
@section('viewJsName', 'stockdetail')
|
||||
|
||||
@push('pageScripts')
|
||||
<script src="{{ $U('/node_modules/jquery-ui-dist/jquery-ui.min.js?v=', true) }}{{ $version }}"></script>
|
||||
<script src="{{ $U('/viewjs/purchase.js?v=', true) }}{{ $version }}"></script>
|
||||
@endpush
|
||||
|
||||
@push('pageStyles')
|
||||
<style>
|
||||
.product-name-cell[data-product-has-picture='true'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>@yield('title')</h1>
|
||||
</div>
|
||||
<div class="col">
|
||||
@include('components.productpicker', array('products' => $products,'disallowAddProductWorkflows' => true))
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table id="stock-detail-table" class="table table-sm table-striped dt-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-right"></th>
|
||||
<th class="d-none">product_id</th> <!-- This must be in the first column for searching -->
|
||||
<th>{{ $__t('Product') }}</th>
|
||||
<th>{{ $__t('Amount') }}</th>
|
||||
<th>{{ $__t('Best before date') }}</th>
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)<th>{{ $__t('Location') }}</th>@endif
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)<th>{{ $__t('Price') }}</th>@endif
|
||||
<th>{{ $__t('Purchased date') }}</th>
|
||||
|
||||
@include('components.userfields_thead', array(
|
||||
'userfields' => $userfields
|
||||
))
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="d-none">
|
||||
@foreach($currentStockDetail as $currentStockEntry)
|
||||
<tr id="stock-{{ $currentStockEntry->id }}-row" class="@if(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime('-1 days')) && $currentStockEntry->amount > 0) table-danger @elseif(GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING && $currentStockEntry->best_before_date < date('Y-m-d 23:59:59', strtotime("+$nextXDays days")) && $currentStockEntry->amount > 0) table-warning @elseif (FindObjectInArrayByPropertyValue($missingProducts, 'id', $currentStockEntry->product_id) !== null) table-info @endif">
|
||||
<td class="fit-content border-right">
|
||||
<a class="btn btn-success btn-sm stock-consume-button @if($currentStockEntry->amount < 1) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Consume %1$s of %2$s', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}"
|
||||
data-stock-id="{{ $currentStockEntry->stock_id }}"
|
||||
data-stockrow-id="{{ $currentStockEntry->id }}"
|
||||
data-location-id="{{ $currentStockEntry->location_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 }}"
|
||||
data-consume-amount="1">
|
||||
<i class="fas fa-utensils"></i> 1
|
||||
</a>
|
||||
<a id="stock-{{ $currentStockEntry->id }}-consume-all-button" class="btn btn-danger btn-sm stock-consume-button @if($currentStockEntry->amount == 0) disabled @endif" href="#" data-toggle="tooltip" data-placement="right" title="{{ $__t('Consume all %s for this stock entry', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}"
|
||||
data-stock-id="{{ $currentStockEntry->stock_id }}"
|
||||
data-stockrow-id="{{ $currentStockEntry->id }}"
|
||||
data-location-id="{{ $currentStockEntry->location_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 }}"
|
||||
data-consume-amount="{{ $currentStockEntry->amount }}">
|
||||
<i class="fas fa-utensils"></i> {{ $__t('All') }}
|
||||
</a>
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_PRODUCT_OPENED_TRACKING)
|
||||
<a class="btn btn-success btn-sm product-open-button @if($currentStockEntry->amount < 1 || $currentStockEntry->amount == $currentStockEntry->amount_opened) disabled @endif" href="#" data-toggle="tooltip" data-placement="left" title="{{ $__t('Mark %1$s of %2$s as open', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}"
|
||||
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>
|
||||
@endif
|
||||
<div class="dropdown d-inline-block">
|
||||
<button class="btn btn-sm btn-light text-secondary" type="button" data-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item product-add-to-shopping-list-button" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-shopping-cart"></i> {{ $__t('Add to shopping list') }}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item product-purchase-button" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-shopping-cart"></i> {{ $__t('Purchase') }}
|
||||
</a>
|
||||
<a class="dropdown-item product-consume-custom-amount-button @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}"
|
||||
data-location-id="{{ $currentStockEntry->location_id }}"
|
||||
data-stock-id="{{ $currentStockEntry->stock_id }}">
|
||||
<i class="fas fa-utensils"></i> {{ $__t('Consume') }}
|
||||
</a>
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||
<a class="dropdown-item product-transfer-button @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}"
|
||||
data-location-id="{{ $currentStockEntry->location_id }}"
|
||||
data-stock-id="{{ $currentStockEntry->stock_id }}">
|
||||
<i class="fas fa-exchange-alt"></i> {{ $__t('Transfer') }}
|
||||
</a>
|
||||
@endif
|
||||
<a class="dropdown-item product-inventory-button" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-list"></i> {{ $__t('Inventory') }}
|
||||
</a>
|
||||
<a class="dropdown-item product-stockedit-button" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}"
|
||||
data-location-id="{{ $currentStockEntry->location_id }}"
|
||||
data-id="{{ $currentStockEntry->id }}">
|
||||
<i class="fas fa-boxes"></i> {{ $__t('Edit stock entry') }}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item product-name-cell" data-product-id="{{ $currentStockEntry->product_id }}" type="button" href="#">
|
||||
<i class="fas fa-info"></i> {{ $__t('Show product details') }}
|
||||
</a>
|
||||
<a class="dropdown-item" type="button" href="{{ $U('/stockjournal?product=') }}{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-file-alt"></i> {{ $__t('Stock journal for this product') }}
|
||||
</a>
|
||||
<a class="dropdown-item" type="button" href="{{ $U('/product/') }}{{ $currentStockEntry->product_id . '?returnto=%2Fstockdetail' }}">
|
||||
<i class="fas fa-edit"></i> {{ $__t('Edit product') }}
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item product-consume-button product-consume-button-spoiled @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="#"
|
||||
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 }}"
|
||||
data-consume-amount="1">
|
||||
<i class="fas fa-utensils"></i> {{ $__t('Consume %1$s of %2$s as spoiled', '1 ' . FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name) }}
|
||||
</a>
|
||||
@if(GROCY_FEATURE_FLAG_RECIPES)
|
||||
<a class="dropdown-item" type="button" href="{{ $U('/recipes?search=') }}{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}">
|
||||
<i class="fas fa-cocktail"></i> {{ $__t('Search for recipes containing this product') }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-none" data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
{{ $currentStockEntry->product_id }}
|
||||
</td>
|
||||
<td class="product-name-cell cursor-link" data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
{{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}
|
||||
</td>
|
||||
<td>
|
||||
<span id="stock-{{ $currentStockEntry->id }}-amount" class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}</span>
|
||||
<span id="stock-{{ $currentStockEntry->id }}-opened-amount" class="small font-italic">@if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif</span>
|
||||
@if($currentStockEntry->is_aggregated_amount == 1)
|
||||
<span class="pl-1 text-secondary">
|
||||
<i class="fas fa-custom-sigma-sign"></i> <span id="product-{{ $currentStockEntry->product_id }}-amount-aggregated" class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount_aggregated }}</span> {{ $__n($currentStockEntry->amount_aggregated, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}
|
||||
@if($currentStockEntry->amount_opened_aggregated > 0)<span id="product-{{ $currentStockEntry->product_id }}-opened-amount-aggregated" class="small font-italic">{{ $__t('%s opened', $currentStockEntry->amount_opened_aggregated) }}</span>@endif
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span id="stock-{{ $currentStockEntry->id }}-best-before-date">{{ $currentStockEntry->best_before_date }}</span>
|
||||
<time id="stock-{{ $currentStockEntry->id }}-best-before-date-timeago" class="timeago timeago-contextual" datetime="{{ $currentStockEntry->best_before_date }} 23:59:59"></time>
|
||||
</td>
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||
<td id="stock-{{ $currentStockEntry->id }}-location" class="location-name-cell cursor-link" data-location-id="{{ $currentStockEntry->location_id }}">
|
||||
{{ FindObjectInArrayByPropertyValue($locations, 'id', $currentStockEntry->location_id)->name }}
|
||||
</td>
|
||||
@endif
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
||||
<td id="stock-{{ $currentStockEntry->id }}-price" class="price-name-cell cursor-link" data-price-id="{{ $currentStockEntry->price }}">
|
||||
{{ $currentStockEntry->price }}
|
||||
</td>
|
||||
@endif
|
||||
<td>
|
||||
<span id="stock-{{ $currentStockEntry->id }}-purchased-date">{{ $currentStockEntry->purchased_date }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="stockdetail-productcard-modal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content text-center">
|
||||
<div class="modal-body">
|
||||
@include('components.productcard')
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ $__t('Close') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
81
views/stockedit.blade.php
Normal file
81
views/stockedit.blade.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
@extends('layout.default')
|
||||
|
||||
@section('title', $__t('Edit stock entry'))
|
||||
@section('activeNav', 'stockedit')
|
||||
@section('viewJsName', 'stockedit')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6 col-xl-4 pb-3">
|
||||
<h1>@yield('title')</h1>
|
||||
|
||||
<form id="stockedit-form" novalidate>
|
||||
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||
@include('components.locationpicker', array(
|
||||
'locations' => $locations
|
||||
))
|
||||
@else
|
||||
<input type="hidden" name="location_id" id="location_id" value="1">
|
||||
@endif
|
||||
|
||||
@include('components.numberpicker', array(
|
||||
'id' => 'amount',
|
||||
'label' => 'Amount',
|
||||
'hintId' => 'amount_qu_unit',
|
||||
'invalidFeedback' => $__t('The amount cannot be lower than %s', '0'),
|
||||
'additionalAttributes' => 'data-not-equal="-1"',
|
||||
'additionalHtmlElements' => '<div id="stockedit-change-info" class="form-text text-muted small d-none"></div>',
|
||||
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-small text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>'
|
||||
))
|
||||
|
||||
@php
|
||||
$additionalGroupCssClasses = '';
|
||||
if (!GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
|
||||
{
|
||||
$additionalGroupCssClasses = 'd-none';
|
||||
}
|
||||
@endphp
|
||||
@include('components.datetimepicker', array(
|
||||
'id' => 'best_before_date',
|
||||
'label' => 'Best before',
|
||||
'format' => 'YYYY-MM-DD',
|
||||
'initWithNow' => false,
|
||||
'limitEndToNow' => false,
|
||||
'limitStartToNow' => false,
|
||||
'invalidFeedback' => $__t('A best before date is required'),
|
||||
'nextInputSelector' => '#best_before_date',
|
||||
'additionalGroupCssClasses' => 'date-only-datetimepicker',
|
||||
'shortcutValue' => '2999-12-31',
|
||||
'shortcutLabel' => 'Never expires',
|
||||
'earlierThanInfoLimit' => date('Y-m-d'),
|
||||
'earlierThanInfoText' => $__t('The given date is earlier than today, are you sure?'),
|
||||
'additionalGroupCssClasses' => $additionalGroupCssClasses
|
||||
))
|
||||
@php $additionalGroupCssClasses = ''; @endphp
|
||||
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
||||
@include('components.numberpicker', array(
|
||||
'id' => 'price',
|
||||
'label' => 'Price',
|
||||
'min' => 0,
|
||||
'step' => 0.01,
|
||||
'value' => '',
|
||||
'hint' => $__t('in %s per purchase quantity unit', GROCY_CURRENCY),
|
||||
'invalidFeedback' => $__t('The price cannot be lower than %s', '0'),
|
||||
'isRequired' => false
|
||||
))
|
||||
@else
|
||||
<input type="hidden" name="price" id="price" value="0">
|
||||
@endif
|
||||
|
||||
<button id="save-stockedit-button" class="btn btn-success">{{ $__t('OK') }}</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-xl-4 hide-when-embedded">
|
||||
@include('components.productcard')
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
|
|
@ -37,6 +37,7 @@
|
|||
<th>{{ $__t('Amount') }}</th>
|
||||
<th>{{ $__t('Booking time') }}</th>
|
||||
<th>{{ $__t('Booking type') }}</th>
|
||||
<th class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">{{ $__t('Location') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="d-none">
|
||||
|
|
@ -65,6 +66,9 @@
|
|||
<td>
|
||||
{{ $__t($stockLogEntry->transaction_type) }}
|
||||
</td>
|
||||
<td class="@if(!GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING) d-none @endif">
|
||||
{{ FindObjectInArrayByPropertyValue($locations, 'id', $stockLogEntry->location_id)->name }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -140,6 +140,12 @@
|
|||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-utensils"></i> {{ $__t('Consume') }}
|
||||
</a>
|
||||
@if(GROCY_FEATURE_FLAG_STOCK_LOCATION_TRACKING)
|
||||
<a class="dropdown-item product-transfer-button @if($currentStockEntry->amount < 1) disabled @endif" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-exchange-alt"></i> {{ $__t('Transfer') }}
|
||||
</a>
|
||||
@endif
|
||||
<a class="dropdown-item product-inventory-button" type="button" href="#"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-list"></i> {{ $__t('Inventory') }}
|
||||
|
|
@ -148,6 +154,10 @@
|
|||
<a class="dropdown-item product-name-cell" data-product-id="{{ $currentStockEntry->product_id }}" type="button" href="#">
|
||||
<i class="fas fa-info"></i> {{ $__t('Show product details') }}
|
||||
</a>
|
||||
<a class="dropdown-item" type="button" href="{{ $U('/stockdetail?product=') }}{{ $currentStockEntry->product_id }}"
|
||||
data-product-id="{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-boxes"></i> {{ $__t('Show stock entries') }}
|
||||
</a>
|
||||
<a class="dropdown-item" type="button" href="{{ $U('/stockjournal?product=') }}{{ $currentStockEntry->product_id }}">
|
||||
<i class="fas fa-file-alt"></i> {{ $__t('Stock journal for this product') }}
|
||||
</a>
|
||||
|
|
|
|||
84
views/transfer.blade.php
Normal file
84
views/transfer.blade.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
@extends('layout.default')
|
||||
|
||||
@section('title', $__t('Transfer'))
|
||||
@section('activeNav', 'transfer')
|
||||
@section('viewJsName', 'transfer')
|
||||
|
||||
@section('content')
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6 col-xl-4 pb-3">
|
||||
<h1>@yield('title')</h1>
|
||||
|
||||
<form id="transfer-form" novalidate>
|
||||
|
||||
@include('components.productpicker', array(
|
||||
'products' => $products,
|
||||
'nextInputSelector' => '#location_id_from',
|
||||
'disallowAddProductWorkflows' => true
|
||||
))
|
||||
|
||||
@php /*@include('components.locationpicker', array(
|
||||
'id' => 'location_from',
|
||||
'locations' => $locations,
|
||||
'isRequired' => true,
|
||||
'label' => 'Transfer From Location'
|
||||
))*/ @endphp
|
||||
|
||||
<div class="form-group">
|
||||
<label for="location_id_from">{{ $__t('From location') }}</label>
|
||||
<select required class="form-control location-combobox" id="location_id_from" name="location_id_from">
|
||||
<option></option>
|
||||
@foreach($locations as $location)
|
||||
<option value="{{ $location->id }}">{{ $location->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="invalid-feedback">{{ $__t('A location is required') }}</div>
|
||||
</div>
|
||||
|
||||
@include('components.numberpicker', array(
|
||||
'id' => 'amount',
|
||||
'label' => 'Amount',
|
||||
'hintId' => 'amount_qu_unit',
|
||||
'min' => 1,
|
||||
'value' => 1,
|
||||
'invalidFeedback' => $__t('The amount cannot be lower than %s', '1'),
|
||||
'additionalHtmlContextHelp' => '<div id="tare-weight-handling-info" class="text-info font-italic d-none">' . $__t('Tare weight handling enabled - please weigh the whole container, the amount to be posted will be automatically calculcated') . '</div>'
|
||||
))
|
||||
|
||||
<div class="form-group">
|
||||
<label for="use_specific_stock_entry">
|
||||
<input type="checkbox" id="use_specific_stock_entry" name="use_specific_stock_entry"> {{ $__t('Use a specific stock item') }}
|
||||
<span class="small text-muted">{{ $__t('The first item in this list would be picked by the default rule which is "First expiring first, then first in first out"') }}</span>
|
||||
</label>
|
||||
<select disabled class="form-control" id="specific_stock_entry" name="specific_stock_entry">
|
||||
<option></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@php /*@include('components.locationpicker', array(
|
||||
'locations' => $locations,
|
||||
'isRequired' => true,
|
||||
'label' => 'Transfer to Location'
|
||||
))*/ @endphp
|
||||
|
||||
<div class="form-group">
|
||||
<label for="location_id_to">{{ $__t('To location') }}</label>
|
||||
<select required class="form-control location-combobox" id="location_id_to" name="location_id_to">
|
||||
<option></option>
|
||||
@foreach($locations as $location)
|
||||
<option value="{{ $location->id }}">{{ $location->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="invalid-feedback">{{ $__t('A location is required') }}</div>
|
||||
</div>
|
||||
|
||||
<button id="save-transfer-button" class="btn btn-success">{{ $__t('OK') }}</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6 col-xl-4 hide-when-embedded">
|
||||
@include('components.productcard')
|
||||
</div>
|
||||
</div>
|
||||
@stop
|
||||
Loading…
Reference in New Issue
Block a user