mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 01:32:38 +00:00
Grocycode, label printing (#1500)
* Grocycode: Productpicker, StockService * Grocycode: Datamatrix generation * Grocycode: Display in UI, make Images downloadable * Grocycode: Do not show on product card * Grocycode: Stockentry Label view * Grocycode: Webhooks & Labelprinter Feature * Grocycode: Manual Label printing * Grocycode: Print Label from product form * Quagga2: use zxing for DataMatrix recognition * Grocycode: Default settings for label printing * Prepare merge of master * Grocycode: docs * Docs: label printing webhook * Review - "grocy" is currently written lower-case everywhere, so let's do this also for "grocycode" - Unified phrases / capitalization - Minor UI adjustments (mainly context menu item ordering / ordering/spacing on product edit page) - Documented API changes for Swagger UI (grocy.openapi.json) - Reverted German localizations (those are managed via Transifex; would cause conflicts when manually edited - will import them later there) - Reverted a somehow messed up localization string (productform/help text for `cumulate_min_stock_amount_of_sub_products`) - Suppress deprecation warnings when generating Datamatrix PNG (otherwise the PNG is invalid, https://github.com/jucksearm/php-barcode/issues/3) - Default `FEATURE_FLAG_LABELPRINTER` to disabled Co-authored-by: Bernd Bestel <bernd@berrnd.de>
This commit is contained in:
parent
d23fda245e
commit
2471e78188
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -14,4 +14,4 @@
|
|||||||
"php-cs-fixer.formatHtml": true,
|
"php-cs-fixer.formatHtml": true,
|
||||||
"php-cs-fixer.autoFixBySemicolon": true,
|
"php-cs-fixer.autoFixBySemicolon": true,
|
||||||
"php-cs-fixer.onsave": true,
|
"php-cs-fixer.onsave": true,
|
||||||
}
|
}
|
@ -11,7 +11,9 @@
|
|||||||
"eluceo/ical": "^0.16.0",
|
"eluceo/ical": "^0.16.0",
|
||||||
"erusev/parsedown": "^1.7",
|
"erusev/parsedown": "^1.7",
|
||||||
"gumlet/php-image-resize": "^1.9",
|
"gumlet/php-image-resize": "^1.9",
|
||||||
"ezyang/htmlpurifier": "^4.13"
|
"ezyang/htmlpurifier": "^4.13",
|
||||||
|
"jucksearm/php-barcode": "^1.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
341
composer.lock
generated
341
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "6155c0eb959bd8118ce2aabce7fd8a6a",
|
"content-hash": "b8b8e77618038c44c21edac2c31c4b67",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "doctrine/inflector",
|
"name": "doctrine/inflector",
|
||||||
@ -508,6 +508,239 @@
|
|||||||
},
|
},
|
||||||
"time": "2019-01-01T13:53:00+00:00"
|
"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",
|
"name": "illuminate/container",
|
||||||
"version": "v5.8.36",
|
"version": "v5.8.36",
|
||||||
@ -828,6 +1061,58 @@
|
|||||||
},
|
},
|
||||||
"time": "2019-06-20T13:13:59+00:00"
|
"time": "2019-06-20T13:13:59+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "jucksearm/php-barcode",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/jucksearm/php-barcode.git",
|
||||||
|
"reference": "066a58776ec9e94dd6d5843c0fb9a3ff95e74d8b"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/jucksearm/php-barcode/zipball/066a58776ec9e94dd6d5843c0fb9a3ff95e74d8b",
|
||||||
|
"reference": "066a58776ec9e94dd6d5843c0fb9a3ff95e74d8b",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=5.4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^6.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"jucksearm\\barcode\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"LGPLv3"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "jucksearm",
|
||||||
|
"email": "jucksearm.bkk@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Barcode Generation Package inspired by Nicola Asuni.",
|
||||||
|
"keywords": [
|
||||||
|
"CODE 128",
|
||||||
|
"barcode",
|
||||||
|
"datamatrix",
|
||||||
|
"pdf417",
|
||||||
|
"php barcode",
|
||||||
|
"qr code",
|
||||||
|
"qrcode"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/jucksearm/php-barcode/issues",
|
||||||
|
"source": "https://github.com/jucksearm/php-barcode/tree/1.0.0"
|
||||||
|
},
|
||||||
|
"time": "2017-06-05T04:41:51+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "morris/lessql",
|
"name": "morris/lessql",
|
||||||
"version": "v0.4.1",
|
"version": "v0.4.1",
|
||||||
@ -1360,6 +1645,58 @@
|
|||||||
},
|
},
|
||||||
"time": "2017-02-14T16:28:37+00:00"
|
"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",
|
"name": "psr/http-factory",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -2528,5 +2865,5 @@
|
|||||||
"php": ">=7.4"
|
"php": ">=7.4"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.0.0"
|
"plugin-api-version": "2.1.0"
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,21 @@ DefaultUserSetting('quagga2_patchsize', 'medium');
|
|||||||
DefaultUserSetting('quagga2_frequency', 10);
|
DefaultUserSetting('quagga2_frequency', 10);
|
||||||
DefaultUserSetting('quagga2_debug', true);
|
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
|
// Feature flags
|
||||||
// grocy was initially about "stock management for your household", many other things
|
// 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
|
// 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_BATTERIES', true);
|
||||||
Setting('FEATURE_FLAG_EQUIPMENT', true);
|
Setting('FEATURE_FLAG_EQUIPMENT', true);
|
||||||
Setting('FEATURE_FLAG_CALENDAR', true);
|
Setting('FEATURE_FLAG_CALENDAR', true);
|
||||||
|
Setting('FEATURE_FLAG_LABELPRINTER', false);
|
||||||
|
|
||||||
// Sub feature flags
|
// Sub feature flags
|
||||||
Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true);
|
Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true);
|
||||||
|
@ -4,6 +4,8 @@ namespace Grocy\Controllers;
|
|||||||
|
|
||||||
use Grocy\Controllers\Users\User;
|
use Grocy\Controllers\Users\User;
|
||||||
use Grocy\Services\StockService;
|
use Grocy\Services\StockService;
|
||||||
|
use Grocy\Helpers\WebhookRunner;
|
||||||
|
use Grocy\Helpers\Grocycode;
|
||||||
|
|
||||||
class StockApiController extends BaseApiController
|
class StockApiController extends BaseApiController
|
||||||
{
|
{
|
||||||
@ -138,8 +140,14 @@ class StockApiController extends BaseApiController
|
|||||||
{
|
{
|
||||||
$transactionType = $requestBody['transactiontype'];
|
$transactionType = $requestBody['transactiontype'];
|
||||||
}
|
}
|
||||||
|
$runPrinterWebhook = false;
|
||||||
|
if (array_key_exists('print_stock_label', $requestBody) && intval($requestBody['print_stock_label']))
|
||||||
|
{
|
||||||
|
$runPrinterWebhook = intval($requestBody['print_stock_label']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
$args['transactionId'] = $transactionId;
|
||||||
return $this->StockTransactions($request, $response, $args);
|
return $this->StockTransactions($request, $response, $args);
|
||||||
}
|
}
|
||||||
@ -604,6 +612,46 @@ class StockApiController extends BaseApiController
|
|||||||
return $this->FilteredApiResponse($response, $this->getStockService()->GetProductStockLocations($args['productId'], $allowSubproductSubstitution), $request->getQueryParams());
|
return $this->FilteredApiResponse($response, $this->getStockService()->GetProductStockLocations($args['productId'], $allowSubproductSubstitution), $request->getQueryParams());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ProductPrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||||
|
{
|
||||||
|
$product = $this->getDatabase()->products()->where('id', $args['productId'])->fetch();
|
||||||
|
|
||||||
|
$webhookData = array_merge([
|
||||||
|
'product' => $product->name,
|
||||||
|
'grocycode' => (string)(new Grocycode(Grocycode::PRODUCT, $product->id)),
|
||||||
|
], GROCY_LABEL_PRINTER_PARAMS);
|
||||||
|
|
||||||
|
if (GROCY_LABEL_PRINTER_RUN_SERVER)
|
||||||
|
{
|
||||||
|
(new WebhookRunner())->run(GROCY_LABEL_PRINTER_WEBHOOK, $webhookData, GROCY_LABEL_PRINTER_HOOK_JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->EmptyApiResponse($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function StockEntryPrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||||
|
{
|
||||||
|
$stockEntry = $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch();
|
||||||
|
$product = $this->getDatabase()->products()->where('id', $stockEntry->product_id)->fetch();
|
||||||
|
|
||||||
|
$webhookData = array_merge([
|
||||||
|
'product' => $product->name,
|
||||||
|
'grocycode' => (string)(new Grocycode(Grocycode::PRODUCT, $stockEntry->product_id, [$stockEntry->stock_id])),
|
||||||
|
], GROCY_LABEL_PRINTER_PARAMS);
|
||||||
|
|
||||||
|
if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
|
||||||
|
{
|
||||||
|
$webhookData['duedate'] = $this->getLocalizationService()->__t('DD') . ': ' . $stockEntry->best_before_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GROCY_LABEL_PRINTER_RUN_SERVER)
|
||||||
|
{
|
||||||
|
(new WebhookRunner())->run(GROCY_LABEL_PRINTER_WEBHOOK, $webhookData, GROCY_LABEL_PRINTER_HOOK_JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->EmptyApiResponse($response);
|
||||||
|
}
|
||||||
|
|
||||||
public function RemoveProductFromShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
public function RemoveProductFromShoppingList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||||
{
|
{
|
||||||
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_DELETE);
|
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_DELETE);
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace Grocy\Controllers;
|
namespace Grocy\Controllers;
|
||||||
|
|
||||||
|
use Grocy\Helpers\Grocycode;
|
||||||
use Grocy\Services\RecipesService;
|
use Grocy\Services\RecipesService;
|
||||||
|
use jucksearm\barcode\lib\DatamatrixFactory;
|
||||||
|
|
||||||
class StockController extends BaseController
|
class StockController extends BaseController
|
||||||
{
|
{
|
||||||
@ -39,7 +41,7 @@ class StockController extends BaseController
|
|||||||
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
|
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
|
||||||
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
|
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
|
||||||
'users' => $usersService->GetUsersAsDto(),
|
'users' => $usersService->GetUsersAsDto(),
|
||||||
'transactionTypes' => GetClassConstants('\Grocy\Services\StockService', 'TRANSACTION_TYPE_'),
|
'transactionTypes' => GetClassConstants('\Grocy\Services\StockService', 'TRANSACTION_TYPE_')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,6 +172,38 @@ class StockController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ProductGrocycodeImage(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||||
|
{
|
||||||
|
$size = $request->getQueryParam('size', null);
|
||||||
|
$product = $this->getDatabase()->products($args['productId']);
|
||||||
|
|
||||||
|
$gc = new Grocycode(Grocycode::PRODUCT, $product->id);
|
||||||
|
|
||||||
|
// Explicitly suppress errors, otherwise deprecations warnings would cause invalid PNG data
|
||||||
|
// See also https://github.com/jucksearm/php-barcode/issues/3
|
||||||
|
$png = @(new DatamatrixFactory())->setCode((string) $gc)->setSize($size)->getDatamatrixPngData();
|
||||||
|
|
||||||
|
$isDownload = $request->getQueryParam('download', false);
|
||||||
|
|
||||||
|
if ($isDownload)
|
||||||
|
{
|
||||||
|
$response = $response->withHeader('Content-Type', 'application/octet-stream')
|
||||||
|
->withHeader('Content-Disposition', 'attachment; filename=grocycode.png')
|
||||||
|
->withHeader('Content-Length', strlen($png))
|
||||||
|
->withHeader('Cache-Control', 'no-cache')
|
||||||
|
->withHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$response = $response->withHeader('Content-Type', 'image/png')
|
||||||
|
->withHeader('Content-Length', strlen($png))
|
||||||
|
->withHeader('Cache-Control', 'no-cache')
|
||||||
|
->withHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
|
||||||
|
}
|
||||||
|
$response->getBody()->write($png);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
public function ProductGroupEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
public function ProductGroupEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||||
{
|
{
|
||||||
if ($args['productGroupId'] == 'new')
|
if ($args['productGroupId'] == 'new')
|
||||||
@ -428,6 +462,47 @@ class StockController extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function StockEntryGrocycodeImage(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||||
|
{
|
||||||
|
$size = $request->getQueryParam('size', null);
|
||||||
|
|
||||||
|
$stockEntry = $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch();
|
||||||
|
$gc = new Grocycode(Grocycode::PRODUCT, $stockEntry->product_id, [$stockEntry->stock_id]);
|
||||||
|
|
||||||
|
// Explicitly suppress errors, otherwise deprecations warnings would cause invalid PNG data
|
||||||
|
// See also https://github.com/jucksearm/php-barcode/issues/3
|
||||||
|
$png = @(new DatamatrixFactory())->setCode((string) $gc)->setSize($size)->getDatamatrixPngData();
|
||||||
|
|
||||||
|
$isDownload = $request->getQueryParam('download', false);
|
||||||
|
|
||||||
|
if ($isDownload)
|
||||||
|
{
|
||||||
|
$response = $response->withHeader('Content-Type', 'application/octet-stream')
|
||||||
|
->withHeader('Content-Disposition', 'attachment; filename=grocycode.png')
|
||||||
|
->withHeader('Content-Length', strlen($png))
|
||||||
|
->withHeader('Cache-Control', 'no-cache')
|
||||||
|
->withHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$response = $response->withHeader('Content-Type', 'image/png')
|
||||||
|
->withHeader('Content-Length', strlen($png))
|
||||||
|
->withHeader('Cache-Control', 'no-cache')
|
||||||
|
->withHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
|
||||||
|
$response->getBody()->write($png);
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function StockEntryGrocycodeLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||||
|
{
|
||||||
|
$stockEntry = $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch();
|
||||||
|
return $this->renderPage($response, 'stockentrylabel', [
|
||||||
|
'stockEntry' => $stockEntry,
|
||||||
|
'product' => $this->getDatabase()->products($stockEntry->product_id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function StockSettings(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
public function StockSettings(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||||
{
|
{
|
||||||
return $this->renderPage($response, 'stocksettings', [
|
return $this->renderPage($response, 'stocksettings', [
|
||||||
|
66
docs/grocycode.md
Normal file
66
docs/grocycode.md
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
grocycode
|
||||||
|
==========
|
||||||
|
|
||||||
|
grocycode is, in essence, a simple way to reference to arbitrary grocy entities.
|
||||||
|
Each grocycode includes a magic, an entitiy identifier, an id and an ordered set of extra data.
|
||||||
|
It is supported to be entered anywhere grocy expects one to read a barcode, but can also reference
|
||||||
|
grocy-internal properties like specific stock entries, or specific batteries.
|
||||||
|
|
||||||
|
Serialization
|
||||||
|
----
|
||||||
|
|
||||||
|
There are three mandatory parts in a grocycode:
|
||||||
|
|
||||||
|
1. The magic `grcy`
|
||||||
|
2. An entity identifer matching the regular expression `[a-z]+` (that is, lowercase english alphabet without any fancy accents, minimum length 1 character).
|
||||||
|
3. An object identifer matching the regular expression `[0-9]+`
|
||||||
|
|
||||||
|
Optionally, any number of further data without format restrictions besides not containing any double colons [0] may be appended.
|
||||||
|
|
||||||
|
These parts are then linearly appended, seperated by a double colon `:`.
|
||||||
|
|
||||||
|
Entity Identifers
|
||||||
|
----
|
||||||
|
|
||||||
|
Currently, there are three different entity types defined:
|
||||||
|
|
||||||
|
- `p` for Products
|
||||||
|
- `b` for Batteries
|
||||||
|
- `c` for Chores
|
||||||
|
|
||||||
|
Example
|
||||||
|
----
|
||||||
|
|
||||||
|
In this example, we encode a *Product* with ID *13*, which results in `grcy:p:13` when serialized.
|
||||||
|
|
||||||
|
Product grocycodes
|
||||||
|
----
|
||||||
|
|
||||||
|
Product grocycodes extend the data format to include an optional stock id, thus may reference a specific stock entry directly.
|
||||||
|
|
||||||
|
Example: `grcy:p:13:60bf8b5244b04`
|
||||||
|
|
||||||
|
Battery grocycodes
|
||||||
|
----
|
||||||
|
|
||||||
|
Currently, Battery grocycodes do not define any extra fields.
|
||||||
|
|
||||||
|
Chore grocycodes
|
||||||
|
----
|
||||||
|
|
||||||
|
Currently, Chore grocycodes do not define any extra fields.
|
||||||
|
|
||||||
|
Visual Encoding
|
||||||
|
----
|
||||||
|
|
||||||
|
Grocy uses DataMatrix 2D Barcodes to encode grocycodes into a visual representation. In principle, there is no problem with using
|
||||||
|
other encoding formats like QR codes; however DataMatrix uses less space for the same information and redundancy and is a bit
|
||||||
|
easier read by 2D barcode scanners, especially on non-flat surfaces.
|
||||||
|
|
||||||
|
You can pick up cheap-ish used scanners from ebay (about 45€ in germany). Make sure to set them to the correct keyboard emulation,
|
||||||
|
so that the double colons get entered correctly.
|
||||||
|
|
||||||
|
|
||||||
|
Notes
|
||||||
|
---
|
||||||
|
[0]: Obviously, it needs to be encoded into some usable visual representation and then read. So probably you only want to encode stuff that can be typed on a keyboard.
|
40
docs/label-printing.md
Normal file
40
docs/label-printing.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
Label printing
|
||||||
|
====
|
||||||
|
|
||||||
|
To enable label printing, set `FEATURE_FLAG_LABELPRINTER` to `true`in your `config.php`. You also need to provide a webhook target that is responsible for printing.
|
||||||
|
|
||||||
|
Why webhook?
|
||||||
|
---
|
||||||
|
|
||||||
|
Label printers come in all shapes and forms, and your particular one is probably not the one used by the author of this feature. Also, grocy may does not have a
|
||||||
|
direct connection to a local label printer (e.g. grocy is hosted in a cloud vps). Thus, a lightweight implementation is provided by grocy: whenever something
|
||||||
|
should print, a POST request to a configured URL is made. The target then is responsible for label printing.
|
||||||
|
|
||||||
|
Reference implementation
|
||||||
|
---
|
||||||
|
|
||||||
|
The webhook was developed and tested against a Brother QL-600 label printer, using endless 62mm label paper. The webhook provider implementation was
|
||||||
|
implemented into [a fork of brother_ql_web](https://github.com/mistressofjellyfish/brother_ql_web).
|
||||||
|
|
||||||
|
Webhook request
|
||||||
|
---
|
||||||
|
|
||||||
|
Requests can be configured to be sent server-side (that is, from the machine hosting grocy through GuzzleHttp) or by an AJAX request directly from the browser.
|
||||||
|
The latter is neccesary for situations where the grocy hosting machine cannot reach your label printer, however server-side requests are a bit faster and
|
||||||
|
tend to be more stable.
|
||||||
|
|
||||||
|
Both methods fire this request upon printing:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /your/printing/api/endpoint HTTP/1.1
|
||||||
|
|
||||||
|
product=<productname>&grocycode=grocy:x:xxx&duedate=DD:%2021-06-09&...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
If specified, the request body may also be JSON encoded, however the fields stay the same.
|
||||||
|
|
||||||
|
Additional POST parameters (like the font to use) may be supplied in `config.php`. Keep in mind that these config values will be distributed to all clients on all requests
|
||||||
|
if the webhook is configured to run client-side.
|
||||||
|
|
||||||
|
The webhook receiver is required to layout and print the resulting label.
|
@ -1402,7 +1402,7 @@
|
|||||||
"in": "path",
|
"in": "path",
|
||||||
"name": "entryId",
|
"name": "entryId",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "A valid stock row id",
|
"description": "A valid stock entry id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
@ -1441,7 +1441,7 @@
|
|||||||
"in": "path",
|
"in": "path",
|
||||||
"name": "entryId",
|
"name": "entryId",
|
||||||
"required": true,
|
"required": true,
|
||||||
"description": "A valid stock row id",
|
"description": "A valid stock entry id",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
}
|
}
|
||||||
@ -1529,6 +1529,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/stock/entry/{entryId}/printlabel": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Prints the label of the given stock entry",
|
||||||
|
"tags": [
|
||||||
|
"Stock"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "entryId",
|
||||||
|
"required": true,
|
||||||
|
"description": "A valid stock entry id",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "The operation was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "The operation was not successful (possible errors are: Not existing stock entry, error on WebHook execution)",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error400"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/stock/volatile": {
|
"/stock/volatile": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Returns all products which are due soon, overdue, expired or currently missing",
|
"summary": "Returns all products which are due soon, overdue, expired or currently missing",
|
||||||
@ -1850,6 +1884,10 @@
|
|||||||
"type": "number",
|
"type": "number",
|
||||||
"format": "integer",
|
"format": "integer",
|
||||||
"description": "If omitted, no store will be affected"
|
"description": "If omitted, no store will be affected"
|
||||||
|
},
|
||||||
|
"print_stock_label": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "True when the stock entry label should be printed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"example": {
|
"example": {
|
||||||
@ -2213,6 +2251,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/stock/products/{productId}/printlabel": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Prints the product label of the given product",
|
||||||
|
"tags": [
|
||||||
|
"Stock"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "productId",
|
||||||
|
"required": true,
|
||||||
|
"description": "A valid product id",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "The operation was successful"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "The operation was not successful (possible errors are: Not existing product, error on WebHook execution)",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error400"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/stock/products/{productIdToKeep}/merge/{productIdToRemove}": {
|
"/stock/products/{productIdToKeep}/merge/{productIdToRemove}": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Merges two products into one",
|
"summary": "Merges two products into one",
|
||||||
|
142
helpers/Grocycode.php
Normal file
142
helpers/Grocycode.php
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Grocy\Helpers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that abstracts grocycode.
|
||||||
|
*
|
||||||
|
* grocycode is a simple, easily serializable format to reference
|
||||||
|
* stuff within grocy. It consists of n (n ≥ 3) double-colon seperated parts:
|
||||||
|
*
|
||||||
|
* 1. The magic `grcy`
|
||||||
|
* 2. A type identifer, must match `[a-z]+` (i.e. only lowercase ascii, minimum length 1 character)
|
||||||
|
* 3. An object id
|
||||||
|
* 4. Any number of further data fields, double-colon seperated.
|
||||||
|
*
|
||||||
|
* @author Katharina Bogad <katharina@hacked.xyz>
|
||||||
|
*/
|
||||||
|
class Grocycode
|
||||||
|
{
|
||||||
|
public const PRODUCT = 'p';
|
||||||
|
public const BATTERY = 'b';
|
||||||
|
public const CHORE = 'c';
|
||||||
|
|
||||||
|
public const MAGIC = 'grcy';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array that registers all valid grocycode types. Register yours here by appending to this array.
|
||||||
|
*/
|
||||||
|
public static $Items = [self::PRODUCT, self::BATTERY, self::CHORE];
|
||||||
|
|
||||||
|
private $type;
|
||||||
|
private $id;
|
||||||
|
private $extra_data = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a grocycode.
|
||||||
|
*
|
||||||
|
* Returns true, if a supplied $code is a valid grocycode, false otherwise.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public static function Validate(string $code)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$gc = new self($code);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception $e)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new instance of the Grocycode class.
|
||||||
|
*
|
||||||
|
* Because php doesn't support overloading, this is a proxy
|
||||||
|
* to either setFromCode($code) or setFromData($type, $id, $extra_data = []).
|
||||||
|
*/
|
||||||
|
public function __construct(...$args)
|
||||||
|
{
|
||||||
|
$argc = count($args);
|
||||||
|
if ($argc == 1)
|
||||||
|
{
|
||||||
|
$this->setFromCode($args[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elseif ($argc == 2 || $argc == 3)
|
||||||
|
{
|
||||||
|
if ($argc == 2)
|
||||||
|
{
|
||||||
|
$args[] = [];
|
||||||
|
}
|
||||||
|
$this->setFromData($args[0], $args[1], $args[2]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception('No suitable overload found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a grocycode.
|
||||||
|
*/
|
||||||
|
private function setFromCode($code)
|
||||||
|
{
|
||||||
|
$parts = array_reverse(explode(':', $barcode));
|
||||||
|
if (array_pop($parts) != self::MAGIC)
|
||||||
|
{
|
||||||
|
throw new \Exception('Not a grocycode');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($this->type = array_pop($parts), self::$Items))
|
||||||
|
{
|
||||||
|
throw new \Exception('Unknown grocycode type');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->id = array_pop($parts);
|
||||||
|
$this->extra_data = array_reverse($parse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a grocycode from data.
|
||||||
|
*/
|
||||||
|
private function setFromData($type, $id, $extra_data = [])
|
||||||
|
{
|
||||||
|
if (!is_array($extra_data))
|
||||||
|
{
|
||||||
|
throw new \Exception('Extra data must be array of string');
|
||||||
|
}
|
||||||
|
if (!in_array($type, self::$Items))
|
||||||
|
{
|
||||||
|
throw new \Exception('Unknown grocycode type');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->type = $type;
|
||||||
|
$this->id = $id;
|
||||||
|
$this->extra_data = $extra_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetId()
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetExtraData()
|
||||||
|
{
|
||||||
|
return $this->extra_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function GetType()
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$arr = array_merge([self::MAGIC, $this->type, $this->id], $this->extra_data);
|
||||||
|
|
||||||
|
return implode(':', $arr);
|
||||||
|
}
|
||||||
|
}
|
48
helpers/WebhookRunner.php
Normal file
48
helpers/WebhookRunner.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Grocy\Helpers;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\ExceptionRequestException;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
class WebhookRunner
|
||||||
|
{
|
||||||
|
private $client;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2077,3 +2077,55 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "A product or a note is required"
|
msgid "A product or a note is required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "grocycode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download stock entry grocycode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Download product grocycode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "grocycode is a unique referer to this product in your grocy instance - print it onto a label and scan it like any other barcode"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
# Abbreviation for "due date"
|
||||||
|
msgid "DD"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Print on label printer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Stock entry label"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "No label"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Single label"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Label per unit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Allow label printing per unit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Allow printing of one label per unit on purchase (after conversion) - e.g. 1 purchased pack adding 10 pieces of stock would print 10 labels"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Error while executing WebHook"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Print product grocycode on label printer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Print stock entry grocycode on label printer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Open stock entry print label in new window"
|
||||||
|
msgstr ""
|
||||||
|
11
migrations/0130.sql
Normal file
11
migrations/0130.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
ALTER TABLE products
|
||||||
|
ADD default_print_stock_label INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE products
|
||||||
|
SET default_print_stock_label = 0;
|
||||||
|
|
||||||
|
ALTER TABLE products
|
||||||
|
ADD allow_label_per_unit INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE products
|
||||||
|
SET allow_label_per_unit = 0;
|
25793
public/components_unmanaged/quagga2-reader-datamatrix/index.js
Normal file
25793
public/components_unmanaged/quagga2-reader-datamatrix/index.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -476,6 +476,24 @@ Grocy.FrontendHelpers.DeleteUserSetting = function(settingsKey, reloadPageOnSucc
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Grocy.FrontendHelpers.RunWebhook = function(webhook, data, repetitions = 1)
|
||||||
|
{
|
||||||
|
Object.assign(data, webhook.extra_data);
|
||||||
|
var hasAlreadyFailed = false;
|
||||||
|
|
||||||
|
for (i = 0; i < repetitions; i++)
|
||||||
|
{
|
||||||
|
$.post(webhook.hook, data).fail(function(req, status, errorThrown)
|
||||||
|
{
|
||||||
|
if (!hasAlreadyFailed)
|
||||||
|
{
|
||||||
|
hasAlreadyFailed = true;
|
||||||
|
Grocy.FrontendHelpers.ShowGenericError(__t("Error while executing WebHook", { "status": status, "errorThrown": errorThrown }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$(document).on("keyup paste change", "input, textarea", function()
|
$(document).on("keyup paste change", "input, textarea", function()
|
||||||
{
|
{
|
||||||
$(this).closest("form").addClass("is-dirty");
|
$(this).closest("form").addClass("is-dirty");
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
Grocy.Components.BarcodeScanner = {};
|
Grocy.Components.BarcodeScanner = {};
|
||||||
|
|
||||||
|
//import Quagga2DatamatrixReader from '../../components_unmanaged/quagga2-reader-datamatrix/index.js'
|
||||||
|
|
||||||
|
Quagga.registerReader("datamatrix", Quagga2DatamatrixReader);
|
||||||
|
|
||||||
Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted = false;
|
Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted = false;
|
||||||
Grocy.Components.BarcodeScanner.CheckCapabilities = async function()
|
Grocy.Components.BarcodeScanner.CheckCapabilities = async function()
|
||||||
{
|
{
|
||||||
@ -96,7 +100,8 @@ Grocy.Components.BarcodeScanner.StartScanning = function()
|
|||||||
readers: [
|
readers: [
|
||||||
"ean_reader",
|
"ean_reader",
|
||||||
"ean_8_reader",
|
"ean_8_reader",
|
||||||
"code_128_reader"
|
"code_128_reader",
|
||||||
|
"datamatrix"
|
||||||
],
|
],
|
||||||
debug: {
|
debug: {
|
||||||
showCanvas: Grocy.UserSettings.quagga2_debug,
|
showCanvas: Grocy.UserSettings.quagga2_debug,
|
||||||
|
@ -147,7 +147,23 @@ $('#product_id_text_input').on('blur', function(e)
|
|||||||
$('#product_id').attr("barcode", "null");
|
$('#product_id').attr("barcode", "null");
|
||||||
|
|
||||||
var input = $('#product_id_text_input').val().toString();
|
var input = $('#product_id_text_input').val().toString();
|
||||||
var possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first();
|
var possibleOptionElement = [];
|
||||||
|
|
||||||
|
// did we enter a grocycode?
|
||||||
|
if (input.startsWith("grcy"))
|
||||||
|
{
|
||||||
|
var gc = input.split(":");
|
||||||
|
if (gc[1] == "p")
|
||||||
|
{
|
||||||
|
// find product id
|
||||||
|
possibleOptionElement = $("#product_id option[value=\"" + gc[2] + "\"]").first();
|
||||||
|
$("#product_id").data("grocycode", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else // process barcode as usual
|
||||||
|
{
|
||||||
|
possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first();
|
||||||
|
}
|
||||||
|
|
||||||
if (GetUriParam('flow') === undefined && input.length > 0 && possibleOptionElement.length > 0)
|
if (GetUriParam('flow') === undefined && input.length > 0 && possibleOptionElement.length > 0)
|
||||||
{
|
{
|
||||||
|
@ -223,6 +223,18 @@ $("#location_id").on('change', function(e)
|
|||||||
{
|
{
|
||||||
stockId = GetUriParam('stockId');
|
stockId = GetUriParam('stockId');
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// try to get stock id from grocycode
|
||||||
|
if ($("#product_id").data("grocycode"))
|
||||||
|
{
|
||||||
|
var gc = $("#product_id").attr("barcode").split(":");
|
||||||
|
if (gc.length == 4)
|
||||||
|
{
|
||||||
|
stockId = gc[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (locationId)
|
if (locationId)
|
||||||
{
|
{
|
||||||
@ -249,6 +261,7 @@ $("#location_id").on('change', function(e)
|
|||||||
|
|
||||||
if (stockEntry.stock_id == stockId)
|
if (stockEntry.stock_id == stockId)
|
||||||
{
|
{
|
||||||
|
$("#use_specific_stock_entry").click();
|
||||||
$("#specific_stock_entry").val(stockId);
|
$("#specific_stock_entry").val(stockId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,6 +301,21 @@ $('#name').focus();
|
|||||||
$('.input-group-qu').trigger('change');
|
$('.input-group-qu').trigger('change');
|
||||||
Grocy.FrontendHelpers.ValidateForm('product-form');
|
Grocy.FrontendHelpers.ValidateForm('product-form');
|
||||||
|
|
||||||
|
$(document).on('click', '.stockentry-grocycode-product-label-print', function(e)
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
document.activeElement.blur();
|
||||||
|
|
||||||
|
var productId = $(e.currentTarget).attr('data-product-id');
|
||||||
|
Grocy.Api.Get('stock/products/' + productId + '/printlabel', function(labelData)
|
||||||
|
{
|
||||||
|
if (Grocy.Webhooks.labelprinter !== undefined)
|
||||||
|
{
|
||||||
|
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on('click', '.qu-conversion-delete-button', function(e)
|
$(document).on('click', '.qu-conversion-delete-button', function(e)
|
||||||
{
|
{
|
||||||
var objectId = $(e.currentTarget).attr('data-qu-conversion-id');
|
var objectId = $(e.currentTarget).attr('data-qu-conversion-id');
|
||||||
@ -388,6 +403,22 @@ $('#qu_id_stock').change(function(e)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#allow_label_per_unit').on('change', function()
|
||||||
|
{
|
||||||
|
if (this.checked)
|
||||||
|
{
|
||||||
|
$('#label-option-per-unit').prop("disabled", false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ($('#default_print_stock_label').val() == "2")
|
||||||
|
{
|
||||||
|
$("#default_print_stock_label").val("0");
|
||||||
|
}
|
||||||
|
$('#label-option-per-unit').prop("disabled", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$(window).on("message", function(e)
|
$(window).on("message", function(e)
|
||||||
{
|
{
|
||||||
var data = e.originalEvent.data;
|
var data = e.originalEvent.data;
|
||||||
|
@ -23,6 +23,7 @@ $('#save-purchase-button').on('click', function(e)
|
|||||||
{
|
{
|
||||||
var jsonData = {};
|
var jsonData = {};
|
||||||
jsonData.amount = jsonForm.amount;
|
jsonData.amount = jsonForm.amount;
|
||||||
|
jsonData.print_stock_label = jsonForm.print_stock_label
|
||||||
|
|
||||||
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
||||||
{
|
{
|
||||||
@ -116,6 +117,30 @@ $('#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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
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) + '<br><a class="btn btn-secondary btn-sm mt-2" href="#" onclick="UndoStockTransaction(\'' + result[0].transaction_id + '\')"><i class="fas fa-undo"></i> ' + __t("Undo") + '</a>';
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonForm.print_stock_label > 0)
|
||||||
|
{
|
||||||
|
var reps = 1;
|
||||||
|
if (jsonForm.print_stock_label == 2)
|
||||||
|
{
|
||||||
|
reps = Math.floor(jsonData.amount);
|
||||||
|
}
|
||||||
|
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, post_data, reps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (GetUriParam("embedded") !== undefined)
|
if (GetUriParam("embedded") !== undefined)
|
||||||
{
|
{
|
||||||
window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl);
|
window.parent.postMessage(WindowMessageBag("ProductChanged", jsonForm.product_id), Grocy.BaseUrl);
|
||||||
@ -279,6 +304,23 @@ if (Grocy.Components.ProductPicker !== undefined)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABELPRINTER)
|
||||||
|
{
|
||||||
|
$("#print_stock_label").val(productDetails.product.default_print_stock_label);
|
||||||
|
if (productDetails.product.allow_label_per_unit)
|
||||||
|
{
|
||||||
|
if ($('#default_print_stock_label').val() == "2")
|
||||||
|
{
|
||||||
|
$("#default_print_stock_label").val("0");
|
||||||
|
}
|
||||||
|
$('#label-option-per-unit').prop("disabled", true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$('#label-option-per-unit').prop("disabled", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$("#display_amount").focus();
|
$("#display_amount").focus();
|
||||||
|
|
||||||
Grocy.FrontendHelpers.ValidateForm('purchase-form');
|
Grocy.FrontendHelpers.ValidateForm('purchase-form');
|
||||||
|
@ -124,6 +124,21 @@ $(document).on("click", ".stock-name-cell", function(e)
|
|||||||
$("#stockentry-productcard-modal").modal("show");
|
$("#stockentry-productcard-modal").modal("show");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.stockentry-grocycode-stockentry-label-print', function(e)
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
document.activeElement.blur();
|
||||||
|
|
||||||
|
var stockId = $(e.currentTarget).attr('data-stock-id');
|
||||||
|
Grocy.Api.Get('stock/entry/' + stockId + '/printlabel', function(labelData)
|
||||||
|
{
|
||||||
|
if (Grocy.Webhooks.labelprinter !== undefined)
|
||||||
|
{
|
||||||
|
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function RefreshStockEntryRow(stockRowId)
|
function RefreshStockEntryRow(stockRowId)
|
||||||
{
|
{
|
||||||
Grocy.Api.Get("stock/entry/" + stockRowId,
|
Grocy.Api.Get("stock/entry/" + stockRowId,
|
||||||
|
@ -101,6 +101,21 @@ $("#search").on("keyup", Delay(function()
|
|||||||
stockOverviewTable.search(value).draw();
|
stockOverviewTable.search(value).draw();
|
||||||
}, 200));
|
}, 200));
|
||||||
|
|
||||||
|
$(document).on('click', '.stockentry-grocycode-product-label-print', function(e)
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
document.activeElement.blur();
|
||||||
|
|
||||||
|
var productId = $(e.currentTarget).attr('data-product-id');
|
||||||
|
Grocy.Api.Get('stock/products/' + productId + '/printlabel', function(labelData)
|
||||||
|
{
|
||||||
|
if (Grocy.Webhooks.labelprinter !== undefined)
|
||||||
|
{
|
||||||
|
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on('click', '.product-consume-button', function(e)
|
$(document).on('click', '.product-consume-button', function(e)
|
||||||
{
|
{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
12
routes.php
12
routes.php
@ -1,10 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Grocy\Middleware\AuthMiddleware;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
|
||||||
use Slim\Routing\RouteCollectorProxy;
|
|
||||||
use Grocy\Middleware\JsonMiddleware;
|
use Grocy\Middleware\JsonMiddleware;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Slim\Routing\RouteCollectorProxy;
|
||||||
|
|
||||||
$app->group('', function (RouteCollectorProxy $group) {
|
$app->group('', function (RouteCollectorProxy $group) {
|
||||||
// System routes
|
// System routes
|
||||||
@ -41,6 +40,7 @@ $app->group('', function (RouteCollectorProxy $group) {
|
|||||||
$group->get('/quantityunitconversion/{quConversionId}', '\Grocy\Controllers\StockController:QuantityUnitConversionEditForm');
|
$group->get('/quantityunitconversion/{quConversionId}', '\Grocy\Controllers\StockController:QuantityUnitConversionEditForm');
|
||||||
$group->get('/productgroups', '\Grocy\Controllers\StockController:ProductGroupsList');
|
$group->get('/productgroups', '\Grocy\Controllers\StockController:ProductGroupsList');
|
||||||
$group->get('/productgroup/{productGroupId}', '\Grocy\Controllers\StockController:ProductGroupEditForm');
|
$group->get('/productgroup/{productGroupId}', '\Grocy\Controllers\StockController:ProductGroupEditForm');
|
||||||
|
$group->get('/product/{productId}/grocycode', '\Grocy\Controllers\StockController:ProductGrocycodeImage');
|
||||||
|
|
||||||
// Stock handling routes
|
// Stock handling routes
|
||||||
if (GROCY_FEATURE_FLAG_STOCK)
|
if (GROCY_FEATURE_FLAG_STOCK)
|
||||||
@ -60,6 +60,8 @@ $app->group('', function (RouteCollectorProxy $group) {
|
|||||||
$group->get('/quantityunitpluraltesting', '\Grocy\Controllers\StockController:QuantityUnitPluralFormTesting');
|
$group->get('/quantityunitpluraltesting', '\Grocy\Controllers\StockController:QuantityUnitPluralFormTesting');
|
||||||
$group->get('/stockjournal/summary', '\Grocy\Controllers\StockController:JournalSummary');
|
$group->get('/stockjournal/summary', '\Grocy\Controllers\StockController:JournalSummary');
|
||||||
$group->get('/productbarcodes/{productBarcodeId}', '\Grocy\Controllers\StockController:ProductBarcodesEditForm');
|
$group->get('/productbarcodes/{productBarcodeId}', '\Grocy\Controllers\StockController:ProductBarcodesEditForm');
|
||||||
|
$group->get('/stockentry/{entryId}/grocycode', '\Grocy\Controllers\StockController:StockEntryGrocycodeImage');
|
||||||
|
$group->get('/stockentry/{entryId}/label', '\Grocy\Controllers\StockController:StockEntryGrocycodeLabel');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stock price tracking
|
// Stock price tracking
|
||||||
@ -206,6 +208,8 @@ $app->group('/api', function (RouteCollectorProxy $group) {
|
|||||||
$group->get('/stock/transactions/{transactionId}', '\Grocy\Controllers\StockApiController:StockTransactions');
|
$group->get('/stock/transactions/{transactionId}', '\Grocy\Controllers\StockApiController:StockTransactions');
|
||||||
$group->post('/stock/transactions/{transactionId}/undo', '\Grocy\Controllers\StockApiController:UndoTransaction');
|
$group->post('/stock/transactions/{transactionId}/undo', '\Grocy\Controllers\StockApiController:UndoTransaction');
|
||||||
$group->get('/stock/barcodes/external-lookup/{barcode}', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
|
$group->get('/stock/barcodes/external-lookup/{barcode}', '\Grocy\Controllers\StockApiController:ExternalBarcodeLookup');
|
||||||
|
$group->get('/stock/products/{productId}/printlabel', '\Grocy\Controllers\StockApiController:ProductPrintLabel');
|
||||||
|
$group->get('/stock/entry/{entryId}/printlabel', '\Grocy\Controllers\StockApiController:StockEntryPrintLabel');
|
||||||
|
|
||||||
// Shopping list
|
// Shopping list
|
||||||
$group->post('/stock/shoppinglist/add-missing-products', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
|
$group->post('/stock/shoppinglist/add-missing-products', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
namespace Grocy\Services;
|
namespace Grocy\Services;
|
||||||
|
|
||||||
|
use Grocy\Helpers\Grocycode;
|
||||||
|
use Grocy\Helpers\WebhookRunner;
|
||||||
|
|
||||||
class StockService extends BaseService
|
class StockService extends BaseService
|
||||||
{
|
{
|
||||||
const TRANSACTION_TYPE_CONSUME = 'consume';
|
const TRANSACTION_TYPE_CONSUME = 'consume';
|
||||||
@ -102,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 = 0)
|
||||||
{
|
{
|
||||||
if (!$this->ProductExists($productId))
|
if (!$this->ProductExists($productId))
|
||||||
{
|
{
|
||||||
@ -173,10 +176,36 @@ class StockService extends BaseService
|
|||||||
'stock_id' => $stockId,
|
'stock_id' => $stockId,
|
||||||
'price' => $price,
|
'price' => $price,
|
||||||
'location_id' => $locationId,
|
'location_id' => $locationId,
|
||||||
'shopping_location_id' => $shoppingLocationId,
|
'shopping_location_id' => $shoppingLocationId
|
||||||
]);
|
]);
|
||||||
$stockRow->save();
|
$stockRow->save();
|
||||||
|
|
||||||
|
if (GROCY_FEATURE_FLAG_LABELPRINTER && GROCY_LABEL_PRINTER_RUN_SERVER && $runWebhook)
|
||||||
|
{
|
||||||
|
$reps = 1;
|
||||||
|
if ($runWebhook == 2)
|
||||||
|
{ // 2 == run $amount times
|
||||||
|
$reps = intval(floor($amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
$webhookData = array_merge([
|
||||||
|
'product' => $productDetails->product->name,
|
||||||
|
'grocycode' => (string)(new Grocycode(Grocycode::PRODUCT, $productId, [$stockId])),
|
||||||
|
], GROCY_LABEL_PRINTER_PARAMS);
|
||||||
|
|
||||||
|
if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
|
||||||
|
{
|
||||||
|
$webhookData['duedate'] = $this->getLocalizationService()->__t('DD') . ': ' . $bestBeforeDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$runner = new WebhookRunner();
|
||||||
|
|
||||||
|
for ($i = 0; $i < $reps; $i++)
|
||||||
|
{
|
||||||
|
$runner->run(GROCY_LABEL_PRINTER_WEBHOOK, $webhookData, GROCY_LABEL_PRINTER_HOOK_JSON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $transactionId;
|
return $transactionId;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -240,7 +269,7 @@ class StockService extends BaseService
|
|||||||
throw new \Exception('Location does not exist');
|
throw new \Exception('Location does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
$productDetails = (object)$this->GetProductDetails($productId);
|
$productDetails = (object) $this->GetProductDetails($productId);
|
||||||
|
|
||||||
// Tare weight handling
|
// Tare weight handling
|
||||||
// The given amount is the new total amount including the container weight (gross)
|
// The given amount is the new total amount including the container weight (gross)
|
||||||
@ -280,7 +309,7 @@ class StockService extends BaseService
|
|||||||
// TODO: This check doesn't really check against products only at the given location
|
// TODO: This check doesn't really check against products only at the given location
|
||||||
// (as GetProductDetails returns the stock_amount_aggregated of all locations)
|
// (as GetProductDetails returns the stock_amount_aggregated of all locations)
|
||||||
// However, $potentialStockEntries are filtered accordingly, so this currently isn't really a problem at the end
|
// However, $potentialStockEntries are filtered accordingly, so this currently isn't really a problem at the end
|
||||||
$productStockAmount = ((object)$this->GetProductDetails($productId))->stock_amount_aggregated;
|
$productStockAmount = ((object) $this->GetProductDetails($productId))->stock_amount_aggregated;
|
||||||
if ($amount > $productStockAmount)
|
if ($amount > $productStockAmount)
|
||||||
{
|
{
|
||||||
throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)');
|
throw new \Exception('Amount to be consumed cannot be > current stock amount (if supplied, at the desired location)');
|
||||||
@ -639,6 +668,13 @@ class StockService extends BaseService
|
|||||||
|
|
||||||
public function GetProductIdFromBarcode(string $barcode)
|
public function GetProductIdFromBarcode(string $barcode)
|
||||||
{
|
{
|
||||||
|
// first, try to parse this as a product grocycode
|
||||||
|
if (Grocycode::Validate($barcode))
|
||||||
|
{
|
||||||
|
$gc = new Grocycode($barcode);
|
||||||
|
return intval($gc->GetId());
|
||||||
|
}
|
||||||
|
|
||||||
$potentialProduct = $this->getDatabase()->product_barcodes()->where('barcode = :1', $barcode)->fetch();
|
$potentialProduct = $this->getDatabase()->product_barcodes()->where('barcode = :1', $barcode)->fetch();
|
||||||
|
|
||||||
if ($potentialProduct === null)
|
if ($potentialProduct === null)
|
||||||
@ -727,7 +763,7 @@ class StockService extends BaseService
|
|||||||
throw new \Exception('Product does not exist or is inactive');
|
throw new \Exception('Product does not exist or is inactive');
|
||||||
}
|
}
|
||||||
|
|
||||||
$productDetails = (object)$this->GetProductDetails($productId);
|
$productDetails = (object) $this->GetProductDetails($productId);
|
||||||
|
|
||||||
if ($price === null)
|
if ($price === null)
|
||||||
{
|
{
|
||||||
@ -786,7 +822,7 @@ class StockService extends BaseService
|
|||||||
throw new \Exception('Product does not exist or is inactive');
|
throw new \Exception('Product does not exist or is inactive');
|
||||||
}
|
}
|
||||||
|
|
||||||
$productDetails = (object)$this->GetProductDetails($productId);
|
$productDetails = (object) $this->GetProductDetails($productId);
|
||||||
$productStockAmountUnopened = floatval($productDetails->stock_amount_aggregated) - floatval($productDetails->stock_amount_opened_aggregated);
|
$productStockAmountUnopened = floatval($productDetails->stock_amount_aggregated) - floatval($productDetails->stock_amount_opened_aggregated);
|
||||||
$potentialStockEntries = $this->GetProductStockEntries($productId, true, $allowSubproductSubstitution);
|
$potentialStockEntries = $this->GetProductStockEntries($productId, true, $allowSubproductSubstitution);
|
||||||
$product = $this->getDatabase()->products($productId);
|
$product = $this->getDatabase()->products($productId);
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
@push('pageScripts')
|
@push('pageScripts')
|
||||||
<script src="{{ $U('/node_modules/@ericblade/quagga2/dist/quagga.min.js?v=', true) }}{{ $version }}"></script>
|
<script src="{{ $U('/node_modules/@ericblade/quagga2/dist/quagga.min.js?v=', true) }}{{ $version }}"></script>
|
||||||
|
<script src="{{ $U('/components_unmanaged/quagga2-reader-datamatrix/index.js', true) }}?v={{ $version }}"></script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@push('pageStyles')
|
@push('pageStyles')
|
||||||
@ -21,8 +22,7 @@
|
|||||||
.combobox-container #barcodescanner-start-button {
|
.combobox-container #barcodescanner-start-button {
|
||||||
margin-right: 36px !important;
|
margin-right: 36px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
@endif
|
@endif
|
@ -68,4 +68,4 @@
|
|||||||
class="font-italic d-none">{{ $__t('No price history available') }}</span>
|
class="font-italic d-none">{{ $__t('No price history available') }}</span>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -95,6 +95,14 @@
|
|||||||
Grocy.CalendarShowWeekNumbers = {{ BoolToString(GROCY_CALENDAR_SHOW_WEEK_OF_YEAR) }};
|
Grocy.CalendarShowWeekNumbers = {{ BoolToString(GROCY_CALENDAR_SHOW_WEEK_OF_YEAR) }};
|
||||||
Grocy.GettextPo = {!! $GettextPo !!};
|
Grocy.GettextPo = {!! $GettextPo !!};
|
||||||
Grocy.FeatureFlags = {!! json_encode($featureFlags) !!};
|
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)
|
@if (GROCY_AUTHENTICATED)
|
||||||
Grocy.UserSettings = {!! json_encode($userSettings) !!};
|
Grocy.UserSettings = {!! json_encode($userSettings) !!};
|
||||||
|
@ -400,6 +400,57 @@
|
|||||||
'entity' => 'products'
|
'entity' => 'products'
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@if(GROCY_FEATURE_FLAG_LABELPRINTER)
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input @if($mode=='edit'
|
||||||
|
&&
|
||||||
|
$product->allow_label_per_unit == 1) checked @endif class="form-check-input custom-control-input" type="checkbox" id="allow_label_per_unit" name="allow_label_per_unit" value="1">
|
||||||
|
<label class="form-check-label custom-control-label"
|
||||||
|
for="allow_label_per_unit">{{ $__t('Allow label printing per unit') }} <i class="fas fa-question-circle text-muted"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="{{ $__t('Allow printing of one label per unit on purchase (after conversion) - e.g. 1 purchased pack adding 10 pieces of stock would print 10 labels') }}"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$no_label = "";
|
||||||
|
$single_label = "";
|
||||||
|
$per_unit_label = "";
|
||||||
|
$disable_per_unit = "";
|
||||||
|
|
||||||
|
if($mode == 'edit') {
|
||||||
|
switch($product->default_print_stock_label) {
|
||||||
|
case 0: $no_label = "selected"; break;
|
||||||
|
case 1: $single_label = "selected"; break;
|
||||||
|
case 2: $per_unit_label = "selected"; break;
|
||||||
|
default: break; // yolo
|
||||||
|
}
|
||||||
|
if($product->allow_label_per_unit == 0) {
|
||||||
|
$disable_per_unit="disabled";
|
||||||
|
$per_unit_label = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="default_print_stock_label">{{ $__t('Stock entry label') }}</label>
|
||||||
|
<select class="form-control"
|
||||||
|
id="default_print_stock_label"
|
||||||
|
name="default_print_stock_label">
|
||||||
|
<option value="0"
|
||||||
|
{{ $no_label }}>{{ $__t('No label') }}</option>
|
||||||
|
<option value="1"
|
||||||
|
{{ $single_label }}>{{ $__t('Single label') }}</option>
|
||||||
|
<option value="2"
|
||||||
|
{{ $per_unit_label }}
|
||||||
|
{{ $disable_per_unit }}
|
||||||
|
id="label-option-per-unit">{{ $__t('Label per unit') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="custom-control custom-checkbox">
|
<div class="custom-control custom-checkbox">
|
||||||
<input @if($mode=='edit'
|
<input @if($mode=='edit'
|
||||||
@ -426,6 +477,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-6 col-xs-12 @if($mode == 'create') d-none @endif">
|
<div class="col-lg-6 col-xs-12 @if($mode == 'create') d-none @endif">
|
||||||
|
|
||||||
<div class="row @if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif">
|
<div class="row @if(!GROCY_FEATURE_FLAG_STOCK) d-none @endif">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="title-related-links">
|
<div class="title-related-links">
|
||||||
@ -533,6 +585,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col clearfix">
|
||||||
|
<div class="title-related-links">
|
||||||
|
<h4>
|
||||||
|
{{ $__t('grocycode') }}
|
||||||
|
<i class="fas fa-question-circle text-muted"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="{{ $__t('grocycode is a unique referer to this product in your grocy instance - print it onto a label and scan it like any other barcode') }}"></i>
|
||||||
|
</h4>
|
||||||
|
<p>
|
||||||
|
<img src="{{ $U('/product/' . $product->id . '/grocycode?size=60') }}"
|
||||||
|
class="float-lg-left">
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a class="btn btn-outline-primary btn-sm"
|
||||||
|
href="{{ $U('/product/' . $product->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a>
|
||||||
|
@if(GROCY_FEATURE_FLAG_LABELPRINTER)
|
||||||
|
<a class="btn btn-outline-primary btn-sm stockentry-grocycode-product-label-print"
|
||||||
|
data-product-id="{{ $product->id }}"
|
||||||
|
href="#">
|
||||||
|
{{ $__t('Print on label printer') }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row @if(GROCY_FEATURE_FLAG_STOCK) mt-5 @endif">
|
<div class="row @if(GROCY_FEATURE_FLAG_STOCK) mt-5 @endif">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="title-related-links">
|
<div class="title-related-links">
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<script>
|
<script>
|
||||||
Grocy.QuantityUnits = {!! json_encode($quantityUnits) !!};
|
Grocy.QuantityUnits = {!! json_encode($quantityUnits) !!};
|
||||||
Grocy.QuantityUnitConversionsResolved = {!! json_encode($quantityUnitConversionsResolved) !!};
|
Grocy.QuantityUnitConversionsResolved = {!! json_encode($quantityUnitConversionsResolved) !!};
|
||||||
Grocy.DefaultMinAmount = '{{$DEFAULT_MIN_AMOUNT}}';
|
Grocy.DefaultMinAmount = '{{ $DEFAULT_MIN_AMOUNT }}';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -148,6 +148,21 @@
|
|||||||
))
|
))
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
@if(GROCY_FEATURE_FLAG_LABELPRINTER)
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="print_stock_label">{{ $__t('Stock entry label') }}</label>
|
||||||
|
<select class="form-control"
|
||||||
|
id="print_stock_label"
|
||||||
|
name="print_stock_label">
|
||||||
|
<option value="0">{{ $__t('No label') }}</option>
|
||||||
|
<option value="1">{{ $__t('Single label') }}</option>
|
||||||
|
<option value="2"
|
||||||
|
id="label-option-per-unit">{{ $__t('Label per unit') }}</option>
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">{{ $__t('A quantity unit is required') }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<button id="save-purchase-button"
|
<button id="save-purchase-button"
|
||||||
class="btn btn-success d-block">{{ $__t('OK') }}</button>
|
class="btn btn-success d-block">{{ $__t('OK') }}</button>
|
||||||
|
|
||||||
|
@ -200,6 +200,26 @@
|
|||||||
href="{{ $U('/product/') }}{{ $stockEntry->product_id . '?returnto=/stockentries' }}">
|
href="{{ $U('/product/') }}{{ $stockEntry->product_id . '?returnto=/stockentries' }}">
|
||||||
{{ $__t('Edit product') }}
|
{{ $__t('Edit product') }}
|
||||||
</a>
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a class="dropdown-item stockentry-grocycode-link"
|
||||||
|
type="button"
|
||||||
|
href="{{ $U('/stockentry/' . $stockEntry->id . '/grocycode?download=true') }}">
|
||||||
|
{{ $__t('Download stock entry grocycode') }}
|
||||||
|
</a>
|
||||||
|
@if(GROCY_FEATURE_FLAG_LABELPRINTER)
|
||||||
|
<a class="dropdown-item stockentry-grocycode-stockentry-label-print"
|
||||||
|
data-stock-id="{{ $stockEntry->id }}"
|
||||||
|
type="button"
|
||||||
|
href="#">
|
||||||
|
{{ $__t('Print stock entry grocycode on label printer') }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
<a class="dropdown-item stockentry-label-link"
|
||||||
|
type="button"
|
||||||
|
target="_blank"
|
||||||
|
href="{{ $U('/stockentry/' . $stockEntry->id . '/label') }}">
|
||||||
|
{{ $__t('Open stock entry print label in new window') }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
40
views/stockentrylabel.blade.php
Normal file
40
views/stockentrylabel.blade.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>{{ $product->name }}</title>
|
||||||
|
<link href="{{ $U('/components_unmanaged/noto-sans-v11-latin/noto-sans-v11-latin.min.css?v=', true) }}{{ $version }}"
|
||||||
|
rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
float: left;
|
||||||
|
margin-right: .5rem;
|
||||||
|
max-height: 25px;
|
||||||
|
width: auto;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productname {
|
||||||
|
font-size: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
<!-- Size gets determined by CSS, so printing works better (more pixels = sharper printed image).
|
||||||
|
Unfortunately, this also means the code is blurred on screen. -->
|
||||||
|
<img src="{{ $U('/stockentry/'. $stockEntry->id . '/grocycode?size=100') }}">
|
||||||
|
<span class="productname">{{ $product->name }}</span><br>
|
||||||
|
@if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
|
||||||
|
<span>{{ $__t('DD') }}: {{ $stockEntry->best_before_date }}</span>
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -298,6 +298,20 @@
|
|||||||
href="{{ $U('/product/') }}{{ $currentStockEntry->product_id . '?returnto=%2Fstockoverview' }}">
|
href="{{ $U('/product/') }}{{ $currentStockEntry->product_id . '?returnto=%2Fstockoverview' }}">
|
||||||
<span class="dropdown-item-text">{{ $__t('Edit product') }}</span>
|
<span class="dropdown-item-text">{{ $__t('Edit product') }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a class="dropdown-item stockentry-grocycode-link"
|
||||||
|
type="button"
|
||||||
|
href="{{ $U('/product/' . $currentStockEntry->product_id . '/grocycode?download=true') }}">
|
||||||
|
{{ $__t('Download product grocycode') }}
|
||||||
|
</a>
|
||||||
|
@if(GROCY_FEATURE_FLAG_LABELPRINTER)
|
||||||
|
<a class="dropdown-item stockentry-grocycode-product-label-print"
|
||||||
|
data-product-id="{{ $currentStockEntry->product_id }}"
|
||||||
|
type="button"
|
||||||
|
href="#">
|
||||||
|
{{ $__t('Print product grocycode on label printer') }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -308,7 +322,8 @@
|
|||||||
<td>
|
<td>
|
||||||
@if($currentStockEntry->product_group_name !== null){{ $currentStockEntry->product_group_name }}@endif
|
@if($currentStockEntry->product_group_name !== null){{ $currentStockEntry->product_group_name }}@endif
|
||||||
</td>
|
</td>
|
||||||
<td data-order={{ $currentStockEntry->amount }}>
|
<td data-order={{
|
||||||
|
$currentStockEntry->amount }}>
|
||||||
<span id="product-{{ $currentStockEntry->product_id }}-amount"
|
<span id="product-{{ $currentStockEntry->product_id }}-amount"
|
||||||
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural) }}</span>
|
class="locale-number locale-number-quantity-amount">{{ $currentStockEntry->amount }}</span> <span id="product-{{ $currentStockEntry->product_id }}-qu-name">{{ $__n($currentStockEntry->amount, $currentStockEntry->qu_unit_name, $currentStockEntry->qu_unit_name_plural) }}</span>
|
||||||
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount"
|
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user