From 7a51fb77b06ab843a511e38148432aac05ebe3e9 Mon Sep 17 00:00:00 2001 From: grocy Date: Thu, 20 Jun 2019 23:10:30 -0500 Subject: [PATCH 01/26] feat: Added recipes/requirements route - feat: Added requirements route to allow clients to access the requirements fulfilments of recipes --- controllers/RecipesApiController.php | 106 +- grocy.openapi.json | 5760 ++++++++++++++------------ routes.php | 2 + 3 files changed, 3181 insertions(+), 2687 deletions(-) diff --git a/controllers/RecipesApiController.php b/controllers/RecipesApiController.php index 5cfa1ce2..819fad6b 100644 --- a/controllers/RecipesApiController.php +++ b/controllers/RecipesApiController.php @@ -1,43 +1,63 @@ -RecipesService = new RecipesService(); - } - - protected $RecipesService; - - public function AddNotFulfilledProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) - { - $requestBody = $request->getParsedBody(); - $excludedProductIds = null; - - if ($requestBody !== null && array_key_exists('excludedProductIds', $requestBody)) - { - $excludedProductIds = $requestBody['excludedProductIds']; - } - - $this->RecipesService->AddNotFulfilledProductsToShoppingList($args['recipeId'], $excludedProductIds); - return $this->EmptyApiResponse($response); - } - - public function ConsumeRecipe(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) - { - try - { - $this->RecipesService->ConsumeRecipe($args['recipeId']); - return $this->EmptyApiResponse($response); - } - catch (\Exception $ex) - { - return $this->GenericErrorResponse($response, $ex->getMessage()); - } - } -} +RecipesService = new RecipesService(); + } + + protected $RecipesService; + + public function AddNotFulfilledProductsToShoppingList(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $requestBody = $request->getParsedBody(); + $excludedProductIds = null; + + if ($requestBody !== null && array_key_exists('excludedProductIds', $requestBody)) + { + $excludedProductIds = $requestBody['excludedProductIds']; + } + + $this->RecipesService->AddNotFulfilledProductsToShoppingList($args['recipeId'], $excludedProductIds); + return $this->EmptyApiResponse($response); + } + + public function ConsumeRecipe(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + $this->RecipesService->ConsumeRecipe($args['recipeId']); + return $this->EmptyApiResponse($response); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } + + public function GetRecipeRequirements(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try { + if(!$args['recipeId']){ + return $this->ApiResponse($this->RecipesService->GetRecipesResolved()); + } + $recipeResolved = FindObjectInArrayByPropertyValue($this->RecipesService->GetRecipesResolved(), 'recipe_id', $args['recipeId']); + if(!$recipeResolved) { + $errorMsg ='Recipe requirments do not exist for recipe_id ' . $args['recipe_id']; + $GenericError = $this->GenericErrorResponse($response, $errorMsg); + return $GenericError; + } + return $this->ApiResponse($recipeResolved); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } +} diff --git a/grocy.openapi.json b/grocy.openapi.json index 5927f4c0..87a61d88 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1,2644 +1,3116 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "grocy REST API", - "description": "Authentication is done via API keys (header *GROCY-API-KEY*), which you can manage [here](PlaceHolderManageApiKeysUrl).
Additionally requests from within the frontend are also valid (via session cookie).", - "version": "xxx", - "contact": { - "email": "bernd@berrnd.de" - }, - "license": { - "name": "grocy.info", - "url": "https://grocy.info" - } - }, - "servers": [ - { - "url": "xxx" - } - ], - "tags": [ - { - "name": "Generic entity interactions", - "description": "A limited set of entities are directly exposed for convenience" - } - ], - "paths": { - "/system/info": { - "get": { - "summary": "Returns information about the installed grocy, PHP and SQLite version", - "tags": [ - "System" - ], - "responses": { - "200": { - "description": "An DbChangedTimeResponse object", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "grocy_version": { - "type": "object", - "properties": { - "Version": { - "type": "string" - }, - "ReleaseDate": { - "type": "string", - "format": "date" - } - } - }, - "php_version": { - "type": "string" - }, - "sqlite_version": { - "type": "string" - } - } - } - } - } - } - } - } - }, - "/system/db-changed-time": { - "get": { - "summary": "Returns the time when the database was last changed", - "tags": [ - "System" - ], - "responses": { - "200": { - "description": "An DbChangedTimeResponse object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DbChangedTimeResponse" - } - } - } - } - } - } - }, - "/system/log-missing-localization": { - "post": { - "summary": "Logs a missing localization string", - "description": "Only when MODE == 'dev', so should only be called then", - "tags": [ - "System" - ], - "requestBody": { - "description": "A valid MissingLocalizationRequest object", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MissingLocalizationRequest" - } - } - } - }, - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/objects/{entity}": { - "get": { - "summary": "Returns all objects of the given entity", - "tags": [ - "Generic entity interactions" - ], - "parameters": [ - { - "in": "path", - "name": "entity", - "required": true, - "description": "A valid entity name", - "schema": { - "$ref": "#/components/internalSchemas/ExposedEntity" - } - } - ], - "responses": { - "200": { - "description": "An entity object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/Product" - }, - { - "$ref": "#/components/schemas/Chore" - }, - { - "$ref": "#/components/schemas/Battery" - }, - { - "$ref": "#/components/schemas/Location" - }, - { - "$ref": "#/components/schemas/QuantityUnit" - }, - { - "$ref": "#/components/schemas/ShoppingListItem" - }, - { - "$ref": "#/components/schemas/StockEntry" - } - ] - } - } - } - } - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - }, - "post": { - "summary": "Adds a single object of the given entity", - "tags": [ - "Generic entity interactions" - ], - "parameters": [ - { - "in": "path", - "name": "entity", - "required": true, - "description": "A valid entity name", - "schema": { - "$ref": "#/components/internalSchemas/ExposedEntity" - } - } - ], - "requestBody": { - "description": "A valid entity object of the entity specified in parameter *entity*", - "required": true, - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/Product" - }, - { - "$ref": "#/components/schemas/Chore" - }, - { - "$ref": "#/components/schemas/Battery" - }, - { - "$ref": "#/components/schemas/Location" - }, - { - "$ref": "#/components/schemas/QuantityUnit" - }, - { - "$ref": "#/components/schemas/ShoppingListItem" - }, - { - "$ref": "#/components/schemas/StockEntry" - } - ] - } - } - } - }, - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/objects/{entity}/{objectId}": { - "get": { - "summary": "Returns a single object of the given entity", - "tags": [ - "Generic entity interactions" - ], - "parameters": [ - { - "in": "path", - "name": "entity", - "required": true, - "description": "A valid entity name", - "schema": { - "$ref": "#/components/internalSchemas/ExposedEntity" - } - }, - { - "in": "path", - "name": "objectId", - "required": true, - "description": "A valid object id of the given entity", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "An entity object", - "content": { - "application/json": { - "schema":{ - "type": "object", - "oneOf": [ - { - "$ref": "#/components/schemas/Product" - }, - { - "$ref": "#/components/schemas/Chore" - }, - { - "$ref": "#/components/schemas/Battery" - }, - { - "$ref": "#/components/schemas/Location" - }, - { - "$ref": "#/components/schemas/QuantityUnit" - }, - { - "$ref": "#/components/schemas/ShoppingListItem" - }, - { - "$ref": "#/components/schemas/StockEntry" - } - ] - } - } - } - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - }, - "put": { - "summary": "Edits the given object of the given entity", - "tags": [ - "Generic entity interactions" - ], - "parameters": [ - { - "in": "path", - "name": "entity", - "required": true, - "description": "A valid entity name", - "schema": { - "$ref": "#/components/internalSchemas/ExposedEntity" - } - }, - { - "in": "path", - "name": "objectId", - "required": true, - "description": "A valid object id of the given entity", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "description": "A valid entity object of the entity specified in parameter *entity*", - "required": true, - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/Product" - }, - { - "$ref": "#/components/schemas/Chore" - }, - { - "$ref": "#/components/schemas/Battery" - }, - { - "$ref": "#/components/schemas/Location" - }, - { - "$ref": "#/components/schemas/QuantityUnit" - }, - { - "$ref": "#/components/schemas/ShoppingListItem" - }, - { - "$ref": "#/components/schemas/StockEntry" - } - ] - } - } - } - }, - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - }, - "delete": { - "summary": "Deletes a single object of the given entity", - "tags": [ - "Generic entity interactions" - ], - "parameters": [ - { - "in": "path", - "name": "entity", - "required": true, - "description": "A valid entity name", - "schema": { - "$ref": "#/components/internalSchemas/ExposedEntity" - } - }, - { - "in": "path", - "name": "objectId", - "required": true, - "description": "A valid object id of the given entity", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/files/{group}/{fileName}": { - "get": { - "summary": "Serves the given file", - "description": "With proper Content-Type header", - "tags": [ - "Files" - ], - "parameters": [ - { - "in": "path", - "name": "group", - "required": true, - "description": "The file group", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "fileName", - "required": true, - "description": "The file name (including extension)
**BASE64 encoded**", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "The binary file contents (Content-Type header is automatically set based on the file type)", - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - }, - "put": { - "summary": "Uploads a single file", - "description": "The file will be stored at /data/storage/{group}/{file_name} (you need to remember the group and file name to get or delete it again)", - "tags": [ - "Files" - ], - "parameters": [ - { - "in": "path", - "name": "group", - "required": true, - "description": "The file group", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "fileName", - "required": true, - "description": "The file name (including extension)
**BASE64 encoded**", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/octet-stream": { - "schema": { - "type": "string", - "format": "binary" - } - } - } - }, - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - }, - "delete": { - "summary": "Deletes the given file", - "tags": [ - "Files" - ], - "parameters": [ - { - "in": "path", - "name": "group", - "required": true, - "description": "The file group", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "fileName", - "required": true, - "description": "The file name (including extension)
**BASE64 encoded**", - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/users": { - "get": { - "summary": "Returns all users", - "tags": [ - "User management" - ], - "responses": { - "200": { - "description": "A list of user objects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UserDto" - } - } - } - } - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - }, - "post": { - "summary": "Creates a new user", - "tags": [ - "User management" - ], - "requestBody": { - "description": "A valid user object", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/users/{userId}": { - "put": { - "summary": "Edits the given user", - "tags": [ - "User management" - ], - "parameters": [ - { - "in": "path", - "name": "userId", - "required": true, - "description": "A valid user id", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "description": "A valid user object", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/User" - } - } - } - }, - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - }, - "delete": { - "summary": "Deletes the given user", - "tags": [ - "User management" - ], - "parameters": [ - { - "in": "path", - "name": "userId", - "required": true, - "description": "A valid user id", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/user/settings/{settingKey}": { - "get": { - "summary": "Gets the given setting of the currently logged in user", - "tags": [ - "User settings" - ], - "parameters": [ - { - "in": "path", - "name": "settingKey", - "required": true, - "description": "The key of the user setting", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "A UserSetting object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserSetting" - } - } - } - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - }, - "put": { - "summary": "Sets the given setting of the currently logged in user", - "tags": [ - "User settings" - ], - "requestBody": { - "description": "A valid UserSetting object", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserSetting" - } - } - } - }, - "parameters": [ - { - "in": "path", - "name": "settingKey", - "required": true, - "description": "The key of the user setting", - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/stock": { - "get": { - "summary": "Returns all products which are currently in stock incl. the next expiring date per product", - "tags": [ - "Stock" - ], - "responses": { - "200": { - "description": "An array of CurrentStockResponse objects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CurrentStockResponse" - } - } - } - } - } - } - } - }, - "/stock/volatile": { - "get": { - "summary": "Returns all products which are expiring soon, are already expired or currently missing", - "tags": [ - "Stock" - ], - "parameters": [ - { - "in": "path", - "name": "expiring_days", - "required": false, - "description": "The number of days in which products are considered expiring soon", - "schema": { - "type": "integer", - "default": 5 - } - } - ], - "responses": { - "200": { - "description": "A CurrentVolatilStockResponse object", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CurrentVolatilStockResponse" - } - } - } - } - } - } - } - }, - "/stock/products/{productId}": { - "get": { - "summary": "Returns details of the given product", - "tags": [ - "Stock" - ], - "parameters": [ - { - "in": "path", - "name": "productId", - "required": true, - "description": "A valid product id", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "A ProductDetailsResponse object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductDetailsResponse" - } - } - } - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing product)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/stock/products/by-barcode/{barcode}": { - "get": { - "summary": "Returns details of the given product by its barcode", - "tags": [ - "Stock" - ], - "parameters": [ - { - "in": "path", - "name": "barcode", - "required": true, - "description": "Barcode", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "A ProductDetailsResponse object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProductDetailsResponse" - } - } - } - }, - "400": { - "description": "The operation was not successful (possible errors are: Unknown barcode)", - "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)", - "tags": [ - "Stock" - ], - "parameters": [ - { - "in": "path", - "name": "productId", - "required": true, - "description": "A valid product id", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "An array of StockEntry objects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/StockEntry" - } - } - } - } - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing product)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/stock/products/{productId}/price-history": { - "get": { - "summary": "Returns the price history of the given product", - "tags": [ - "Stock" - ], - "parameters": [ - { - "in": "path", - "name": "productId", - "required": true, - "description": "A valid product id", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "An array of ProductPriceHistory objects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProductPriceHistory" - } - } - } - } - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing product)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/stock/products/{productId}/add": { - "post": { - "summary": "Adds the given amount of the given product to stock", - "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", - "format": "double", - "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" - }, - "transaction_type": { - "$ref": "#/components/internalSchemas/StockTransactionType" - }, - "price": { - "type": "number", - "format": "double", - "description": "The price per purchase quantity unit in configured currency" - }, - "location_id": { - "type": "number", - "format": "integer", - "description": "If omitted, the default location of the product is used" - } - }, - "example": { - "amount": 1, - "best_before_date": "2019-01-19", - "transaction_type": "purchase", - "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/products/{productId}/consume": { - "post": { - "summary": "Removes the given amount of the given product from stock", - "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": "double", - "description": "The amount to remove - 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" - }, - "transaction_type": { - "$ref": "#/components/internalSchemas/StockTransactionType" - }, - "spoiled": { - "type": "boolean", - "description": "True when the given product was spoiled, defaults to false" - }, - "stock_entry_id": { - "type": "string", - "description": "A specific stock entry id to consume, if used, the amount has to be 1" - }, - "recipe_id": { - "type": "number", - "format": "integer", - "description": "A valid recipe id for which this product was used (for statistical purposes only)" - } - }, - "example": { - "amount": 1, - "transaction_type": "consume", - "spoiled": false - } - } - } - } - }, - "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, given amount > current stock amount)", - "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)", - "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": { - "new_amount": { - "type": "integer", - "description": "The new current amount for the given product - 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 which applies to added products" - } - } - } - } - } - }, - "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)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/stock/products/{productId}/open": { - "post": { - "summary": "Marks the given amount of the given product as opened", - "tags": [ - "Stock" - ], - "parameters": [ - { - "in": "path", - "name": "productId", - "required": true, - "description": "A valid product id", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "amount": { - "type": "double", - "description": "The amount to mark as opened" - }, - "stock_entry_id": { - "type": "string", - "description": "A specific stock entry id to open, if used, the amount has to be 1" - } - }, - "example": { - "amount": 1 - } - } - } - } - }, - "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, given amount > current unopened stock amount)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/stock/shoppinglist/add-missing-products": { - "post": { - "summary": "Adds currently missing products (below defined min. stock amount) to the shopping list", - "tags": [ - "Stock" - ], - "responses": { - "204": { - "description": "The operation was successful" - } - } - } - }, - "/stock/shoppinglist/clear": { - "post": { - "summary": "Removes all items from the shopping list", - "tags": [ - "Stock" - ], - "responses": { - "204": { - "description": "The operation was successful" - } - } - } - }, - "/stock/bookings/{bookingId}/undo": { - "post": { - "summary": "Undoes a booking", - "tags": [ - "Stock" - ], - "parameters": [ - { - "in": "path", - "name": "bookingId", - "required": true, - "description": "A valid stock booking id", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing booking)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/stock/barcodes/external-lookup": { - "get": { - "summary": "Executes an external barcode lookoup via the configured plugin with the given barcode", - "tags": [ - "Stock" - ], - "parameters": [ - { - "in": "query", - "name": "barcode", - "required": true, - "description": "The barcode to lookup up", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "add", - "required": false, - "description": "When true, the product is added to the database on a successful lookup and the new product id is in included in the response", - "schema": { - "type": "boolean", - "default": false - } - } - ], - "responses": { - "200": { - "description": "An ExternalBarcodeLookupResponse object or null, when nothing was found for the given barcode", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExternalBarcodeLookupResponse" - } - } - } - }, - "400": { - "description": "The operation was not successful (possible errors are: Plugin error)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/recipes/{recipeId}/add-not-fulfilled-products-to-shoppinglist": { - "post": { - "summary": "Adds all missing products for the given recipe to the shopping list", - "tags": [ - "Recipes" - ], - "parameters": [ - { - "in": "path", - "name": "recipeId", - "required": true, - "description": "A valid recipe id", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": false, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "excludedProductIds": { - "type": "array", - "items":{ - "type": "number", - "format": "integer" - }, - "description": "An optional array of product ids to exclude them from being put on the shopping list" - } - } - } - } - } - }, - "responses": { - "204": { - "description": "The operation was successful" - } - } - } - }, - "/recipes/{recipeId}/consume": { - "post": { - "summary": "Consumes all products of the given recipe", - "tags": [ - "Recipes" - ], - "parameters": [ - { - "in": "path", - "name": "recipeId", - "required": true, - "description": "A valid recipe id", - "schema": { - "type": "string" - } - } - ], - "responses": { - "204": { - "description": "The operation was successful" - } - } - } - }, - "/chores": { - "get": { - "summary": "Returns all chores incl. the next estimated execution time per chore", - "tags": [ - "Chores" - ], - "responses": { - "200": { - "description": "An array of CurrentChoreResponse objects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CurrentChoreResponse" - } - } - } - } - } - } - } - }, - "/chores/{choreId}": { - "get": { - "summary": "Returns details of the given chore", - "tags": [ - "Chores" - ], - "parameters": [ - { - "in": "path", - "name": "choreId", - "required": true, - "description": "A valid chore id", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "A ChoreDetailsResponse object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChoreDetailsResponse" - } - } - } - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing chore)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/chores/{choreId}/execute": { - "post": { - "summary": "Tracks an execution of the given chore", - "tags": [ - "Chores" - ], - "parameters": [ - { - "in": "path", - "name": "choreId", - "required": true, - "description": "A valid chore id", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tracked_time": { - "type": "string", - "format": "date-time", - "description": "The time of when the chore was executed, when omitted, the current time is used" - }, - "done_by": { - "type": "integer", - "description": "A valid user id of who executed this chore, when omitted, the currently authenticated user will be used" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "The operation was successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ChoreLogEntry" - } - } - } - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing chore)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/chores/executions/{executionId}/undo": { - "post": { - "summary": "Undoes a chore execution", - "tags": [ - "Chores" - ], - "parameters": [ - { - "in": "path", - "name": "executionId", - "required": true, - "description": "A valid chore execution id", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing booking)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/batteries": { - "get": { - "summary": "Returns all batteries incl. the next estimated charge time per battery", - "tags": [ - "Batteries" - ], - "responses": { - "200": { - "description": "An array of CurrentBatteryResponse objects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CurrentBatteryResponse" - } - } - } - } - } - } - } - }, - "/batteries/{batteryId}": { - "get": { - "summary": "Returns details of the given battery", - "tags": [ - "Batteries" - ], - "parameters": [ - { - "in": "path", - "name": "batteryId", - "required": true, - "description": "A valid battery id", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "A BatteryDetailsResponse object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatteryDetailsResponse" - } - } - } - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing battery)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/batteries/{batteryId}/charge": { - "post": { - "summary": "Tracks a charge cycle of the given battery", - "tags": [ - "Batteries" - ], - "parameters": [ - { - "in": "path", - "name": "batteryId", - "required": true, - "description": "A valid battery id", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tracked_time": { - "type": "string", - "format": "date-time", - "description": "The time of when the battery was charged, when omitted, the current time is used" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "The operation was successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BatteryChargeCycleEntry" - } - } - } - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing battery)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/batteries/charge-cycles/{chargeCycleId}/undo": { - "post": { - "summary": "Undoes a battery charge cycle", - "tags": [ - "Batteries" - ], - "parameters": [ - { - "in": "path", - "name": "chargeCycleId", - "required": true, - "description": "A valid charge cycle id", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing booking)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/tasks": { - "get": { - "summary": "Returns all tasks which are not done yet", - "tags": [ - "Tasks" - ], - "responses": { - "200": { - "description": "An array of Task objects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Task" - } - } - } - } - } - } - } - }, - "/tasks/{taskId}/complete": { - "post": { - "summary": "Marks the given task as completed", - "tags": [ - "Tasks" - ], - "parameters": [ - { - "in": "path", - "name": "taskId", - "required": true, - "description": "A valid task id", - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "done_time": { - "type": "string", - "format": "date-time", - "description": "The time of when the task was completed, when omitted, the current time is used" - } - } - } - } - } - }, - "responses": { - "204": { - "description": "The operation was successful" - }, - "400": { - "description": "The operation was not successful (possible errors are: Not existing task)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/calendar/ical": { - "get": { - "summary": "Returns the calendar in iCal format", - "tags": [ - "Calendar" - ], - "responses": { - "200": { - "description": "The iCal file contents", - "content": { - "text/calendar": { - "schema": { - "type": "string" - } - } - } - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/calendar/ical/sharing-link": { - "get": { - "summary": "Returns a (public) sharing link for the calendar in iCal format", - "tags": [ - "Calendar" - ], - "responses": { - "200": { - "description": "The (public) sharing link for the calendar in iCal format", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - } - } - } - } - } - } - } - } - }, - "components": { - "internalSchemas": { - "ExposedEntity": { - "type": "string", - "enum": [ - "products", - "chores", - "batteries", - "locations", - "quantity_units", - "shopping_list", - "recipes", - "recipes_pos", - "recipes_nestings", - "tasks", - "task_categories", - "product_groups", - "equipment", - "api_keys" - ] - }, - "ExposedEntitiesPreventListing": { - "type": "string", - "enum": [ - "api_keys" - ] - }, - "StockTransactionType": { - "type": "string", - "enum": [ - "purchase", - "consume", - "inventory-correction", - "product-opened" - ] - } - }, - "schemas": { - "Product": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "location_id": { - "type": "integer" - }, - "qu_id_purchase": { - "type": "integer" - }, - "qu_id_stock": { - "type": "integer" - }, - "qu_factor_purchase_to_stock": { - "type": "number", - "format": "double" - }, - "barcode": { - "type": "string", - "description": "Can contain multiple barcodes separated by comma" - }, - "min_stock_amount": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "default_best_before_days": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "picture_file_name": { - "type": "string" - }, - "allow_partial_units_in_stock": { - "type": "boolean" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "QuantityUnit": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "name_plural": { - "type": "string" - }, - "description": { - "type": "string" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "Location": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "StockEntry": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "product_id": { - "type": "integer" - }, - "amount": { - "type": "double" - }, - "best_before_date": { - "type": "string", - "format": "date" - }, - "purchased_date": { - "type": "string", - "format": "date" - }, - "stock_id": { - "type": "string", - "description": "A unique id which references this stock entry during its lifetime" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "ProductDetailsResponse": { - "type": "object", - "properties": { - "product": { - "$ref": "#/components/schemas/Product" - }, - "quantity_unit_purchase": { - "$ref": "#/components/schemas/QuantityUnit" - }, - "quantity_unit_stock": { - "$ref": "#/components/schemas/QuantityUnit" - }, - "last_purchased": { - "type": "string", - "format": "date" - }, - "last_used": { - "type": "string", - "format": "date-time" - }, - "stock_amount": { - "type": "integer" - }, - "stock_amount_opened": { - "type": "integer" - }, - "next_best_before_date": { - "type": "string", - "format": "date-time" - }, - "last_price": { - "type": "number", - "format": "double" - }, - "location": { - "$ref": "#/components/schemas/Location" - } - } - }, - "ProductPriceHistory": { - "type": "object", - "properties": { - "date": { - "type": "string", - "format": "date-time" - }, - "price": { - "type": "number", - "format": "double" - } - } - }, - "ExternalBarcodeLookupResponse": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "location_id": { - "type": "integer" - }, - "qu_id_purchase": { - "type": "integer" - }, - "qu_id_stock": { - "type": "integer" - }, - "qu_factor_purchase_to_stock": { - "type": "number", - "format": "double" - }, - "barcode": { - "type": "string", - "description": "Can contain multiple barcodes separated by comma" - }, - "id": { - "type": "integer", - "description": "The id of the added product, only included when the producted was added to the database" - } - } - }, - "ChoreDetailsResponse": { - "type": "object", - "properties": { - "chore": { - "$ref": "#/components/schemas/Chore" - }, - "last_tracked": { - "type": "string", - "format": "date-time", - "description": "When this chore was last tracked" - }, - "track_count": { - "type": "integer", - "description": "How often this chore was tracked so far" - }, - "last_done_by": { - "$ref": "#/components/schemas/UserDto" - }, - "next_estimated_execution_time": { - "type": "string", - "format": "date-time" - } - } - }, - "BatteryDetailsResponse": { - "type": "object", - "properties": { - "chore": { - "$ref": "#/components/schemas/Battery" - }, - "last_charged": { - "type": "string", - "format": "date-time", - "description": "When this battery was last charged" - }, - "charge_cycles_count": { - "type": "integer", - "description": "How often this battery was charged so far" - }, - "next_estimated_charge_time": { - "type": "string", - "format": "date-time" - } - } - }, - "Session": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "session_key": { - "type": "string" - }, - "expires": { - "type": "string", - "format": "date-time" - }, - "last_used": { - "type": "string", - "format": "date-time" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "username": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "password": { - "type": "string" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "UserDto": { - "type": "object", - "description": "A user object without the *password* and with an additional *display_name* property", - "properties": { - "id": { - "type": "integer" - }, - "username": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "display_name": { - "type": "string" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "ApiKey": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "api_key": { - "type": "string" - }, - "expires": { - "type": "string", - "format": "date-time" - }, - "last_used": { - "type": "string", - "format": "date-time" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "ShoppingListItem": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "product_id": { - "type": "integer" - }, - "note": { - "type": "string" - }, - "amount": { - "type": "double", - "minimum": 0, - "default": 0, - "description": "The manual entered amount" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "Battery": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "used_in": { - "type": "string" - }, - "charge_interval_days": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "BatteryChargeCycleEntry": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "battery_id": { - "type": "integer" - }, - "tracked_time": { - "type": "string", - "format": "date-time" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "Chore": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "period_type": { - "type": "string", - "enum": [ - "manually", - "dynamic-regular" - ] - }, - "period_days": { - "type": "integer" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "ChoreLogEntry": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "chore_id": { - "type": "integer" - }, - "tracked_time": { - "type": "string", - "format": "date-time" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "StockLogEntry": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "product_id": { - "type": "integer" - }, - "amount": { - "type": "double" - }, - "best_before_date": { - "type": "string", - "format": "date" - }, - "purchased_date": { - "type": "string", - "format": "date-time" - }, - "used_date": { - "type": "string", - "format": "date-time" - }, - "spoiled": { - "type": "boolean", - "default": false - }, - "stock_id": { - "type": "string" - }, - "transaction_type": { - "$ref": "#/components/internalSchemas/StockTransactionType" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "GenericErrorResponse": { - "type": "object", - "properties": { - "error_message": { - "type": "string" - } - }, - "example": { - "error_message": "The error message..." - } - }, - "CurrentStockResponse": { - "type": "object", - "properties": { - "product_id": { - "type": "integer" - }, - "amount": { - "type": "double" - }, - "best_before_date": { - "type": "string", - "format": "date", - "description": "The next best before date for this product" - } - } - }, - "CurrentChoreResponse": { - "type": "object", - "properties": { - "chore_id": { - "type": "integer" - }, - "last_tracked_time": { - "type": "string", - "format": "date-time" - }, - "next_estimated_execution_time": { - "type": "string", - "format": "date-time", - "description": "The next estimated execution time of this chore, 2999-12-31 23:59:59 when the given chore has a period_type of manually" - } - } - }, - "CurrentBatteryResponse": { - "type": "object", - "properties": { - "battery_id": { - "type": "integer" - }, - "last_tracked_time": { - "type": "string", - "format": "date-time" - }, - "next_estimated_charge_time": { - "type": "string", - "format": "date-time", - "description": "The next estimated charge time of this battery, 2999-12-31 23:59:59 when the given battery has no charge_interval_days defined" - } - } - }, - "CurrentVolatilStockResponse": { - "type": "object", - "properties": { - "expiring_products": { - "type": "array", - "items":{ - "$ref": "#/components/schemas/Product" - } - }, - "expired_products": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Product" - } - }, - "missing_products": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Product" - } - } - } - }, - "Task": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "due_date": { - "type": "string", - "format": "date-time" - }, - "done": { - "type": "integer" - }, - "done_timestamp": { - "type": "string", - "format": "date-time" - }, - "category_id": { - "type": "integer" - }, - "assigned_to_user_id": { - "type": "integer" - }, - "row_created_timestamp": { - "type": "string", - "format": "date-time" - } - } - }, - "DbChangedTimeResponse": { - "type": "object", - "properties": { - "changed_time": { - "type": "string", - "format": "date-time" - } - } - }, - "UserSetting": { - "type": "object", - "properties": { - "value": { - "type": "string" - } - } - }, - "MissingLocalizationRequest": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - } - } - }, - "securitySchemes": { - "ApiKeyAuth": { - "type": "apiKey", - "in": "header", - "name": "GROCY-API-KEY" - } - } - }, - "security": [ - { - "ApiKeyAuth": [ ] - } - ] -} +{ + "openapi": "3.0.0", + "info": { + "title": "grocy REST API", + "description": "Authentication is done via API keys (header *GROCY-API-KEY*), which you can manage [here](PlaceHolderManageApiKeysUrl).
Additionally requests from within the frontend are also valid (via session cookie).", + "version": "xxx", + "contact": { + "email": "bernd@berrnd.de" + }, + "license": { + "name": "grocy.info", + "url": "https://grocy.info" + } + }, + "servers": [ + { + "url": "xxx" + } + ], + "tags": [ + { + "name": "Generic entity interactions", + "description": "A limited set of entities are directly exposed for convenience" + } + ], + "paths": { + "/system/info": { + "get": { + "summary": "Returns information about the installed grocy, PHP and SQLite version", + "tags": [ + "System" + ], + "responses": { + "200": { + "description": "An DbChangedTimeResponse object", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "grocy_version": { + "type": "object", + "properties": { + "Version": { + "type": "string" + }, + "ReleaseDate": { + "type": "string", + "format": "date" + } + } + }, + "php_version": { + "type": "string" + }, + "sqlite_version": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "/system/db-changed-time": { + "get": { + "summary": "Returns the time when the database was last changed", + "tags": [ + "System" + ], + "responses": { + "200": { + "description": "An DbChangedTimeResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DbChangedTimeResponse" + } + } + } + } + } + } + }, + "/system/log-missing-localization": { + "post": { + "summary": "Logs a missing localization string", + "description": "Only when MODE == 'dev', so should only be called then", + "tags": [ + "System" + ], + "requestBody": { + "description": "A valid MissingLocalizationRequest object", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MissingLocalizationRequest" + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/objects/{entity}": { + "get": { + "summary": "Returns all objects of the given entity", + "tags": [ + "Generic entity interactions" + ], + "parameters": [ + { + "in": "path", + "name": "entity", + "required": true, + "description": "A valid entity name", + "schema": { + "$ref": "#/components/internalSchemas/ExposedEntity" + } + } + ], + "responses": { + "200": { + "description": "An entity object", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/Product" + }, + { + "$ref": "#/components/schemas/Chore" + }, + { + "$ref": "#/components/schemas/Battery" + }, + { + "$ref": "#/components/schemas/Location" + }, + { + "$ref": "#/components/schemas/QuantityUnit" + }, + { + "$ref": "#/components/schemas/ShoppingListItem" + }, + { + "$ref": "#/components/schemas/StockEntry" + } + ] + } + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + }, + "post": { + "summary": "Adds a single object of the given entity", + "tags": [ + "Generic entity interactions" + ], + "parameters": [ + { + "in": "path", + "name": "entity", + "required": true, + "description": "A valid entity name", + "schema": { + "$ref": "#/components/internalSchemas/ExposedEntity" + } + } + ], + "requestBody": { + "description": "A valid entity object of the entity specified in parameter *entity*", + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Product" + }, + { + "$ref": "#/components/schemas/Chore" + }, + { + "$ref": "#/components/schemas/Battery" + }, + { + "$ref": "#/components/schemas/Location" + }, + { + "$ref": "#/components/schemas/QuantityUnit" + }, + { + "$ref": "#/components/schemas/ShoppingListItem" + }, + { + "$ref": "#/components/schemas/StockEntry" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "created_object_id": { + "type": "number", + "format": "integer", + "description": "The id of the created object" + } + } + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/objects/{entity}/{objectId}": { + "get": { + "summary": "Returns a single object of the given entity", + "tags": [ + "Generic entity interactions" + ], + "parameters": [ + { + "in": "path", + "name": "entity", + "required": true, + "description": "A valid entity name", + "schema": { + "$ref": "#/components/internalSchemas/ExposedEntity" + } + }, + { + "in": "path", + "name": "objectId", + "required": true, + "description": "A valid object id of the given entity", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "An entity object", + "content": { + "application/json": { + "schema":{ + "type": "object", + "oneOf": [ + { + "$ref": "#/components/schemas/Product" + }, + { + "$ref": "#/components/schemas/Chore" + }, + { + "$ref": "#/components/schemas/Battery" + }, + { + "$ref": "#/components/schemas/Location" + }, + { + "$ref": "#/components/schemas/QuantityUnit" + }, + { + "$ref": "#/components/schemas/ShoppingListItem" + }, + { + "$ref": "#/components/schemas/StockEntry" + } + ] + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + }, + "put": { + "summary": "Edits the given object of the given entity", + "tags": [ + "Generic entity interactions" + ], + "parameters": [ + { + "in": "path", + "name": "entity", + "required": true, + "description": "A valid entity name", + "schema": { + "$ref": "#/components/internalSchemas/ExposedEntity" + } + }, + { + "in": "path", + "name": "objectId", + "required": true, + "description": "A valid object id of the given entity", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "description": "A valid entity object of the entity specified in parameter *entity*", + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Product" + }, + { + "$ref": "#/components/schemas/Chore" + }, + { + "$ref": "#/components/schemas/Battery" + }, + { + "$ref": "#/components/schemas/Location" + }, + { + "$ref": "#/components/schemas/QuantityUnit" + }, + { + "$ref": "#/components/schemas/ShoppingListItem" + }, + { + "$ref": "#/components/schemas/StockEntry" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Deletes a single object of the given entity", + "tags": [ + "Generic entity interactions" + ], + "parameters": [ + { + "in": "path", + "name": "entity", + "required": true, + "description": "A valid entity name", + "schema": { + "$ref": "#/components/internalSchemas/ExposedEntity" + } + }, + { + "in": "path", + "name": "objectId", + "required": true, + "description": "A valid object id of the given entity", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/userfields/{entity}/{objectId}": { + "get": { + "summary": "Returns all userfields with their values of the given object of the given entity", + "tags": [ + "Generic entity interactions" + ], + "parameters": [ + { + "in": "path", + "name": "entity", + "required": true, + "description": "A valid entity name", + "schema": { + "$ref": "#/components/internalSchemas/ExposedEntity" + } + }, + { + "in": "path", + "name": "objectId", + "required": true, + "description": "A valid object id of the given entity", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "An entity object", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Just key/value pairs of userfields" + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + }, + "put": { + "summary": "Edits the given userfields of the given object of the given entity", + "tags": [ + "Generic entity interactions" + ], + "parameters": [ + { + "in": "path", + "name": "entity", + "required": true, + "description": "A valid entity name", + "schema": { + "$ref": "#/components/internalSchemas/ExposedEntity" + } + }, + { + "in": "path", + "name": "objectId", + "required": true, + "description": "A valid object id of the given entity", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "description": "A valid entity object of the entity specified in parameter *entity*", + "required": true, + "content": { + "application/json": { + "schema": { + "description": "Just key/value pairs of userfields" + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/files/{group}/{fileName}": { + "get": { + "summary": "Serves the given file", + "description": "With proper Content-Type header", + "tags": [ + "Files" + ], + "parameters": [ + { + "in": "path", + "name": "group", + "required": true, + "description": "The file group", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "fileName", + "required": true, + "description": "The file name (including extension)
**BASE64 encoded**", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The binary file contents (Content-Type header is automatically set based on the file type)", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + }, + "put": { + "summary": "Uploads a single file", + "description": "The file will be stored at /data/storage/{group}/{file_name} (you need to remember the group and file name to get or delete it again)", + "tags": [ + "Files" + ], + "parameters": [ + { + "in": "path", + "name": "group", + "required": true, + "description": "The file group", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "fileName", + "required": true, + "description": "The file name (including extension)
**BASE64 encoded**", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Deletes the given file", + "tags": [ + "Files" + ], + "parameters": [ + { + "in": "path", + "name": "group", + "required": true, + "description": "The file group", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "fileName", + "required": true, + "description": "The file name (including extension)
**BASE64 encoded**", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/users": { + "get": { + "summary": "Returns all users", + "tags": [ + "User management" + ], + "responses": { + "200": { + "description": "A list of user objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDto" + } + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + }, + "post": { + "summary": "Creates a new user", + "tags": [ + "User management" + ], + "requestBody": { + "description": "A valid user object", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/users/{userId}": { + "put": { + "summary": "Edits the given user", + "tags": [ + "User management" + ], + "parameters": [ + { + "in": "path", + "name": "userId", + "required": true, + "description": "A valid user id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "description": "A valid user object", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Deletes the given user", + "tags": [ + "User management" + ], + "parameters": [ + { + "in": "path", + "name": "userId", + "required": true, + "description": "A valid user id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/user/settings/{settingKey}": { + "get": { + "summary": "Gets the given setting of the currently logged in user", + "tags": [ + "User settings" + ], + "parameters": [ + { + "in": "path", + "name": "settingKey", + "required": true, + "description": "The key of the user setting", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A UserSetting object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSetting" + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + }, + "put": { + "summary": "Sets the given setting of the currently logged in user", + "tags": [ + "User settings" + ], + "requestBody": { + "description": "A valid UserSetting object", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserSetting" + } + } + } + }, + "parameters": [ + { + "in": "path", + "name": "settingKey", + "required": true, + "description": "The key of the user setting", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock": { + "get": { + "summary": "Returns all products which are currently in stock incl. the next expiring date per product", + "tags": [ + "Stock" + ], + "responses": { + "200": { + "description": "An array of CurrentStockResponse objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CurrentStockResponse" + } + } + } + } + } + } + } + }, + "/stock/volatile": { + "get": { + "summary": "Returns all products which are expiring soon, are already expired or currently missing", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "expiring_days", + "required": false, + "description": "The number of days in which products are considered expiring soon", + "schema": { + "type": "integer", + "default": 5 + } + } + ], + "responses": { + "200": { + "description": "A CurrentVolatilStockResponse object", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CurrentVolatilStockResponse" + } + } + } + } + } + } + } + }, + "/stock/products/{productId}": { + "get": { + "summary": "Returns details of the given product", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "description": "A valid product id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A ProductDetailsResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductDetailsResponse" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/products/by-barcode/{barcode}": { + "get": { + "summary": "Returns details of the given product by its barcode", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "barcode", + "required": true, + "description": "Barcode", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A ProductDetailsResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProductDetailsResponse" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Unknown barcode)", + "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)", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "description": "A valid product id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "An array of StockEntry objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StockEntry" + } + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/products/{productId}/price-history": { + "get": { + "summary": "Returns the price history of the given product", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "description": "A valid product id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "An array of ProductPriceHistory objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProductPriceHistory" + } + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing product)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/products/{productId}/add": { + "post": { + "summary": "Adds the given amount of the given product to stock", + "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", + "format": "double", + "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" + }, + "transaction_type": { + "$ref": "#/components/internalSchemas/StockTransactionType" + }, + "price": { + "type": "number", + "format": "double", + "description": "The price per purchase quantity unit in configured currency" + }, + "location_id": { + "type": "number", + "format": "integer", + "description": "If omitted, the default location of the product is used" + } + }, + "example": { + "amount": 1, + "best_before_date": "2019-01-19", + "transaction_type": "purchase", + "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/products/{productId}/consume": { + "post": { + "summary": "Removes the given amount of the given product from stock", + "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": "double", + "description": "The amount to remove - 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" + }, + "transaction_type": { + "$ref": "#/components/internalSchemas/StockTransactionType" + }, + "spoiled": { + "type": "boolean", + "description": "True when the given product was spoiled, defaults to false" + }, + "stock_entry_id": { + "type": "string", + "description": "A specific stock entry id to consume, if used, the amount has to be 1" + }, + "recipe_id": { + "type": "number", + "format": "integer", + "description": "A valid recipe id for which this product was used (for statistical purposes only)" + } + }, + "example": { + "amount": 1, + "transaction_type": "consume", + "spoiled": false + } + } + } + } + }, + "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, given amount > current stock amount)", + "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)", + "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": { + "new_amount": { + "type": "integer", + "description": "The new current amount for the given product - 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 which applies to added products" + }, + "location_id": { + "type": "number", + "format": "integer", + "description": "If omitted, the default location of the product is used (only applies to added products)" + }, + "price": { + "type": "number", + "format": "double", + "description": "If omitted, the last price of the product is used (only applies to added products)" + } + } + } + } + } + }, + "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)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/products/{productId}/open": { + "post": { + "summary": "Marks the given amount of the given product as opened", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "description": "A valid product id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "amount": { + "type": "double", + "description": "The amount to mark as opened" + }, + "stock_entry_id": { + "type": "string", + "description": "A specific stock entry id to open, if used, the amount has to be 1" + } + }, + "example": { + "amount": 1 + } + } + } + } + }, + "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, given amount > current unopened stock amount)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/shoppinglist/add-missing-products": { + "post": { + "summary": "Adds currently missing products (below defined min. stock amount) to the given shopping list", + "tags": [ + "Stock" + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list_id": { + "type": "integer", + "description": "The shopping list to use, when omitted, the default shopping list (with id 1) is used" + } + }, + "example": { + "list_id": 2 + } + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing shopping list)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/shoppinglist/clear": { + "post": { + "summary": "Removes all items from the given shopping list", + "tags": [ + "Stock" + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "list_id": { + "type": "integer", + "description": "The shopping list id to clear, when omitted, the default shopping list (with id 1) is used" + } + }, + "example": { + "list_id": 2 + } + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing shopping list)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/bookings/{bookingId}/undo": { + "post": { + "summary": "Undoes a booking", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "path", + "name": "bookingId", + "required": true, + "description": "A valid stock booking id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing booking)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/stock/barcodes/external-lookup": { + "get": { + "summary": "Executes an external barcode lookoup via the configured plugin with the given barcode", + "tags": [ + "Stock" + ], + "parameters": [ + { + "in": "query", + "name": "barcode", + "required": true, + "description": "The barcode to lookup up", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "add", + "required": false, + "description": "When true, the product is added to the database on a successful lookup and the new product id is in included in the response", + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "An ExternalBarcodeLookupResponse object or null, when nothing was found for the given barcode", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalBarcodeLookupResponse" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Plugin error)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/recipes/{recipeId}/add-not-fulfilled-products-to-shoppinglist": { + "post": { + "summary": "Adds all missing products for the given recipe to the shopping list", + "tags": [ + "Recipes" + ], + "parameters": [ + { + "in": "path", + "name": "recipeId", + "required": true, + "description": "A valid recipe id", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "excludedProductIds": { + "type": "array", + "items":{ + "type": "number", + "format": "integer" + }, + "description": "An optional array of product ids to exclude them from being put on the shopping list" + } + } + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + } + } + } + }, + "/recipes/{recipeId}/requirements": { + "get": { + "summary": "Get requirements for recipe", + "tags": [ + "Recipes" + ], + "parameters": [ + { + "in": "path", + "name": "recipeId", + "required": true, + "description": "A valid recipe id", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A requirements recipe object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecipeRequirements" + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/recipes/requirements": { + "get": { + "summary": "Get all requirements for recipes", + "tags": [ + "Recipes" + ], + "responses": { + "200": { + "description": "An array of requirements recipe objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/RecipeRequirements" + } + ] + } + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/recipes/{recipeId}/consume": { + "post": { + "summary": "Consumes all products of the given recipe", + "tags": [ + "Recipes" + ], + "parameters": [ + { + "in": "path", + "name": "recipeId", + "required": true, + "description": "A valid recipe id", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + } + } + } + }, + "/chores": { + "get": { + "summary": "Returns all chores incl. the next estimated execution time per chore", + "tags": [ + "Chores" + ], + "responses": { + "200": { + "description": "An array of CurrentChoreResponse objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CurrentChoreResponse" + } + } + } + } + } + } + } + }, + "/chores/{choreId}": { + "get": { + "summary": "Returns details of the given chore", + "tags": [ + "Chores" + ], + "parameters": [ + { + "in": "path", + "name": "choreId", + "required": true, + "description": "A valid chore id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A ChoreDetailsResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChoreDetailsResponse" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing chore)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/chores/{choreId}/execute": { + "post": { + "summary": "Tracks an execution of the given chore", + "tags": [ + "Chores" + ], + "parameters": [ + { + "in": "path", + "name": "choreId", + "required": true, + "description": "A valid chore id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tracked_time": { + "type": "string", + "format": "date-time", + "description": "The time of when the chore was executed, when omitted, the current time is used" + }, + "done_by": { + "type": "integer", + "description": "A valid user id of who executed this chore, when omitted, the currently authenticated user will be used" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChoreLogEntry" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing chore)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/chores/executions/{executionId}/undo": { + "post": { + "summary": "Undoes a chore execution", + "tags": [ + "Chores" + ], + "parameters": [ + { + "in": "path", + "name": "executionId", + "required": true, + "description": "A valid chore execution id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing booking)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/batteries": { + "get": { + "summary": "Returns all batteries incl. the next estimated charge time per battery", + "tags": [ + "Batteries" + ], + "responses": { + "200": { + "description": "An array of CurrentBatteryResponse objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CurrentBatteryResponse" + } + } + } + } + } + } + } + }, + "/batteries/{batteryId}": { + "get": { + "summary": "Returns details of the given battery", + "tags": [ + "Batteries" + ], + "parameters": [ + { + "in": "path", + "name": "batteryId", + "required": true, + "description": "A valid battery id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "A BatteryDetailsResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatteryDetailsResponse" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing battery)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/batteries/{batteryId}/charge": { + "post": { + "summary": "Tracks a charge cycle of the given battery", + "tags": [ + "Batteries" + ], + "parameters": [ + { + "in": "path", + "name": "batteryId", + "required": true, + "description": "A valid battery id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "tracked_time": { + "type": "string", + "format": "date-time", + "description": "The time of when the battery was charged, when omitted, the current time is used" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The operation was successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatteryChargeCycleEntry" + } + } + } + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing battery)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/batteries/charge-cycles/{chargeCycleId}/undo": { + "post": { + "summary": "Undoes a battery charge cycle", + "tags": [ + "Batteries" + ], + "parameters": [ + { + "in": "path", + "name": "chargeCycleId", + "required": true, + "description": "A valid charge cycle id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing booking)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/tasks": { + "get": { + "summary": "Returns all tasks which are not done yet", + "tags": [ + "Tasks" + ], + "responses": { + "200": { + "description": "An array of Task objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Task" + } + } + } + } + } + } + } + }, + "/tasks/{taskId}/complete": { + "post": { + "summary": "Marks the given task as completed", + "tags": [ + "Tasks" + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "description": "A valid task id", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "done_time": { + "type": "string", + "format": "date-time", + "description": "The time of when the task was completed, when omitted, the current time is used" + } + } + } + } + } + }, + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing task)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/calendar/ical": { + "get": { + "summary": "Returns the calendar in iCal format", + "tags": [ + "Calendar" + ], + "responses": { + "200": { + "description": "The iCal file contents", + "content": { + "text/calendar": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, + "/calendar/ical/sharing-link": { + "get": { + "summary": "Returns a (public) sharing link for the calendar in iCal format", + "tags": [ + "Calendar" + ], + "responses": { + "200": { + "description": "The (public) sharing link for the calendar in iCal format", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "components": { + "internalSchemas": { + "ExposedEntity": { + "type": "string", + "enum": [ + "products", + "chores", + "batteries", + "locations", + "quantity_units", + "shopping_list", + "shopping_lists", + "recipes", + "recipes_pos", + "recipes_nestings", + "tasks", + "task_categories", + "product_groups", + "equipment", + "api_keys", + "userfields", + "meal_plan" + ] + }, + "ExposedEntitiesPreventListing": { + "type": "string", + "enum": [ + "api_keys" + ] + }, + "StockTransactionType": { + "type": "string", + "enum": [ + "purchase", + "consume", + "inventory-correction", + "product-opened" + ] + } + }, + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location_id": { + "type": "integer" + }, + "qu_id_purchase": { + "type": "integer" + }, + "qu_id_stock": { + "type": "integer" + }, + "enable_tare_weight_handling": { + "type": "integer" + }, + "not_check_stock_fulfillment_for_recipes": { + "type": "integer" + }, + "product_group_id": { + "type": "integer" + }, + "qu_factor_purchase_to_stock": { + "type": "number", + "format": "double" + }, + "tare_weight": { + "type": "number", + "format": "double" + }, + "barcode": { + "type": "string", + "description": "Can contain multiple barcodes separated by comma" + }, + "min_stock_amount": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "default_best_before_days": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "default_best_before_days_after_open": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "picture_file_name": { + "type": "string" + }, + "allow_partial_units_in_stock": { + "type": "boolean" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + }, + "example": { + "id": "1", + "name": "Cookies", + "description": null, + "location_id": "4", + "qu_id_purchase": "3", + "qu_id_stock": "3", + "qu_factor_purchase_to_stock": "1.0", + "barcode": "cok1", + "min_stock_amount": "8", + "default_best_before_days": "0", + "row_created_timestamp": "2019-05-02 20:12:26", + "product_group_id": "1", + "picture_file_name": "cookies.jpg", + "default_best_before_days_after_open": "0", + "allow_partial_units_in_stock": "0", + "enable_tare_weight_handling": "0", + "tare_weight": "0.0", + "not_check_stock_fulfillment_for_recipes": "0" + } + }, + "QuantityUnit": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "name_plural": { + "type": "string" + }, + "description": { + "type": "string" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + }, + "plural_forms": { + "type": "string" + } + }, + "example": { + "id": "2", + "name": "Piece", + "description": null, + "row_created_timestamp": "2019-05-02 20:12:25", + "name_plural": "Pieces", + "plural_forms": null + } + }, + "Location": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + }, + "example": { + "id": "2", + "name": "0", + "description": null, + "row_created_timestamp": "2019-05-02 20:12:25" + } + }, + "StockEntry": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "product_id": { + "type": "integer" + }, + "location_id": { + "type": "integer" + }, + "amount": { + "type": "double" + }, + "best_before_date": { + "type": "string", + "format": "date" + }, + "purchased_date": { + "type": "string", + "format": "date" + }, + "stock_id": { + "type": "string", + "description": "A unique id which references this stock entry during its lifetime" + }, + "price": { + "type": "double" + }, + "open": { + "type": "integer" + }, + "opened_date": { + "type": "string", + "format": "date" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + }, + "example": { + "id": "77", + "product_id": "1", + "amount": "2", + "best_before_date": "2019-07-07", + "purchased_date": "2019-05-03", + "stock_id": "5ccc6b2421979", + "price": null, + "open": "0", + "opened_date": null, + "row_created_timestamp": "2019-05-03 18:24:04", + "location_id": "4" + } + }, + "RecipeRequirements": { + "type": "object", + "properties": { + "recipe_id": { + "type": "integer" + }, + "need_fulfilled": { + "type": "boolean" + }, + "need_fulfilled_with_shopping_list": { + "type": "boolean" + }, + "missing_products_count": { + "type": "integer" + }, + "costs": { + "type": "number", + "format": "double" + } + }, + "example": { + "recipe_id": "1", + "need_fulfilled": "0", + "need_fulfilled_with_shopping_list": "0", + "missing_products_count": "2", + "costs": "17.74" + } + }, + "ProductDetailsResponse": { + "type": "object", + "properties": { + "product": { + "$ref": "#/components/schemas/Product" + }, + "quantity_unit_purchase": { + "$ref": "#/components/schemas/QuantityUnit" + }, + "quantity_unit_stock": { + "$ref": "#/components/schemas/QuantityUnit" + }, + "last_purchased": { + "type": "string", + "format": "date" + }, + "last_used": { + "type": "string", + "format": "date-time" + }, + "stock_amount": { + "type": "integer" + }, + "stock_amount_opened": { + "type": "integer" + }, + "next_best_before_date": { + "type": "string", + "format": "date-time" + }, + "last_price": { + "type": "number", + "format": "double" + }, + "location": { + "$ref": "#/components/schemas/Location" + }, + "average_shelf_life_days": { + "type": "number", + "format": "integer" + }, + "spoil_rate_percent": { + "type": "number", + "format": "double" + } + }, + "example": { + "product": { + "id": "1", + "name": "Cookies", + "description": null, + "location_id": "4", + "qu_id_purchase": "3", + "qu_id_stock": "3", + "qu_factor_purchase_to_stock": "1.0", + "barcode": "cok1", + "min_stock_amount": "8", + "default_best_before_days": "0", + "row_created_timestamp": "2019-05-02 20:12:26", + "product_group_id": "1", + "picture_file_name": "cookies.jpg", + "default_best_before_days_after_open": "0", + "allow_partial_units_in_stock": "0", + "enable_tare_weight_handling": "0", + "tare_weight": "0.0", + "not_check_stock_fulfillment_for_recipes": "0" + }, + "last_purchased": null, + "last_used": null, + "stock_amount": "2", + "stock_amount_opened": null, + "quantity_unit_purchase": { + "id": "3", + "name": "Pack", + "description": null, + "row_created_timestamp": "2019-05-02 20:12:25", + "name_plural": "Packs", + "plural_forms": null + }, + "quantity_unit_stock": { + "id": "3", + "name": "Pack", + "description": null, + "row_created_timestamp": "2019-05-02 20:12:25", + "name_plural": "Packs", + "plural_forms": null + }, + "last_price": null, + "next_best_before_date": "2019-07-07", + "location": { + "id": "4", + "name": "Candy cupboard", + "description": null, + "row_created_timestamp": "2019-05-02 20:12:25" + }, + "average_shelf_life_days": -1, + "spoil_rate_percent": 0 + } + }, + "ProductPriceHistory": { + "type": "object", + "properties": { + "date": { + "type": "string", + "format": "date-time" + }, + "price": { + "type": "number", + "format": "double" + } + } + }, + "ExternalBarcodeLookupResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "location_id": { + "type": "integer" + }, + "qu_id_purchase": { + "type": "integer" + }, + "qu_id_stock": { + "type": "integer" + }, + "qu_factor_purchase_to_stock": { + "type": "number", + "format": "double" + }, + "barcode": { + "type": "string", + "description": "Can contain multiple barcodes separated by comma" + }, + "id": { + "type": "integer", + "description": "The id of the added product, only included when the producted was added to the database" + } + } + }, + "ChoreDetailsResponse": { + "type": "object", + "properties": { + "chore": { + "$ref": "#/components/schemas/Chore" + }, + "last_tracked": { + "type": "string", + "format": "date-time", + "description": "When this chore was last tracked" + }, + "track_count": { + "type": "integer", + "description": "How often this chore was tracked so far" + }, + "last_done_by": { + "$ref": "#/components/schemas/UserDto" + }, + "next_estimated_execution_time": { + "type": "string", + "format": "date-time" + } + }, + "example": { + "chore": { + "id": 0, + "name": "string", + "description": "string", + "period_type": "manually", + "period_days": 0, + "row_created_timestamp": "2019-05-04T11:31:04.563Z" + }, + "last_tracked": "2019-05-04T11:31:04.563Z", + "track_count": 0, + "last_done_by": { + "id": 0, + "username": "string", + "first_name": "string", + "last_name": "string", + "display_name": "string", + "row_created_timestamp": "2019-05-04T11:31:04.564Z" + }, + "next_estimated_execution_time": "2019-05-04T11:31:04.564Z" + } + }, + "BatteryDetailsResponse": { + "type": "object", + "properties": { + "chore": { + "$ref": "#/components/schemas/Battery" + }, + "last_charged": { + "type": "string", + "format": "date-time", + "description": "When this battery was last charged" + }, + "charge_cycles_count": { + "type": "integer", + "description": "How often this battery was charged so far" + }, + "next_estimated_charge_time": { + "type": "string", + "format": "date-time" + } + }, + "example": { + "battery": { + "id": "1", + "name": "Battery1", + "description": "Warranty ends 2023", + "used_in": "TV remote control", + "charge_interval_days": "0", + "row_created_timestamp": "2019-05-02 20:12:26" + }, + "last_charged": "2019-03-13 18:12:28", + "charge_cycles_count": 4, + "next_estimated_charge_time": "2999-12-31 23:59:59" + } + }, + "Session": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "session_key": { + "type": "string" + }, + "expires": { + "type": "string", + "format": "date-time" + }, + "last_used": { + "type": "string", + "format": "date-time" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "UserDto": { + "type": "object", + "description": "A user object without the *password* and with an additional *display_name* property", + "properties": { + "id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "display_name": { + "type": "string" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "ApiKey": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "api_key": { + "type": "string" + }, + "expires": { + "type": "string", + "format": "date-time" + }, + "last_used": { + "type": "string", + "format": "date-time" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "ShoppingListItem": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "product_id": { + "type": "integer" + }, + "note": { + "type": "string" + }, + "amount": { + "type": "double", + "minimum": 0, + "default": 0, + "description": "The manual entered amount" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "Battery": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "used_in": { + "type": "string" + }, + "charge_interval_days": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "BatteryChargeCycleEntry": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "battery_id": { + "type": "integer" + }, + "tracked_time": { + "type": "string", + "format": "date-time" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "Chore": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "period_type": { + "type": "string", + "enum": [ + "manually", + "dynamic-regular" + ] + }, + "period_days": { + "type": "integer" + }, + "track_date_only": { + "type": "boolean" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "ChoreLogEntry": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "chore_id": { + "type": "integer" + }, + "tracked_time": { + "type": "string", + "format": "date-time" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "StockLogEntry": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "product_id": { + "type": "integer" + }, + "amount": { + "type": "double" + }, + "best_before_date": { + "type": "string", + "format": "date" + }, + "purchased_date": { + "type": "string", + "format": "date-time" + }, + "used_date": { + "type": "string", + "format": "date-time" + }, + "spoiled": { + "type": "boolean", + "default": false + }, + "stock_id": { + "type": "string" + }, + "transaction_type": { + "$ref": "#/components/internalSchemas/StockTransactionType" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "GenericErrorResponse": { + "type": "object", + "properties": { + "error_message": { + "type": "string" + } + }, + "example": { + "error_message": "The error message..." + } + }, + "CurrentStockResponse": { + "type": "object", + "properties": { + "product_id": { + "type": "integer" + }, + "amount": { + "type": "double" + }, + "best_before_date": { + "type": "string", + "format": "date", + "description": "The next best before date for this product" + } + } + }, + "CurrentChoreResponse": { + "type": "object", + "properties": { + "chore_id": { + "type": "integer" + }, + "last_tracked_time": { + "type": "string", + "format": "date-time" + }, + "next_estimated_execution_time": { + "type": "string", + "format": "date-time", + "description": "The next estimated execution time of this chore, 2999-12-31 23:59:59 when the given chore has a period_type of manually" + } + } + }, + "CurrentBatteryResponse": { + "type": "object", + "properties": { + "battery_id": { + "type": "integer" + }, + "last_tracked_time": { + "type": "string", + "format": "date-time" + }, + "next_estimated_charge_time": { + "type": "string", + "format": "date-time", + "description": "The next estimated charge time of this battery, 2999-12-31 23:59:59 when the given battery has no charge_interval_days defined" + } + } + }, + "CurrentVolatilStockResponse": { + "type": "object", + "properties": { + "expiring_products": { + "type": "array", + "items":{ + "$ref": "#/components/schemas/Product" + } + }, + "expired_products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + }, + "missing_products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + }, + "Task": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "due_date": { + "type": "string", + "format": "date-time" + }, + "done": { + "type": "integer" + }, + "done_timestamp": { + "type": "string", + "format": "date-time" + }, + "category_id": { + "type": "integer" + }, + "assigned_to_user_id": { + "type": "integer" + }, + "row_created_timestamp": { + "type": "string", + "format": "date-time" + } + } + }, + "DbChangedTimeResponse": { + "type": "object", + "properties": { + "changed_time": { + "type": "string", + "format": "date-time" + } + } + }, + "UserSetting": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + } + }, + "MissingLocalizationRequest": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "GROCY-API-KEY" + } + } + }, + "security": [ + { + "ApiKeyAuth": [ ] + } + ] +} diff --git a/routes.php b/routes.php index d22b1dd6..c2b9ec13 100644 --- a/routes.php +++ b/routes.php @@ -156,6 +156,8 @@ $app->group('/api', function() if (GROCY_FEATURE_FLAG_RECIPES) { $this->post('/recipes/{recipeId}/add-not-fulfilled-products-to-shoppinglist', '\Grocy\Controllers\RecipesApiController:AddNotFulfilledProductsToShoppingList'); + $this->get('/recipes/{recipeId}/requirements', '\Grocy\Controllers\RecipesApiController:GetRecipeRequirements'); + $this->get('/recipes/requirements', '\Grocy\Controllers\RecipesApiController:GetRecipeRequirements'); $this->post('/recipes/{recipeId}/consume', '\Grocy\Controllers\RecipesApiController:ConsumeRecipe'); } From 0c0e8c6957ab9df15bd9b73d686f4cafdcc2bcd9 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 13:13:38 +0200 Subject: [PATCH 02/26] Fixed the consume success message on stock overview page (fixes #302) --- public/viewjs/stockoverview.js | 7 +++---- views/stockoverview.blade.php | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index b8ac1e3d..018469d2 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -89,8 +89,6 @@ $(document).on('click', '.product-consume-button', function(e) 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 consumeAmount = $(e.currentTarget).attr('data-consume-amount'); Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount }, @@ -127,8 +125,9 @@ $(document).on('click', '.product-consume-button', function(e) } else { + $('#product-' + productId + '-qu-name').text(__n(newAmount, result.quantity_unit_stock.name, result.quantity_unit_stock.name_plural)); $('#product-' + productId + '-amount').parent().effect('highlight', { }, 500); - $('#product-' + productId + '-amount').fadeOut(500, function() + $('#product-' + productId + '-amount').fadeOut(500, function () { $(this).text(newAmount).fadeIn(500); }); @@ -157,7 +156,7 @@ $(document).on('click', '.product-consume-button', function(e) } Grocy.FrontendHelpers.EndUiBusy(); - toastr.success(__t('Removed %1$s of %2$s from stock', consumeAmount, productQuName, productName)); + toastr.success(__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)); RefreshContextualTimeago(); RefreshStatistics(); }, diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index 8425b887..44e7002b 100644 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -129,7 +129,7 @@ {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }} - {{ $currentStockEntry->amount }} {{ $__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) }} + {{ $currentStockEntry->amount }} {{ $__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) }} @if($currentStockEntry->amount_opened > 0){{ $__t('%s opened', $currentStockEntry->amount_opened) }}@endif From 3fcede0b7c58c2b09d553d15ab39eb7c35ca2757 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 13:32:40 +0200 Subject: [PATCH 03/26] Fix that "Track date only" cannot be tracked <> today (fixes #300) --- controllers/ChoresApiController.php | 2 +- public/viewjs/components/datetimepicker.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/controllers/ChoresApiController.php b/controllers/ChoresApiController.php index 9805a107..7b4c28bc 100644 --- a/controllers/ChoresApiController.php +++ b/controllers/ChoresApiController.php @@ -21,7 +21,7 @@ class ChoresApiController extends BaseApiController try { $trackedTime = date('Y-m-d H:i:s'); - if (array_key_exists('tracked_time', $requestBody) && IsIsoDateTime($requestBody['tracked_time'])) + if (array_key_exists('tracked_time', $requestBody) && (IsIsoDateTime($requestBody['tracked_time']) || IsIsoDate($requestBody['tracked_time']))) { $trackedTime = $requestBody['tracked_time']; } diff --git a/public/viewjs/components/datetimepicker.js b/public/viewjs/components/datetimepicker.js index c6fc63a6..6b220478 100644 --- a/public/viewjs/components/datetimepicker.js +++ b/public/viewjs/components/datetimepicker.js @@ -48,6 +48,15 @@ Grocy.Components.DateTimePicker.ChangeFormat = function(format) $(".datetimepicker").datetimepicker("destroy"); Grocy.Components.DateTimePicker.GetInputElement().data("format", format); Grocy.Components.DateTimePicker.Init(); + + if (format == "YYYY-MM-DD") + { + Grocy.Components.DateTimePicker.GetInputElement().addClass("date-only-datetimepicker"); + } + else + { + Grocy.Components.DateTimePicker.GetInputElement().removeClass("date-only-datetimepicker"); + } } var startDate = null; From 67cfd0ba5f6193c6c8480de25524b56cfe339f7d Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 13:55:29 +0200 Subject: [PATCH 04/26] Remove the internal meal plan recipe when removing a meal plan entry (fixes #298) --- migrations/0073.sql | 92 +++++++++++++++++++++++++++++++++++++++ public/viewjs/mealplan.js | 2 +- 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 migrations/0073.sql diff --git a/migrations/0073.sql b/migrations/0073.sql new file mode 100644 index 00000000..01b41cf3 --- /dev/null +++ b/migrations/0073.sql @@ -0,0 +1,92 @@ +DROP TRIGGER create_internal_recipe; +CREATE TRIGGER create_internal_recipe AFTER INSERT ON meal_plan +BEGIN + /* This contains practically the same logic as the trigger remove_internal_recipe */ + + -- Create a recipe per day + DELETE FROM recipes + WHERE name = NEW.day + AND type = 'mealplan-day'; + + INSERT OR REPLACE INTO recipes + (id, name, type) + VALUES + ((SELECT MIN(id) - 1 FROM recipes), NEW.day, 'mealplan-day'); + + -- Create a recipe per week + DELETE FROM recipes + WHERE name = LTRIM(STRFTIME('%Y-%W', NEW.day), '0') + AND type = 'mealplan-week'; + + INSERT INTO recipes + (id, name, type) + VALUES + ((SELECT MIN(id) - 1 FROM recipes), LTRIM(STRFTIME('%Y-%W', NEW.day), '0'), 'mealplan-week'); + + -- Delete all current nestings entries for the day and week recipe + DELETE FROM recipes_nestings + WHERE recipe_id IN (SELECT id FROM recipes WHERE name = NEW.day AND type = 'mealplan-day') + OR recipe_id IN (SELECT id FROM recipes WHERE name = NEW.day AND type = 'mealplan-week'); + + -- Add all recipes for this day as included recipes in the day-recipe + INSERT INTO recipes_nestings + (recipe_id, includes_recipe_id, servings) + SELECT (SELECT id FROM recipes WHERE name = NEW.day AND type = 'mealplan-day'), recipe_id, SUM(servings) + FROM meal_plan + WHERE day = NEW.day + GROUP BY recipe_id; + + -- Add all recipes for this week as included recipes in the week-recipe + INSERT INTO recipes_nestings + (recipe_id, includes_recipe_id, servings) + SELECT (SELECT id FROM recipes WHERE name = LTRIM(STRFTIME('%Y-%W', NEW.day), '0') AND type = 'mealplan-week'), recipe_id, SUM(servings) + FROM meal_plan + WHERE STRFTIME('%Y-%W', day) = STRFTIME('%Y-%W', NEW.day) + GROUP BY recipe_id; +END; + +CREATE TRIGGER remove_internal_recipe AFTER DELETE ON meal_plan +BEGIN + /* This contains practically the same logic as the trigger create_internal_recipe */ + + -- Create a recipe per day + DELETE FROM recipes + WHERE name = OLD.day + AND type = 'mealplan-day'; + + INSERT OR REPLACE INTO recipes + (id, name, type) + VALUES + ((SELECT MIN(id) - 1 FROM recipes), OLD.day, 'mealplan-day'); + + -- Create a recipe per week + DELETE FROM recipes + WHERE name = LTRIM(STRFTIME('%Y-%W', OLD.day), '0') + AND type = 'mealplan-week'; + + INSERT INTO recipes + (id, name, type) + VALUES + ((SELECT MIN(id) - 1 FROM recipes), LTRIM(STRFTIME('%Y-%W', OLD.day), '0'), 'mealplan-week'); + + -- Delete all current nestings entries for the day and week recipe + DELETE FROM recipes_nestings + WHERE recipe_id IN (SELECT id FROM recipes WHERE name = OLD.day AND type = 'mealplan-day') + OR recipe_id IN (SELECT id FROM recipes WHERE name = OLD.day AND type = 'mealplan-week'); + + -- Add all recipes for this day as included recipes in the day-recipe + INSERT INTO recipes_nestings + (recipe_id, includes_recipe_id, servings) + SELECT (SELECT id FROM recipes WHERE name = OLD.day AND type = 'mealplan-day'), recipe_id, SUM(servings) + FROM meal_plan + WHERE day = OLD.day + GROUP BY recipe_id; + + -- Add all recipes for this week as included recipes in the week-recipe + INSERT INTO recipes_nestings + (recipe_id, includes_recipe_id, servings) + SELECT (SELECT id FROM recipes WHERE name = LTRIM(STRFTIME('%Y-%W', OLD.day), '0') AND type = 'mealplan-week'), recipe_id, SUM(servings) + FROM meal_plan + WHERE STRFTIME('%Y-%W', day) = STRFTIME('%Y-%W', OLD.day) + GROUP BY recipe_id; +END; diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index d3d1b062..e1b7313d 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -103,7 +103,7 @@ $(document).on("click", ".remove-recipe-button", function(e) Grocy.Api.Delete('objects/meal_plan/' + mealPlanEntry.id.toString(), { }, function(result) { - calendar.fullCalendar('removeEvents', [mealPlanEntry.id]); + window.location.reload(); }, function(xhr) { From b76e51ba41236b30527df076debd04fff0171405 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 14:48:46 +0200 Subject: [PATCH 05/26] Fixed nested recipes costs calculation (fixes #299) --- localization/strings.pot | 3 ++ migrations/0074.sql | 85 +++++++++++++++++++++++++++++++++++++++ public/js/extensions.js | 7 ++++ public/viewjs/mealplan.js | 20 ++++++++- 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 migrations/0074.sql diff --git a/localization/strings.pot b/localization/strings.pot index 56e6796b..ad48c99c 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1268,3 +1268,6 @@ msgstr "" msgid "Booking has subsequent dependent bookings, undo not possible" msgstr "" + +msgid "per serving" +msgstr "" diff --git a/migrations/0074.sql b/migrations/0074.sql new file mode 100644 index 00000000..faeb0b6a --- /dev/null +++ b/migrations/0074.sql @@ -0,0 +1,85 @@ +DROP VIEW recipes_pos_resolved; +CREATE VIEW recipes_pos_resolved +AS + +-- Multiplication by 1.0 to force conversion to float (REAL) + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount, + IFNULL(sc.amount, 0) AS stock_amount, + CASE WHEN IFNULL(sc.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled, + CASE WHEN IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END < 0 THEN ABS(IFNULL(sc.amount, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END)) ELSE 0 END AS missing_amount, + IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list, + CASE WHEN IFNULL(sc.amount, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list, + rp.qu_id, + (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + rp.id, -- Just a dummy id column + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_price pcp + ON rp.product_id = pcp.product_id +WHERE rp.not_check_stock_fulfillment = 0 + +UNION + +-- Just add all recipe positions which should not be checked against stock with fulfilled need + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount, + IFNULL(sc.amount, 0) AS stock_amount, + 1 AS need_fulfilled, + 0 AS missing_amount, + IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list, + 1 AS need_fulfilled_with_shopping_list, + rp.qu_id, + (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + rp.id, -- Just a dummy id column + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_price pcp + ON rp.product_id = pcp.product_id +WHERE rp.not_check_stock_fulfillment = 1; diff --git a/public/js/extensions.js b/public/js/extensions.js index f6bbac37..269a2c88 100644 --- a/public/js/extensions.js +++ b/public/js/extensions.js @@ -37,6 +37,13 @@ GetUriParam = function(key) } }; +UpdateUriParam = function(key, value) +{ + var queryParameters = new URLSearchParams(location.search); + queryParameters.set(key, value); + window.history.replaceState({ }, "", decodeURIComponent(`${location.pathname}?${queryParameters}`)); +}; + IsTouchInputDevice = function() { if (("ontouchstart" in window) || window.DocumentTouch && document instanceof DocumentTouch) diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index e1b7313d..0170af49 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -1,4 +1,6 @@ -var calendar = $("#calendar").fullCalendar({ +var firstRender = true; + +var calendar = $("#calendar").fullCalendar({ "themeSystem": "bootstrap4", "header": { "left": "title", @@ -11,6 +13,15 @@ "defaultView": "basicWeek", "viewRender": function(view) { + if (firstRender) + { + firstRender = false + } + else + { + UpdateUriParam("week", view.start.format("YYYY-MM-DD")); + } + $(".fc-day-header").append(''); var weekRecipeName = view.start.year().toString() + "-" + (view.start.week() - 1).toString(); @@ -62,7 +73,7 @@
' + recipe.name + '
\
' + __n(mealPlanEntry.servings, "%s serving", "%s servings") + '
\
' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '
\ -
' + resolvedRecipe.costs + '
\ +
' + resolvedRecipe.costs + ' ' + __t('per serving') + '
\
\ \ \ @@ -77,6 +88,11 @@ "eventAfterAllRender": function(view) { RefreshLocaleNumberDisplay(); + + if (GetUriParam("week") !== undefined) + { + $("#calendar").fullCalendar("gotoDate", GetUriParam("week")); + } }, }); From d34c7b0a87b72fd2d681f6c8e9083e58b58141c6 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 14:56:56 +0200 Subject: [PATCH 06/26] Created the changelog for the next version --- changelog/50_2.4.3_2019-xx-xx.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/50_2.4.3_2019-xx-xx.md diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md new file mode 100644 index 00000000..9afdb414 --- /dev/null +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -0,0 +1,4 @@ +- Fixed the messed up message/toast after consuming a product from the stock overview page +- Fixed that "Track date only" chores were always tracked today, regardless of the given date +- Fixed that the "week costs" were wrong after removing a meal plan entry +- Fixed wrong recipes costs calculation with nested recipes when the base recipe servings are > 1 (also affected the meal plan when adding such a recipe there) From 482a520062edb59a3d5a069904f4878d7d178466 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 15:28:49 +0200 Subject: [PATCH 07/26] Slightly modified new recipe stock fulfillment API endpoints (references #289) --- changelog/50_2.4.3_2019-xx-xx.md | 1 + controllers/RecipesApiController.php | 41 +++--- grocy.openapi.json | 208 +++++++++++++-------------- routes.php | 4 +- 4 files changed, 128 insertions(+), 126 deletions(-) diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index 9afdb414..792d434b 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -2,3 +2,4 @@ - Fixed that "Track date only" chores were always tracked today, regardless of the given date - Fixed that the "week costs" were wrong after removing a meal plan entry - Fixed wrong recipes costs calculation with nested recipes when the base recipe servings are > 1 (also affected the meal plan when adding such a recipe there) +- Improved recipes API - added new endpoints to get stock fulfillment information (thanks @Aerex) diff --git a/controllers/RecipesApiController.php b/controllers/RecipesApiController.php index 819fad6b..3f3ee0cf 100644 --- a/controllers/RecipesApiController.php +++ b/controllers/RecipesApiController.php @@ -41,23 +41,28 @@ class RecipesApiController extends BaseApiController } } - public function GetRecipeRequirements(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) - { - try { - if(!$args['recipeId']){ - return $this->ApiResponse($this->RecipesService->GetRecipesResolved()); - } - $recipeResolved = FindObjectInArrayByPropertyValue($this->RecipesService->GetRecipesResolved(), 'recipe_id', $args['recipeId']); - if(!$recipeResolved) { - $errorMsg ='Recipe requirments do not exist for recipe_id ' . $args['recipe_id']; - $GenericError = $this->GenericErrorResponse($response, $errorMsg); - return $GenericError; - } - return $this->ApiResponse($recipeResolved); - } - catch (\Exception $ex) - { + public function GetRecipeFulfillment(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + if(!isset($args['recipeId'])) + { + return $this->ApiResponse($this->RecipesService->GetRecipesResolved()); + } + + $recipeResolved = FindObjectInArrayByPropertyValue($this->RecipesService->GetRecipesResolved(), 'recipe_id', $args['recipeId']); + if(!$recipeResolved) + { + throw new \Exception('Recipe does not exist'); + } + else + { + return $this->ApiResponse($recipeResolved); + } + } + catch (\Exception $ex) + { return $this->GenericErrorResponse($response, $ex->getMessage()); - } - } + } + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index c886bd34..292b64da 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1663,6 +1663,47 @@ } } }, + "/recipes/{recipeId}/fulfillment": { + "get": { + "summary": "Get stock fulfillment information for the given recipe", + "tags": [ + "Recipes" + ], + "parameters": [ + { + "in": "path", + "name": "recipeId", + "required": true, + "description": "A valid recipe id", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "A RecipeFulfillmentResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecipeFulfillmentResponse" + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/recipes/{recipeId}/consume": { "post": { "summary": "Consumes all products of the given recipe", @@ -1687,84 +1728,39 @@ } } }, - "/recipes/{recipeId}/requirements": { - "get": { - "summary": "Get requirements for recipe", - "tags": [ - "Recipes" - ], - "parameters": [ - { - "in": "path", - "name": "recipeId", - "required": true, - "description": "A valid recipe id", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "A requirements recipe object", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RecipeRequirements" - } - } - } - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, - "/recipes/requirements": { - "get": { - "summary": "Get all requirements for recipes", - "tags": [ - "Recipes" - ], - "responses": { - "200": { - "description": "An array of requirements recipe objects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/RecipeRequirements" - } - ] - } - } - } - } - }, - "400": { - "description": "The operation was not successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GenericErrorResponse" - } - } - } - } - } - } - }, + "/recipes/fulfillment": { + "get": { + "summary": "Get stock fulfillment information for all recipe", + "tags": [ + "Recipes" + ], + "responses": { + "200": { + "description": "An array of RecipeFulfillmentResponse objects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecipeFulfillmentResponse" + } + } + } + } + }, + "400": { + "description": "The operation was not successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/chores": { "get": { "summary": "Returns all chores incl. the next estimated execution time per chore", @@ -2453,34 +2449,34 @@ "location_id": "4" } }, - "RecipeRequirements": { - "type": "object", - "properties": { - "recipe_id": { - "type": "integer" - }, - "need_fulfilled": { - "type": "boolean" - }, - "need_fulfilled_with_shopping_list": { - "type": "boolean" - }, - "missing_products_count": { - "type": "integer" - }, - "costs": { - "type": "number", - "format": "double" - } - }, - "example": { - "recipe_id": "1", - "need_fulfilled": "0", - "need_fulfilled_with_shopping_list": "0", - "missing_products_count": "2", - "costs": "17.74" - } - }, + "RecipeFulfillmentResponse": { + "type": "object", + "properties": { + "recipe_id": { + "type": "integer" + }, + "need_fulfilled": { + "type": "boolean" + }, + "need_fulfilled_with_shopping_list": { + "type": "boolean" + }, + "missing_products_count": { + "type": "integer" + }, + "costs": { + "type": "number", + "format": "double" + } + }, + "example": { + "recipe_id": "1", + "need_fulfilled": "0", + "need_fulfilled_with_shopping_list": "0", + "missing_products_count": "2", + "costs": "17.74" + } + }, "ProductDetailsResponse": { "type": "object", "properties": { diff --git a/routes.php b/routes.php index 17fe9d49..38a24ab2 100644 --- a/routes.php +++ b/routes.php @@ -173,9 +173,9 @@ $app->group('/api', function() if (GROCY_FEATURE_FLAG_RECIPES) { $this->post('/recipes/{recipeId}/add-not-fulfilled-products-to-shoppinglist', '\Grocy\Controllers\RecipesApiController:AddNotFulfilledProductsToShoppingList'); - $this->get('/recipes/{recipeId}/requirements', '\Grocy\Controllers\RecipesApiController:GetRecipeRequirements'); - $this->get('/recipes/requirements', '\Grocy\Controllers\RecipesApiController:GetRecipeRequirements'); + $this->get('/recipes/{recipeId}/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment'); $this->post('/recipes/{recipeId}/consume', '\Grocy\Controllers\RecipesApiController:ConsumeRecipe'); + $this->get('/recipes/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment'); } // Chores From df529c3c0befd526ce707e31d3cc0a386d70cb12 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 15:43:54 +0200 Subject: [PATCH 08/26] Show 2999-12-31 as "Never" everywhere (closes #296) --- changelog/50_2.4.3_2019-xx-xx.md | 1 + localization/strings.pot | 3 + public/js/grocy.js | 204 ++++++++++++++++--------------- 3 files changed, 110 insertions(+), 98 deletions(-) diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index 792d434b..9e2a1526 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -3,3 +3,4 @@ - Fixed that the "week costs" were wrong after removing a meal plan entry - Fixed wrong recipes costs calculation with nested recipes when the base recipe servings are > 1 (also affected the meal plan when adding such a recipe there) - Improved recipes API - added new endpoints to get stock fulfillment information (thanks @Aerex) +- Improved date display for products that never expires (instead of "2999-12-31" now just "Never" will be shown) diff --git a/localization/strings.pot b/localization/strings.pot index ad48c99c..80af570c 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1271,3 +1271,6 @@ msgstr "" msgid "per serving" msgstr "" + +msgid "Never" +msgstr "" diff --git a/public/js/grocy.js b/public/js/grocy.js index c87747b0..ba7fe5d8 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -1,101 +1,4 @@ -Grocy.Translator = new Translator(Grocy.GettextPo); -__t = function(text, ...placeholderValues) -{ - if (Grocy.Mode === "dev") - { - var text2 = text; - Grocy.Api.Post('system/log-missing-localization', { "text": text2 }); - } - - return Grocy.Translator.__(text, ...placeholderValues) -} -__n = function(number, singularForm, pluralForm) -{ - if (Grocy.Mode === "dev") - { - var singularForm2 = singularForm; - Grocy.Api.Post('system/log-missing-localization', { "text": singularForm2 }); - } - - return Grocy.Translator.n__(singularForm, pluralForm, number, number) -} - -U = function(relativePath) -{ - return Grocy.BaseUrl.replace(/\/$/, '') + relativePath; -} - -if (!Grocy.ActiveNav.isEmpty()) -{ - var menuItem = $('#sidebarResponsive').find("[data-nav-for-page='" + Grocy.ActiveNav + "']"); - menuItem.addClass('active-page'); - - var parentMenuSelector = menuItem.data("sub-menu-of"); - if (typeof parentMenuSelector !== "undefined") - { - $(parentMenuSelector).collapse("show"); - $(parentMenuSelector).prev(".nav-link-collapse").addClass("active-page"); - } -} - -var observer = new MutationObserver(function(mutations) -{ - mutations.forEach(function(mutation) - { - if (mutation.attributeName === "class") - { - var attributeValue = $(mutation.target).prop(mutation.attributeName); - if (attributeValue.contains("sidenav-toggled")) - { - window.localStorage.setItem("sidebar_state", "collapsed"); - } - else - { - window.localStorage.setItem("sidebar_state", "expanded"); - } - } - }); -}); -observer.observe(document.body, { - attributes: true -}); -if (window.localStorage.getItem("sidebar_state") === "collapsed") -{ - $("#sidenavToggler").click(); -} - -$.timeago.settings.allowFuture = true; -RefreshContextualTimeago = function() -{ - $("time.timeago").each(function() - { - var element = $(this); - var timestamp = element.attr("datetime"); - element.timeago("update", timestamp); - }); -} -RefreshContextualTimeago(); - -toastr.options = { - toastClass: 'alert', - closeButton: true, - timeOut: 20000, - extendedTimeOut: 5000 -}; - -window.FontAwesomeConfig = { - searchPseudoElements: true -} - -// Don't show tooltips on touch input devices -if (IsTouchInputDevice()) -{ - var css = document.createElement("style"); - css.innerHTML = ".tooltip { display: none; }"; - document.body.appendChild(css); -} - -Grocy.Api = { }; +Grocy.Api = { }; Grocy.Api.Get = function(apiFunction, success, error) { var xhr = new XMLHttpRequest(); @@ -323,6 +226,111 @@ Grocy.Api.DeleteFile = function(fileName, group, success, error) xhr.send(); }; +Grocy.Translator = new Translator(Grocy.GettextPo); +__t = function(text, ...placeholderValues) +{ + if (Grocy.Mode === "dev") + { + var text2 = text; + Grocy.Api.Post('system/log-missing-localization', { "text": text2 }); + } + + return Grocy.Translator.__(text, ...placeholderValues) +} +__n = function(number, singularForm, pluralForm) +{ + if (Grocy.Mode === "dev") + { + var singularForm2 = singularForm; + Grocy.Api.Post('system/log-missing-localization', { "text": singularForm2 }); + } + + return Grocy.Translator.n__(singularForm, pluralForm, number, number) +} + +U = function(relativePath) +{ + return Grocy.BaseUrl.replace(/\/$/, '') + relativePath; +} + +if (!Grocy.ActiveNav.isEmpty()) +{ + var menuItem = $('#sidebarResponsive').find("[data-nav-for-page='" + Grocy.ActiveNav + "']"); + menuItem.addClass('active-page'); + + var parentMenuSelector = menuItem.data("sub-menu-of"); + if (typeof parentMenuSelector !== "undefined") + { + $(parentMenuSelector).collapse("show"); + $(parentMenuSelector).prev(".nav-link-collapse").addClass("active-page"); + } +} + +var observer = new MutationObserver(function(mutations) +{ + mutations.forEach(function(mutation) + { + if (mutation.attributeName === "class") + { + var attributeValue = $(mutation.target).prop(mutation.attributeName); + if (attributeValue.contains("sidenav-toggled")) + { + window.localStorage.setItem("sidebar_state", "collapsed"); + } + else + { + window.localStorage.setItem("sidebar_state", "expanded"); + } + } + }); +}); +observer.observe(document.body, { + attributes: true +}); +if (window.localStorage.getItem("sidebar_state") === "collapsed") +{ + $("#sidenavToggler").click(); +} + +$.timeago.settings.allowFuture = true; +RefreshContextualTimeago = function() +{ + $("time.timeago").each(function() + { + var element = $(this); + var timestamp = element.attr("datetime"); + var isNever = timestamp && timestamp.substring(0, 10) == "2999-12-31"; + if (isNever) + { + element.prev().text(__t("Never")); + } + else + { + element.timeago("update", timestamp); + } + }); +} +RefreshContextualTimeago(); + +toastr.options = { + toastClass: 'alert', + closeButton: true, + timeOut: 20000, + extendedTimeOut: 5000 +}; + +window.FontAwesomeConfig = { + searchPseudoElements: true +} + +// Don't show tooltips on touch input devices +if (IsTouchInputDevice()) +{ + var css = document.createElement("style"); + css.innerHTML = ".tooltip { display: none; }"; + document.body.appendChild(css); +} + Grocy.FrontendHelpers = { }; Grocy.FrontendHelpers.ValidateForm = function(formId) { From c6c10c87e47df835140573b830d0f0c9d677692c Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 17:19:28 +0200 Subject: [PATCH 09/26] Improved date display for dates of today and no time Instead of the hours since midnight now just "Today" will be shown --- changelog/50_2.4.3_2019-xx-xx.md | 1 + localization/strings.pot | 3 +++ public/js/grocy.js | 5 +++++ public/viewjs/components/batterycard.js | 3 ++- public/viewjs/components/chorecard.js | 3 ++- public/viewjs/components/datetimepicker.js | 3 ++- public/viewjs/components/productcard.js | 5 +++-- 7 files changed, 18 insertions(+), 5 deletions(-) diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index 9e2a1526..2d9fe270 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -4,3 +4,4 @@ - Fixed wrong recipes costs calculation with nested recipes when the base recipe servings are > 1 (also affected the meal plan when adding such a recipe there) - Improved recipes API - added new endpoints to get stock fulfillment information (thanks @Aerex) - Improved date display for products that never expires (instead of "2999-12-31" now just "Never" will be shown) +- Improved date display for dates of today and no time (instead of the hours since midnight now just "Today" will be shown) diff --git a/localization/strings.pot b/localization/strings.pot index 80af570c..35ace53f 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1274,3 +1274,6 @@ msgstr "" msgid "Never" msgstr "" + +msgid "Today" +msgstr "" diff --git a/public/js/grocy.js b/public/js/grocy.js index ba7fe5d8..745f115f 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -300,10 +300,15 @@ RefreshContextualTimeago = function() var element = $(this); var timestamp = element.attr("datetime"); var isNever = timestamp && timestamp.substring(0, 10) == "2999-12-31"; + var isToday = timestamp && timestamp.length == 10 && timestamp.substring(0, 10) == moment().format("YYYY-MM-DD"); if (isNever) { element.prev().text(__t("Never")); } + if (isToday) + { + element.text(__t("Today")); + } else { element.timeago("update", timestamp); diff --git a/public/viewjs/components/batterycard.js b/public/viewjs/components/batterycard.js index 982a3733..1349be8d 100644 --- a/public/viewjs/components/batterycard.js +++ b/public/viewjs/components/batterycard.js @@ -8,13 +8,14 @@ Grocy.Components.BatteryCard.Refresh = function(batteryId) $('#batterycard-battery-name').text(batteryDetails.battery.name); $('#batterycard-battery-used_in').text(batteryDetails.battery.used_in); $('#batterycard-battery-last-charged').text((batteryDetails.last_charged || __t('never'))); - $('#batterycard-battery-last-charged-timeago').text($.timeago(batteryDetails.last_charged || '')); + $('#batterycard-battery-last-charged-timeago').attr("datetime", batteryDetails.last_charged || ''); $('#batterycard-battery-charge-cycles-count').text((batteryDetails.charge_cycles_count || '0')); $('#batterycard-battery-edit-button').attr("href", U("/battery/" + batteryDetails.battery.id.toString())); $('#batterycard-battery-edit-button').removeClass("disabled"); EmptyElementWhenMatches('#batterycard-battery-last-charged-timeago', __t('timeago_nan')); + RefreshContextualTimeago(); }, function(xhr) { diff --git a/public/viewjs/components/chorecard.js b/public/viewjs/components/chorecard.js index d66b112e..90322ff8 100644 --- a/public/viewjs/components/chorecard.js +++ b/public/viewjs/components/chorecard.js @@ -7,7 +7,7 @@ Grocy.Components.ChoreCard.Refresh = function(choreId) { $('#chorecard-chore-name').text(choreDetails.chore.name); $('#chorecard-chore-last-tracked').text((choreDetails.last_tracked || __t('never'))); - $('#chorecard-chore-last-tracked-timeago').text($.timeago(choreDetails.last_tracked || '')); + $('#chorecard-chore-last-tracked-timeago').attr("datetime", choreDetails.last_tracked || ''); $('#chorecard-chore-tracked-count').text((choreDetails.tracked_count || '0')); $('#chorecard-chore-last-done-by').text((choreDetails.last_done_by.display_name || __t('Unknown'))); @@ -15,6 +15,7 @@ Grocy.Components.ChoreCard.Refresh = function(choreId) $('#chorecard-chore-edit-button').removeClass("disabled"); EmptyElementWhenMatches('#chorecard-chore-last-tracked-timeago', __t('timeago_nan')); + RefreshContextualTimeago(); }, function(xhr) { diff --git a/public/viewjs/components/datetimepicker.js b/public/viewjs/components/datetimepicker.js index 6b220478..82e5dc91 100644 --- a/public/viewjs/components/datetimepicker.js +++ b/public/viewjs/components/datetimepicker.js @@ -235,8 +235,9 @@ Grocy.Components.DateTimePicker.GetInputElement().on('keyup', function(e) Grocy.Components.DateTimePicker.GetInputElement().on('input', function(e) { - $('#datetimepicker-timeago').text($.timeago(Grocy.Components.DateTimePicker.GetValue())); + $('#datetimepicker-timeago').attr("datetime", Grocy.Components.DateTimePicker.GetValue()); EmptyElementWhenMatches('#datetimepicker-timeago', __t('timeago_nan')); + RefreshContextualTimeago(); }); $('.datetimepicker').on('update.datetimepicker', function(e) diff --git a/public/viewjs/components/productcard.js b/public/viewjs/components/productcard.js index c274493b..2562fea8 100644 --- a/public/viewjs/components/productcard.js +++ b/public/viewjs/components/productcard.js @@ -12,9 +12,9 @@ Grocy.Components.ProductCard.Refresh = function(productId) $('#productcard-product-stock-amount').text(stockAmount); $('#productcard-product-stock-qu-name').text(__n(stockAmount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural)); $('#productcard-product-last-purchased').text((productDetails.last_purchased || __t('never')).substring(0, 10)); - $('#productcard-product-last-purchased-timeago').text($.timeago(productDetails.last_purchased || '')); + $('#productcard-product-last-purchased-timeago').attr("datetime", productDetails.last_purchased || ''); $('#productcard-product-last-used').text((productDetails.last_used || __t('never')).substring(0, 10)); - $('#productcard-product-last-used-timeago').text($.timeago(productDetails.last_used || '')); + $('#productcard-product-last-used-timeago').attr("datetime", productDetails.last_used || ''); $('#productcard-product-location').text(productDetails.location.name); $('#productcard-product-spoil-rate').text(parseFloat(productDetails.spoil_rate_percent).toLocaleString(undefined, { style: "percent" })); @@ -71,6 +71,7 @@ Grocy.Components.ProductCard.Refresh = function(productId) EmptyElementWhenMatches('#productcard-product-last-purchased-timeago', __t('timeago_nan')); EmptyElementWhenMatches('#productcard-product-last-used-timeago', __t('timeago_nan')); + RefreshContextualTimeago(); }, function(xhr) { From e4d26bb8fd882202adf7a81ff13bb7c21451b277 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 17:31:17 +0200 Subject: [PATCH 10/26] Make it possible to switch shopping list items between shopping lists (closes #284) --- changelog/50_2.4.3_2019-xx-xx.md | 1 + controllers/StockController.php | 2 ++ public/viewjs/shoppinglistitemform.js | 14 +++++++++----- views/shoppinglistitemform.blade.php | 9 +++++++++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index 2d9fe270..0273776e 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -5,3 +5,4 @@ - Improved recipes API - added new endpoints to get stock fulfillment information (thanks @Aerex) - Improved date display for products that never expires (instead of "2999-12-31" now just "Never" will be shown) - Improved date display for dates of today and no time (instead of the hours since midnight now just "Today" will be shown) +- Improved shopping list handling, items can now be switched between lists (there is a shopping list dropdown on the item edit page) diff --git a/controllers/StockController.php b/controllers/StockController.php index a322b0ea..a395d07a 100644 --- a/controllers/StockController.php +++ b/controllers/StockController.php @@ -222,6 +222,7 @@ class StockController extends BaseController { return $this->AppContainer->view->render($response, 'shoppinglistitemform', [ 'products' => $this->Database->products()->orderBy('name'), + 'shoppingLists' => $this->Database->shopping_lists()->orderBy('name'), 'mode' => 'create' ]); } @@ -230,6 +231,7 @@ class StockController extends BaseController return $this->AppContainer->view->render($response, 'shoppinglistitemform', [ 'listItem' => $this->Database->shopping_list($args['itemId']), 'products' => $this->Database->products()->orderBy('name'), + 'shoppingLists' => $this->Database->shopping_lists()->orderBy('name'), 'mode' => 'edit' ]); } diff --git a/public/viewjs/shoppinglistitemform.js b/public/viewjs/shoppinglistitemform.js index c7248a0d..c2113aad 100644 --- a/public/viewjs/shoppinglistitemform.js +++ b/public/viewjs/shoppinglistitemform.js @@ -3,7 +3,6 @@ e.preventDefault(); var jsonData = $('#shoppinglist-form').serializeJSON(); - jsonData.shopping_list_id = GetUriParam("list"); Grocy.FrontendHelpers.BeginUiBusy("shoppinglist-form"); if (Grocy.EditMode === 'create') @@ -11,7 +10,7 @@ Grocy.Api.Post('objects/shopping_list', jsonData, function(result) { - window.location.href = U('/shoppinglist?list=' + GetUriParam("list")); + window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); }, function(xhr) { @@ -25,7 +24,7 @@ Grocy.Api.Put('objects/shopping_list/' + Grocy.EditObjectId, jsonData, function(result) { - window.location.href = U('/shoppinglist?list=' + GetUriParam("list")); + window.location.href = U('/shoppinglist?list=' + $("#shopping_list_id").val().toString()); }, function(xhr) { @@ -93,12 +92,12 @@ $('#amount').on('focus', function(e) } }); -$('#shoppinglist-form input').keyup(function (event) +$('#shoppinglist-form input').keyup(function(event) { Grocy.FrontendHelpers.ValidateForm('shoppinglist-form'); }); -$('#shoppinglist-form input').keydown(function (event) +$('#shoppinglist-form input').keydown(function(event) { if (event.keyCode === 13) //Enter { @@ -114,3 +113,8 @@ $('#shoppinglist-form input').keydown(function (event) } } }); + +if (GetUriParam("list") !== undefined) +{ + $("#shopping_list_id").val(GetUriParam("list")); +} diff --git a/views/shoppinglistitemform.blade.php b/views/shoppinglistitemform.blade.php index be88b71b..06021d18 100644 --- a/views/shoppinglistitemform.blade.php +++ b/views/shoppinglistitemform.blade.php @@ -21,6 +21,15 @@
+
+ + +
+ @php if($mode == 'edit') { $productId = $listItem->product_id; } else { $productId = ''; } @endphp @include('components.productpicker', array( 'products' => $products, From b24683f95431ad882bb267014e34fff35ac87b20 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 17:56:59 +0200 Subject: [PATCH 11/26] Added the possibility to mark a shopping list item as "done" (closes #257) --- changelog/50_2.4.3_2019-xx-xx.md | 4 +++- migrations/0075.sql | 2 ++ public/viewjs/shoppinglist.js | 40 ++++++++++++++++++++++++++++++++ views/shoppinglist.blade.php | 7 +++++- 4 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 migrations/0075.sql diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index 0273776e..6c50b76e 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -5,4 +5,6 @@ - Improved recipes API - added new endpoints to get stock fulfillment information (thanks @Aerex) - Improved date display for products that never expires (instead of "2999-12-31" now just "Never" will be shown) - Improved date display for dates of today and no time (instead of the hours since midnight now just "Today" will be shown) -- Improved shopping list handling, items can now be switched between lists (there is a shopping list dropdown on the item edit page) +- Improved shopping list handling + - Items can now be switched between lists (there is a shopping list dropdown on the item edit page) + - Items can now be marked as "done" (new check mark button per item, when clicked, the item will be displayed greyed out, when clicked again the item will be displayed normally again) diff --git a/migrations/0075.sql b/migrations/0075.sql new file mode 100644 index 00000000..8bcf7e8b --- /dev/null +++ b/migrations/0075.sql @@ -0,0 +1,2 @@ +ALTER TABLE shopping_list +ADD done INT DEFAULT 0; diff --git a/public/viewjs/shoppinglist.js b/public/viewjs/shoppinglist.js index 15ff902d..e625692b 100644 --- a/public/viewjs/shoppinglist.js +++ b/public/viewjs/shoppinglist.js @@ -259,6 +259,46 @@ $(document).on('click', '#shopping-list-stock-add-workflow-skip-button', functio window.postMessage(WindowMessageBag("Ready"), Grocy.BaseUrl); }); +$(document).on('click', '.order-listitem-button', function(e) +{ + e.preventDefault(); + + Grocy.FrontendHelpers.BeginUiBusy(); + + var listItemId = $(e.currentTarget).attr('data-item-id'); + + var done = 1; + if ($(e.currentTarget).attr('data-item-done') == 1) + { + done = 0; + } + + $(e.currentTarget).attr('data-item-done', done); + + Grocy.Api.Put('objects/shopping_list/' + listItemId, { 'done': done }, + function() + { + if (done == 1) + { + $('#shoppinglistitem-' + listItemId + '-row').addClass("text-muted"); + $('#shoppinglistitem-' + listItemId + '-row').addClass("text-strike-through"); + } + else + { + $('#shoppinglistitem-' + listItemId + '-row').removeClass("text-muted"); + $('#shoppinglistitem-' + listItemId + '-row').removeClass("text-strike-through"); + } + + Grocy.FrontendHelpers.EndUiBusy(); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy(); + console.error(xhr); + } + ); +}); + function OnListItemRemoved() { if ($(".shopping-list-stock-add-workflow-list-item-button").length === 0) diff --git a/views/shoppinglist.blade.php b/views/shoppinglist.blade.php index c0c7b673..8a669370 100644 --- a/views/shoppinglist.blade.php +++ b/views/shoppinglist.blade.php @@ -85,8 +85,13 @@ @foreach($listItems as $listItem) - + + + + From 8c205941c727855eaeb12512ee8d67ef66d034c3 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 18:15:53 +0200 Subject: [PATCH 12/26] Added that products can now also be consumed as spoiled from the stock overview page (option in the more/context menu per line) (closes #251) --- changelog/50_2.4.3_2019-xx-xx.md | 1 + localization/strings.pot | 3 +++ public/viewjs/stockoverview.js | 11 +++++++++-- views/stockoverview.blade.php | 8 ++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index 6c50b76e..cdbb5024 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -8,3 +8,4 @@ - Improved shopping list handling - Items can now be switched between lists (there is a shopping list dropdown on the item edit page) - Items can now be marked as "done" (new check mark button per item, when clicked, the item will be displayed greyed out, when clicked again the item will be displayed normally again) +- Improved that products can now also be consumed as spoiled from the stock overview page (option in the more/context menu per line) diff --git a/localization/strings.pot b/localization/strings.pot index 35ace53f..bdcfe217 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1277,3 +1277,6 @@ msgstr "" msgid "Today" msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index 018469d2..4e0d16df 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -90,8 +90,9 @@ $(document).on('click', '.product-consume-button', function(e) var productId = $(e.currentTarget).attr('data-product-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 }, + Grocy.Api.Post('stock/products/' + productId + '/consume', { 'amount': consumeAmount, 'spoiled': wasSpoiled }, function() { Grocy.Api.Get('stock/products/' + productId, @@ -155,8 +156,14 @@ $(document).on('click', '.product-consume-button', function(e) }); } + 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); + if (wasSpoiled) + { + toastMessage += " (" + __t("Spoiled") + ")"; + } + Grocy.FrontendHelpers.EndUiBusy(); - toastr.success(__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)); + toastr.success(toastMessage); RefreshContextualTimeago(); RefreshStatistics(); }, diff --git a/views/stockoverview.blade.php b/views/stockoverview.blade.php index 44e7002b..4b07bc0a 100644 --- a/views/stockoverview.blade.php +++ b/views/stockoverview.blade.php @@ -122,6 +122,14 @@ {{ $__t('Edit product') }} + + + {{ $__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) }} + From 09b23847b54dddf4070c5d584405d552cc0644a1 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 18:29:18 +0200 Subject: [PATCH 13/26] Added a new `config.php` setting `DISABLE_AUTH` to be able to disable authentication / the login screen (closes #246) --- app.php | 9 +++++++++ changelog/50_2.4.3_2019-xx-xx.md | 1 + config-dist.php | 4 ++++ middleware/ApiKeyAuthMiddleware.php | 2 +- middleware/SessionAuthMiddleware.php | 2 +- 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app.php b/app.php index df81207c..448058a8 100644 --- a/app.php +++ b/app.php @@ -40,6 +40,15 @@ require_once __DIR__ . '/vendor/autoload.php'; require_once GROCY_DATAPATH . '/config.php'; require_once __DIR__ . '/config-dist.php'; //For not in own config defined values we use the default ones +// Definitions for disabled authentication mode +if (GROCY_DISABLE_AUTH === true) +{ + if (!defined('GROCY_USER_ID')) + { + define('GROCY_USER_ID', 1); + } +} + // Setup base application $appContainer = new \Slim\Container([ 'settings' => [ diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index cdbb5024..ec8a8d11 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -9,3 +9,4 @@ - Items can now be switched between lists (there is a shopping list dropdown on the item edit page) - Items can now be marked as "done" (new check mark button per item, when clicked, the item will be displayed greyed out, when clicked again the item will be displayed normally again) - Improved that products can now also be consumed as spoiled from the stock overview page (option in the more/context menu per line) +- Added a new `config.php` setting `DISABLE_AUTH` to be able to disable authentication / the login screen diff --git a/config-dist.php b/config-dist.php index 2b3f2c4b..029da686 100644 --- a/config-dist.php +++ b/config-dist.php @@ -41,6 +41,10 @@ Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin'); # set this to true Setting('DISABLE_URL_REWRITING', false); +# Set this to true if you want to disable authentication / the login screen, +# places where user context is needed will then use the default (first existing) user +Setting('DISABLE_AUTH', false); + diff --git a/middleware/ApiKeyAuthMiddleware.php b/middleware/ApiKeyAuthMiddleware.php index dd0bc62b..82487c03 100644 --- a/middleware/ApiKeyAuthMiddleware.php +++ b/middleware/ApiKeyAuthMiddleware.php @@ -22,7 +22,7 @@ class ApiKeyAuthMiddleware extends BaseMiddleware $route = $request->getAttribute('route'); $routeName = $route->getName(); - if (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL) + if (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL || GROCY_DISABLE_AUTH) { define('GROCY_AUTHENTICATED', true); $response = $next($request, $response); diff --git a/middleware/SessionAuthMiddleware.php b/middleware/SessionAuthMiddleware.php index df7c7a11..5cf436cc 100644 --- a/middleware/SessionAuthMiddleware.php +++ b/middleware/SessionAuthMiddleware.php @@ -25,7 +25,7 @@ class SessionAuthMiddleware extends BaseMiddleware { $response = $next($request, $response); } - elseif (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL) + elseif (GROCY_IS_DEMO_INSTALL || GROCY_IS_EMBEDDED_INSTALL || GROCY_DISABLE_AUTH) { $user = $sessionService->GetDefaultUser(); define('GROCY_AUTHENTICATED', true); From 1eb1aa8b11d4814352a9531c9157d351ad4bdf8c Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 20:02:40 +0200 Subject: [PATCH 14/26] Added a "consume this recipe"-button to the meal plan (and also a button to consume all recipes for a whole week) (closes #283) --- changelog/50_2.4.3_2019-xx-xx.md | 2 + localization/strings.pot | 3 ++ migrations/0076.sql | 87 ++++++++++++++++++++++++++++++++ public/viewjs/mealplan.js | 57 ++++++++++++++++++++- public/viewjs/recipes.js | 1 + services/RecipesService.php | 4 +- views/recipes.blade.php | 4 +- 7 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 migrations/0076.sql diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index ec8a8d11..63b8bf3f 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -2,6 +2,7 @@ - Fixed that "Track date only" chores were always tracked today, regardless of the given date - Fixed that the "week costs" were wrong after removing a meal plan entry - Fixed wrong recipes costs calculation with nested recipes when the base recipe servings are > 1 (also affected the meal plan when adding such a recipe there) +- Fixed consuming recipes did not consume ingredients of the nested recipes - Improved recipes API - added new endpoints to get stock fulfillment information (thanks @Aerex) - Improved date display for products that never expires (instead of "2999-12-31" now just "Never" will be shown) - Improved date display for dates of today and no time (instead of the hours since midnight now just "Today" will be shown) @@ -9,4 +10,5 @@ - Items can now be switched between lists (there is a shopping list dropdown on the item edit page) - Items can now be marked as "done" (new check mark button per item, when clicked, the item will be displayed greyed out, when clicked again the item will be displayed normally again) - Improved that products can now also be consumed as spoiled from the stock overview page (option in the more/context menu per line) +- Added a "consume this recipe"-button to the meal plan (and also a button to consume all recipes for a whole week) - Added a new `config.php` setting `DISABLE_AUTH` to be able to disable authentication / the login screen diff --git a/localization/strings.pot b/localization/strings.pot index bdcfe217..13c81bc9 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1280,3 +1280,6 @@ msgstr "" msgid "Consume %1$s of %2$s as spoiled" msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" diff --git a/migrations/0076.sql b/migrations/0076.sql new file mode 100644 index 00000000..c2b6b9e0 --- /dev/null +++ b/migrations/0076.sql @@ -0,0 +1,87 @@ +DROP VIEW recipes_pos_resolved; +CREATE VIEW recipes_pos_resolved +AS + +-- Multiplication by 1.0 to force conversion to float (REAL) + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount, + IFNULL(sc.amount, 0) AS stock_amount, + CASE WHEN IFNULL(sc.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled, + CASE WHEN IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END < 0 THEN ABS(IFNULL(sc.amount, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END)) ELSE 0 END AS missing_amount, + IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list, + CASE WHEN IFNULL(sc.amount, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list, + rp.qu_id, + (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + rp.id, -- Just a dummy id column + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount, + rp.only_check_single_unit_in_stock +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_price pcp + ON rp.product_id = pcp.product_id +WHERE rp.not_check_stock_fulfillment = 0 + +UNION + +-- Just add all recipe positions which should not be checked against stock with fulfilled need + +SELECT + r.id AS recipe_id, + rp.id AS recipe_pos_id, + rp.product_id AS product_id, + rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount, + IFNULL(sc.amount, 0) AS stock_amount, + 1 AS need_fulfilled, + 0 AS missing_amount, + IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list, + 1 AS need_fulfilled_with_shopping_list, + rp.qu_id, + (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END / p.qu_factor_purchase_to_stock) * pcp.last_price AS costs, + CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos, + rp.ingredient_group, + rp.id, -- Just a dummy id column + rnr.includes_recipe_id as child_recipe_id, + rp.note, + rp.variable_amount AS recipe_variable_amount, + rp.only_check_single_unit_in_stock +FROM recipes r +JOIN recipes_nestings_resolved rnr + ON r.id = rnr.recipe_id +JOIN recipes rnrr + ON rnr.includes_recipe_id = rnrr.id +JOIN recipes_pos rp + ON rnr.includes_recipe_id = rp.recipe_id +JOIN products p + ON rp.product_id = p.id +LEFT JOIN ( + SELECT product_id, SUM(amount) AS amount + FROM shopping_list + GROUP BY product_id) sl + ON rp.product_id = sl.product_id +LEFT JOIN stock_current sc + ON rp.product_id = sc.product_id +LEFT JOIN products_current_price pcp + ON rp.product_id = pcp.product_id +WHERE rp.not_check_stock_fulfillment = 1; diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 0170af49..9a35d182 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -29,6 +29,7 @@ var calendar = $("#calendar").fullCalendar({ var weekCosts = 0; var weekRecipeOrderMissingButtonHtml = ""; + var weekRecipeConsumeButtonHtml = ""; if (weekRecipe !== null) { weekCosts = FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).costs; @@ -38,9 +39,15 @@ var calendar = $("#calendar").fullCalendar({ { weekRecipeOrderMissingButtonDisabledClasses = "disabled"; } + var weekRecipeConsumeButtonDisabledClasses = ""; + if (FindObjectInArrayByPropertyValue(recipesResolved, "recipe_id", weekRecipe.id).need_fulfilled == 0) + { + weekRecipeConsumeButtonDisabledClasses = "disabled"; + } weekRecipeOrderMissingButtonHtml = '' + weekRecipeConsumeButtonHtml = '' } - $(".fc-header-toolbar .fc-center").html("

" + __t("Week costs") + ': ' + weekCosts.toString() + " " + weekRecipeOrderMissingButtonHtml + "

"); + $(".fc-header-toolbar .fc-center").html("

" + __t("Week costs") + ': ' + weekCosts.toString() + " " + weekRecipeOrderMissingButtonHtml + weekRecipeConsumeButtonHtml + "

"); }, "eventRender": function(event, element) { @@ -60,6 +67,12 @@ var calendar = $("#calendar").fullCalendar({ recipeOrderMissingButtonDisabledClasses = "disabled"; } + var recipeConsumeButtonDisabledClasses = ""; + if (resolvedRecipe.need_fulfilled == 0) + { + recipeConsumeButtonDisabledClasses = "disabled"; + } + var fulfillmentInfoHtml = __t('Enough in stock'); var fulfillmentIconHtml = ''; if (resolvedRecipe.need_fulfilled != 1) @@ -77,6 +90,7 @@ var calendar = $("#calendar").fullCalendar({
\ \ \ + \
\ '); @@ -230,3 +244,44 @@ $(document).on('click', '.recipe-order-missing-button', function(e) } }); }); + +$(document).on('click', '.recipe-consume-button', function(e) +{ + var objectName = $(e.currentTarget).attr('data-recipe-name'); + var objectId = $(e.currentTarget).attr('data-recipe-id'); + + bootbox.confirm({ + message: __t('Are you sure to consume all ingredients needed by recipe "%s" (ingredients marked with "check only if a single unit is in stock" will be ignored)?', objectName), + buttons: { + confirm: { + label: __t('Yes'), + className: 'btn-success' + }, + cancel: { + label: __t('No'), + className: 'btn-danger' + } + }, + callback: function(result) + { + if (result === true) + { + Grocy.FrontendHelpers.BeginUiBusy(); + + Grocy.Api.Post('recipes/' + objectId + '/consume', { }, + function(result) + { + Grocy.FrontendHelpers.EndUiBusy(); + toastr.success(__t('Removed all ingredients of recipe "%s" from stock', objectName)); + }, + function(xhr) + { + toastr.warning(__t('Not all ingredients of recipe "%s" are in stock, nothing removed', objectName)); + Grocy.FrontendHelpers.EndUiBusy(); + console.error(xhr); + } + ); + } + } + }); +}); diff --git a/public/viewjs/recipes.js b/public/viewjs/recipes.js index c1dde598..624a87ff 100644 --- a/public/viewjs/recipes.js +++ b/public/viewjs/recipes.js @@ -187,6 +187,7 @@ $("#selectedRecipeConsumeButton").on('click', function(e) function(xhr) { Grocy.FrontendHelpers.EndUiBusy(); + toastr.warning(__t('Not all ingredients of recipe "%s" are in stock, nothing removed', objectName)); console.error(xhr); } ); diff --git a/services/RecipesService.php b/services/RecipesService.php index 9011a6ce..966e1f5c 100644 --- a/services/RecipesService.php +++ b/services/RecipesService.php @@ -67,12 +67,12 @@ class RecipesService extends BaseService throw new \Exception('Recipe does not exist'); } - $recipePositions = $this->Database->recipes_pos()->where('recipe_id', $recipeId)->fetchAll(); + $recipePositions = $this->Database->recipes_pos_resolved()->where('recipe_id', $recipeId)->fetchAll(); foreach ($recipePositions as $recipePosition) { if ($recipePosition->only_check_single_unit_in_stock == 0) { - $this->StockService->ConsumeProduct($recipePosition->product_id, $recipePosition->amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId); + $this->StockService->ConsumeProduct($recipePosition->product_id, $recipePosition->recipe_amount, false, StockService::TRANSACTION_TYPE_CONSUME, 'default', $recipeId); } } } diff --git a/views/recipes.blade.php b/views/recipes.blade.php index d16964c1..c4972dc9 100644 --- a/views/recipes.blade.php +++ b/views/recipes.blade.php @@ -121,10 +121,10 @@
{{ $selectedRecipe->name }}   - + - +    From 914dde4609a122362d3d594ad3b5d7d0215b01ac Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 20:19:21 +0200 Subject: [PATCH 15/26] Added a new `config.php` setting `CALENDAR_FIRST_DAY_OF_WEEK` to be able to change the first day of a week used for calendar views (closes #256) --- changelog/50_2.4.3_2019-xx-xx.md | 3 ++- config-dist.php | 5 +++++ public/viewjs/calendar.js | 9 ++++++++- public/viewjs/mealplan.js | 7 +++++++ views/layout/default.blade.php | 1 + 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index 63b8bf3f..9a3a938d 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -11,4 +11,5 @@ - Items can now be marked as "done" (new check mark button per item, when clicked, the item will be displayed greyed out, when clicked again the item will be displayed normally again) - Improved that products can now also be consumed as spoiled from the stock overview page (option in the more/context menu per line) - Added a "consume this recipe"-button to the meal plan (and also a button to consume all recipes for a whole week) -- Added a new `config.php` setting `DISABLE_AUTH` to be able to disable authentication / the login screen +- Added a new `config.php` setting `DISABLE_AUTH` to be able to disable authentication / the login screen, defaults to `false` +- Added a new `config.php` setting `CALENDAR_FIRST_DAY_OF_WEEK` to be able to change the first day of a week used for calendar views (meal plan for example) in the frontend, defaults to locale default diff --git a/config-dist.php b/config-dist.php index 029da686..65423f78 100644 --- a/config-dist.php +++ b/config-dist.php @@ -21,6 +21,11 @@ Setting('MODE', 'production'); # one of the other available localization files in the "/localization" directory Setting('CULTURE', 'en'); +# This is used to define the first day of a week for calendar views in the frontend, +# leave empty to use the locale default +# Needs to be a number where Sunday = 0, Monday = 1 and so forth +Setting('CALENDAR_FIRST_DAY_OF_WEEK', ''); + # To keep it simple: grocy does not handle any currency conversions, # this here is used to format all money values, # so doesn't matter really matter, but should be the diff --git a/public/viewjs/calendar.js b/public/viewjs/calendar.js index c6e27c3b..0745bdf5 100644 --- a/public/viewjs/calendar.js +++ b/public/viewjs/calendar.js @@ -1,4 +1,10 @@ -$("#calendar").fullCalendar({ +var firstDay = null; +if (!Grocy.CalendarFirstDayOfWeek.isEmpty()) +{ + firstDay = parseInt(Grocy.CalendarFirstDayOfWeek); +} + +$("#calendar").fullCalendar({ "themeSystem": "bootstrap4", "header": { "left": "month,basicWeek,listWeek", @@ -6,6 +12,7 @@ "right": "prev,next" }, "weekNumbers": true, + "firstDay": firstDay, "eventLimit": true, "eventSources": fullcalendarEventSources }); diff --git a/public/viewjs/mealplan.js b/public/viewjs/mealplan.js index 9a35d182..33d30478 100644 --- a/public/viewjs/mealplan.js +++ b/public/viewjs/mealplan.js @@ -1,5 +1,11 @@ var firstRender = true; +var firstDay = null; +if (!Grocy.CalendarFirstDayOfWeek.isEmpty()) +{ + firstDay = parseInt(Grocy.CalendarFirstDayOfWeek); +} + var calendar = $("#calendar").fullCalendar({ "themeSystem": "bootstrap4", "header": { @@ -11,6 +17,7 @@ var calendar = $("#calendar").fullCalendar({ "eventLimit": true, "eventSources": fullcalendarEventSources, "defaultView": "basicWeek", + "firstDay": firstDay, "viewRender": function(view) { if (firstRender) diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index 95a6f2d1..d7d94a97 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -52,6 +52,7 @@ Grocy.ActiveNav = '@yield('activeNav', '')'; Grocy.Culture = '{{ GROCY_CULTURE }}'; Grocy.Currency = '{{ GROCY_CURRENCY }}'; + Grocy.CalendarFirstDayOfWeek = '{{ GROCY_CALENDAR_FIRST_DAY_OF_WEEK }}'; Grocy.GettextPo = {!! $GettextPo !!}; Grocy.UserSettings = {!! json_encode($userSettings) !!}; Grocy.FeatureFlags = {!! json_encode($featureFlags) !!}; From b4d2e2a20a423d8e2ac8766bebbff08401b8a8ff Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 20:34:01 +0200 Subject: [PATCH 16/26] Added the possibility to undo a task (closes #252) --- changelog/50_2.4.3_2019-xx-xx.md | 1 + controllers/TasksApiController.php | 13 ++++++++++++ grocy.openapi.json | 34 ++++++++++++++++++++++++++++++ localization/strings.pot | 3 +++ public/viewjs/tasks.js | 26 +++++++++++++++++++++++ routes.php | 1 + services/TasksService.php | 16 ++++++++++++++ views/tasks.blade.php | 10 ++++++++- 8 files changed, 103 insertions(+), 1 deletion(-) diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-xx-xx.md index 9a3a938d..0a0674f1 100644 --- a/changelog/50_2.4.3_2019-xx-xx.md +++ b/changelog/50_2.4.3_2019-xx-xx.md @@ -11,5 +11,6 @@ - Items can now be marked as "done" (new check mark button per item, when clicked, the item will be displayed greyed out, when clicked again the item will be displayed normally again) - Improved that products can now also be consumed as spoiled from the stock overview page (option in the more/context menu per line) - Added a "consume this recipe"-button to the meal plan (and also a button to consume all recipes for a whole week) +- Added the possibility to undo a task (new button per task, only visible when task is already completed) and also a corresponding API endpoint - Added a new `config.php` setting `DISABLE_AUTH` to be able to disable authentication / the login screen, defaults to `false` - Added a new `config.php` setting `CALENDAR_FIRST_DAY_OF_WEEK` to be able to change the first day of a week used for calendar views (meal plan for example) in the frontend, defaults to locale default diff --git a/controllers/TasksApiController.php b/controllers/TasksApiController.php index fa9c8606..af4edda1 100644 --- a/controllers/TasksApiController.php +++ b/controllers/TasksApiController.php @@ -39,4 +39,17 @@ class TasksApiController extends BaseApiController return $this->GenericErrorResponse($response, $ex->getMessage()); } } + + public function UndoTask(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + $this->TasksService->UndoTask($args['taskId']); + return $this->EmptyApiResponse($response); + } + catch (\Exception $ex) + { + return $this->GenericErrorResponse($response, $ex->getMessage()); + } + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index 292b64da..15e009fd 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -2151,6 +2151,40 @@ } } }, + "/tasks/{taskId}/undo": { + "post": { + "summary": "Marks the given task as not completed", + "tags": [ + "Tasks" + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "description": "A valid task id", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "The operation was successful" + }, + "400": { + "description": "The operation was not successful (possible errors are: Not existing task)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GenericErrorResponse" + } + } + } + } + } + } + }, "/calendar/ical": { "get": { "summary": "Returns the calendar in iCal format", diff --git a/localization/strings.pot b/localization/strings.pot index 13c81bc9..a71486c5 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -1283,3 +1283,6 @@ msgstr "" msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" msgstr "" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/public/viewjs/tasks.js b/public/viewjs/tasks.js index edbdd0c9..4b0f4220 100644 --- a/public/viewjs/tasks.js +++ b/public/viewjs/tasks.js @@ -102,6 +102,32 @@ $(document).on('click', '.do-task-button', function(e) ); }); +$(document).on('click', '.undo-task-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 taskId = $(e.currentTarget).attr('data-task-id'); + var taskName = $(e.currentTarget).attr('data-task-name'); + + Grocy.Api.Post('tasks/' + taskId + '/undo', { }, + function() + { + window.location.reload(); + }, + function(xhr) + { + Grocy.FrontendHelpers.EndUiBusy(); + console.error(xhr); + } + ); +}); + $(document).on('click', '.delete-task-button', function (e) { e.preventDefault(); diff --git a/routes.php b/routes.php index 38a24ab2..f32726ef 100644 --- a/routes.php +++ b/routes.php @@ -201,6 +201,7 @@ $app->group('/api', function() { $this->get('/tasks', '\Grocy\Controllers\TasksApiController:Current'); $this->post('/tasks/{taskId}/complete', '\Grocy\Controllers\TasksApiController:MarkTaskAsCompleted'); + $this->post('/tasks/{taskId}/undo', '\Grocy\Controllers\TasksApiController:UndoTask'); } // Calendar diff --git a/services/TasksService.php b/services/TasksService.php index a9262fdd..1219b4eb 100644 --- a/services/TasksService.php +++ b/services/TasksService.php @@ -26,6 +26,22 @@ class TasksService extends BaseService return true; } + public function UndoTask($taskId) + { + if (!$this->TaskExists($taskId)) + { + throw new \Exception('Task does not exist'); + } + + $taskRow = $this->Database->tasks()->where('id = :1', $taskId)->fetch(); + $taskRow->update(array( + 'done' => 0, + 'done_timestamp' => null + )); + + return true; + } + private function TaskExists($taskId) { $taskRow = $this->Database->tasks()->where('id = :1', $taskId)->fetch(); diff --git a/views/tasks.blade.php b/views/tasks.blade.php index 4c951a8a..b2fad076 100644 --- a/views/tasks.blade.php +++ b/views/tasks.blade.php @@ -73,11 +73,19 @@ @foreach($tasks as $task) - name) }}" + @if($task->done == 0) + name) }}" data-task-id="{{ $task->id }}" data-task-name="{{ $task->name }}"> + @else + name) }}" + data-task-id="{{ $task->id }}" + data-task-name="{{ $task->name }}"> + + + @endif From 001d5c5d1d09b3174b960ad78ebd70dffb9b5170 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sat, 6 Jul 2019 20:43:30 +0200 Subject: [PATCH 17/26] Prepared next release --- ...3_2019-xx-xx.md => 50_2.4.3_2019-07-06.md} | 0 localization/de/strings.po | 22 ++++++++++++++++- localization/en_GB/strings.po | 18 ++++++++++++++ localization/es/demo_data.po | 21 ++++++++-------- localization/fr/strings.po | 21 +++++++++++++++- localization/it/chore_types.po | 9 +++---- localization/nl/component_translations.po | 5 ++-- localization/no/strings.po | 18 ++++++++++++++ localization/pl/strings.po | 24 +++++++++++++++++++ localization/ru/strings.po | 24 +++++++++++++++++++ version.json | 4 ++-- 11 files changed, 146 insertions(+), 20 deletions(-) rename changelog/{50_2.4.3_2019-xx-xx.md => 50_2.4.3_2019-07-06.md} (100%) diff --git a/changelog/50_2.4.3_2019-xx-xx.md b/changelog/50_2.4.3_2019-07-06.md similarity index 100% rename from changelog/50_2.4.3_2019-xx-xx.md rename to changelog/50_2.4.3_2019-07-06.md diff --git a/localization/de/strings.po b/localization/de/strings.po index 099f89c6..9747f8c5 100644 --- a/localization/de/strings.po +++ b/localization/de/strings.po @@ -670,7 +670,7 @@ msgstr "" msgid "Removed all ingredients of recipe \"%s\" from stock" msgstr "" -"Alle Zutaten, die vom Rezept \"%s\" benötigt werden, wurdem aus dem Bestand " +"Alle Zutaten, die vom Rezept \"%s\" benötigt werden, wurden aus dem Bestand " "entfernt" msgid "Consume all ingredients needed by this recipe" @@ -1374,3 +1374,23 @@ msgid "Booking has subsequent dependent bookings, undo not possible" msgstr "" "Die Buchung hat nachfolgende abhängige Buchungen, rückgängig machen nicht " "möglich" + +msgid "per serving" +msgstr "pro Portion" + +msgid "Never" +msgstr "Nie" + +msgid "Today" +msgstr "Heute" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "Verbrauche %1$s %2$s als verdorben" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" +"Nicht alle Zutaten, die vom Rezept \"%s\" benötigt werden, sind vorrätig, es" +" wurde nichts aus dem Bestand entfernt" + +msgid "Undo task \"%s\"" +msgstr "Aufgabe \"%s\" rückgängig machen" diff --git a/localization/en_GB/strings.po b/localization/en_GB/strings.po index b2296527..9dec2974 100644 --- a/localization/en_GB/strings.po +++ b/localization/en_GB/strings.po @@ -1308,3 +1308,21 @@ msgstr "" msgid "Booking has subsequent dependent bookings, undo not possible" msgstr "" + +msgid "per serving" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/localization/es/demo_data.po b/localization/es/demo_data.po index 1a2c784b..c86e7c2b 100644 --- a/localization/es/demo_data.po +++ b/localization/es/demo_data.po @@ -1,6 +1,7 @@ # Translators: # Bernd Bestel , 2019 # Fernando Sánchez , 2019 +# Ankue , 2019 # msgid "" msgstr "" @@ -8,7 +9,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n" -"Last-Translator: Fernando Sánchez , 2019\n" +"Last-Translator: Ankue , 2019\n" "Language-Team: Spanish (https://www.transifex.com/grocy/teams/93189/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -37,8 +38,8 @@ msgstr "Nevera" msgid "Piece" msgid_plural "Pieces" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Pieza" +msgstr[1] "Piezas" msgid "Pack" msgid_plural "Packs" @@ -47,8 +48,8 @@ msgstr[1] "" msgid "Glass" msgid_plural "Glasses" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Vaso" +msgstr[1] "Vasos" msgid "Tin" msgid_plural "Tins" @@ -57,8 +58,8 @@ msgstr[1] "" msgid "Can" msgid_plural "Cans" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Lata" +msgstr[1] "Latas" msgid "Bunch" msgid_plural "Bunches" @@ -172,8 +173,8 @@ msgstr "Usuario de demostración" msgid "Gram" msgid_plural "Grams" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Gramo" +msgstr[1] "Gramos" msgid "Flour" msgstr "Harina" @@ -281,4 +282,4 @@ msgid "Swedish" msgstr "Sueco" msgid "Polish" -msgstr "" +msgstr "Polaco" diff --git a/localization/fr/strings.po b/localization/fr/strings.po index 56ddbc7d..3baffb66 100644 --- a/localization/fr/strings.po +++ b/localization/fr/strings.po @@ -5,6 +5,7 @@ # Cedric Octave , 2019 # Hydreliox Hydreliox , 2019 # Matthieu K, 2019 +# Mathieu Fortin , 2019 # msgid "" msgstr "" @@ -12,7 +13,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n" -"Last-Translator: Matthieu K, 2019\n" +"Last-Translator: Mathieu Fortin , 2019\n" "Language-Team: French (https://www.transifex.com/grocy/teams/93189/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1369,4 +1370,22 @@ msgid "Marked task %s as completed on %s" msgstr "" msgid "Booking has subsequent dependent bookings, undo not possible" +msgstr "La réservation a des dépendances, impossible de revenir en arrière" + +msgid "per serving" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" + +msgid "Undo task \"%s\"" msgstr "" diff --git a/localization/it/chore_types.po b/localization/it/chore_types.po index ddae56c3..25159fd9 100644 --- a/localization/it/chore_types.po +++ b/localization/it/chore_types.po @@ -1,5 +1,6 @@ # Translators: # Bernd Bestel , 2019 +# Matteo Piotto , 2019 # msgid "" msgstr "" @@ -7,7 +8,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n" -"Last-Translator: Bernd Bestel , 2019\n" +"Last-Translator: Matteo Piotto , 2019\n" "Language-Team: Italian (https://www.transifex.com/grocy/teams/93189/it/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -23,10 +24,10 @@ msgid "dynamic-regular" msgstr "Regolatore dinamico" msgid "daily" -msgstr "" +msgstr "Giornalmente" msgid "weekly" -msgstr "" +msgstr "Settimanalmente" msgid "monthly" -msgstr "" +msgstr "Mensilmente" diff --git a/localization/nl/component_translations.po b/localization/nl/component_translations.po index 880c06b8..b0a5667f 100644 --- a/localization/nl/component_translations.po +++ b/localization/nl/component_translations.po @@ -1,5 +1,6 @@ # Translators: # Bernd Bestel , 2019 +# Giel Janssens , 2019 # msgid "" msgstr "" @@ -7,7 +8,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n" -"Last-Translator: Bernd Bestel , 2019\n" +"Last-Translator: Giel Janssens , 2019\n" "Language-Team: Dutch (https://www.transifex.com/grocy/teams/93189/nl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -26,7 +27,7 @@ msgid "moment_locale" msgstr "nl" msgid "datatables_localization" -msgstr "" +msgstr "nl-NL" msgid "summernote_locale" msgstr "nl-NL" diff --git a/localization/no/strings.po b/localization/no/strings.po index 514286cd..632188b5 100644 --- a/localization/no/strings.po +++ b/localization/no/strings.po @@ -1357,3 +1357,21 @@ msgstr "" msgid "Booking has subsequent dependent bookings, undo not possible" msgstr "" + +msgid "per serving" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/localization/pl/strings.po b/localization/pl/strings.po index 6afbd148..65bc20ec 100644 --- a/localization/pl/strings.po +++ b/localization/pl/strings.po @@ -1387,3 +1387,27 @@ msgstr "Dni \"blisko terminu wykonania zadania\"" msgid "Products" msgstr "Produkty" + +msgid "Marked task %s as completed on %s" +msgstr "Oznaczono zadanie %s jako wykonane dnia %s " + +msgid "Booking has subsequent dependent bookings, undo not possible" +msgstr "Rezerwacja ma kolejne zależne rezerwacje, nie można jej cofnąć" + +msgid "per serving" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "Today" +msgstr "" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/localization/ru/strings.po b/localization/ru/strings.po index ed314ec0..cb1faea6 100644 --- a/localization/ru/strings.po +++ b/localization/ru/strings.po @@ -1391,3 +1391,27 @@ msgstr "Критерий для \"подходит срок задач\" в дн msgid "Products" msgstr "Продукты" + +msgid "Marked task %s as completed on %s" +msgstr "Пометить задачу \"%s\" как выполненную %s" + +msgid "Booking has subsequent dependent bookings, undo not possible" +msgstr "У данного действия есть зависимые действия, отмена невозможна" + +msgid "per serving" +msgstr "на порцию" + +msgid "Never" +msgstr "Никогда" + +msgid "Today" +msgstr "Сегодня" + +msgid "Consume %1$s of %2$s as spoiled" +msgstr "Употребить %1$s %2$s как испорченное" + +msgid "Not all ingredients of recipe \"%s\" are in stock, nothing removed" +msgstr "Не все ингредиенты рецепта \"%s\" есть в запасе, ничего не изъято" + +msgid "Undo task \"%s\"" +msgstr "" diff --git a/version.json b/version.json index bb67ee32..fa5db305 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "Version": "2.4.2", - "ReleaseDate": "2019-06-09" + "Version": "2.4.3", + "ReleaseDate": "2019-07-06" } From 84e6e253ea76b96d336c4c15b6cd00a2c67ed629 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 7 Jul 2019 09:04:40 +0200 Subject: [PATCH 18/26] Fixed date "never" display on stock overview page (again closes #296) --- public/js/grocy.js | 4 +++- public/viewjs/stockoverview.js | 18 ++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/public/js/grocy.js b/public/js/grocy.js index 745f115f..2a64f45b 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -299,13 +299,15 @@ RefreshContextualTimeago = function() { var element = $(this); var timestamp = element.attr("datetime"); + var isNever = timestamp && timestamp.substring(0, 10) == "2999-12-31"; var isToday = timestamp && timestamp.length == 10 && timestamp.substring(0, 10) == moment().format("YYYY-MM-DD"); + if (isNever) { element.prev().text(__t("Never")); } - if (isToday) + else if (isToday) { element.text(__t("Today")); } diff --git a/public/viewjs/stockoverview.js b/public/viewjs/stockoverview.js index 4e0d16df..316afa46 100644 --- a/public/viewjs/stockoverview.js +++ b/public/viewjs/stockoverview.js @@ -164,8 +164,13 @@ $(document).on('click', '.product-consume-button', function(e) Grocy.FrontendHelpers.EndUiBusy(); toastr.success(toastMessage); - RefreshContextualTimeago(); RefreshStatistics(); + + // Needs to be delayed because of the animation above the date-text would be wrong if fired immediately... + setTimeout(function () + { + RefreshContextualTimeago(); + }, 520); }, function(xhr) { @@ -220,14 +225,14 @@ $(document).on('click', '.product-open-button', function(e) } $('#product-' + productId + '-next-best-before-date').parent().effect('highlight', {}, 500); - $('#product-' + productId + '-next-best-before-date').fadeOut(500, function () + $('#product-' + productId + '-next-best-before-date').fadeOut(500, function() { $(this).text(result.next_best_before_date).fadeIn(500); }); $('#product-' + productId + '-next-best-before-date-timeago').attr('datetime', result.next_best_before_date); $('#product-' + productId + '-opened-amount').parent().effect('highlight', {}, 500); - $('#product-' + productId + '-opened-amount').fadeOut(500, function () + $('#product-' + productId + '-opened-amount').fadeOut(500, function() { $(this).text(__t('%s opened', result.stock_amount_opened)).fadeIn(500); }); @@ -239,8 +244,13 @@ $(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)); - RefreshContextualTimeago(); RefreshStatistics(); + + // Needs to be delayed because of the animation above the date-text would be wrong if fired immediately... + setTimeout(function() + { + RefreshContextualTimeago(); + }, 600); }, function(xhr) { From 87976b86d9b008bebdc86baeb71bdec0fdfadc10 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 7 Jul 2019 09:25:13 +0200 Subject: [PATCH 19/26] Also display price data from inventory corrections, not only purchases (fixes #303) --- services/StockService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/StockService.php b/services/StockService.php index deed7afe..13101dec 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -82,7 +82,7 @@ class StockService extends BaseService $averageShelfLifeDays = intval($this->Database->stock_average_product_shelf_life()->where('id', $productId)->fetch()->average_shelf_life_days); $lastPrice = null; - $lastLogRow = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2 AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch(); + $lastLogRow = $this->Database->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->orderBy('row_created_timestamp', 'DESC')->limit(1)->fetch(); if ($lastLogRow !== null && !empty($lastLogRow)) { $lastPrice = $lastLogRow->price; @@ -120,7 +120,7 @@ class StockService extends BaseService } $returnData = array(); - $rows = $this->Database->stock_log()->where('product_id = :1 AND transaction_type = :2 AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE)->whereNOT('price', null)->orderBy('purchased_date', 'DESC'); + $rows = $this->Database->stock_log()->where('product_id = :1 AND transaction_type IN (:2, :3) AND undone = 0', $productId, self::TRANSACTION_TYPE_PURCHASE, self::TRANSACTION_TYPE_INVENTORY_CORRECTION)->whereNOT('price', null)->orderBy('purchased_date', 'DESC'); foreach ($rows as $row) { $returnData[] = array( From 52dd01f31345ab92525e7943bdcf172cd167f875 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 7 Jul 2019 09:29:04 +0200 Subject: [PATCH 20/26] Fixed that a string was never translated --- views/inventory.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/inventory.blade.php b/views/inventory.blade.php index 42de3518..758118bc 100644 --- a/views/inventory.blade.php +++ b/views/inventory.blade.php @@ -59,7 +59,7 @@ @include('components.locationpicker', array( 'locations' => $locations, - 'hint' => 'This will apply to added products' + 'hint' => $__t('This will apply to added products') )) From 4822d9a4b8b92773e7c6779ed038789f84a137a9 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 7 Jul 2019 10:10:20 +0200 Subject: [PATCH 21/26] Fixed date-only-datetimepicker width --- public/css/grocy.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/css/grocy.css b/public/css/grocy.css index 1ec0fced..d9eb9949 100644 --- a/public/css/grocy.css +++ b/public/css/grocy.css @@ -270,7 +270,7 @@ html { } /* Third party component customizations - Tempus Dominus */ -.date-only-datetimepicker .bootstrap-datetimepicker-widget.dropdown-menu { +.bootstrap-datetimepicker-widget.dropdown-menu { width: auto !important; } From 13c432b0cf6549c0edaee742948ae1a803fb21b2 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 7 Jul 2019 19:19:54 +0200 Subject: [PATCH 22/26] Fixed weekly chores were scheduled on the same day (fixes #304) --- migrations/0077.sql | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 migrations/0077.sql diff --git a/migrations/0077.sql b/migrations/0077.sql new file mode 100644 index 00000000..ca753808 --- /dev/null +++ b/migrations/0077.sql @@ -0,0 +1,26 @@ +DROP VIEW chores_current; +CREATE VIEW chores_current +AS +SELECT + h.id AS chore_id, + MAX(l.tracked_time) AS last_tracked_time, + CASE h.period_type + WHEN 'manually' THEN '2999-12-31 23:59:59' + WHEN 'dynamic-regular' THEN DATETIME(MAX(l.tracked_time), '+' || CAST(h.period_days AS TEXT) || ' day') + WHEN 'daily' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+1 day') + WHEN 'weekly' THEN + CASE + WHEN period_config LIKE '%sunday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 0') + WHEN period_config LIKE '%monday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 1') + WHEN period_config LIKE '%tuesday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 2') + WHEN period_config LIKE '%wednesday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 3') + WHEN period_config LIKE '%thursday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 4') + WHEN period_config LIKE '%friday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 5') + WHEN period_config LIKE '%saturday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 6') + END + WHEN 'monthly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+1 month', 'start of month', '+' || CAST(h.period_days - 1 AS TEXT) || ' day') + END AS next_estimated_execution_time +FROM chores h +LEFT JOIN chores_log l + ON h.id = l.chore_id +GROUP BY h.id, h.period_days; From 6e3407b1573eedf6833cf34acddf5d2f1e0d6fe2 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 7 Jul 2019 19:38:57 +0200 Subject: [PATCH 23/26] Always show "Track date only" shore execution times without the time part --- migrations/0077.sql | 3 ++- public/js/grocy.js | 6 ++++++ views/choresjournal.blade.php | 4 ++-- views/choresoverview.blade.php | 4 ++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/migrations/0077.sql b/migrations/0077.sql index ca753808..55c4ec2e 100644 --- a/migrations/0077.sql +++ b/migrations/0077.sql @@ -19,7 +19,8 @@ SELECT WHEN period_config LIKE '%saturday%' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '1 days', 'weekday 6') END WHEN 'monthly' THEN DATETIME(IFNULL(MAX(l.tracked_time), DATETIME('now', 'localtime')), '+1 month', 'start of month', '+' || CAST(h.period_days - 1 AS TEXT) || ' day') - END AS next_estimated_execution_time + END AS next_estimated_execution_time, + h.track_date_only FROM chores h LEFT JOIN chores_log l ON h.id = l.chore_id diff --git a/public/js/grocy.js b/public/js/grocy.js index 2a64f45b..a44c9c6c 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -302,6 +302,7 @@ RefreshContextualTimeago = function() var isNever = timestamp && timestamp.substring(0, 10) == "2999-12-31"; var isToday = timestamp && timestamp.length == 10 && timestamp.substring(0, 10) == moment().format("YYYY-MM-DD"); + var isDateWithoutTime = element.hasClass("timeago-date-only"); if (isNever) { @@ -315,6 +316,11 @@ RefreshContextualTimeago = function() { element.timeago("update", timestamp); } + + if (isDateWithoutTime) + { + element.prev().text(element.prev().text().substring(0, 10)); + } }); } RefreshContextualTimeago(); diff --git a/views/choresjournal.blade.php b/views/choresjournal.blade.php index c97aae37..96cab621 100644 --- a/views/choresjournal.blade.php +++ b/views/choresjournal.blade.php @@ -55,8 +55,8 @@ @endif - {{ $choreLogEntry->tracked_time }} - + {{ $choreLogEntry->tracked_time }} + @if ($choreLogEntry->done_by_user_id !== null && !empty($choreLogEntry->done_by_user_id)) diff --git a/views/choresoverview.blade.php b/views/choresoverview.blade.php index 9d327ada..59a67a20 100644 --- a/views/choresoverview.blade.php +++ b/views/choresoverview.blade.php @@ -85,14 +85,14 @@ @if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY) {{ $curentChoreEntry->next_estimated_execution_time }} - + @else ... @endif {{ $curentChoreEntry->last_tracked_time }} - + @if(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s')) overdue @elseif(FindObjectInArrayByPropertyValue($chores, 'id', $curentChoreEntry->chore_id)->period_type !== \Grocy\Services\ChoresService::CHORE_TYPE_MANUALLY && $curentChoreEntry->next_estimated_execution_time < date('Y-m-d H:i:s', strtotime("+$nextXDays days"))) duesoon @endif From 430286ae9e30573053d57cd7cdc4bbf5324b5f37 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 7 Jul 2019 19:47:05 +0200 Subject: [PATCH 24/26] Don't consider a chores executed when the execution was undone --- migrations/0077.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/migrations/0077.sql b/migrations/0077.sql index 55c4ec2e..808db214 100644 --- a/migrations/0077.sql +++ b/migrations/0077.sql @@ -24,4 +24,5 @@ SELECT FROM chores h LEFT JOIN chores_log l ON h.id = l.chore_id + AND l.undone = 0 GROUP BY h.id, h.period_days; From 197b83fee89c70013003d48dcf45ab113dfbcbe6 Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 7 Jul 2019 19:51:40 +0200 Subject: [PATCH 25/26] Prepared next release --- changelog/51_2.4.4_2019-07-07.md | 6 ++++++ version.json | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 changelog/51_2.4.4_2019-07-07.md diff --git a/changelog/51_2.4.4_2019-07-07.md b/changelog/51_2.4.4_2019-07-07.md new file mode 100644 index 00000000..a33396ec --- /dev/null +++ b/changelog/51_2.4.4_2019-07-07.md @@ -0,0 +1,6 @@ +- Fixed that price data (last price & chart) was not taken from inventory correction bookings, only purchases +- Fixed weekly chores were scheduled on the same after execution +- Fixed that undone chores were also included in "Last tracked" +- Fixed the date-time-picker width was too narrow sometimes +- Improved that execution dates "Track date only" chores will never include the time part +- Improved date display for products that never expire (again, there was a display problem after consuming an item on the stock overview page) diff --git a/version.json b/version.json index fa5db305..685ae57f 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "Version": "2.4.3", - "ReleaseDate": "2019-07-06" + "Version": "2.4.4", + "ReleaseDate": "2019-07-07" } From 091145c62c3dff9896b7b046e88a834c09d2daad Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Sun, 7 Jul 2019 20:00:05 +0200 Subject: [PATCH 26/26] Minor typos... --- changelog/51_2.4.4_2019-07-07.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog/51_2.4.4_2019-07-07.md b/changelog/51_2.4.4_2019-07-07.md index a33396ec..2b91b837 100644 --- a/changelog/51_2.4.4_2019-07-07.md +++ b/changelog/51_2.4.4_2019-07-07.md @@ -1,6 +1,6 @@ - Fixed that price data (last price & chart) was not taken from inventory correction bookings, only purchases -- Fixed weekly chores were scheduled on the same after execution +- Fixed weekly chores were scheduled on the same day after execution - Fixed that undone chores were also included in "Last tracked" - Fixed the date-time-picker width was too narrow sometimes -- Improved that execution dates "Track date only" chores will never include the time part +- Improved that execution dates of "Track date only" chores will never display the time part - Improved date display for products that never expire (again, there was a display problem after consuming an item on the stock overview page)