mirror of
https://github.com/grocy/grocy.git
synced 2025-08-18 19:37:12 +00:00
Added a plugin system for looking up products against external services by barcode (references #6)
This commit is contained in:
@@ -43,6 +43,11 @@ The following shorthands are available:
|
|||||||
Wherever a button contains a bold highlighted letter, this is a shortcut key.
|
Wherever a button contains a bold highlighted letter, this is a shortcut key.
|
||||||
Example: Button "Add as new **p**roduct" can be "pressed" by using the `P` key on your keyboard.
|
Example: Button "Add as new **p**roduct" can be "pressed" by using the `P` key on your keyboard.
|
||||||
|
|
||||||
|
### Barcode lookup via external services
|
||||||
|
Products can be directly added to the database via looking them up against external services by a barcode.
|
||||||
|
This is currently only possible through the REST API.
|
||||||
|
There is no plugin included for any service, see the reference implementation in `data/plugins/DemoBarcodeLookupPlugin.php`.
|
||||||
|
|
||||||
### Database migrations
|
### Database migrations
|
||||||
Database schema migration is automatically done when visiting the root (`/`) route (click on the logo in the left upper edge).
|
Database schema migration is automatically done when visiting the root (`/`) route (click on the logo in the left upper edge).
|
||||||
|
|
||||||
|
@@ -15,3 +15,8 @@ define('CULTURE', 'en');
|
|||||||
# should be just "/" when running directly under the root of a (sub)domain
|
# should be just "/" when running directly under the root of a (sub)domain
|
||||||
# or for example "https:/example.com/grocy" when using a subdirectory
|
# or for example "https:/example.com/grocy" when using a subdirectory
|
||||||
define('BASE_URL', '/');
|
define('BASE_URL', '/');
|
||||||
|
|
||||||
|
# The plugin to use for external barcode lookups,
|
||||||
|
# must be the filename without .php extension and must be located in /data/plugins,
|
||||||
|
# see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation
|
||||||
|
define('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
|
||||||
|
@@ -105,4 +105,22 @@ class StockApiController extends BaseApiController
|
|||||||
$this->StockService->AddMissingProductsToShoppingList();
|
$this->StockService->AddMissingProductsToShoppingList();
|
||||||
return $this->VoidApiActionResponse($response);
|
return $this->VoidApiActionResponse($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ExternalBarcodeLookup(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$addFoundProduct = false;
|
||||||
|
if (isset($request->getQueryParams()['add']) && ($request->getQueryParams()['add'] === 'true' || $request->getQueryParams()['add'] === 1))
|
||||||
|
{
|
||||||
|
$addFoundProduct = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->ApiResponse($this->StockService->ExternalBarcodeLookup($args['barcode'], $addFoundProduct));
|
||||||
|
}
|
||||||
|
catch (\Exception $ex)
|
||||||
|
{
|
||||||
|
return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1
data/.gitignore
vendored
1
data/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
*
|
*
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!viewcache
|
!viewcache
|
||||||
|
!plugins
|
||||||
|
3
data/plugins/.gitignore
vendored
Normal file
3
data/plugins/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
!DemoBarcodeLookupPlugin.php
|
78
data/plugins/DemoBarcodeLookupPlugin.php
Normal file
78
data/plugins/DemoBarcodeLookupPlugin.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use \Grocy\Helpers\BaseBarcodeLookupPlugin;
|
||||||
|
|
||||||
|
/*
|
||||||
|
This class must extend BaseBarcodeLookupPlugin (in namespace \Grocy\Helpers)
|
||||||
|
*/
|
||||||
|
class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
To use this plugin, configure it in data/config.php like this:
|
||||||
|
define('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin');
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
To try it:
|
||||||
|
Call the API function at /api/stock/external-barcode-lookup/{barcode}
|
||||||
|
|
||||||
|
When you also add ?add=true as a query parameter to the API call,
|
||||||
|
on a successful lookup the product is added to the database and in the output
|
||||||
|
the new product id is included (automatically, nothing to do here in the plugin)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Provided references:
|
||||||
|
|
||||||
|
$this->Locations contains all locations
|
||||||
|
$this->QuantityUnits contains all quantity units
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Useful hints:
|
||||||
|
|
||||||
|
Get a quantity unit by name:
|
||||||
|
$quantityUnit = FindObjectInArrayByPropertyValue($this->QuantityUnits, 'name', 'Piece');
|
||||||
|
|
||||||
|
Get a location by name:
|
||||||
|
$location = FindObjectInArrayByPropertyValue($this->Locations, 'name', 'Fridge');
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
This class must implement the protected abstract function ExecuteLookup($barcode),
|
||||||
|
which is called with the barcode that needs to be looked up and must return an
|
||||||
|
associative array of the product model or null, when nothing was found for the barcode.
|
||||||
|
|
||||||
|
The returned array must contain at least these properties:
|
||||||
|
array(
|
||||||
|
'name' => '',
|
||||||
|
'location_id' => 1, // A valid id of a location object, check against $this->Locations
|
||||||
|
'qu_id_purchase' => 1, // A valid id of quantity unit object, check against $this->QuantityUnits
|
||||||
|
'qu_id_stock' => 1, // A valid id of quantity unit object, check against $this->QuantityUnits
|
||||||
|
'qu_factor_purchase_to_stock' => 1, // Normally 1 when quantity unit stock and purchase is the same
|
||||||
|
'barcode' => $barcode // The barcode of the product, maybe just pass through $barcode or manipulate it if necessary
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
protected function ExecuteLookup($barcode)
|
||||||
|
{
|
||||||
|
if ($barcode === 'x') // Demonstration when nothing is found
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
elseif ($barcode === 'e') // Demonstration when an error occurred
|
||||||
|
{
|
||||||
|
throw new \Exception('This is the error message from the plugin...');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
'name' => 'LookedUpProduct_' . RandomString(5),
|
||||||
|
'location_id' => $this->Locations[0]->id,
|
||||||
|
'qu_id_purchase' => $this->QuantityUnits[0]->id,
|
||||||
|
'qu_id_stock' => $this->QuantityUnits[0]->id,
|
||||||
|
'qu_factor_purchase_to_stock' => 1,
|
||||||
|
'barcode' => $barcode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -650,6 +650,57 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/stock/external-barcode-lookup/{barcode}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Executes an external barcode lookoup via the configured plugin with the given barcode",
|
||||||
|
"tags": [
|
||||||
|
"Stock"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"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 output",
|
||||||
|
"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": "A VoidApiActionResponse object (possible errors are: Plugin error)",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/habits/track-habit-execution/{habitId}": {
|
"/habits/track-habit-execution/{habitId}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Tracks an execution of the given habit",
|
"description": "Tracks an execution of the given habit",
|
||||||
@@ -992,6 +1043,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"HabitDetailsResponse": {
|
"HabitDetailsResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
80
helpers/BaseBarcodeLookupPlugin.php
Normal file
80
helpers/BaseBarcodeLookupPlugin.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Grocy\Helpers;
|
||||||
|
|
||||||
|
abstract class BaseBarcodeLookupPlugin
|
||||||
|
{
|
||||||
|
final public function __construct($locations, $quantityUnits)
|
||||||
|
{
|
||||||
|
$this->Locations = $locations;
|
||||||
|
$this->QuantityUnits = $quantityUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected $Locations;
|
||||||
|
protected $QuantityUnits;
|
||||||
|
|
||||||
|
abstract protected function ExecuteLookup($barcode);
|
||||||
|
|
||||||
|
final public function Lookup($barcode)
|
||||||
|
{
|
||||||
|
$pluginOutput = $this->ExecuteLookup($barcode);
|
||||||
|
|
||||||
|
if ($pluginOutput === null)
|
||||||
|
{
|
||||||
|
return $pluginOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin must return an associative array
|
||||||
|
if (!is_array($pluginOutput))
|
||||||
|
{
|
||||||
|
throw new \Exception('Plugin output must be an associative array');
|
||||||
|
}
|
||||||
|
if (!IsAssociativeArray($pluginOutput)) // $pluginOutput is at least an indexed array here
|
||||||
|
{
|
||||||
|
throw new \Exception('Plugin output must be an associative array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for minimum needed properties
|
||||||
|
$minimunNeededProperties = array(
|
||||||
|
'name',
|
||||||
|
'location_id',
|
||||||
|
'qu_id_purchase',
|
||||||
|
'qu_id_stock',
|
||||||
|
'qu_factor_purchase_to_stock',
|
||||||
|
'barcode'
|
||||||
|
);
|
||||||
|
foreach ($minimunNeededProperties as $prop)
|
||||||
|
{
|
||||||
|
if (!array_key_exists($prop, $pluginOutput))
|
||||||
|
{
|
||||||
|
throw new \Exception("Plugin output does not provide needed property $prop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// $pluginOutput contains all needed properties here
|
||||||
|
|
||||||
|
// Check referenced entity ids are valid
|
||||||
|
$locationId = $pluginOutput['location_id'];
|
||||||
|
if (FindObjectInArrayByPropertyValue($this->Locations, 'id', $locationId) === null)
|
||||||
|
{
|
||||||
|
throw new \Exception("Location $locationId is not a valid location id");
|
||||||
|
}
|
||||||
|
$quIdPurchase = $pluginOutput['qu_id_purchase'];
|
||||||
|
if (FindObjectInArrayByPropertyValue($this->QuantityUnits, 'id', $quIdPurchase) === null)
|
||||||
|
{
|
||||||
|
throw new \Exception("Location $quIdPurchase is not a valid quantity unit id");
|
||||||
|
}
|
||||||
|
$quIdStock = $pluginOutput['qu_id_stock'];
|
||||||
|
if (FindObjectInArrayByPropertyValue($this->QuantityUnits, 'id', $quIdStock) === null)
|
||||||
|
{
|
||||||
|
throw new \Exception("Location $quIdStock is not a valid quantity unit id");
|
||||||
|
}
|
||||||
|
$quFactor = $pluginOutput['qu_factor_purchase_to_stock'];
|
||||||
|
if (empty($quFactor) || !is_numeric($quFactor))
|
||||||
|
{
|
||||||
|
throw new \Exception('Quantity unit factor is empty or not a number');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pluginOutput;
|
||||||
|
}
|
||||||
|
}
|
@@ -72,3 +72,9 @@ function RandomString($length, $allowedChars = '0123456789abcdefghijklmnopqrstuv
|
|||||||
|
|
||||||
return $randomString;
|
return $randomString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IsAssociativeArray(array $array)
|
||||||
|
{
|
||||||
|
$keys = array_keys($array);
|
||||||
|
return array_keys($keys) !== $keys;
|
||||||
|
}
|
||||||
|
@@ -70,6 +70,7 @@ $app->group('/api', function()
|
|||||||
$this->get('/stock/get-product-details/{productId}', 'Grocy\Controllers\StockApiController:ProductDetails');
|
$this->get('/stock/get-product-details/{productId}', 'Grocy\Controllers\StockApiController:ProductDetails');
|
||||||
$this->get('/stock/get-current-stock', 'Grocy\Controllers\StockApiController:CurrentStock');
|
$this->get('/stock/get-current-stock', 'Grocy\Controllers\StockApiController:CurrentStock');
|
||||||
$this->get('/stock/add-missing-products-to-shoppinglist', 'Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
|
$this->get('/stock/add-missing-products-to-shoppinglist', 'Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
|
||||||
|
$this->get('/stock/external-barcode-lookup/{barcode}', 'Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
|
||||||
|
|
||||||
$this->get('/habits/track-habit-execution/{habitId}', 'Grocy\Controllers\HabitsApiController:TrackHabitExecution');
|
$this->get('/habits/track-habit-execution/{habitId}', 'Grocy\Controllers\HabitsApiController:TrackHabitExecution');
|
||||||
$this->get('/habits/get-habit-details/{habitId}', 'Grocy\Controllers\HabitsApiController:HabitDetails');
|
$this->get('/habits/get-habit-details/{habitId}', 'Grocy\Controllers\HabitsApiController:HabitDetails');
|
||||||
|
@@ -208,4 +208,44 @@ class StockService extends BaseService
|
|||||||
$productRow = $this->Database->products()->where('id = :1', $productId)->fetch();
|
$productRow = $this->Database->products()->where('id = :1', $productId)->fetch();
|
||||||
return $productRow !== null;
|
return $productRow !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function LoadBarcodeLookupPlugin()
|
||||||
|
{
|
||||||
|
$pluginName = defined('STOCK_BARCODE_LOOKUP_PLUGIN') ? STOCK_BARCODE_LOOKUP_PLUGIN : '';
|
||||||
|
if (empty($pluginName))
|
||||||
|
{
|
||||||
|
throw new \Exception('No barcode lookup plugin defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = __DIR__ . "/../data/plugins/$pluginName.php";
|
||||||
|
if (file_exists($path))
|
||||||
|
{
|
||||||
|
require_once $path;
|
||||||
|
return new $pluginName($this->Database->locations()->fetchAll(), $this->Database->quantity_units()->fetchAll());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new \Exception("Plugin $pluginName was not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ExternalBarcodeLookup($barcode, $addFoundProduct)
|
||||||
|
{
|
||||||
|
$plugin = $this->LoadBarcodeLookupPlugin();
|
||||||
|
$pluginOutput = $plugin->Lookup($barcode);
|
||||||
|
|
||||||
|
if ($pluginOutput !== null) // Lookup was successful
|
||||||
|
{
|
||||||
|
if ($addFoundProduct === true)
|
||||||
|
{
|
||||||
|
// Add product to database and include new product id in output
|
||||||
|
$newRow = $this->Database->products()->createRow($pluginOutput);
|
||||||
|
$newRow->save();
|
||||||
|
|
||||||
|
$pluginOutput['id'] = $newRow->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $pluginOutput;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1 +1 @@
|
|||||||
1.9.1
|
1.9.2
|
||||||
|
Reference in New Issue
Block a user