diff --git a/README.md b/README.md index f3d98a6f..ece4641b 100644 --- a/README.md +++ b/README.md @@ -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). -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 diff --git a/changelog/77_UNRELEASED_xxxx-xx-xx.md b/changelog/77_UNRELEASED_xxxx-xx-xx.md index 859b3981..8ffe9473 100644 --- a/changelog/77_UNRELEASED_xxxx-xx-xx.md +++ b/changelog/77_UNRELEASED_xxxx-xx-xx.md @@ -13,7 +13,9 @@ - Added a new product picker workflow "External barcode lookup (via plugin)" - 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 + - 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` ### Shopping list diff --git a/config-dist.php b/config-dist.php index 79475979..880c3750 100644 --- a/config-dist.php +++ b/config-dist.php @@ -66,7 +66,7 @@ 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 -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 Setting('DISABLE_URL_REWRITING', false); diff --git a/data/plugins/DemoBarcodeLookupPlugin.php b/data/plugins/DemoBarcodeLookupPlugin.php index 56eda3c2..8320262b 100644 --- a/data/plugins/DemoBarcodeLookupPlugin.php +++ b/data/plugins/DemoBarcodeLookupPlugin.php @@ -44,17 +44,23 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin /* 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. + 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( + The returned array must be a valid product object (see the "products" database table for all available properties/columns): + [ + // Required properties: '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_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 - ) + + // Optional virtual properties + 'image_url' => '' // When provided, the corresponding image will be downloaded and set as the product picture + ] */ protected function ExecuteLookup($barcode) { @@ -72,9 +78,9 @@ class DemoBarcodeLookupPlugin extends BaseBarcodeLookupPlugin { return [ 'name' => 'LookedUpProduct_' . RandomString(5), - 'location_id' => $this->Locations[0]->id, - 'qu_id_purchase' => $this->QuantityUnits[0]->id, - 'qu_id_stock' => $this->QuantityUnits[0]->id, + '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 ]; diff --git a/data/plugins/OpenFoodFactsBarcodeLookupPlugin.php b/data/plugins/OpenFoodFactsBarcodeLookupPlugin.php new file mode 100644 index 00000000..45ea6f3e --- /dev/null +++ b/data/plugins/OpenFoodFactsBarcodeLookupPlugin.php @@ -0,0 +1,46 @@ + 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 + ]; + } + } +} diff --git a/public/js/grocy.js b/public/js/grocy.js index bbc21578..2dbe3525 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -403,7 +403,8 @@ Grocy.FrontendHelpers.ShowGenericError = function(message, exception) bootbox.alert({ title: __t('Error details'), message: '

' + errorDetails + '

', - closeButton: false + closeButton: false, + className: "wider" }); } }); diff --git a/public/viewjs/components/productpicker.js b/public/viewjs/components/productpicker.js index 3504471d..8d8538ee 100644 --- a/public/viewjs/components/productpicker.js +++ b/public/viewjs/components/productpicker.js @@ -257,6 +257,7 @@ $('#product_id_text_input').on('blur', function(e) callback: function() { Grocy.Components.ProductPicker.PopupOpen = false; + Grocy.FrontendHelpers.BeginUiBusy($("form").first().attr("id")); Grocy.Api.Get("stock/barcodes/external-lookup/" + encodeURIComponent(input) + "?add=true", function(pluginResponse) @@ -264,15 +265,17 @@ $('#product_id_text_input').on('blur', function(e) if (pluginResponse == null) { toastr.warning(__t("Nothing was found for the given barcode")); + Grocy.FrontendHelpers.EndUiBusy($("form").first().attr("id")); } 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) { Grocy.FrontendHelpers.ShowGenericError("Error while executing the barcode lookup plugin", xhr.response); + Grocy.FrontendHelpers.EndUiBusy($("form").first().attr("id")); } ); } diff --git a/services/BaseService.php b/services/BaseService.php index cff227d8..1fba730a 100644 --- a/services/BaseService.php +++ b/services/BaseService.php @@ -47,7 +47,7 @@ class BaseService return LocalizationService::getInstance(GROCY_LOCALE); } - protected function getStockservice() + protected function getStockService() { return StockService::getInstance(); } @@ -66,4 +66,9 @@ class BaseService { return PrintService::getInstance(); } + + protected function getFilesService() + { + return FilesService::getInstance(); + } } diff --git a/services/StockService.php b/services/StockService.php index d2a15bff..af8090c5 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -4,6 +4,7 @@ namespace Grocy\Services; use Grocy\Helpers\Grocycode; use Grocy\Helpers\WebhookRunner; +use GuzzleHttp\Client; class StockService extends BaseService { @@ -603,7 +604,26 @@ class StockService extends BaseService { // Add product to database and include new product id in output $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->save();