mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Added a plugin system for looking up products against external services by barcode (references #6)
This commit is contained in:
parent
4853174d03
commit
a9a1358b08
@ -43,6 +43,11 @@ The following shorthands are available:
|
||||
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.
|
||||
|
||||
### 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 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
|
||||
# or for example "https:/example.com/grocy" when using a subdirectory
|
||||
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();
|
||||
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
|
||||
!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}": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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;
|
||||
}
|
||||
|
||||
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-current-stock', 'Grocy\Controllers\StockApiController:CurrentStock');
|
||||
$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/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();
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user