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.autoFixBySemicolon": true,
|
||||
"php-cs-fixer.onsave": true,
|
||||
}
|
||||
}
|
@ -11,7 +11,9 @@
|
||||
"eluceo/ical": "^0.16.0",
|
||||
"erusev/parsedown": "^1.7",
|
||||
"gumlet/php-image-resize": "^1.9",
|
||||
"ezyang/htmlpurifier": "^4.13"
|
||||
"ezyang/htmlpurifier": "^4.13",
|
||||
"jucksearm/php-barcode": "^1.0",
|
||||
"guzzlehttp/guzzle": "^7.0"
|
||||
},
|
||||
"autoload": {
|
||||
"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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6155c0eb959bd8118ce2aabce7fd8a6a",
|
||||
"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",
|
||||
@ -828,6 +1061,58 @@
|
||||
},
|
||||
"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",
|
||||
"version": "v0.4.1",
|
||||
@ -1360,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",
|
||||
@ -2528,5 +2865,5 @@
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"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_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', false);
|
||||
|
||||
// Sub feature flags
|
||||
Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true);
|
||||
|
@ -4,6 +4,8 @@ namespace Grocy\Controllers;
|
||||
|
||||
use Grocy\Controllers\Users\User;
|
||||
use Grocy\Services\StockService;
|
||||
use Grocy\Helpers\WebhookRunner;
|
||||
use Grocy\Helpers\Grocycode;
|
||||
|
||||
class StockApiController extends BaseApiController
|
||||
{
|
||||
@ -138,8 +140,14 @@ class StockApiController extends BaseApiController
|
||||
{
|
||||
$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;
|
||||
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());
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
User::checkPermission($request, User::PERMISSION_SHOPPINGLIST_ITEMS_DELETE);
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||
namespace Grocy\Controllers;
|
||||
|
||||
use Grocy\Helpers\Grocycode;
|
||||
use Grocy\Services\RecipesService;
|
||||
use jucksearm\barcode\lib\DatamatrixFactory;
|
||||
|
||||
class StockController extends BaseController
|
||||
{
|
||||
@ -39,7 +41,7 @@ class StockController extends BaseController
|
||||
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
|
||||
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
|
||||
'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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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",
|
||||
"name": "entryId",
|
||||
"required": true,
|
||||
"description": "A valid stock row id",
|
||||
"description": "A valid stock entry id",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
@ -1441,7 +1441,7 @@
|
||||
"in": "path",
|
||||
"name": "entryId",
|
||||
"required": true,
|
||||
"description": "A valid stock row id",
|
||||
"description": "A valid stock entry id",
|
||||
"schema": {
|
||||
"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": {
|
||||
"get": {
|
||||
"summary": "Returns all products which are due soon, overdue, expired or currently missing",
|
||||
@ -1850,6 +1884,10 @@
|
||||
"type": "number",
|
||||
"format": "integer",
|
||||
"description": "If omitted, no store will be affected"
|
||||
},
|
||||
"print_stock_label": {
|
||||
"type": "boolean",
|
||||
"description": "True when the stock entry label should be printed"
|
||||
}
|
||||
},
|
||||
"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}": {
|
||||
"post": {
|
||||
"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"
|
||||
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()
|
||||
{
|
||||
$(this).closest("form").addClass("is-dirty");
|
||||
|
@ -1,5 +1,9 @@
|
||||
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.CheckCapabilities = async function()
|
||||
{
|
||||
@ -96,7 +100,8 @@ Grocy.Components.BarcodeScanner.StartScanning = function()
|
||||
readers: [
|
||||
"ean_reader",
|
||||
"ean_8_reader",
|
||||
"code_128_reader"
|
||||
"code_128_reader",
|
||||
"datamatrix"
|
||||
],
|
||||
debug: {
|
||||
showCanvas: Grocy.UserSettings.quagga2_debug,
|
||||
|
@ -147,7 +147,23 @@ $('#product_id_text_input').on('blur', function(e)
|
||||
$('#product_id').attr("barcode", "null");
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -223,6 +223,18 @@ $("#location_id").on('change', function(e)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -249,6 +261,7 @@ $("#location_id").on('change', function(e)
|
||||
|
||||
if (stockEntry.stock_id == stockId)
|
||||
{
|
||||
$("#use_specific_stock_entry").click();
|
||||
$("#specific_stock_entry").val(stockId);
|
||||
}
|
||||
}
|
||||
|
@ -301,6 +301,21 @@ $('#name').focus();
|
||||
$('.input-group-qu').trigger('change');
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var data = e.originalEvent.data;
|
||||
|
@ -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,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>';
|
||||
|
||||
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)
|
||||
{
|
||||
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();
|
||||
|
||||
Grocy.FrontendHelpers.ValidateForm('purchase-form');
|
||||
|
@ -124,6 +124,21 @@ $(document).on("click", ".stock-name-cell", function(e)
|
||||
$("#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)
|
||||
{
|
||||
Grocy.Api.Get("stock/entry/" + stockRowId,
|
||||
|
@ -101,6 +101,21 @@ $("#search").on("keyup", Delay(function()
|
||||
stockOverviewTable.search(value).draw();
|
||||
}, 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)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
12
routes.php
12
routes.php
@ -1,10 +1,9 @@
|
||||
<?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 Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
|
||||
$app->group('', function (RouteCollectorProxy $group) {
|
||||
// System routes
|
||||
@ -41,6 +40,7 @@ $app->group('', function (RouteCollectorProxy $group) {
|
||||
$group->get('/quantityunitconversion/{quConversionId}', '\Grocy\Controllers\StockController:QuantityUnitConversionEditForm');
|
||||
$group->get('/productgroups', '\Grocy\Controllers\StockController:ProductGroupsList');
|
||||
$group->get('/productgroup/{productGroupId}', '\Grocy\Controllers\StockController:ProductGroupEditForm');
|
||||
$group->get('/product/{productId}/grocycode', '\Grocy\Controllers\StockController:ProductGrocycodeImage');
|
||||
|
||||
// Stock handling routes
|
||||
if (GROCY_FEATURE_FLAG_STOCK)
|
||||
@ -60,6 +60,8 @@ $app->group('', function (RouteCollectorProxy $group) {
|
||||
$group->get('/quantityunitpluraltesting', '\Grocy\Controllers\StockController:QuantityUnitPluralFormTesting');
|
||||
$group->get('/stockjournal/summary', '\Grocy\Controllers\StockController:JournalSummary');
|
||||
$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
|
||||
@ -206,6 +208,8 @@ $app->group('/api', function (RouteCollectorProxy $group) {
|
||||
$group->get('/stock/transactions/{transactionId}', '\Grocy\Controllers\StockApiController:StockTransactions');
|
||||
$group->post('/stock/transactions/{transactionId}/undo', '\Grocy\Controllers\StockApiController:UndoTransaction');
|
||||
$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
|
||||
$group->post('/stock/shoppinglist/add-missing-products', '\Grocy\Controllers\StockApiController:AddMissingProductsToShoppingList');
|
||||
|
@ -2,6 +2,9 @@
|
||||
|
||||
namespace Grocy\Services;
|
||||
|
||||
use Grocy\Helpers\Grocycode;
|
||||
use Grocy\Helpers\WebhookRunner;
|
||||
|
||||
class StockService extends BaseService
|
||||
{
|
||||
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))
|
||||
{
|
||||
@ -173,10 +176,36 @@ class StockService extends BaseService
|
||||
'stock_id' => $stockId,
|
||||
'price' => $price,
|
||||
'location_id' => $locationId,
|
||||
'shopping_location_id' => $shoppingLocationId,
|
||||
'shopping_location_id' => $shoppingLocationId
|
||||
]);
|
||||
$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;
|
||||
}
|
||||
else
|
||||
@ -240,7 +269,7 @@ class StockService extends BaseService
|
||||
throw new \Exception('Location does not exist');
|
||||
}
|
||||
|
||||
$productDetails = (object)$this->GetProductDetails($productId);
|
||||
$productDetails = (object) $this->GetProductDetails($productId);
|
||||
|
||||
// Tare weight handling
|
||||
// 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
|
||||
// (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
|
||||
$productStockAmount = ((object)$this->GetProductDetails($productId))->stock_amount_aggregated;
|
||||
$productStockAmount = ((object) $this->GetProductDetails($productId))->stock_amount_aggregated;
|
||||
if ($amount > $productStockAmount)
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 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();
|
||||
|
||||
if ($potentialProduct === null)
|
||||
@ -727,7 +763,7 @@ class StockService extends BaseService
|
||||
throw new \Exception('Product does not exist or is inactive');
|
||||
}
|
||||
|
||||
$productDetails = (object)$this->GetProductDetails($productId);
|
||||
$productDetails = (object) $this->GetProductDetails($productId);
|
||||
|
||||
if ($price === null)
|
||||
{
|
||||
@ -786,7 +822,7 @@ class StockService extends BaseService
|
||||
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);
|
||||
$potentialStockEntries = $this->GetProductStockEntries($productId, true, $allowSubproductSubstitution);
|
||||
$product = $this->getDatabase()->products($productId);
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
@push('pageScripts')
|
||||
<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
|
||||
|
||||
@push('pageStyles')
|
||||
@ -21,8 +22,7 @@
|
||||
.combobox-container #barcodescanner-start-button {
|
||||
margin-right: 36px !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@endif
|
||||
@endif
|
@ -68,4 +68,4 @@
|
||||
class="font-italic d-none">{{ $__t('No price history available') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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) !!};
|
||||
|
@ -400,6 +400,57 @@
|
||||
'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="custom-control custom-checkbox">
|
||||
<input @if($mode=='edit'
|
||||
@ -426,6 +477,7 @@
|
||||
</div>
|
||||
|
||||
<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="col">
|
||||
<div class="title-related-links">
|
||||
@ -533,6 +585,34 @@
|
||||
</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="col">
|
||||
<div class="title-related-links">
|
||||
|
@ -12,7 +12,7 @@
|
||||
<script>
|
||||
Grocy.QuantityUnits = {!! json_encode($quantityUnits) !!};
|
||||
Grocy.QuantityUnitConversionsResolved = {!! json_encode($quantityUnitConversionsResolved) !!};
|
||||
Grocy.DefaultMinAmount = '{{$DEFAULT_MIN_AMOUNT}}';
|
||||
Grocy.DefaultMinAmount = '{{ $DEFAULT_MIN_AMOUNT }}';
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
@ -148,6 +148,21 @@
|
||||
))
|
||||
@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"
|
||||
class="btn btn-success d-block">{{ $__t('OK') }}</button>
|
||||
|
||||
|
@ -200,6 +200,26 @@
|
||||
href="{{ $U('/product/') }}{{ $stockEntry->product_id . '?returnto=/stockentries' }}">
|
||||
{{ $__t('Edit product') }}
|
||||
</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>
|
||||
</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' }}">
|
||||
<span class="dropdown-item-text">{{ $__t('Edit product') }}</span>
|
||||
</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>
|
||||
</td>
|
||||
@ -308,7 +322,8 @@
|
||||
<td>
|
||||
@if($currentStockEntry->product_group_name !== null){{ $currentStockEntry->product_group_name }}@endif
|
||||
</td>
|
||||
<td data-order={{ $currentStockEntry->amount }}>
|
||||
<td data-order={{
|
||||
$currentStockEntry->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>
|
||||
<span id="product-{{ $currentStockEntry->product_id }}-opened-amount"
|
||||
|
Loading…
x
Reference in New Issue
Block a user