diff --git a/README.md b/README.md
index ece4641b..a74b42fa 100644
--- a/README.md
+++ b/README.md
@@ -129,11 +129,11 @@ Example: Button "**P** Add as new product" can be "pressed" by using the `P` key
Products can be directly added to the database via looking them up against external services by a barcode.
-This can be done in-place using the product picker workflow "External barcode lookup (via plugin)" (the workflow dialog is displayed when entering something unknown in any product input field).
+This can be done in-place using the product picker workflow "External barcode lookup" (the workflow dialog is displayed when entering something unknown in any product input field).
A plugin for [Open Food Facts](https://world.openfoodfacts.org/) is included and used by default (see the `data/config.php` option `STOCK_BARCODE_LOOKUP_PLUGIN`).
-See that plugin or the reference implementation in `data/plugins/DemoBarcodeLookupPlugin.php` if you want to build a plugin.
+See that plugin or `plugins/DemoBarcodeLookupPlugin.php` for a commented example implementation if you want to build a plugin.
### Database migrations
diff --git a/changelog/52_2.5.0_2019-09-22.md b/changelog/52_2.5.0_2019-09-22.md
index efc1fad2..5334feb5 100644
--- a/changelog/52_2.5.0_2019-09-22.md
+++ b/changelog/52_2.5.0_2019-09-22.md
@@ -9,7 +9,7 @@
- Implemented using [QuaggaJS](https://github.com/serratus/quaggaJS) - camera stream processing happens totally offline / client-side
- Please note due to browser security restrictions, this only works when serving Grocy via a secure connection (`https://`)
- There is also a `config.php` setting `DISABLE_BROWSER_BARCODE_CAMERA_SCANNING` to disable this, if you don't need it at all (defaults to `false`)
-- I you have problems that barcodes are not recognized properly, there is a little "barcode scanner testing page" at [/barcodescannertesting](https://demo.grocy.info/barcodescannertesting)
+- If you have problems that barcodes are not recognized properly, there is a little "barcode scanner testing page" at [/barcodescannertesting](https://demo.grocy.info/barcodescannertesting)
- => Quick video demo: https://www.youtube.com/watch?v=Y5YH6IJFnfc
### Stock improvements/fixes
diff --git a/changelog/77_UNRELEASED_xxxx-xx-xx.md b/changelog/77_UNRELEASED_xxxx-xx-xx.md
index 8ffe9473..ef25e8e6 100644
--- a/changelog/77_UNRELEASED_xxxx-xx-xx.md
+++ b/changelog/77_UNRELEASED_xxxx-xx-xx.md
@@ -10,7 +10,7 @@
### Stock
-- Added a new product picker workflow "External barcode lookup (via plugin)"
+- Added a new product picker workflow "External barcode lookup"
- This executes the configured barcode lookup plugin with the given barcode
- If the lookup was successful, the product edit page of the created product is displayed, where the product setup can be completed (if required)
- After that, the transaction is continued with that product as usual
diff --git a/config-dist.php b/config-dist.php
index 880c3750..e763c5e2 100644
--- a/config-dist.php
+++ b/config-dist.php
@@ -64,8 +64,9 @@ Setting('BASE_PATH', '');
Setting('BASE_URL', '/');
// The plugin to use for external barcode lookups,
-// must be the filename (folder /data/plugins) without the .php extension,
-// see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation
+// must be the filename (folder "/plugins" for built-in plugins or "/data/plugins" for user plugins) without the .php extension,
+// see /plugins/DemoBarcodeLookupPlugin.php for a commented example implementation
+// Leave empty to disable external barcode lookups
Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'OpenFoodFactsBarcodeLookupPlugin');
// If, however, your webserver does not support URL rewriting, set this to true
diff --git a/controllers/BaseController.php b/controllers/BaseController.php
index f1f15166..d948df20 100644
--- a/controllers/BaseController.php
+++ b/controllers/BaseController.php
@@ -29,7 +29,7 @@ class BaseController
}
protected $AppContainer;
- private $View;
+ protected $View;
protected function getApiKeyService()
{
diff --git a/controllers/StockController.php b/controllers/StockController.php
index e434c37e..d06c4c4c 100644
--- a/controllers/StockController.php
+++ b/controllers/StockController.php
@@ -4,6 +4,7 @@ namespace Grocy\Controllers;
use Grocy\Helpers\Grocycode;
use Grocy\Services\RecipesService;
+use DI\Container;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
@@ -11,6 +12,24 @@ class StockController extends BaseController
{
use GrocycodeTrait;
+ public function __construct(Container $container)
+ {
+ parent::__construct($container);
+
+ try
+ {
+ $externalBarcodeLookupPluginName = $this->getStockService()->GetExternalBarcodeLookupPluginName();
+ }
+ catch (\Exception)
+ {
+ $externalBarcodeLookupPluginName = '';
+ }
+ finally
+ {
+ $this->View->set('ExternalBarcodeLookupPluginName', $externalBarcodeLookupPluginName);
+ }
+ }
+
public function Consume(Request $request, Response $response, array $args)
{
return $this->renderPage($response, 'consume', [
diff --git a/data/plugins/.gitignore b/data/plugins/.gitignore
index a1b771e5..d6b7ef32 100644
--- a/data/plugins/.gitignore
+++ b/data/plugins/.gitignore
@@ -1,3 +1,2 @@
*
!.gitignore
-!DemoBarcodeLookupPlugin.php
diff --git a/helpers/BaseBarcodeLookupPlugin.php b/helpers/BaseBarcodeLookupPlugin.php
index 1978cfeb..88736966 100644
--- a/helpers/BaseBarcodeLookupPlugin.php
+++ b/helpers/BaseBarcodeLookupPlugin.php
@@ -4,14 +4,19 @@ namespace Grocy\Helpers;
abstract class BaseBarcodeLookupPlugin
{
- final public function __construct($locations, $quantityUnits)
+ // That's a "self-referencing constant" and forces the child class to define it
+ public const PLUGIN_NAME = self::PLUGIN_NAME;
+
+ final public function __construct($locations, $quantityUnits, $userSettings)
{
$this->Locations = $locations;
$this->QuantityUnits = $quantityUnits;
+ $this->UserSettings = $userSettings;
}
protected $Locations;
protected $QuantityUnits;
+ protected $UserSettings;
final public function Lookup($barcode)
{
@@ -40,8 +45,8 @@ abstract class BaseBarcodeLookupPlugin
'location_id',
'qu_id_purchase',
'qu_id_stock',
- 'qu_factor_purchase_to_stock',
- 'barcode'
+ '__qu_factor_purchase_to_stock',
+ '__barcode'
];
foreach ($minimunNeededProperties as $prop)
@@ -58,25 +63,25 @@ abstract class BaseBarcodeLookupPlugin
$locationId = $pluginOutput['location_id'];
if (FindObjectInArrayByPropertyValue($this->Locations, 'id', $locationId) === null)
{
- throw new \Exception("Location $locationId is not a valid location id");
+ throw new \Exception("Provided location_id ($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");
+ throw new \Exception("Provided qu_id_purchase ($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");
+ throw new \Exception("Provided qu_id_stock ($quIdStock) is not a valid quantity unit id");
}
- $quFactor = $pluginOutput['qu_factor_purchase_to_stock'];
+ $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');
+ throw new \Exception('Provided __qu_factor_purchase_to_stock is empty or not a number');
}
return $pluginOutput;
diff --git a/localization/strings.pot b/localization/strings.pot
index 9bcde374..f0e77718 100644
--- a/localization/strings.pot
+++ b/localization/strings.pot
@@ -2424,7 +2424,7 @@ msgid_plural "This means %1$s labels will be printed"
msgstr[0] ""
msgstr[1] ""
-msgid "External barcode lookup (via plugin)"
+msgid "External barcode lookup"
msgstr ""
msgid "Error while executing the barcode lookup plugin"
diff --git a/migrations/0240.php b/migrations/0240.php
new file mode 100644
index 00000000..68655a3f
--- /dev/null
+++ b/migrations/0240.php
@@ -0,0 +1,10 @@
+Locations contains all locations
$this->QuantityUnits contains all quantity units
+ $this->UserSettings contains all user settings
*/
/*
@@ -41,12 +42,14 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
$location = FindObjectInArrayByPropertyValue($this->Locations, 'name', 'Fridge');
*/
+ // Provide a name
+ public const PLUGIN_NAME = 'Demo';
+
/*
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 be a valid product object (see the "products" database table for all available properties/columns):
+ associative array of the product model (see the "products" database table for all available properties/columns)
+ or null when nothing was found for the barcode:
[
// Required properties:
'name' => '',
@@ -54,12 +57,12 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
'qu_id_purchase' => 1, // A valid id of a quantity unit object, check against $this->QuantityUnits
'qu_id_stock' => 1, // A valid id of a quantity unit object, check against $this->QuantityUnits
- // These are virtual properties (not part of the product object, will be automatically handled as needed)
- '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
+ // Required virtual properties (not part of the product object, will be automatically handled as needed):
+ '__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
- // Optional virtual properties
- 'image_url' => '' // When provided, the corresponding image will be downloaded and set as the product picture
+ // Optional virtual properties (not part of the product object, will be automatically handled as needed):
+ '__image_url' => '' // When provided, the corresponding image will be downloaded and set as the product picture
]
*/
protected function ExecuteLookup($barcode)
@@ -76,13 +79,27 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
}
else
{
+ // Take the preset user setting or otherwise simply the first existing location
+ $locationId = $this->Locations[0]->id;
+ if ($this->UserSettings['product_presets_location_id'] != -1)
+ {
+ $locationId = $this->UserSettings['product_presets_location_id'];
+ }
+
+ // Take the preset user setting or otherwise simply the first existing quantity unit
+ $quId = $this->QuantityUnits[0]->id;
+ if ($this->UserSettings['product_presets_qu_id'] != -1)
+ {
+ $quId = $this->UserSettings['product_presets_qu_id'];
+ }
+
return [
'name' => 'LookedUpProduct_' . RandomString(5),
- 'location_id' => $this->Locations[0]->id, // Take the first location as a default
- 'qu_id_purchase' => $this->QuantityUnits[0]->id, // Take the first QU as a default
- 'qu_id_stock' => $this->QuantityUnits[0]->id, // Take the first QU as a default
- 'qu_factor_purchase_to_stock' => 1,
- 'barcode' => $barcode
+ 'location_id' => $locationId,
+ 'qu_id_purchase' => $quId,
+ 'qu_id_stock' => $quId,
+ '__qu_factor_purchase_to_stock' => 1,
+ '__barcode' => $barcode
];
}
}
diff --git a/data/plugins/OpenFoodFactsBarcodeLookupPlugin.php b/plugins/OpenFoodFactsBarcodeLookupPlugin.php
similarity index 54%
rename from data/plugins/OpenFoodFactsBarcodeLookupPlugin.php
rename to plugins/OpenFoodFactsBarcodeLookupPlugin.php
index 45ea6f3e..8ba8fe19 100644
--- a/data/plugins/OpenFoodFactsBarcodeLookupPlugin.php
+++ b/plugins/OpenFoodFactsBarcodeLookupPlugin.php
@@ -10,10 +10,12 @@ use GuzzleHttp\Client;
class OpenFoodFactsBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
{
+ public const PLUGIN_NAME = 'Open Food Facts';
+
protected function ExecuteLookup($barcode)
{
$webClient = new Client(['http_errors' => false]);
- $response = $webClient->request('GET', "https://world.openfoodfacts.net/api/v2/product/$barcode?fields=product_name,image_url");
+ $response = $webClient->request('GET', "https://world.openfoodfacts.net/api/v2/product/$barcode?fields=product_name,image_url", ['headers' => ['User-Agent' => 'GrocyOpenFoodFactsBarcodeLookupPlugin/1.0 (https://grocy.info)']]);
$statusCode = $response->getStatusCode();
// Guzzle throws exceptions for connection errors, so nothing to do on that here
@@ -32,14 +34,28 @@ class OpenFoodFactsBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
$imageUrl = $data->product->image_url;
}
+ // Take the preset user setting or otherwise simply the first existing location
+ $locationId = $this->Locations[0]->id;
+ if ($this->UserSettings['product_presets_location_id'] != -1)
+ {
+ $locationId = $this->UserSettings['product_presets_location_id'];
+ }
+
+ // Take the preset user setting or otherwise simply the first existing quantity unit
+ $quId = $this->QuantityUnits[0]->id;
+ if ($this->UserSettings['product_presets_qu_id'] != -1)
+ {
+ $quId = $this->UserSettings['product_presets_qu_id'];
+ }
+
return [
'name' => $data->product->product_name,
- '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,
- 'image_url' => $imageUrl
+ 'location_id' => $locationId,
+ 'qu_id_purchase' => $quId,
+ 'qu_id_stock' => $quId,
+ '__qu_factor_purchase_to_stock' => 1,
+ '__barcode' => $barcode,
+ '__image_url' => $imageUrl
];
}
}
diff --git a/public/viewjs/components/productpicker.js b/public/viewjs/components/productpicker.js
index 8d8538ee..8001f45e 100644
--- a/public/viewjs/components/productpicker.js
+++ b/public/viewjs/components/productpicker.js
@@ -250,9 +250,13 @@ $('#product_id_text_input').on('blur', function(e)
Grocy.Components.ProductPicker.PopupOpen = false;
window.location.href = U('/product/new?flow=InplaceNewProductWithBarcode&barcode=' + encodeURIComponent(input) + '&returnto=' + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceAddBarcodeToExistingProduct&barcode=" + input + "&" + embedded) + "&" + embedded);
}
- },
- barcodepluginlookup: {
- label: 'E ' + __t('External barcode lookup (via plugin)'),
+ }
+ };
+
+ if (Grocy.ExternalBarcodeLookupPluginName)
+ {
+ buttons.barcodepluginlookup = {
+ label: 'E ' + __t('External barcode lookup') + ' ' + Grocy.ExternalBarcodeLookupPluginName + '',
className: 'btn-dark add-new-product-plugin-dialog-button responsive-button ' + addProductWorkflowsAdditionalCssClasses,
callback: function()
{
@@ -279,8 +283,8 @@ $('#product_id_text_input').on('blur', function(e)
}
);
}
- }
- };
+ };
+ }
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_DISABLE_BROWSER_BARCODE_CAMERA_SCANNING)
{
@@ -332,7 +336,8 @@ $('#product_id_text_input').on('blur', function(e)
size: 'large',
backdrop: true,
closeButton: false,
- buttons: buttons
+ buttons: buttons,
+ className: "wider"
}).on('keypress', function(e)
{
if (e.key === 'B' || e.key === 'b')
diff --git a/services/BaseService.php b/services/BaseService.php
index 1fba730a..13825068 100644
--- a/services/BaseService.php
+++ b/services/BaseService.php
@@ -71,4 +71,9 @@ class BaseService
{
return FilesService::getInstance();
}
+
+ protected function getApplicationService()
+ {
+ return ApplicationService::getInstance();
+ }
}
diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php
index 55dc546e..e319424e 100644
--- a/services/DemoDataGeneratorService.php
+++ b/services/DemoDataGeneratorService.php
@@ -220,6 +220,9 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO userfield_values (field_id, object_id, value) VALUES (1, 2, '{$this->__t_sql('Example field value...')}');
INSERT INTO userfield_values (field_id, object_id, value) VALUES (2, 2, '{$this->__t_sql('Example field value...')}');
+ INSERT INTO user_settings(user_id, key, value) VALUES (1, 'product_presets_location_id', '3'); -- Pantry
+ INSERT INTO user_settings(user_id, key, value) VALUES (1, 'product_presets_qu_id', '3'); -- Pack
+
INSERT INTO migrations (migration) VALUES (-1);
";
$this->getDatabaseService()->ExecuteDbStatement($sql);
diff --git a/services/StockService.php b/services/StockService.php
index af8090c5..0e3b17f3 100644
--- a/services/StockService.php
+++ b/services/StockService.php
@@ -592,9 +592,15 @@ class StockService extends BaseService
return $transactionId;
}
+ public function GetExternalBarcodeLookupPluginName()
+ {
+ $plugin = $this->LoadExternalBarcodeLookupPlugin();
+ return $plugin::PLUGIN_NAME;
+ }
+
public function ExternalBarcodeLookup($barcode, $addFoundProduct)
{
- $plugin = $this->LoadBarcodeLookupPlugin();
+ $plugin = $this->LoadExternalBarcodeLookupPlugin();
$pluginOutput = $plugin->Lookup($barcode);
if ($pluginOutput !== null)
@@ -602,21 +608,24 @@ class StockService extends BaseService
// Lookup was successful
if ($addFoundProduct === true)
{
+ if ($this->getDatabase()->products()->where('name = :1', $pluginOutput['name'])->fetch() !== null)
+ {
+ throw new \Exception('Product "' . $pluginOutput['name'] . '" already exists');
+ }
+
// Add product to database and include new product id in output
$productData = $pluginOutput;
- unset($productData['barcode'], $productData['qu_factor_purchase_to_stock'], $productData['image_url']); // Virtual lookup plugin properties
+ unset($productData['__barcode'], $productData['__qu_factor_purchase_to_stock'], $productData['__image_url']); // Virtual lookup plugin properties
// Download and save image if provided
- if (isset($pluginOutput['image_url']) && !empty($pluginOutput['image_url']))
+ if (isset($pluginOutput['__image_url']) && !empty($pluginOutput['__image_url']))
{
try
{
$webClient = new Client();
- $response = $webClient->request('GET', $pluginOutput['image_url']);
- $fileName = $pluginOutput['barcode'] . '.' . pathinfo($pluginOutput['image_url'], PATHINFO_EXTENSION);
- $fileHandle = fopen($this->getFilesService()->GetFilePath('productpictures', $fileName), 'wb');
- fwrite($fileHandle, $response->getBody());
- fclose($fileHandle);
+ $response = $webClient->request('GET', $pluginOutput['__image_url'], ['headers' => ['User-Agent' => 'Grocy/' . $this->getApplicationService()->GetInstalledVersion()->Version . ' (https://grocy.info)']]);
+ $fileName = $pluginOutput['__barcode'] . '.' . pathinfo($pluginOutput['__image_url'], PATHINFO_EXTENSION);
+ file_put_contents($this->getFilesService()->GetFilePath('productpictures', $fileName), $response->getBody());
$productData['picture_file_name'] = $fileName;
}
catch (\Exception)
@@ -630,7 +639,7 @@ class StockService extends BaseService
$this->getDatabase()->product_barcodes()->createRow([
'product_id' => $newProductRow->id,
- 'barcode' => $pluginOutput['barcode']
+ 'barcode' => $pluginOutput['__barcode']
])->save();
if ($pluginOutput['qu_id_stock'] != $pluginOutput['qu_id_purchase'])
@@ -639,7 +648,7 @@ class StockService extends BaseService
'product_id' => $newProductRow->id,
'from_qu_id' => $pluginOutput['qu_id_purchase'],
'to_qu_id' => $pluginOutput['qu_id_stock'],
- 'factor' => $pluginOutput['qu_factor_purchase_to_stock'],
+ 'factor' => $pluginOutput['__qu_factor_purchase_to_stock'],
])->save();
}
@@ -1730,7 +1739,7 @@ class StockService extends BaseService
}
}
- private function LoadBarcodeLookupPlugin()
+ private function LoadExternalBarcodeLookupPlugin()
{
$pluginName = defined('GROCY_STOCK_BARCODE_LOOKUP_PLUGIN') ? GROCY_STOCK_BARCODE_LOOKUP_PLUGIN : '';
if (empty($pluginName))
@@ -1738,11 +1747,18 @@ class StockService extends BaseService
throw new \Exception('No barcode lookup plugin defined');
}
- $path = GROCY_DATAPATH . "/plugins/$pluginName.php";
- if (file_exists($path))
+ // User plugins take precedence
+ $standardPluginPath = __DIR__ . "/../plugins/$pluginName.php";
+ $userPluginPath = GROCY_DATAPATH . "/plugins/$pluginName.php";
+ if (file_exists($userPluginPath))
{
- require_once $path;
- return new $pluginName($this->getDatabase()->locations()->where('active = 1')->fetchAll(), $this->getDatabase()->quantity_units()->fetchAll());
+ require_once $userPluginPath;
+ return new $pluginName($this->getDatabase()->locations()->where('active = 1')->fetchAll(), $this->getDatabase()->quantity_units()->where('active = 1')->fetchAll(), $this->getUsersService()->GetUserSettings(GROCY_USER_ID));
+ }
+ elseif (file_exists($standardPluginPath))
+ {
+ require_once $standardPluginPath;
+ return new $pluginName($this->getDatabase()->locations()->where('active = 1')->fetchAll(), $this->getDatabase()->quantity_units()->where('active = 1')->fetchAll(), $this->getUsersService()->GetUserSettings(GROCY_USER_ID));
}
else
{
diff --git a/views/components/productpicker.blade.php b/views/components/productpicker.blade.php
index e9815c82..d7defad8 100644
--- a/views/components/productpicker.blade.php
+++ b/views/components/productpicker.blade.php
@@ -4,6 +4,11 @@
@push('componentScripts')
@endpush
+@push('componentScripts')
+
+@endpush
@endonce
@php if(empty($disallowAddProductWorkflows)) { $disallowAddProductWorkflows = false; } @endphp