Implemented frontend external barcode lookup workflow + a plugin for Open Food Facts (closes #158)

This commit is contained in:
Bernd Bestel 2025-01-11 20:04:32 +01:00
parent a2c2049037
commit c9ffe4885d
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
9 changed files with 101 additions and 16 deletions

View File

@ -131,7 +131,9 @@ Products can be directly added to the database via looking them up against exter
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 (via plugin)" (the workflow dialog is displayed when entering something unknown in any product input field).
There is no plugin included for any service, see the reference implementation in `data/plugins/DemoBarcodeLookupPlugin.php`. 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.
### Database migrations ### Database migrations

View File

@ -13,7 +13,9 @@
- Added a new product picker workflow "External barcode lookup (via plugin)" - Added a new product picker workflow "External barcode lookup (via plugin)"
- This executes the configured barcode lookup plugin with the given barcode - 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) - 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 - After that, the transaction is continued with that product as usual
- A plugin for [Open Food Facts](https://world.openfoodfacts.org/) is now included and used by default (see the `config.php` option `STOCK_BARCODE_LOOKUP_PLUGIN` and maybe change it as needed)
- The product name and image (and of course the barcode itself) are taken over from Open Food Facts to the product being looked up
- Optimized that when moving a product to a freezer location (so when freezing it) the due date will no longer be replaced when the product option "Default due days after freezing" is set to `0` - Optimized that when moving a product to a freezer location (so when freezing it) the due date will no longer be replaced when the product option "Default due days after freezing" is set to `0`
### Shopping list ### Shopping list

View File

@ -66,7 +66,7 @@ Setting('BASE_URL', '/');
// The plugin to use for external barcode lookups, // The plugin to use for external barcode lookups,
// must be the filename (folder /data/plugins) without the .php extension, // must be the filename (folder /data/plugins) without the .php extension,
// see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation // see /data/plugins/DemoBarcodeLookupPlugin.php for an example implementation
Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'DemoBarcodeLookupPlugin'); Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'OpenFoodFactsBarcodeLookupPlugin');
// If, however, your webserver does not support URL rewriting, set this to true // If, however, your webserver does not support URL rewriting, set this to true
Setting('DISABLE_URL_REWRITING', false); Setting('DISABLE_URL_REWRITING', false);

View File

@ -44,17 +44,23 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
/* /*
This class must implement the protected abstract function ExecuteLookup($barcode), 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 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. associative array of the product model or null, when nothing was found for the barcode
The returned array must contain at least these properties: The returned array must be a valid product object (see the "products" database table for all available properties/columns):
array( [
// Required properties:
'name' => '', 'name' => '',
'location_id' => 1, // A valid id of a location object, check against $this->Locations '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_purchase' => 1, // A valid id of a quantity unit object, check against $this->QuantityUnits
'qu_id_stock' => 1, // A valid id of 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 '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 '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
]
*/ */
protected function ExecuteLookup($barcode) protected function ExecuteLookup($barcode)
{ {
@ -72,9 +78,9 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
{ {
return [ return [
'name' => 'LookedUpProduct_' . RandomString(5), 'name' => 'LookedUpProduct_' . RandomString(5),
'location_id' => $this->Locations[0]->id, 'location_id' => $this->Locations[0]->id, // Take the first location as a default
'qu_id_purchase' => $this->QuantityUnits[0]->id, 'qu_id_purchase' => $this->QuantityUnits[0]->id, // Take the first QU as a default
'qu_id_stock' => $this->QuantityUnits[0]->id, 'qu_id_stock' => $this->QuantityUnits[0]->id, // Take the first QU as a default
'qu_factor_purchase_to_stock' => 1, 'qu_factor_purchase_to_stock' => 1,
'barcode' => $barcode 'barcode' => $barcode
]; ];

View File

@ -0,0 +1,46 @@
<?php
use Grocy\Helpers\BaseBarcodeLookupPlugin;
use GuzzleHttp\Client;
/*
To use this plugin, configure it in data/config.php like this:
Setting('STOCK_BARCODE_LOOKUP_PLUGIN', 'OpenFoodFactsBarcodeLookupPlugin');
*/
class OpenFoodFactsBarcodeLookupPlugin extends BaseBarcodeLookupPlugin
{
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");
$statusCode = $response->getStatusCode();
// Guzzle throws exceptions for connection errors, so nothing to do on that here
$data = json_decode($response->getBody());
if ($statusCode == 404 || $data->status != 1)
{
// Nothing found for the given barcode
return null;
}
else
{
$imageUrl = '';
if (isset($data->product->image_url) && !empty($data->product->image_url))
{
$imageUrl = $data->product->image_url;
}
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
];
}
}
}

View File

@ -403,7 +403,8 @@ Grocy.FrontendHelpers.ShowGenericError = function(message, exception)
bootbox.alert({ bootbox.alert({
title: __t('Error details'), title: __t('Error details'),
message: '<p class="text-monospace my-0">' + errorDetails + '</p>', message: '<p class="text-monospace my-0">' + errorDetails + '</p>',
closeButton: false closeButton: false,
className: "wider"
}); });
} }
}); });

