mirror of
https://github.com/grocy/grocy.git
synced 2026-04-08 05:36:15 +02:00
Merge branch 'master' into master
This commit is contained in:
commit
3d7b176344
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,
|
||||||
}
|
}
|
||||||
|
|
@ -21,10 +21,10 @@ Please don't send me private messages regarding grocy help. I check the issue tr
|
||||||
See the website for a list of community contributed Add-ons / Tools: [https://grocy.info/addons](https://grocy.info/addons)
|
See the website for a list of community contributed Add-ons / Tools: [https://grocy.info/addons](https://grocy.info/addons)
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete household management"-thing. ERP your fridge!
|
A household needs to be managed. I did this so far (almost 10 years) with my first self written software (a C# Windows forms application) and with a bunch of Excel sheets. The software is a pain to use and Excel is Excel. So I searched for and tried different things for a (very) long time, nothing 100 % fitted, so this is my aim for a "complete household management"-thing. ERP your fridge!
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
> Checkout [grocy-desktop](https://github.com/grocy/grocy-desktop), if you want to run grocy without having to manage a webserver just like a normal ("indows) desktop application.
|
> Checkout [grocy-desktop](https://github.com/grocy/grocy-desktop), if you want to run grocy without having to manage a webserver just like a normal (Windows) desktop application.
|
||||||
>
|
>
|
||||||
> Directly download the [latest release](https://releases.grocy.info/latest-desktop) - the installation is nothing more than just clicking 2 times "next".
|
> Directly download the [latest release](https://releases.grocy.info/latest-desktop) - the installation is nothing more than just clicking 2 times "next".
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,44 @@
|
||||||
- Fixed that tracking chores with "Done by" a different user was not possible
|
### New feature: (Own) Product and stock entry labels/barcodes ("grocycode")
|
||||||
|
- Print own labels/barcodes for products and/or every stock entry and then scan that code on every place a product or stock entry can be selected
|
||||||
|
- Can be printed (or downloaded) via
|
||||||
|
- The product edit page
|
||||||
|
- The context/more menu per line on the stock overview and stock entries page
|
||||||
|
- Automatically on purchase (new option on the purchase page, defaults can be configured per product)
|
||||||
|
- The used barcode type is `DataMatrix`
|
||||||
|
- Label printer functionality can be enabled via the new feature flag `FEATURE_FLAG_LABELPRINTER` (defaults to disabled)
|
||||||
|
- Label printer communication happens via WebHooks - see the new `LABEL_PRINTER*` `config.php` options
|
||||||
|
- Those grocycodes can also be used without a label printer - you can view or download the pictures and print them manually
|
||||||
|
- More information:
|
||||||
|
- https://github.com/grocy/grocy/blob/master/docs/grocycode.md
|
||||||
|
- https://github.com/grocy/grocy/blob/master/docs/label-printing.md
|
||||||
|
- (Thanks a lot @mistressofjellyfish)
|
||||||
|
|
||||||
|
### Stock improvements/fixes
|
||||||
|
- Product barcodes are now enforced to be unique across products
|
||||||
- Fixed that editing stock entries was not possible
|
- Fixed that editing stock entries was not possible
|
||||||
- Fixed that shopping list prints had a grey background (thanks @Forceu)
|
|
||||||
- Improved/fixed the form validation on the shopping list item page (thanks @Forceu)
|
|
||||||
- Fixed that consuming with Scan Mode was not possible
|
- Fixed that consuming with Scan Mode was not possible
|
||||||
- Some night mode style improvements (thanks @BlizzWave and @KTibow)
|
- Fixed that the current stock total value (header of the stock overview page) didn't include decimal amounts (thanks @Ape)
|
||||||
|
- Fixed that the transfer page was not fully populated when opening it from the stock entries page
|
||||||
|
|
||||||
|
### Shopping list improvements/fixes
|
||||||
|
- The amount now defaults to `1` for adding items quicker
|
||||||
|
- Fixed that shopping list prints had a grey background (thanks @Forceu)
|
||||||
|
- Fixed the form validation on the shopping list item page (thanks @Forceu)
|
||||||
|
|
||||||
|
### Recipe improvements/fixes
|
||||||
- Recipe printing improvements (thanks @Ape)
|
- Recipe printing improvements (thanks @Ape)
|
||||||
|
- Calories are now always displayed per single serving (on the recipe and meal plan page)
|
||||||
|
- Fixed that "Only check if any amount is in stock" (recipe ingredient option) didn't work for stock amounts < 1
|
||||||
|
|
||||||
|
### Chores fixes
|
||||||
|
- Fixed that tracking chores with "Done by" a different user was not possible
|
||||||
|
|
||||||
|
### Userfield fixes
|
||||||
|
- Fixed that numeric Userfields were initialised with `1.0`
|
||||||
|
|
||||||
|
### General & other improvements/fixes
|
||||||
|
- Some night mode style improvements (thanks @BlizzWave and @KTibow)
|
||||||
|
- Fixed that the number picker up/down buttons did not work when the input field was empty or contained an invalid number
|
||||||
|
|
||||||
|
### API fixes
|
||||||
|
- Fixed that due soon products with `due_type` = "Expiration date" were missing in `due_products` of the `/stock/volatile` endpoint
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
"ezyang/htmlpurifier": "^4.13",
|
"ezyang/htmlpurifier": "^4.13",
|
||||||
"jucksearm/php-barcode": "^1.0",
|
"jucksearm/php-barcode": "^1.0",
|
||||||
"guzzlehttp/guzzle": "^7.0",
|
"guzzlehttp/guzzle": "^7.0",
|
||||||
"mike42/escpos-php": "^3.0"
|
"mike42/escpos-php": "^3.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ DefaultUserSetting('stock_default_consume_amount_use_quick_consume_amount', fals
|
||||||
DefaultUserSetting('scan_mode_consume_enabled', false);
|
DefaultUserSetting('scan_mode_consume_enabled', false);
|
||||||
DefaultUserSetting('scan_mode_purchase_enabled', false);
|
DefaultUserSetting('scan_mode_purchase_enabled', false);
|
||||||
DefaultUserSetting('show_icon_on_stock_overview_page_when_product_is_on_shopping_list', true);
|
DefaultUserSetting('show_icon_on_stock_overview_page_when_product_is_on_shopping_list', true);
|
||||||
DefaultUserSetting('show_purchased_date_on_purchase', false); // Wheter the purchased date should be editable on purchase (defaults to today otherwise)
|
DefaultUserSetting('show_purchased_date_on_purchase', false); // Whether the purchased date should be editable on purchase (defaults to today otherwise)
|
||||||
DefaultUserSetting('show_warning_on_purchase_when_due_date_is_earlier_than_next', true); // Show a warning on purchase when the due date of the purchased product is earlier than the next due date in stock
|
DefaultUserSetting('show_warning_on_purchase_when_due_date_is_earlier_than_next', true); // Show a warning on purchase when the due date of the purchased product is earlier than the next due date in stock
|
||||||
|
|
||||||
// Shopping list settings
|
// Shopping list settings
|
||||||
|
|
@ -175,6 +175,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
|
||||||
|
|
@ -188,6 +203,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);
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,7 @@ class BaseController
|
||||||
if (self::$htmlPurifierInstance == null)
|
if (self::$htmlPurifierInstance == null)
|
||||||
{
|
{
|
||||||
$htmlPurifierConfig = \HTMLPurifier_Config::createDefault();
|
$htmlPurifierConfig = \HTMLPurifier_Config::createDefault();
|
||||||
|
$htmlPurifierConfig->set('Cache.SerializerPath', GROCY_DATAPATH . '/viewcache');
|
||||||
$htmlPurifierConfig->set('HTML.Allowed', 'div,b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src],table[border|width|style],tbody,tr,td,th,blockquote');
|
$htmlPurifierConfig->set('HTML.Allowed', 'div,b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src],table[border|width|style],tbody,tr,td,th,blockquote');
|
||||||
$htmlPurifierConfig->set('CSS.AllowedProperties', 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align');
|
$htmlPurifierConfig->set('CSS.AllowedProperties', 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align');
|
||||||
$htmlPurifierConfig->set('URI.AllowedSchemes', ['data' => true]);
|
$htmlPurifierConfig->set('URI.AllowedSchemes', ['data' => true]);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,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
|
||||||
{
|
{
|
||||||
|
|
@ -137,8 +139,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);
|
||||||
}
|
}
|
||||||
|
|
@ -603,6 +611,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->ApiResponse($response, $webhookData);
|
||||||
|
}
|
||||||
|
|
||||||
|
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->ApiResponse($response, $webhookData);
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -1405,7 +1405,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"
|
||||||
}
|
}
|
||||||
|
|
@ -1444,7 +1444,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"
|
||||||
}
|
}
|
||||||
|
|
@ -1532,6 +1532,48 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/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": {
|
||||||
|
"200": {
|
||||||
|
"description": "The operation was successful",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "WebHook data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
|
@ -1853,6 +1895,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": {
|
||||||
|
|
@ -2216,6 +2262,48 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/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": {
|
||||||
|
"200": {
|
||||||
|
"description": "The operation was successful",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "WebHook data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2141,3 +2141,4 @@ msgstr ""
|
||||||
|
|
||||||
msgid "Unable to print"
|
msgid "Unable to print"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ abstract class AuthMiddleware extends BaseMiddleware
|
||||||
/**
|
/**
|
||||||
* @param array $postParams
|
* @param array $postParams
|
||||||
* @return bool True/False if the provided credentials were valid
|
* @return bool True/False if the provided credentials were valid
|
||||||
* @throws \Exception Throws an \Exception if an error happended during credentials processing or if this AuthMiddleware doesn't provide credentials processing (e. g. handles this externally)
|
* @throws \Exception Throws an \Exception if an error happened during credentials processing or if this AuthMiddleware doesn't provide credentials processing (e. g. handles this externally)
|
||||||
*/
|
*/
|
||||||
abstract public static function ProcessLogin(array $postParams);
|
abstract public static function ProcessLogin(array $postParams);
|
||||||
|
|
||||||
|
|
|
||||||
13
migrations/0128.sql
Normal file
13
migrations/0128.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- Duplicate product barcodes were most probably not created on purpose,
|
||||||
|
-- so just keep the newer one for any duplicates
|
||||||
|
DELETE FROM product_barcodes
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT MIN(id)
|
||||||
|
FROM product_barcodes
|
||||||
|
GROUP BY barcode
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX ix_product_barcodes ON product_barcodes (
|
||||||
|
barcode
|
||||||
|
);
|
||||||
99
migrations/0129.sql
Normal file
99
migrations/0129.sql
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
DROP VIEW recipes_pos_resolved;
|
||||||
|
CREATE VIEW recipes_pos_resolved
|
||||||
|
AS
|
||||||
|
|
||||||
|
-- Multiplication by 1.0 to force conversion to float (REAL)
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
r.id AS recipe_id,
|
||||||
|
rp.id AS recipe_pos_id,
|
||||||
|
rp.product_id AS product_id,
|
||||||
|
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 * CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN r.base_servings * rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
|
||||||
|
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
|
||||||
|
CASE WHEN IFNULL(sc.amount_aggregated, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled,
|
||||||
|
CASE WHEN IFNULL(sc.amount_aggregated, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END < 0 THEN ABS(IFNULL(sc.amount_aggregated, 0) - (CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 * CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN r.base_servings * rnrr.base_servings*1.0 ELSE 1 END) END)) ELSE 0 END AS missing_amount,
|
||||||
|
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
|
||||||
|
CASE WHEN IFNULL(sc.amount_aggregated, 0) + (CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END * p.qu_factor_purchase_to_stock) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list,
|
||||||
|
rp.qu_id,
|
||||||
|
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END) * rp.amount * pop.price * rp.price_factor AS costs,
|
||||||
|
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
|
||||||
|
rp.ingredient_group,
|
||||||
|
pg.name as product_group,
|
||||||
|
rp.id, -- Just a dummy id column
|
||||||
|
r.type as recipe_type,
|
||||||
|
rnr.includes_recipe_id as child_recipe_id,
|
||||||
|
rp.note,
|
||||||
|
rp.variable_amount AS recipe_variable_amount,
|
||||||
|
rp.only_check_single_unit_in_stock,
|
||||||
|
rp.amount / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) AS calories,
|
||||||
|
p.active AS product_active
|
||||||
|
FROM recipes r
|
||||||
|
JOIN recipes_nestings_resolved rnr
|
||||||
|
ON r.id = rnr.recipe_id
|
||||||
|
JOIN recipes rnrr
|
||||||
|
ON rnr.includes_recipe_id = rnrr.id
|
||||||
|
JOIN recipes_pos rp
|
||||||
|
ON rnr.includes_recipe_id = rp.recipe_id
|
||||||
|
JOIN products p
|
||||||
|
ON rp.product_id = p.id
|
||||||
|
LEFT JOIN product_groups pg
|
||||||
|
ON p.product_group_id = pg.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT product_id, SUM(amount) AS amount
|
||||||
|
FROM shopping_list
|
||||||
|
GROUP BY product_id) sl
|
||||||
|
ON rp.product_id = sl.product_id
|
||||||
|
LEFT JOIN stock_current sc
|
||||||
|
ON rp.product_id = sc.product_id
|
||||||
|
LEFT JOIN products_oldest_stock_unit_price pop
|
||||||
|
ON rp.product_id = pop.product_id
|
||||||
|
WHERE rp.not_check_stock_fulfillment = 0
|
||||||
|
|
||||||
|
UNION
|
||||||
|
|
||||||
|
-- Just add all recipe positions which should not be checked against stock with fulfilled need
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
r.id AS recipe_id,
|
||||||
|
rp.id AS recipe_pos_id,
|
||||||
|
rp.product_id AS product_id,
|
||||||
|
rp.amount * (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 * CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN r.base_servings * rnrr.base_servings*1.0 ELSE 1 END) AS recipe_amount,
|
||||||
|
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
|
||||||
|
1 AS need_fulfilled,
|
||||||
|
0 AS missing_amount,
|
||||||
|
IFNULL(sl.amount, 0) * p.qu_factor_purchase_to_stock AS amount_on_shopping_list,
|
||||||
|
1 AS need_fulfilled_with_shopping_list,
|
||||||
|
rp.qu_id,
|
||||||
|
(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE (r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) END) * rp.amount * IFNULL(pop.price, 0) * rp.price_factor AS costs,
|
||||||
|
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
|
||||||
|
rp.ingredient_group,
|
||||||
|
pg.name as product_group,
|
||||||
|
rp.id, -- Just a dummy id column
|
||||||
|
r.type as recipe_type,
|
||||||
|
rnr.includes_recipe_id as child_recipe_id,
|
||||||
|
rp.note,
|
||||||
|
rp.variable_amount AS recipe_variable_amount,
|
||||||
|
rp.only_check_single_unit_in_stock,
|
||||||
|
rp.amount / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p.calories, 0) AS calories,
|
||||||
|
p.active AS product_active
|
||||||
|
FROM recipes r
|
||||||
|
JOIN recipes_nestings_resolved rnr
|
||||||
|
ON r.id = rnr.recipe_id
|
||||||
|
JOIN recipes rnrr
|
||||||
|
ON rnr.includes_recipe_id = rnrr.id
|
||||||
|
JOIN recipes_pos rp
|
||||||
|
ON rnr.includes_recipe_id = rp.recipe_id
|
||||||
|
JOIN products p
|
||||||
|
ON rp.product_id = p.id
|
||||||
|
LEFT JOIN product_groups pg
|
||||||
|
ON p.product_group_id = pg.id
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT product_id, SUM(amount) AS amount
|
||||||
|
FROM shopping_list
|
||||||
|
GROUP BY product_id) sl
|
||||||
|
ON rp.product_id = sl.product_id
|
||||||
|
LEFT JOIN stock_current sc
|
||||||
|
ON rp.product_id = sc.product_id
|
||||||
|
LEFT JOIN products_oldest_stock_unit_price pop
|
||||||
|
ON rp.product_id = pop.product_id
|
||||||
|
WHERE rp.not_check_stock_fulfillment = 1;
|
||||||
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
|
|
@ -7,6 +7,15 @@ body.night-mode {
|
||||||
filter: invert(0.9) hue-rotate(176deg);
|
filter: invert(0.9) hue-rotate(176deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navbar-light .navbar-toggler {
|
||||||
|
color: rgba(255, 255, 255, .5);
|
||||||
|
border-color: rgba(255, 255, 255,.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.night-mode .navbar-light .navbar-toggler-icon {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
.night-mode .table-info,
|
.night-mode .table-info,
|
||||||
.night-mode .table-info > td,
|
.night-mode .table-info > td,
|
||||||
.night-mode .table-info > th,
|
.night-mode .table-info > th,
|
||||||
|
|
@ -42,16 +51,20 @@ body.night-mode {
|
||||||
border-color: #1ed1ee !important;
|
border-color: #1ed1ee !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.night-mode .btn-outline-info:hover {
|
||||||
|
color: #e1e1e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.night-mode .btn-info {
|
.night-mode .btn-info {
|
||||||
color: #c1c1c1;
|
color: #c1c1c1;
|
||||||
background-color: #07373f;
|
background-color: #0c5f6e;
|
||||||
border-color: #07373f;
|
border-color: #0c5f6e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.night-mode .btn-warning {
|
.night-mode .btn-warning {
|
||||||
color: #c1c1c1;
|
color: #c1c1c1;
|
||||||
background-color: #473604;
|
background-color: #a9810a;
|
||||||
border-color: #473604;
|
border-color: #a9810a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.night-mode .btn-danger {
|
.night-mode .btn-danger {
|
||||||
|
|
@ -107,15 +120,15 @@ body.night-mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
.night-mode .bg-warning {
|
.night-mode .bg-warning {
|
||||||
background-color: #473604!important;
|
background-color: #473604 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.night-mode .bg-info {
|
.night-mode .bg-info {
|
||||||
background-color: #07373f!important;
|
background-color: #07373f !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.night-mode .bg-danger {
|
.night-mode .bg-danger {
|
||||||
background-color: #471116!important;
|
background-color: #471116 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.night-mode .form-control:focus {
|
.night-mode .form-control:focus {
|
||||||
|
|
@ -129,6 +142,16 @@ body.night-mode {
|
||||||
background-color: #333131;
|
background-color: #333131;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.night-mode .custom-select {
|
||||||
|
color: #c1c1c1;
|
||||||
|
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23c1c1c1' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.night-mode .custom-select:valid,
|
||||||
|
.night-mode .custom-select:invalid {
|
||||||
|
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3e%3cpath fill='%23c1c1c1' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.night-mode .dropdown-item:focus,
|
.night-mode .dropdown-item:focus,
|
||||||
.night-mode .dropdown-item:hover {
|
.night-mode .dropdown-item:hover {
|
||||||
color: #16181b;
|
color: #16181b;
|
||||||
|
|
@ -136,7 +159,7 @@ body.night-mode {
|
||||||
}
|
}
|
||||||
|
|
||||||
.night-mode .dropdown-item {
|
.night-mode .dropdown-item {
|
||||||
color: #7c7b6f;
|
color: #c1c1c1;
|
||||||
background-color: #333131;
|
background-color: #333131;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,7 +232,7 @@ body.night-mode {
|
||||||
.night-mode .navbar-sidenav > li:hover,
|
.night-mode .navbar-sidenav > li:hover,
|
||||||
.night-mode .sidenav-second-level > li:hover,
|
.night-mode .sidenav-second-level > li:hover,
|
||||||
.night-mode .navbar-nav .dropdown-item:hover {
|
.night-mode .navbar-nav .dropdown-item:hover {
|
||||||
box-shadow: inset 5px 0 0 #112a3f !important;
|
box-shadow: inset 5px 0 0 #92bee2 !important;
|
||||||
background-color: #383838 !important;
|
background-color: #383838 !important;
|
||||||
color: #c1c1c1 !important;
|
color: #c1c1c1 !important;
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +240,7 @@ body.night-mode {
|
||||||
.night-mode .navbar-sidenav > li > a:focus,
|
.night-mode .navbar-sidenav > li > a:focus,
|
||||||
.night-mode .sidenav-second-level > li > a:focus,
|
.night-mode .sidenav-second-level > li > a:focus,
|
||||||
.night-mode .navbar-nav .dropdown-item:focus {
|
.night-mode .navbar-nav .dropdown-item:focus {
|
||||||
box-shadow: inset 5px 0 0 #350a0f !important;
|
box-shadow: inset 5px 0 0 #ff7585 !important;
|
||||||
background-color: #383838 !important;
|
background-color: #383838 !important;
|
||||||
color: #c1c1c1 !important;
|
color: #c1c1c1 !important;
|
||||||
}
|
}
|
||||||
|
|
@ -266,14 +289,32 @@ body.night-mode {
|
||||||
background-color: #4c4e50;
|
background-color: #4c4e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.night-mode .warning-message {
|
||||||
|
background-color: #7c5e10;
|
||||||
|
color: #d1d1d1;
|
||||||
|
}
|
||||||
|
|
||||||
.night-mode .secondary-message {
|
.night-mode .secondary-message {
|
||||||
border-top-color: #4c4e50;
|
background-color: #4e575f;
|
||||||
|
color: #d1d1d1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.night-mode .error-message {
|
||||||
|
background-color: #780a0a;
|
||||||
|
color: #d1d1d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.night-mode .normal-message {
|
.night-mode .normal-message {
|
||||||
border-top-color: #07373f;
|
background-color: #2d3a8c;
|
||||||
|
color: #d1d1d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.night-mode .text-muted {
|
.night-mode .text-muted {
|
||||||
color: #8f9ba5 !important;
|
color: #8f9ba5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.night-mode .qr-code {
|
||||||
|
background-color: #eee;
|
||||||
|
padding: min(1.5vw, 20px);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,7 @@ function QrCodeImgHtml(text)
|
||||||
includetext: false
|
includetext: false
|
||||||
});
|
});
|
||||||
img.src = dummyCanvas.toDataURL("image/png");
|
img.src = dummyCanvas.toDataURL("image/png");
|
||||||
|
img.classList.add("qr-code");
|
||||||
|
|
||||||
return img.outerHTML;
|
return img.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
@ -32,7 +36,7 @@ Grocy.Components.BarcodeScanner.CheckCapabilities = async function()
|
||||||
Grocy.Components.BarcodeScanner.TorchOn(track);
|
Grocy.Components.BarcodeScanner.TorchOn(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduce the height of the video, if it's heigher than then the viewport
|
// Reduce the height of the video, if it's higher than then the viewport
|
||||||
if (!Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted)
|
if (!Grocy.Components.BarcodeScanner.LiveVideoSizeAdjusted)
|
||||||
{
|
{
|
||||||
var bc = document.getElementById('barcodescanner-container');
|
var bc = document.getElementById('barcodescanner-container');
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
$(".numberpicker-down-button").unbind('click').on("click", function()
|
$(".numberpicker-down-button").unbind('click').on("click", function()
|
||||||
{
|
{
|
||||||
var inputElement = $(this).parent().parent().find('input[type="number"]');
|
var inputElement = $(this).parent().parent().find('input[type="number"]');
|
||||||
inputElement.val(parseFloat(inputElement.val()) - 1);
|
inputElement.val(parseFloat(inputElement.val() || 1) - 1);
|
||||||
inputElement.trigger('keyup');
|
inputElement.trigger('keyup');
|
||||||
inputElement.trigger('change');
|
inputElement.trigger('change');
|
||||||
});
|
});
|
||||||
|
|
@ -9,7 +9,7 @@ $(".numberpicker-down-button").unbind('click').on("click", function()
|
||||||
$(".numberpicker-up-button").unbind('click').on("click", function()
|
$(".numberpicker-up-button").unbind('click').on("click", function()
|
||||||
{
|
{
|
||||||
var inputElement = $(this).parent().parent().find('input[type="number"]');
|
var inputElement = $(this).parent().parent().find('input[type="number"]');
|
||||||
inputElement.val(parseFloat(inputElement.val()) + 1);
|
inputElement.val(parseFloat(inputElement.val() || 0) + 1);
|
||||||
inputElement.trigger('keyup');
|
inputElement.trigger('keyup');
|
||||||
inputElement.trigger('change');
|
inputElement.trigger('change');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,12 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||||
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id);
|
Grocy.Components.ProductAmountPicker.SetQuantityUnit(productDetails.default_quantity_unit_purchase.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($("#display_amount").val().toString().isEmpty())
|
||||||
|
{
|
||||||
|
$("#display_amount").val(1);
|
||||||
|
$("#display_amount").trigger("change");
|
||||||
|
}
|
||||||
|
|
||||||
$('#display_amount').focus();
|
$('#display_amount').focus();
|
||||||
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
|
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
|
||||||
Grocy.ShoppingListItemFormInitialLoadDone = true;
|
Grocy.ShoppingListItemFormInitialLoadDone = true;
|
||||||
|
|
@ -244,8 +250,8 @@ if (GetUriParam("embedded") !== undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
var eitherRequiredFields = $("#product_id,#product_id_text_input,#note");
|
var eitherRequiredFields = $("#product_id,#product_id_text_input,#note");
|
||||||
eitherRequiredFields.prop('required',"");
|
eitherRequiredFields.prop('required', "");
|
||||||
eitherRequiredFields.on('input', function ()
|
eitherRequiredFields.on('input', function()
|
||||||
{
|
{
|
||||||
eitherRequiredFields.not(this).prop('required', !$(this).val().length);
|
eitherRequiredFields.not(this).prop('required', !$(this).val().length);
|
||||||
Grocy.FrontendHelpers.ValidateForm('shoppinglist-form');
|
Grocy.FrontendHelpers.ValidateForm('shoppinglist-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();
|
||||||
|
|
@ -214,12 +229,6 @@ function RefreshStatistics()
|
||||||
Grocy.Api.Get('stock',
|
Grocy.Api.Get('stock',
|
||||||
function(result)
|
function(result)
|
||||||
{
|
{
|
||||||
var amountSum = 0;
|
|
||||||
result.forEach(element =>
|
|
||||||
{
|
|
||||||
amountSum += parseInt(element.amount);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
||||||
{
|
{
|
||||||
$("#info-current-stock").text(__n(result.length, '%s Product', '%s Products'));
|
$("#info-current-stock").text(__n(result.length, '%s Product', '%s Products'));
|
||||||
|
|
@ -229,7 +238,7 @@ function RefreshStatistics()
|
||||||
var valueSum = 0;
|
var valueSum = 0;
|
||||||
result.forEach(element =>
|
result.forEach(element =>
|
||||||
{
|
{
|
||||||
valueSum += parseInt(element.value);
|
valueSum += parseFloat(element.value);
|
||||||
});
|
});
|
||||||
$("#info-current-stock").text(__n(result.length, '%s Product', '%s Products') + ", " + __t('%s total value', valueSum.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency })));
|
$("#info-current-stock").text(__n(result.length, '%s Product', '%s Products') + ", " + __t('%s total value', valueSum.toLocaleString(undefined, { style: "currency", currency: Grocy.Currency })));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,12 +126,15 @@
|
||||||
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||||
{
|
{
|
||||||
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||||
if ($("#use_specific_stock_entry").is(":checked"))
|
if ($("#use_specific_stock_entry").is(":checked") && GetUriParam("stockId") == null)
|
||||||
{
|
{
|
||||||
$("#use_specific_stock_entry").click();
|
$("#use_specific_stock_entry").click();
|
||||||
}
|
}
|
||||||
$("#location_id_to").val("");
|
$("#location_id_to").val("");
|
||||||
$("#location_id_from").val("");
|
if (GetUriParam("stockId") == null)
|
||||||
|
{
|
||||||
|
$("#location_id_from").val("");
|
||||||
|
}
|
||||||
|
|
||||||
var productId = $(e.target).val();
|
var productId = $(e.target).val();
|
||||||
|
|
||||||
|
|
@ -185,6 +188,12 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
|
||||||
$("#location_id_from").trigger('change');
|
$("#location_id_from").trigger('change');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (GetUriParam("locationId") != null)
|
||||||
|
{
|
||||||
|
$("#location_id_from").val(GetUriParam("locationId"));
|
||||||
|
$("#location_id_from").trigger("change");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
function(xhr)
|
function(xhr)
|
||||||
{
|
{
|
||||||
|
|
@ -284,7 +293,7 @@ $("#location_id_from").on('change', function(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
$("#specific_stock_entry").find("option").remove().end().append("<option></option>");
|
||||||
if ($("#use_specific_stock_entry").is(":checked"))
|
if ($("#use_specific_stock_entry").is(":checked") && GetUriParam("stockId") == null)
|
||||||
{
|
{
|
||||||
$("#use_specific_stock_entry").click();
|
$("#use_specific_stock_entry").click();
|
||||||
}
|
}
|
||||||
|
|
@ -475,6 +484,7 @@ if (GetUriParam("embedded") !== undefined)
|
||||||
$("#location_id_from").trigger('change');
|
$("#location_id_from").trigger('change');
|
||||||
$("#use_specific_stock_entry").click();
|
$("#use_specific_stock_entry").click();
|
||||||
$("#use_specific_stock_entry").trigger('change');
|
$("#use_specific_stock_entry").trigger('change');
|
||||||
|
Grocy.Components.ProductPicker.GetPicker().trigger('change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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)');
|
||||||
|
|
@ -520,7 +549,6 @@ class StockService extends BaseService
|
||||||
{
|
{
|
||||||
$currentStock = $this->GetCurrentStock(false);
|
$currentStock = $this->GetCurrentStock(false);
|
||||||
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime("+$days days")), '<');
|
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'best_before_date', date('Y-m-d 23:59:59', strtotime("+$days days")), '<');
|
||||||
$currentStock = FindAllObjectsInArrayByPropertyValue($currentStock, 'due_type', 1);
|
|
||||||
|
|
||||||
if ($excludeOverdue)
|
if ($excludeOverdue)
|
||||||
{
|
{
|
||||||
|
|
@ -640,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)
|
||||||
|
|
@ -728,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)
|
||||||
{
|
{
|
||||||
|
|
@ -787,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);
|
||||||
|
|
@ -1041,8 +1076,8 @@ class StockService extends BaseService
|
||||||
|
|
||||||
if ($productDetails->product->enable_tare_weight_handling == 1)
|
if ($productDetails->product->enable_tare_weight_handling == 1)
|
||||||
{
|
{
|
||||||
// Hard fail for now, as we not yet support transfering tare weight enabled products
|
// Hard fail for now, as we not yet support transferring tare weight enabled products
|
||||||
throw new \Exception('Transfering tare weight enabled products is not yet possible');
|
throw new \Exception('Transferring tare weight enabled products is not yet possible');
|
||||||
if ($amount < floatval($productDetails->product->tare_weight))
|
if ($amount < floatval($productDetails->product->tare_weight))
|
||||||
{
|
{
|
||||||
throw new \Exception('The amount cannot be lower than the defined tare weight');
|
throw new \Exception('The amount cannot be lower than the defined tare weight');
|
||||||
|
|
@ -1056,7 +1091,7 @@ class StockService extends BaseService
|
||||||
|
|
||||||
if ($amount > $productStockAmountAtFromLocation)
|
if ($amount > $productStockAmountAtFromLocation)
|
||||||
{
|
{
|
||||||
throw new \Exception('Amount to be transfered cannot be > current stock amount at the source location');
|
throw new \Exception('Amount to be transferred cannot be > current stock amount at the source location');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($specificStockEntryId !== 'default')
|
if ($specificStockEntryId !== 'default')
|
||||||
|
|
@ -1187,7 +1222,7 @@ class StockService extends BaseService
|
||||||
'amount' => $restStockAmount
|
'amount' => $restStockAmount
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// The transfered amount gets into a new stock entry
|
// The transferred amount gets into a new stock entry
|
||||||
$stockEntryNew = $this->getDatabase()->stock()->createRow([
|
$stockEntryNew = $this->getDatabase()->stock()->createRow([
|
||||||
'product_id' => $stockEntry->product_id,
|
'product_id' => $stockEntry->product_id,
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -34,7 +34,8 @@
|
||||||
'min' => 0,
|
'min' => 0,
|
||||||
'isRequired' => false,
|
'isRequired' => false,
|
||||||
'additionalCssClasses' => 'userfield-input',
|
'additionalCssClasses' => 'userfield-input',
|
||||||
'additionalAttributes' => 'data-userfield-name="' . $userfield->name . '"'
|
'additionalAttributes' => 'data-userfield-name="' . $userfield->name . '"',
|
||||||
|
'value' => ''
|
||||||
))
|
))
|
||||||
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_DECIMAL_NUMBER)
|
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_DECIMAL_NUMBER)
|
||||||
@include('components.numberpicker', array(
|
@include('components.numberpicker', array(
|
||||||
|
|
@ -45,7 +46,8 @@
|
||||||
'decimals' => 4,
|
'decimals' => 4,
|
||||||
'isRequired' => false,
|
'isRequired' => false,
|
||||||
'additionalCssClasses' => 'userfield-input',
|
'additionalCssClasses' => 'userfield-input',
|
||||||
'additionalAttributes' => 'data-userfield-name="' . $userfield->name . '"'
|
'additionalAttributes' => 'data-userfield-name="' . $userfield->name . '"',
|
||||||
|
'value' => ''
|
||||||
))
|
))
|
||||||
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_DATE)
|
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_DATE)
|
||||||
@include('components.datetimepicker', array(
|
@include('components.datetimepicker', array(
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
'id' => 'purchased_date',
|
'id' => 'purchased_date',
|
||||||
'label' => 'Purchased date',
|
'label' => 'Purchased date',
|
||||||
'format' => 'YYYY-MM-DD',
|
'format' => 'YYYY-MM-DD',
|
||||||
'hint' => 'This will apply to added products',
|
'hint' => $__t('This will apply to added products'),
|
||||||
'initWithNow' => true,
|
'initWithNow' => true,
|
||||||
'limitEndToNow' => false,
|
'limitEndToNow' => false,
|
||||||
'limitStartToNow' => false,
|
'limitStartToNow' => false,
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
@include('components.datetimepicker', array(
|
@include('components.datetimepicker', array(
|
||||||
'id' => 'best_before_date',
|
'id' => 'best_before_date',
|
||||||
'label' => 'Due date',
|
'label' => 'Due date',
|
||||||
'hint' => 'This will apply to added products',
|
'hint' => $__t('This will apply to added products'),
|
||||||
'format' => 'YYYY-MM-DD',
|
'format' => 'YYYY-MM-DD',
|
||||||
'initWithNow' => false,
|
'initWithNow' => false,
|
||||||
'limitEndToNow' => false,
|
'limitEndToNow' => false,
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
@ -609,7 +689,7 @@
|
||||||
@endif
|
@endif
|
||||||
</td>
|
</td>
|
||||||
<td class="font-italic">
|
<td class="font-italic">
|
||||||
{!! $__t('This means 1 %1$s is the same as %2$s %3$s', FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->from_qu_id)->name, '<span class="locale-number locale-number-quantity-amount">' . $quConversion->factor . '</span>', FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->to_qu_id)->name) !!}
|
{!! $__t('This means 1 %1$s is the same as %2$s %3$s', FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->from_qu_id)->name, '<span class="locale-number locale-number-quantity-amount">' . $quConversion->factor . '</span>', $__n($quConversion->factor, FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->to_qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $quConversion->to_qu_id)->name_plural)) !!}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -311,16 +311,20 @@
|
||||||
<div class="row ml-1">
|
<div class="row ml-1">
|
||||||
@if(!empty($calories) && intval($calories) > 0)
|
@if(!empty($calories) && intval($calories) > 0)
|
||||||
<div class="col-6 col-xl-3">
|
<div class="col-6 col-xl-3">
|
||||||
<label>{{ $__t('Energy (kcal)') }}</label>
|
<label>{{ $__t('Energy (kcal)') }}</label>
|
||||||
|
<i class="fas fa-question-circle text-muted d-print-none"
|
||||||
|
data-toggle="tooltip"
|
||||||
|
title="{{ $__t('per serving') }}"></i>
|
||||||
<h3 class="locale-number locale-number-generic pt-0">{{ $calories }}</h3>
|
<h3 class="locale-number locale-number-generic pt-0">{{ $calories }}</h3>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<label>{{ $__t('Costs') }} </label>
|
<label>{{ $__t('Costs') }}
|
||||||
<i class="fas fa-question-circle text-muted d-print-none"
|
<i class="fas fa-question-circle text-muted d-print-none"
|
||||||
data-toggle="tooltip"
|
data-toggle="tooltip"
|
||||||
title="{{ $__t('Based on the prices of the default consume rule which is "Opened first, then first due first, then first in first out"') }}"></i>
|
title="{{ $__t('Based on the prices of the default consume rule which is "Opened first, then first due first, then first in first out"') }}"></i>
|
||||||
|
</label>
|
||||||
<h3 class="locale-number locale-number-currency pt-0">{{ $costs }}</h3>
|
<h3 class="locale-number locale-number-currency pt-0">{{ $costs }}</h3>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -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…
Reference in New Issue
Block a user