From 074f045f402ef3182692b4d8f7e30f4564b371c4 Mon Sep 17 00:00:00 2001 From: Katharina Bogad Date: Tue, 8 Jun 2021 16:26:12 +0200 Subject: [PATCH] Grocycode: Webhooks & Labelprinter Feature --- composer.json | 3 +- composer.lock | 287 ++++++++++++++++++++++++++++- config-dist.php | 16 ++ controllers/StockApiController.php | 9 +- helpers/WebhookRunner.php | 48 +++++ localization/de/strings.po | 5 +- localization/strings.pot | 3 + public/js/grocy.js | 10 + public/viewjs/purchase.js | 16 ++ services/StockService.php | 19 +- views/layout/default.blade.php | 8 + views/purchase.blade.php | 19 +- 12 files changed, 434 insertions(+), 9 deletions(-) create mode 100644 helpers/WebhookRunner.php diff --git a/composer.json b/composer.json index 33b0a597..8f3f6abd 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "erusev/parsedown": "^1.7", "gumlet/php-image-resize": "^1.9", "ezyang/htmlpurifier": "^4.13", - "jucksearm/php-barcode": "^1.0" + "jucksearm/php-barcode": "^1.0", + "guzzlehttp/guzzle": "^7.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 9480cbc3..dbb53b2c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2f15993a36a55cfc2018969a144c73b0", + "content-hash": "b8b8e77618038c44c21edac2c31c4b67", "packages": [ { "name": "doctrine/inflector", @@ -508,6 +508,239 @@ }, "time": "2019-01-01T13:53:00+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7008573787b430c1c1f650e3722d9bba59967628" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628", + "reference": "7008573787b430c1c1f650e3722d9bba59967628", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.4", + "guzzlehttp/psr7": "^1.7 || ^2.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.3-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://github.com/alexeyshockov", + "type": "github" + }, + { + "url": "https://github.com/gmponos", + "type": "github" + } + ], + "time": "2021-03-23T11:33:13+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.4.1" + }, + "time": "2021-03-07T09:25:29+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.8.2" + }, + "time": "2021-04-26T09:17:50+00:00" + }, { "name": "illuminate/container", "version": "v5.8.36", @@ -1412,6 +1645,58 @@ }, "time": "2017-02-14T16:28:37+00:00" }, + { + "name": "psr/http-client", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, { "name": "psr/http-factory", "version": "1.0.1", diff --git a/config-dist.php b/config-dist.php index bc8f003e..37411082 100644 --- a/config-dist.php +++ b/config-dist.php @@ -159,6 +159,21 @@ DefaultUserSetting('quagga2_patchsize', 'medium'); DefaultUserSetting('quagga2_frequency', 10); DefaultUserSetting('quagga2_debug', true); +// Label Printer Settings +// This is the URI that grocy will POST to when asked to print a label. +Setting('LABEL_PRINTER_WEBHOOK', ''); +// This setting decides whether the webhook will be called server- or clientside. +// If the machine grocy runs on has a network connection to the host +// the webhook receiver is on, this is probably a good idea. +// If, for example, grocy runs in the cloud and your printer daemon +// runs locally to you, set this to false to let your browser call +// the webhook instead. +Setting('LABEL_PRINTER_RUN_SERVER', true); +// Additional Parameters supplied to the webhook. +Setting('LABEL_PRINTER_PARAMS', ['font_family' => 'Source Sans Pro (Regular)']); +// Use JSON or normal POST request variables? +Setting('LABEL_PRINTER_HOOK_JSON', false); + // Feature flags // grocy was initially about "stock management for your household", many other things // came and still come by, because they are useful - here you can disable the parts @@ -172,6 +187,7 @@ Setting('FEATURE_FLAG_TASKS', true); Setting('FEATURE_FLAG_BATTERIES', true); Setting('FEATURE_FLAG_EQUIPMENT', true); Setting('FEATURE_FLAG_CALENDAR', true); +Setting('FEATURE_FLAG_LABELPRINTER', true); // Sub feature flags Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true); diff --git a/controllers/StockApiController.php b/controllers/StockApiController.php index 542631ba..10735f21 100644 --- a/controllers/StockApiController.php +++ b/controllers/StockApiController.php @@ -4,6 +4,7 @@ namespace Grocy\Controllers; use Grocy\Controllers\Users\User; use Grocy\Services\StockService; +use Grocy\Helpers\WebhookRunner; class StockApiController extends BaseApiController { @@ -138,8 +139,14 @@ class StockApiController extends BaseApiController { $transactionType = $requestBody['transactiontype']; } + $runPrinterWebhook = false; + if (array_key_exists('print_stock_label', $requestBody) && boolval($requestBody['print_stock_label'])) + { + $runPrinterWebhook = true; + } + + $transactionId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId, $shoppingLocationId, $unusedTransactionId, $runPrinterWebhook); - $transactionId = $this->getStockService()->AddProduct($args['productId'], $requestBody['amount'], $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId, $shoppingLocationId); $args['transactionId'] = $transactionId; return $this->StockTransactions($request, $response, $args); } diff --git a/helpers/WebhookRunner.php b/helpers/WebhookRunner.php new file mode 100644 index 00000000..a7a1aa25 --- /dev/null +++ b/helpers/WebhookRunner.php @@ -0,0 +1,48 @@ +client = new Client(['timeout' => 2.0]); + } + + public function run($url, $args, $json = false) + { + $reqArgs = []; + if ($json) + { + $reqArgs = ['json' => $args]; + } + else + { + $reqArgs = ['form_params' => $args]; + } + try + { + file_put_contents('php://stderr', 'Running Webhook: ' . $url . "\n" . print_r($reqArgs, true)); + + $this->client->request('POST', $url, $reqArgs); + } + catch (RequestException $e) + { + file_put_contents('php://stderr', 'Webhook failed: ' . $url . "\n" . $e->getMessage()); + } + } + + public function runAll($urls, $args) + { + foreach ($urls as $url) + { + $this->run($url, $args); + } + } +} diff --git a/localization/de/strings.po b/localization/de/strings.po index 35cd5934..c103006c 100644 --- a/localization/de/strings.po +++ b/localization/de/strings.po @@ -2394,4 +2394,7 @@ msgid "Open print label in new Window" msgstr "Druck-Label in neuem Fenster öffnen" msgid "DD" -msgstr "MHD" \ No newline at end of file +msgstr "MHD" + +msgid "Print stock label" +msgstr "Etikett drucken" \ No newline at end of file diff --git a/localization/strings.pot b/localization/strings.pot index a49d2312..21be827b 100644 --- a/localization/strings.pot +++ b/localization/strings.pot @@ -2106,3 +2106,6 @@ msgstr "" msgid "DD" msgstr "" + +msgid "Print stock label" +msgstr "" diff --git a/public/js/grocy.js b/public/js/grocy.js index 71e76528..4e956250 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -476,6 +476,16 @@ Grocy.FrontendHelpers.DeleteUserSetting = function(settingsKey, reloadPageOnSucc ); } +Grocy.FrontendHelpers.RunWebhook = function(webhook, data) +{ + Object.assign(data, webhook.extra_data); + + $.post(webhook.hook, data).fail(function(req, status, errorThrown) + { + Grocy.FrontendHelpers.ShowGenericError(__t("Unable to connect to webhook.", { "status": status, "errorThrown": errorThrown })); + }); +} + $(document).on("keyup paste change", "input, textarea", function() { $(this).closest("form").addClass("is-dirty"); diff --git a/public/viewjs/purchase.js b/public/viewjs/purchase.js index ba47ca7d..760a1cd8 100644 --- a/public/viewjs/purchase.js +++ b/public/viewjs/purchase.js @@ -23,6 +23,7 @@ $('#save-purchase-button').on('click', function(e) { var jsonData = {}; jsonData.amount = jsonForm.amount; + jsonData.print_stock_label = jsonForm.print_stock_label if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) { @@ -116,6 +117,21 @@ $('#save-purchase-button').on('click', function(e) } var successMessage = __t('Added %1$s of %2$s to stock', amountMessage + " " + __n(amountMessage, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural), productDetails.product.name) + '
' + __t("Undo") + ''; + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABELPRINTER) + { + if (Grocy.Webhooks.labelprinter !== undefined) + { + var post_data = {}; + post_data.product = productDetails.product.name; + post_data.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + result[0].stock_id + if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) + { + post_data.duedate = __t('DD') + ': ' + result[0].best_before_date + } + Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, post_data); + } + } + if (GetUriParam("embedded") !== undefined) { window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl); diff --git a/services/StockService.php b/services/StockService.php index d7efedce..23c321fb 100644 --- a/services/StockService.php +++ b/services/StockService.php @@ -1,7 +1,9 @@ amount < $amountToAdd) - { + { $alreadyExistingEntry->update([ 'amount' => $amountToAdd, 'shopping_list_id' => $listId @@ -103,7 +105,7 @@ class StockService extends BaseService } } - public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null) + public function AddProduct(int $productId, float $amount, $bestBeforeDate, $transactionType, $purchasedDate, $price, $locationId = null, $shoppingLocationId = null, &$transactionId = null, $runWebhook = false, &$webhookData = null) { if (!$this->ProductExists($productId)) { @@ -178,6 +180,17 @@ class StockService extends BaseService ]); $stockRow->save(); + if (GROCY_FEATURE_FLAG_LABELPRINTER && GROCY_LABEL_PRINTER_RUN_SERVER && $runWebhook) + { + $webhookData = array_merge([ + 'product' => $productDetails->product->name, + 'grocycode' => (string)(new Grocycode(Grocycode::PRODUCT, $productId, [$stockId])), + 'duedate' => $this->getLocalizationService()->__t('DD') . ': ' . $bestBeforeDate, + ], GROCY_LABEL_PRINTER_PARAMS); + + (new WebhookRunner())->run(GROCY_LABEL_PRINTER_WEBHOOK, $webhookData, GROCY_LABEL_PRINTER_HOOK_JSON); + } + return $transactionId; } else @@ -452,7 +465,7 @@ class StockService extends BaseService if ($pluginOutput !== null) { // Lookup was successful if ($addFoundProduct === true) - { + { // Add product to database and include new product id in output $newRow = $this->getDatabase()->products()->createRow($pluginOutput); $newRow->save(); diff --git a/views/layout/default.blade.php b/views/layout/default.blade.php index fceaaa00..7684cbcc 100644 --- a/views/layout/default.blade.php +++ b/views/layout/default.blade.php @@ -95,6 +95,14 @@ Grocy.CalendarShowWeekNumbers = {{ BoolToString(GROCY_CALENDAR_SHOW_WEEK_OF_YEAR) }}; Grocy.GettextPo = {!! $GettextPo !!}; Grocy.FeatureFlags = {!! json_encode($featureFlags) !!}; + Grocy.Webhooks = { + @if(GROCY_FEATURE_FLAG_LABELPRINTER && !GROCY_LABEL_PRINTER_RUN_SERVER) + "labelprinter" : { + "hook" : "{{ GROCY_LABEL_PRINTER_WEBHOOK}}", + "extra_data" : {!! json_encode(GROCY_LABEL_PRINTER_PARAMS) !!} + } + @endif + }; @if (GROCY_AUTHENTICATED) Grocy.UserSettings = {!! json_encode($userSettings) !!}; diff --git a/views/purchase.blade.php b/views/purchase.blade.php index 679fa3e2..ccd3bed0 100644 --- a/views/purchase.blade.php +++ b/views/purchase.blade.php @@ -10,9 +10,9 @@ @section('content')
@@ -148,6 +148,21 @@ )) @endif + @if(GROCY_FEATURE_FLAG_LABELPRINTER) +
+
+ + +
+
+ @endif +