View File

@ -257,6 +257,7 @@ $('#product_id_text_input').on('blur', function(e)
callback: function() callback: function()
{ {
Grocy.Components.ProductPicker.PopupOpen = false; Grocy.Components.ProductPicker.PopupOpen = false;
Grocy.FrontendHelpers.BeginUiBusy($("form").first().attr("id"));
Grocy.Api.Get("stock/barcodes/external-lookup/" + encodeURIComponent(input) + "?add=true", Grocy.Api.Get("stock/barcodes/external-lookup/" + encodeURIComponent(input) + "?add=true",
function(pluginResponse) function(pluginResponse)
@ -264,15 +265,17 @@ $('#product_id_text_input').on('blur', function(e)
if (pluginResponse == null) if (pluginResponse == null)
{ {
toastr.warning(__t("Nothing was found for the given barcode")); toastr.warning(__t("Nothing was found for the given barcode"));
Grocy.FrontendHelpers.EndUiBusy($("form").first().attr("id"));
} }
else else
{ {
window.location.href = U("/product/" + pluginResponse.id + "?flow=InplaceNewProductByPlugin&returnto=" + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceNewProductWithName&" + embedded) + "&" + embedded); window.location.href = U("/product/" + pluginResponse.id + "?flow=InplaceNewProductByExternalBarcodeLookupPlugin&returnto=" + encodeURIComponent(Grocy.CurrentUrlRelative + "?flow=InplaceNewProductWithName&" + embedded) + "&" + embedded);
} }
}, },
function(xhr) function(xhr)
{ {
Grocy.FrontendHelpers.ShowGenericError("Error while executing the barcode lookup plugin", xhr.response); Grocy.FrontendHelpers.ShowGenericError("Error while executing the barcode lookup plugin", xhr.response);
Grocy.FrontendHelpers.EndUiBusy($("form").first().attr("id"));
} }
); );
} }

View File

@ -47,7 +47,7 @@ class BaseService
return LocalizationService::getInstance(GROCY_LOCALE); return LocalizationService::getInstance(GROCY_LOCALE);
} }
protected function getStockservice() protected function getStockService()
{ {
return StockService::getInstance(); return StockService::getInstance();
} }
@ -66,4 +66,9 @@ class BaseService
{ {
return PrintService::getInstance(); return PrintService::getInstance();
} }
protected function getFilesService()
{
return FilesService::getInstance();
}
} }

View File

@ -4,6 +4,7 @@ namespace Grocy\Services;
use Grocy\Helpers\Grocycode; use Grocy\Helpers\Grocycode;
use Grocy\Helpers\WebhookRunner; use Grocy\Helpers\WebhookRunner;
use GuzzleHttp\Client;
class StockService extends BaseService class StockService extends BaseService
{ {
@ -603,7 +604,26 @@ class StockService extends BaseService
{ {
// Add product to database and include new product id in output // Add product to database and include new product id in output
$productData = $pluginOutput; $productData = $pluginOutput;
unset($productData['barcode'], $productData['qu_factor_purchase_to_stock']); 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']))
{
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);
$productData['picture_file_name'] = $fileName;
}
catch (\Exception)
{
// Ignore
}
}
$newProductRow = $this->getDatabase()->products()->createRow($productData); $newProductRow = $this->getDatabase()->products()->createRow($productData);
$newProductRow->save(); $newProductRow->save();