mirror of
				https://github.com/grocy/grocy.git
				synced 2025-10-31 18:49:38 +00:00 
			
		
		
		
	Implemented frontend external barcode lookup workflow + a plugin for Open Food Facts (closes #158)
This commit is contained in:
		| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
| 			]; | ||||
|   | ||||
							
								
								
									
										46
									
								
								data/plugins/OpenFoodFactsBarcodeLookupPlugin.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								data/plugins/OpenFoodFactsBarcodeLookupPlugin.php
									
									
									
									
									
										Normal 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 | ||||
| 			]; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -403,7 +403,8 @@ Grocy.FrontendHelpers.ShowGenericError = function(message, exception) | ||||
| 			bootbox.alert({ | ||||
| 				title: __t('Error details'), | ||||
| 				message: '<p class="text-monospace my-0">' + errorDetails + '</p>', | ||||
| 				closeButton: false | ||||
| 				closeButton: false, | ||||
| 				className: "wider" | ||||
| 			}); | ||||
| 		} | ||||
| 	}); | ||||
|   | ||||
| @@ -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")); | ||||
| 							} | ||||
| 						); | ||||
| 					} | ||||
|   | ||||
| @@ -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(); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user