Squashed commit

Improve journal pages loading time (new date range filter)
Various small style adjustments (meal plan page and others)
Pulled German translations from Transifex
Show the shopping list total value (closes #1309)
Make it possible to copy recipes (closes #714)
Implemented optional "auto decimal separator for price inputs" (closes #1345)
Removed table grouped column fixed order restriction (closes #1402)
Don't filter out style, class, id attributes of html text (closes #1298)
Added product picture as column on the stock overview page (closes #1283)
Added grocycodes also for chores and batteries (+ camera barcode scanning for /choretracking and /batterytracking, this now closes #221)
This commit is contained in:
Bernd Bestel
2021-07-13 19:29:23 +02:00
parent 8d2c3ae584
commit 91d8eaeb74
70 changed files with 1476 additions and 491 deletions

View File

@@ -1,27 +1,27 @@
> ⚠️ The following PHP extensions are now additionally required: `json`, `intl`, `zlib` > ⚠️ The following PHP extensions are now additionally required: `json`, `intl`, `zlib`
> ⚠️ PHP 8 is now supported and from now on the only tested runtime version (but as of now, PHP 7.2 should still work). > ⚠️ PHP 8 is now supported and from now on the only tested runtime version (although currently PHP 7.2 should still work).
### New feature: (Own) Product and stock entry labels/barcodes ("grocycode") ### New feature: (Own) Product/stock entry/chores/batteries 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 - Print own labels/barcodes for products/stock entries/chores/batteries and then scan that code on every place a product/stock entry/chore/battery can be selected
- Can be printed (or downloaded) via - Can be printed (or downloaded) via
- The product edit page - The product/chore/battery edit page
- The context/more menu per line on the stock overview and stock entries page - The context/more menu per line on the overview pages and for stock entries on the stock entries page
- Automatically on purchase (new option on the purchase page, defaults can be configured per product) - Automatically on purchase (new option on the purchase page, defaults can be configured per product) for stock entries
- The used barcode type can be configured via the `config.php` option `GROCYCODE_TYPE`: - The used barcode type can be configured via the `config.php` option `GROCYCODE_TYPE`:
- `1D` (default) will produce a `Code128` 1D barcode (supported by the integrated camera barcode scanner) - `1D` (default) will produce a `Code128` 1D barcode (supported by the integrated camera barcode scanner)
- `2D` will produce a `DataMatrix` 2D barcode (currently not supported by the integrated camera barcode scanner, but can be probably printed smaller) - `2D` will produce a `DataMatrix` 2D barcode (currently not supported by the integrated camera barcode scanner, but can be probably printed smaller)
- Label printer functionality can be enabled via the new feature flag `FEATURE_FLAG_LABELPRINTER` (defaults to disabled) - Label printer functionality can be enabled via the new feature flag `FEATURE_FLAG_LABEL_PRINTER` (defaults to disabled)
- Label printer communication happens via WebHooks - see the new `LABEL_PRINTER*` `config.php` options - 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 - grocycodes can also be used without a label printer - you can view or download thm as pictures and print them manually
- More information: - More information:
- https://github.com/grocy/grocy/blob/master/docs/grocycode.md - https://github.com/grocy/grocy/blob/master/docs/grocycode.md
- https://github.com/grocy/grocy/blob/master/docs/label-printing.md - https://github.com/grocy/grocy/blob/master/docs/label-printing.md
- (Thanks a lot @mistressofjellyfish) - (Thanks a lot @mistressofjellyfish for the initial work on this)
### New feature: Shopping list thermal printer support ### New feature: Shopping list thermal printer support
- The shopping list can now be printed on a thermal printer - The shopping list can now be printed on a thermal printer
- The printer must compatible to the `ESC/POS` protocol and needs to be locally attached or network reachable to/by the machine hosting grocy (so the server) - The printer must be compatible to the `ESC/POS` protocol and needs to be locally attached or network reachable to/by the machine hosting grocy (so the server)
- See the new `TPRINTER*` `config.php` options to configure the printer connection and other options - See the new `TPRINTER*` `config.php` options to configure the printer connection and other options
- => New button on the shopping list print dialog - => New button on the shopping list print dialog
- Can be enabled via the new feature flag `FEATURE_FLAG_THERMAL_PRINTER` (defaults to disabled) - Can be enabled via the new feature flag `FEATURE_FLAG_THERMAL_PRINTER` (defaults to disabled)
@@ -31,11 +31,13 @@
- Product barcodes are now enforced to be unique across products - Product barcodes are now enforced to be unique across products
- On the stock overview page it's now also possible to search/filter by product barcodes (via the general search field) - On the stock overview page it's now also possible to search/filter by product barcodes (via the general search field)
- The product picker on the consume and transfer page now only shows products which are currently in stock - The product picker on the consume and transfer page now only shows products which are currently in stock
- Added a filter option to only show in-stock products on the stock overview and products list page (master data) - Added a filter option to only show in-stock products on the stock overview and products list (master data) page
- Added new columns on the stock overview page (hidden by default): Product description, product default location, parent product - Added new columns on the stock overview page (hidden by default): Product description, product default location, parent product, product picture
- Added a new product option "Should not be frozen" (defaults to disabled and only visible when `FEATURE_FLAG_STOCK_PRODUCT_FREEZING` is enabled) - Added a new product option "Should not be frozen" (defaults to disabled and only visible when `FEATURE_FLAG_STOCK_PRODUCT_FREEZING` is enabled)
- When enabled, on moving the product to a freezer location (so when freezing it), a corresponding warning will be shown - When enabled, on moving the product to a freezer location (so when freezing it), a corresponding warning will be shown
- Optimized that when opening a product which has "Default due days after opened" set, the resulting date now never extends the original due date - Optimized that when opening a product which has "Default due days after opened" set, the resulting date now never extends the original due date
- Added a new stock setting (top right corner settings menu) "Add decimal separator automatically for price inputs" (defaults to disabled)
- When enabled, you always have to enter the value including decimal places, the decimal separator will be automatically added based on the amount of allowed decimal places
- Fixed that editing stock entries was not possible - Fixed that editing stock entries was not possible
- Fixed that consuming with Scan Mode was not possible - Fixed that consuming with Scan Mode was not possible
- Fixed that the current stock total value (header of the stock overview page) didn't include decimal amounts (thanks @Ape) - Fixed that the current stock total value (header of the stock overview page) didn't include decimal amounts (thanks @Ape)
@@ -50,6 +52,7 @@
### Shopping list improvements/fixes ### Shopping list improvements/fixes
- The amount now defaults to `1` for adding items quicker - The amount now defaults to `1` for adding items quicker
- Added a status filter for only _done_ items - Added a status filter for only _done_ items
- The total value is now also shown (based on "Last price (Total)" per item, displayed on the page header and only when `FEATURE_FLAG_STOCK_PRICE_TRACKING` is enabled)
- Fixed that shopping list prints had a grey background (thanks @Forceu) - Fixed that shopping list prints had a grey background (thanks @Forceu)
- Fixed the form validation on the shopping list item page (thanks @Forceu) - Fixed the form validation on the shopping list item page (thanks @Forceu)
- Fixed that when adding products to the shopping list from the stock overview page, the used quantity unit was always the products default purchase QU (and not the selected one) - Fixed that when adding products to the shopping list from the stock overview page, the used quantity unit was always the products default purchase QU (and not the selected one)
@@ -60,6 +63,7 @@
- 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) - Calories are now always displayed per single serving (on the recipe and meal plan page)
- The note of an ingredient will now also be added to the corresponding shopping list item when using "Put missing products on the shopping list" - The note of an ingredient will now also be added to the corresponding shopping list item when using "Put missing products on the shopping list"
- It's now possible to copy a recipe (button/dropdown menu item per recipe)
- Fixed that the recipe page was slow when there were a lot meal plan recipe entries - Fixed that the recipe page was slow when there were a lot meal plan recipe entries
- Fixed that "Only check if any amount is in stock" (recipe ingredient option) didn't work for stock amounts < 1 - Fixed that "Only check if any amount is in stock" (recipe ingredient option) didn't work for stock amounts < 1
- Fixed that when adding missing items to the shopping list, on the popup deselected items got also added - Fixed that when adding missing items to the shopping list, on the popup deselected items got also added
@@ -72,6 +76,7 @@
- Meal plan entries can now be visually marked as "done" (new button per entry) - Meal plan entries can now be visually marked as "done" (new button per entry)
- This happens automatically on consuming a recipe/product from the meal plan page - This happens automatically on consuming a recipe/product from the meal plan page
- It's now possible to copy all entries of a day to another day (in the dropdown of the add button in the header of each day column) - It's now possible to copy all entries of a day to another day (in the dropdown of the add button in the header of each day column)
- The "Display recipe" button was removed, instead clicking the recipe title now displays the recipe (and this now also works for products; shows the product card)
- Fixed that stock fulfillment checking used the desired servings of the recipe (those selected on the recipes page, not them from the meal plan entry) - Fixed that stock fulfillment checking used the desired servings of the recipe (those selected on the recipes page, not them from the meal plan entry)
### Chores improvements/fixes ### Chores improvements/fixes
@@ -92,6 +97,7 @@
- The username attribute is now configurable - The username attribute is now configurable
- Filtering of accounts is now possible - Filtering of accounts is now possible
- => See the new `LDAP*` `config.php` options - => See the new `LDAP*` `config.php` options
- Improved the page loading time of all journal pages (stock/chores/batteries) by adding a new date range filter
- Some night mode style improvements (thanks @BlizzWave and @KTibow) - Some night mode style improvements (thanks @BlizzWave and @KTibow)
- Help tooltips are now additionally also triggered by clicking on them (instead of only hovering them, which doesn't work on mobile / touch devices) - Help tooltips are now additionally also triggered by clicking on them (instead of only hovering them, which doesn't work on mobile / touch devices)
- The camera barcode scanner now also supports Code 39 barcodes (used for example in Germany on pharma products (PZN)) (thanks @andreheuer) - The camera barcode scanner now also supports Code 39 barcodes (used for example in Germany on pharma products (PZN)) (thanks @andreheuer)
@@ -101,8 +107,9 @@
### API improvements/fixes ### API improvements/fixes
> ❗ Numbers are now returned as numbers (so technically without quotes around them, were strings for nearly all endpoints before - should practically be no real difference) > ❗ Numbers are now returned as numbers (so technically without quotes around them, were strings for nearly all endpoints before - should practically be no real difference)
- Added a new API endpoint `/system/localization-strings` to get the localization strings (gettext JSON representation; in the by the user desired language) - Added a new endpoint `/system/localization-strings` to get the localization strings (gettext JSON representation; in the by the user desired language)
- The `GET /chores` endpoint now also returns the `next_execution_assigned_user` per chore (like the endpoint `GET /chores/{choreId}` already did for a single chore) - Added a new endpoint `/recipes/{recipeId}/copy` to copy a recipe
- The `GET /chores` endpoint now also returns the `next_execution_assigned_user` object per chore (like the endpoint `GET /chores/{choreId}` already did for a single chore)
- The `GET /tasks` endpoint now also returns the assigned user and category object per task - The `GET /tasks` endpoint now also returns the assigned user and category object per task
- Empty Userfields are now also returned (were previously omitted, endpoint `GET /objects/{entity}` and `GET /objects/{entity}/{objectId}`) - Empty Userfields are now also returned (were previously omitted, endpoint `GET /objects/{entity}` and `GET /objects/{entity}/{objectId}`)
- Fixed that due soon products with `due_type` = "Expiration date" were missing in `due_products` of the `/stock/volatile` endpoint - Fixed that due soon products with `due_type` = "Expiration date" were missing in `due_products` of the `/stock/volatile` endpoint

View File

@@ -148,6 +148,7 @@ DefaultUserSetting('product_presets_product_group_id', -1); // Default product g
DefaultUserSetting('product_presets_qu_id', -1); // Default quantity unit id for new products (-1 means no quantity unit is preset) DefaultUserSetting('product_presets_qu_id', -1); // Default quantity unit id for new products (-1 means no quantity unit is preset)
DefaultUserSetting('stock_decimal_places_amounts', 4); // Default decimal places allowed for amounts DefaultUserSetting('stock_decimal_places_amounts', 4); // Default decimal places allowed for amounts
DefaultUserSetting('stock_decimal_places_prices', 2); // Default decimal places allowed for prices DefaultUserSetting('stock_decimal_places_prices', 2); // Default decimal places allowed for prices
DefaultUserSetting('stock_auto_decimal_separator_prices', false);
DefaultUserSetting('stock_due_soon_days', 5); DefaultUserSetting('stock_due_soon_days', 5);
DefaultUserSetting('stock_default_purchase_amount', 0); DefaultUserSetting('stock_default_purchase_amount', 0);
DefaultUserSetting('stock_default_consume_amount', 1); DefaultUserSetting('stock_default_consume_amount', 1);
@@ -204,7 +205,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); Setting('FEATURE_FLAG_LABEL_PRINTER', false);
// Sub feature flags // Sub feature flags
Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true); Setting('FEATURE_FLAG_STOCK_PRICE_TRACKING', true);

View File

@@ -210,7 +210,8 @@ class BaseController
{ {
$htmlPurifierConfig = \HTMLPurifier_Config::createDefault(); $htmlPurifierConfig = \HTMLPurifier_Config::createDefault();
$htmlPurifierConfig->set('Cache.SerializerPath', GROCY_DATAPATH . '/viewcache'); $htmlPurifierConfig->set('Cache.SerializerPath', GROCY_DATAPATH . '/viewcache');
$htmlPurifierConfig->set('HTML.Allowed', 'div,b,strong,i,em,u,a[href|title|target],iframe[src|width|height|frameborder],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|target],iframe[src|width|height|frameborder],ul,ol,li,p[style],br,span[style],img[width|height|alt|src],table[border|width|style],tbody,tr,td,th,blockquote,*[style|class|id]');
$htmlPurifierConfig->set('Attr.EnableID', true);
$htmlPurifierConfig->set('HTML.SafeIframe', true); $htmlPurifierConfig->set('HTML.SafeIframe', true);
$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, 'http' => true, 'https' => true]); $htmlPurifierConfig->set('URI.AllowedSchemes', ['data' => true, 'http' => true, 'https' => true]);

View File

@@ -3,6 +3,8 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User; use Grocy\Controllers\Users\User;
use Grocy\Helpers\WebhookRunner;
use Grocy\Helpers\Grocycode;
class BatteriesApiController extends BaseApiController class BatteriesApiController extends BaseApiController
{ {
@@ -62,6 +64,30 @@ class BatteriesApiController extends BaseApiController
} }
} }
public function BatteryPrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
try
{
$battery = $this->getDatabase()->batteries()->where('id', $args['batteryId'])->fetch();
$webhookData = array_merge([
'battery' => $battery->name,
'grocycode' => (string)(new Grocycode(Grocycode::BATTERY, $args['batteryId'])),
], 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);
}
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage());
}
}
public function __construct(\DI\Container $container) public function __construct(\DI\Container $container)
{ {
parent::__construct($container); parent::__construct($container);

View File

@@ -2,6 +2,10 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Helpers\Grocycode;
use jucksearm\barcode\lib\BarcodeFactory;
use jucksearm\barcode\lib\DatamatrixFactory;
class BatteriesController extends BaseController class BatteriesController extends BaseController
{ {
public function BatteriesList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function BatteriesList(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
@@ -48,8 +52,25 @@ class BatteriesController extends BaseController
public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if (isset($request->getQueryParams()['months']) && filter_var($request->getQueryParams()['months'], FILTER_VALIDATE_INT) !== false)
{
$months = $request->getQueryParams()['months'];
$where = "tracked_time > DATE(DATE('now', 'localtime'), '-$months months')";
}
else
{
// Default 2 years
$where = "tracked_time > DATE(DATE('now', 'localtime'), '-24 months')";
}
if (isset($request->getQueryParams()['battery']) && filter_var($request->getQueryParams()['battery'], FILTER_VALIDATE_INT) !== false)
{
$batteryId = $request->getQueryParams()['battery'];
$where .= " AND battery_id = $batteryId";
}
return $this->renderPage($response, 'batteriesjournal', [ return $this->renderPage($response, 'batteriesjournal', [
'chargeCycles' => $this->getDatabase()->battery_charge_cycles()->orderBy('tracked_time', 'DESC'), 'chargeCycles' => $this->getDatabase()->battery_charge_cycles()->where($where)->orderBy('tracked_time', 'DESC'),
'batteries' => $this->getDatabase()->batteries()->where('active = 1')->orderBy('name', 'COLLATE NOCASE') 'batteries' => $this->getDatabase()->batteries()->where('active = 1')->orderBy('name', 'COLLATE NOCASE')
]); ]);
} }
@@ -75,6 +96,40 @@ class BatteriesController extends BaseController
]); ]);
} }
public function BatteryGrocycodeImage(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$size = $request->getQueryParam('size', null);
$gc = new Grocycode(Grocycode::BATTERY, $args['batteryId']);
if (GROCY_GROCYCODE_TYPE == '2D')
{
$png = (new DatamatrixFactory())->setCode((string) $gc)->setSize($size)->getDatamatrixPngData();
}
else
{
$png = (new BarcodeFactory())->setType('C128')->setCode((string) $gc)->setHeight($size)->getBarcodePngData();
}
$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 __construct(\DI\Container $container) public function __construct(\DI\Container $container)
{ {
parent::__construct($container); parent::__construct($container);

View File

@@ -3,6 +3,8 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Controllers\Users\User; use Grocy\Controllers\Users\User;
use Grocy\Helpers\WebhookRunner;
use Grocy\Helpers\Grocycode;
class ChoresApiController extends BaseApiController class ChoresApiController extends BaseApiController
{ {
@@ -108,6 +110,30 @@ class ChoresApiController extends BaseApiController
} }
} }
public function ChorePrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
try
{
$chore = $this->getDatabase()->chores()->where('id', $args['choreId'])->fetch();
$webhookData = array_merge([
'chore' => $chore->name,
'grocycode' => (string)(new Grocycode(Grocycode::CHORE, $args['choreId'])),
], 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);
}
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage());
}
}
public function __construct(\DI\Container $container) public function __construct(\DI\Container $container)
{ {
parent::__construct($container); parent::__construct($container);

View File

@@ -2,6 +2,10 @@
namespace Grocy\Controllers; namespace Grocy\Controllers;
use Grocy\Helpers\Grocycode;
use jucksearm\barcode\lib\BarcodeFactory;
use jucksearm\barcode\lib\DatamatrixFactory;
class ChoresController extends BaseController class ChoresController extends BaseController
{ {
public function ChoreEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ChoreEditForm(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
@@ -59,8 +63,25 @@ class ChoresController extends BaseController
public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if (isset($request->getQueryParams()['months']) && filter_var($request->getQueryParams()['months'], FILTER_VALIDATE_INT) !== false)
{
$months = $request->getQueryParams()['months'];
$where = "tracked_time > DATE(DATE('now', 'localtime'), '-$months months')";
}
else
{
// Default 1 year
$where = "tracked_time > DATE(DATE('now', 'localtime'), '-12 months')";
}
if (isset($request->getQueryParams()['chore']) && filter_var($request->getQueryParams()['chore'], FILTER_VALIDATE_INT) !== false)
{
$choreId = $request->getQueryParams()['chore'];
$where .= " AND chore_id = $choreId";
}
return $this->renderPage($response, 'choresjournal', [ return $this->renderPage($response, 'choresjournal', [
'choresLog' => $this->getDatabase()->chores_log()->orderBy('tracked_time', 'DESC'), 'choresLog' => $this->getDatabase()->chores_log()->where($where)->orderBy('tracked_time', 'DESC'),
'chores' => $this->getDatabase()->chores()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'), 'chores' => $this->getDatabase()->chores()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'users' => $this->getDatabase()->users()->orderBy('username'), 'users' => $this->getDatabase()->users()->orderBy('username'),
'userfields' => $this->getUserfieldsService()->GetFields('chores_log'), 'userfields' => $this->getUserfieldsService()->GetFields('chores_log'),
@@ -92,6 +113,40 @@ class ChoresController extends BaseController
]); ]);
} }
public function ChoreGrocycodeImage(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
$size = $request->getQueryParam('size', null);
$gc = new Grocycode(Grocycode::CHORE, $args['choreId']);
if (GROCY_GROCYCODE_TYPE == '2D')
{
$png = (new DatamatrixFactory())->setCode((string) $gc)->setSize($size)->getDatamatrixPngData();
}
else
{
$png = (new BarcodeFactory())->setType('C128')->setCode((string) $gc)->setHeight($size)->getBarcodePngData();
}
$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 __construct(\DI\Container $container) public function __construct(\DI\Container $container)
{ {
parent::__construct($container); parent::__construct($container);

View File

@@ -63,6 +63,20 @@ class RecipesApiController extends BaseApiController
} }
} }
public function CopyRecipe(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{
try
{
return $this->ApiResponse($response, [
'created_object_id' => $this->getRecipesService()->CopyRecipe($args['recipeId'])
]);
}
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage());
}
}
public function __construct(\DI\Container $container) public function __construct(\DI\Container $container)
{ {
parent::__construct($container); parent::__construct($container);

View File

@@ -620,42 +620,56 @@ class StockApiController extends BaseApiController
public function ProductPrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ProductPrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
$product = $this->getDatabase()->products()->where('id', $args['productId'])->fetch(); try
$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); $product = $this->getDatabase()->products()->where('id', $args['productId'])->fetch();
}
return $this->ApiResponse($response, $webhookData); $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);
}
catch (\Exception $ex)
{
return $this->GenericErrorResponse($response, $ex->getMessage());
}
} }
public function StockEntryPrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function StockEntryPrintLabel(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
$stockEntry = $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch(); try
$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; $stockEntry = $this->getDatabase()->stock()->where('id', $args['entryId'])->fetch();
} $product = $this->getDatabase()->products()->where('id', $stockEntry->product_id)->fetch();
if (GROCY_LABEL_PRINTER_RUN_SERVER) $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['due_date'] = $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);
}
catch (\Exception $ex)
{ {
(new WebhookRunner())->run(GROCY_LABEL_PRINTER_WEBHOOK, $webhookData, GROCY_LABEL_PRINTER_HOOK_JSON); return $this->GenericErrorResponse($response, $ex->getMessage());
} }
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)

View File

@@ -35,10 +35,27 @@ class StockController extends BaseController
public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function Journal(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
if (isset($request->getQueryParams()['months']) && filter_var($request->getQueryParams()['months'], FILTER_VALIDATE_INT) !== false)
{
$months = $request->getQueryParams()['months'];
$where = "row_created_timestamp > DATE(DATE('now', 'localtime'), '-$months months')";
}
else
{
// Default 6 months
$where = "row_created_timestamp > DATE(DATE('now', 'localtime'), '-6 months')";
}
if (isset($request->getQueryParams()['product']) && filter_var($request->getQueryParams()['product'], FILTER_VALIDATE_INT) !== false)
{
$productId = $request->getQueryParams()['product'];
$where .= " AND product_id = $productId";
}
$usersService = $this->getUsersService(); $usersService = $this->getUsersService();
return $this->renderPage($response, 'stockjournal', [ return $this->renderPage($response, 'stockjournal', [
'stockLog' => $this->getDatabase()->uihelper_stock_journal()->orderBy('row_created_timestamp', 'DESC'), 'stockLog' => $this->getDatabase()->uihelper_stock_journal()->where($where)->orderBy('row_created_timestamp', 'DESC'),
'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(),
@@ -176,9 +193,7 @@ class StockController extends BaseController
public function ProductGrocycodeImage(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args) public function ProductGrocycodeImage(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
{ {
$size = $request->getQueryParam('size', null); $size = $request->getQueryParam('size', null);
$product = $this->getDatabase()->products($args['productId']); $gc = new Grocycode(Grocycode::PRODUCT, $args['productId']);
$gc = new Grocycode(Grocycode::PRODUCT, $product->id);
if (GROCY_GROCYCODE_TYPE == '2D') if (GROCY_GROCYCODE_TYPE == '2D')
{ {
@@ -190,7 +205,6 @@ class StockController extends BaseController
} }
$isDownload = $request->getQueryParam('download', false); $isDownload = $request->getQueryParam('download', false);
if ($isDownload) if ($isDownload)
{ {
$response = $response->withHeader('Content-Type', 'application/octet-stream') $response = $response->withHeader('Content-Type', 'application/octet-stream')
@@ -489,7 +503,6 @@ class StockController extends BaseController
} }
$isDownload = $request->getQueryParam('download', false); $isDownload = $request->getQueryParam('download', false);
if ($isDownload) if ($isDownload)
{ {
$response = $response->withHeader('Content-Type', 'application/octet-stream') $response = $response->withHeader('Content-Type', 'application/octet-stream')

View File

@@ -1,7 +1,7 @@
Label printing 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. To enable label printing, set `FEATURE_FLAG_LABEL_PRINTER` to `true`in your `config.php`. You also need to provide a webhook target that is responsible for printing.
Why webhook? Why webhook?
--- ---
@@ -28,7 +28,7 @@ Both methods fire this request upon printing:
``` ```
POST /your/printing/api/endpoint HTTP/1.1 POST /your/printing/api/endpoint HTTP/1.1
product=<productname>&grocycode=grocy:x:xxx&duedate=DD:%2021-06-09&... product=<productname>&grocycode=grocy:x:xxx&due_date=DD:%2021-06-09&...
``` ```
@@ -37,4 +37,4 @@ If specified, the request body may also be JSON encoded, however the fields stay
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 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. if the webhook is configured to run client-side.
The webhook receiver is required to layout and print the resulting label. The webhook receiver is required to layout and print the resulting label.

View File

@@ -1555,7 +1555,7 @@
}, },
"/stock/entry/{entryId}/printlabel": { "/stock/entry/{entryId}/printlabel": {
"get": { "get": {
"summary": "Prints the label of the given stock entry", "summary": "Prints the grocycode / stock entry label of the given entry on the configured label printer",
"tags": [ "tags": [
"Stock" "Stock"
], ],
@@ -2285,7 +2285,7 @@
}, },
"/stock/products/{productId}/printlabel": { "/stock/products/{productId}/printlabel": {
"get": { "get": {
"summary": "Prints the product label of the given product", "summary": "Prints the grocycode label of the given product on the configured label printer",
"tags": [ "tags": [
"Stock" "Stock"
], ],
@@ -3462,6 +3462,54 @@
} }
} }
}, },
"/recipes/{recipeId}/copy": {
"post": {
"summary": "Copies a recipe",
"tags": [
"Recipes"
],
"parameters": [
{
"in": "path",
"name": "recipeId",
"required": true,
"description": "A valid recipe id of the recipe to copy",
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "The operation was successful",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"created_object_id": {
"type": "number",
"format": "integer",
"description": "The id of the created recipe"
}
}
}
}
}
},
"400": {
"description": "The operation was not successful (possible errors are: Invalid recipe id)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error400"
}
}
}
}
}
}
},
"/chores": { "/chores": {
"get": { "get": {
"summary": "Returns all chores incl. the next estimated execution time per chore", "summary": "Returns all chores incl. the next estimated execution time per chore",
@@ -3688,6 +3736,48 @@
} }
} }
}, },
"/chores/{choreId}/printlabel": {
"get": {
"summary": "Prints the grocycode label of the given chore on the configured label printer",
"tags": [
"Chores"
],
"parameters": [
{
"in": "path",
"name": "choreId",
"required": true,
"description": "A valid chore 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 chore, error on WebHook execution)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error400"
}
}
}
}
}
}
},
"/batteries": { "/batteries": {
"get": { "get": {
"summary": "Returns all batteries incl. the next estimated charge time per battery", "summary": "Returns all batteries incl. the next estimated charge time per battery",
@@ -3868,6 +3958,48 @@
} }
} }
}, },
"/batteries/{batteryId}/printlabel": {
"get": {
"summary": "Prints the grocycode label of the given battery on the configured label printer",
"tags": [
"Batteries"
],
"parameters": [
{
"in": "path",
"name": "batteryId",
"required": true,
"description": "A valid battery 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 battery, error on WebHook execution)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error400"
}
}
}
}
}
}
},
"/tasks": { "/tasks": {
"get": { "get": {
"summary": "Returns all tasks which are not done yet", "summary": "Returns all tasks which are not done yet",

View File

@@ -1,7 +1,6 @@
# #
# Translators: # Translators:
# Bernd Bestel <bernd@berrnd.de>, 2019 # Bernd Bestel <bernd@berrnd.de>, 2021
# @RubenKelevra <ruben@freifunk-nrw.de>, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
@@ -9,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n"
"PO-Revision-Date: 2019-09-17 10:45+0000\n" "PO-Revision-Date: 2019-09-17 10:45+0000\n"
"Last-Translator: @RubenKelevra <ruben@freifunk-nrw.de>, 2021\n" "Last-Translator: Bernd Bestel <bernd@berrnd.de>, 2021\n"
"Language-Team: German (https://www.transifex.com/grocy/teams/93189/de/)\n" "Language-Team: German (https://www.transifex.com/grocy/teams/93189/de/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -19,7 +18,7 @@ msgstr ""
"X-Domain: grocy/chore_assignment_types\n" "X-Domain: grocy/chore_assignment_types\n"
msgid "no-assignment" msgid "no-assignment"
msgstr "keine Zuweisung" msgstr "Niemandem zuweisen"
msgid "who-least-did-first" msgid "who-least-did-first"
msgstr "Wer es am seltensten gemacht hat zuerst" msgstr "Wer es am seltensten gemacht hat zuerst"

View File

@@ -1,7 +1,6 @@
# #
# Translators: # Translators:
# Bernd Bestel <bernd@berrnd.de>, 2019 # Bernd Bestel <bernd@berrnd.de>, 2021
# @RubenKelevra <ruben@freifunk-nrw.de>, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
@@ -9,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n"
"PO-Revision-Date: 2019-05-01 17:42+0000\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n"
"Last-Translator: @RubenKelevra <ruben@freifunk-nrw.de>, 2021\n" "Last-Translator: Bernd Bestel <bernd@berrnd.de>, 2021\n"
"Language-Team: German (https://www.transifex.com/grocy/teams/93189/de/)\n" "Language-Team: German (https://www.transifex.com/grocy/teams/93189/de/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -22,7 +21,7 @@ msgid "manually"
msgstr "Manuell" msgstr "Manuell"
msgid "dynamic-regular" msgid "dynamic-regular"
msgstr "Dynamisch-Regelmäßig" msgstr "Dynamisch regelmäßig"
msgid "daily" msgid "daily"
msgstr "Täglich" msgstr "Täglich"

View File

@@ -1,7 +1,6 @@
# #
# Translators: # Translators:
# Bernd Bestel <bernd@berrnd.de>, 2020 # Bernd Bestel <bernd@berrnd.de>, 2021
# @RubenKelevra <ruben@freifunk-nrw.de>, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
@@ -9,7 +8,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n"
"PO-Revision-Date: 2019-05-01 17:42+0000\n" "PO-Revision-Date: 2019-05-01 17:42+0000\n"
"Last-Translator: @RubenKelevra <ruben@freifunk-nrw.de>, 2021\n" "Last-Translator: Bernd Bestel <bernd@berrnd.de>, 2021\n"
"Language-Team: German (https://www.transifex.com/grocy/teams/93189/de/)\n" "Language-Team: German (https://www.transifex.com/grocy/teams/93189/de/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -19,7 +18,7 @@ msgstr ""
"X-Domain: grocy/demo_data\n" "X-Domain: grocy/demo_data\n"
msgid "Cookies" msgid "Cookies"
msgstr "Kekse" msgstr "Cookies"
msgid "Chocolate" msgid "Chocolate"
msgstr "Schokolade" msgstr "Schokolade"
@@ -91,7 +90,7 @@ msgid "Cheese"
msgstr "Käse" msgstr "Käse"
msgid "Cold cuts" msgid "Cold cuts"
msgstr "Wurst-Aufschnitt" msgstr "Aufschnitt"
msgid "Paprika" msgid "Paprika"
msgstr "Paprika" msgstr "Paprika"
@@ -115,19 +114,19 @@ msgid "Warranty ends"
msgstr "Garantie endet" msgstr "Garantie endet"
msgid "TV remote control" msgid "TV remote control"
msgstr "TV-Fernbedienung" msgstr "TV Fernbedienung"
msgid "Alarm clock" msgid "Alarm clock"
msgstr "Wecker" msgstr "Wecker"
msgid "Heat remote control" msgid "Heat remote control"
msgstr "Fernbedienung der Heizung" msgstr "Fernbedienung Heizung"
msgid "Lawn mowed in the garden" msgid "Lawn mowed in the garden"
msgstr "Rasen im Garten gemäht" msgstr "Rasen im Garten gemäht"
msgid "Some good snacks" msgid "Some good snacks"
msgstr "Gutes Knabberzeug" msgstr "Paar gute Snacks"
msgid "Pizza dough" msgid "Pizza dough"
msgstr "Pizzateig" msgstr "Pizzateig"
@@ -163,10 +162,10 @@ msgid "Italian"
msgstr "Italienisch" msgstr "Italienisch"
msgid "This is the note content of the recipe ingredient" msgid "This is the note content of the recipe ingredient"
msgstr "Dies ist die Notiz zur Zutat" msgstr "Dies ist der Inhalt der Notiz der Zutat"
msgid "Demo User" msgid "Demo User"
msgstr "Demo-Benutzer" msgstr "Demo Benutzer"
msgid "Gram" msgid "Gram"
msgid_plural "Grams" msgid_plural "Grams"
@@ -198,14 +197,13 @@ msgid "Fork and improve grocy"
msgstr "grocy forken und verbessern" msgstr "grocy forken und verbessern"
msgid "Find a solution for what to do when I forget the door keys" msgid "Find a solution for what to do when I forget the door keys"
msgstr "" msgstr "Eine Lösung für \"Haustürschlüssel vergessen\" finden"
"Eine Lösung finden, was zu tun ist, wenn ich die Türschlüssel vergesse"
msgid "Sweets" msgid "Sweets"
msgstr "Süßigkeiten" msgstr "Süßigkeiten"
msgid "Bakery products" msgid "Bakery products"
msgstr "Bäckerei-Produkte" msgstr "Bäckerei Produkte"
msgid "Tinned food" msgid "Tinned food"
msgstr "Konservern" msgstr "Konservern"
@@ -214,7 +212,7 @@ msgid "Butchery products"
msgstr "Metzgerei" msgstr "Metzgerei"
msgid "Vegetables/Fruits" msgid "Vegetables/Fruits"
msgstr "Obst und Gemüse" msgstr "Obst/Gemüse"
msgid "Refrigerated products" msgid "Refrigerated products"
msgstr "Kühlregal" msgstr "Kühlregal"
@@ -294,7 +292,7 @@ msgstr[0] "Scheibe"
msgstr[1] "Scheiben" msgstr[1] "Scheiben"
msgid "Example userentity" msgid "Example userentity"
msgstr "Beispiel-Benutzerentität" msgstr "Beispiel Benutzerentität"
msgid "This is an example user entity..." msgid "This is an example user entity..."
msgstr "Dies ist eine Beispiel-Benutzerentität" msgstr "Dies ist eine Beispiel-Benutzerentität"
@@ -303,7 +301,7 @@ msgid "Custom field"
msgstr "Benutzerdefiniertes Feld" msgstr "Benutzerdefiniertes Feld"
msgid "Example field value..." msgid "Example field value..."
msgstr "Beispiel-Feldwert..." msgstr "Beispiel Feldwert..."
msgid "Waffle rolls" msgid "Waffle rolls"
msgstr "Waffelröllchen" msgstr "Waffelröllchen"
@@ -330,7 +328,7 @@ msgid "current release"
msgstr "aktuelles Release" msgstr "aktuelles Release"
msgid "not yet released" msgid "not yet released"
msgstr "noch nicht erschienen" msgstr "noch nicht freigegeben"
msgid "Portuguese (Brazil)" msgid "Portuguese (Brazil)"
msgstr "Portugiesisch (Brasilien)" msgstr "Portugiesisch (Brasilien)"

View File

@@ -1,7 +1,7 @@
# #
# Translators: # Translators:
# Bernd Bestel <bernd@berrnd.de>, 2020
# @RubenKelevra <ruben@freifunk-nrw.de>, 2021 # @RubenKelevra <ruben@freifunk-nrw.de>, 2021
# Bernd Bestel <bernd@berrnd.de>, 2021
# #
msgid "" msgid ""
msgstr "" msgstr ""
@@ -9,7 +9,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n" "POT-Creation-Date: 2019-05-01T17:59:17+00:00\n"
"PO-Revision-Date: 2020-08-29 16:33+0000\n" "PO-Revision-Date: 2020-08-29 16:33+0000\n"
"Last-Translator: @RubenKelevra <ruben@freifunk-nrw.de>, 2021\n" "Last-Translator: Bernd Bestel <bernd@berrnd.de>, 2021\n"
"Language-Team: German (https://www.transifex.com/grocy/teams/93189/de/)\n" "Language-Team: German (https://www.transifex.com/grocy/teams/93189/de/)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -36,23 +36,23 @@ msgstr "Benutzer anzeigen"
# Edit own user data / change own password # Edit own user data / change own password
msgid "USERS_EDIT_SELF" msgid "USERS_EDIT_SELF"
msgstr "Eigene Benutzerdaten bearbeiten und eigenes Passwort ändern" msgstr "Eigene Benutzerdaten bearbeiten / eigenes Passwort ändern"
# Undo charge cycle # Undo charge cycle
msgid "BATTERIES_UNDO_CHARGE_CYCLE" msgid "BATTERIES_UNDO_CHARGE_CYCLE"
msgstr "Akku-Ladezyklus rückgängig machen" msgstr "Ladezyklus rückgängig machen"
# Track charge cycle # Track charge cycle
msgid "BATTERIES_TRACK_CHARGE_CYCLE" msgid "BATTERIES_TRACK_CHARGE_CYCLE"
msgstr "Akku-Ladezyklus erfassen" msgstr "Ladezyklus erfassen"
# Track execution # Track execution
msgid "CHORE_TRACK_EXECUTION" msgid "CHORE_TRACK_EXECUTION"
msgstr "Hausarbeit-Ausführung erfassen" msgstr "Ausführung erfassen"
# Undo execution # Undo execution
msgid "CHORE_UNDO_EXECUTION" msgid "CHORE_UNDO_EXECUTION"
msgstr "Hausarbeit-Ausführung rückgängig machen" msgstr "Ausführung rückgängig machen"
# Edit master data # Edit master data
msgid "MASTER_DATA_EDIT" msgid "MASTER_DATA_EDIT"
@@ -60,15 +60,15 @@ msgstr "Stammdaten bearbeiten"
# Undo execution # Undo execution
msgid "TASKS_UNDO_EXECUTION" msgid "TASKS_UNDO_EXECUTION"
msgstr "Aufgabe-Ausführung rückgängig machen" msgstr "Ausführung rückgängig machen"
# Mark completed # Mark completed
msgid "TASKS_MARK_COMPLETED" msgid "TASKS_MARK_COMPLETED"
msgstr "Aufgabe als erledigt markieren" msgstr "Als erledigt markieren"
# Edit stock entries # Edit stock entries
msgid "STOCK_EDIT" msgid "STOCK_EDIT"
msgstr "Lagerbestand bearbeiten" msgstr "Bestandseinträge bearbeiten"
# Transfer # Transfer
msgid "STOCK_TRANSFER" msgid "STOCK_TRANSFER"
@@ -92,11 +92,11 @@ msgstr "Einkauf"
# Add items # Add items
msgid "SHOPPINGLIST_ITEMS_ADD" msgid "SHOPPINGLIST_ITEMS_ADD"
msgstr "Einträge zur Einkaufsliste hinzufügen" msgstr "Eintrag hinzufügen"
# Remove items # Remove items
msgid "SHOPPINGLIST_ITEMS_DELETE" msgid "SHOPPINGLIST_ITEMS_DELETE"
msgstr "Einträge von Einkaufsliste entfernen" msgstr "Eintrag entfernen"
# User management # User management
msgid "USERS" msgid "USERS"
@@ -104,11 +104,11 @@ msgstr "Benutzerverwaltung"
# Stock # Stock
msgid "STOCK" msgid "STOCK"
msgstr "Lager" msgstr "Bestand"
# Shopping list # Shopping list
msgid "SHOPPINGLIST" msgid "SHOPPINGLIST"
msgstr "Einkaufsliste" msgstr "Einkaufszettel"
# Chores # Chores
msgid "CHORES" msgid "CHORES"
@@ -116,7 +116,7 @@ msgstr "Hausarbeiten"
# Batteries # Batteries
msgid "BATTERIES" msgid "BATTERIES"
msgstr "Akkus" msgstr "Batterien"
# Tasks # Tasks
msgid "TASKS" msgid "TASKS"
@@ -128,7 +128,7 @@ msgstr "Rezepte"
# Equipment # Equipment
msgid "EQUIPMENT" msgid "EQUIPMENT"
msgstr "Haushaltsgeräte" msgstr "Ausstattung"
# Calendar # Calendar
msgid "CALENDAR" msgid "CALENDAR"

File diff suppressed because it is too large Load Diff

View File

@@ -2084,13 +2084,11 @@ msgstr ""
msgid "Download" msgid "Download"
msgstr "" msgstr ""
msgid "Download stock entry grocycode" # Example: Download *Product* grocycode
msgid "Download %s grocycode"
msgstr "" msgstr ""
msgid "Download product grocycode" msgid "grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode"
msgstr ""
msgid "grocycode is a unique referer to this product in your grocy instance - print it onto a label and scan it like any other barcode"
msgstr "" msgstr ""
# Abbreviation for "due date" # Abbreviation for "due date"
@@ -2121,13 +2119,11 @@ msgstr ""
msgid "Error while executing WebHook" msgid "Error while executing WebHook"
msgstr "" msgstr ""
msgid "Print product grocycode on label printer" # Example: Print *Product* grocycode on label printer
msgid "Print %s grocycode on label printer"
msgstr "" msgstr ""
msgid "Print stock entry grocycode on label printer" msgid "Open stock entry label in new window"
msgstr ""
msgid "Open stock entry print label in new window"
msgstr "" msgstr ""
msgid "Thermal printer" msgid "Thermal printer"
@@ -2193,3 +2189,37 @@ msgstr ""
msgid "Add recipe" msgid "Add recipe"
msgstr "" msgstr ""
msgid "Copy this day"
msgstr ""
msgid "Date range"
msgstr ""
msgid "%s month"
msgid_plural "%s months"
msgstr[0] ""
msgstr[1] ""
msgid "%s year"
msgid_plural "%s years"
msgstr[0] ""
msgstr[1] ""
msgid "Display product"
msgstr ""
msgid "Copy recipe"
msgstr ""
msgid "Copy of %s"
msgstr ""
msgid "Add decimal separator automatically for price inputs"
msgstr ""
msgid "When enabled, you always have to enter the value including decimal places, the decimal separator will be automatically added based on the amount of allowed decimal places"
msgstr ""
msgid "Stock entry"
msgstr ""

View File

@@ -33,7 +33,8 @@ SELECT
p.description as product_description, p.description as product_description,
l.name AS product_default_location_name, l.name AS product_default_location_name,
p_parent.id AS parent_product_id, p_parent.id AS parent_product_id,
p_parent.name AS parent_product_name p_parent.name AS parent_product_name,
p.picture_file_name AS product_picture_file_name
FROM ( FROM (
SELECT * SELECT *
FROM stock_current FROM stock_current
@@ -92,7 +93,8 @@ SELECT
p.description AS product_description, p.description AS product_description,
l.name AS product_default_location_name, l.name AS product_default_location_name,
p_parent.id AS parent_product_id, p_parent.id AS parent_product_id,
p_parent.name AS parent_product_name p_parent.name AS parent_product_name,
p.picture_file_name AS product_picture_file_name
FROM ( FROM (
SELECT * SELECT *
FROM stock_current FROM stock_current

28
migrations/0147.sql Normal file
View File

@@ -0,0 +1,28 @@
DROP VIEW uihelper_stock_journal;
CREATE VIEW uihelper_stock_journal
AS
SELECT
sl.id,
sl.row_created_timestamp,
sl.correlation_id,
sl.undone,
sl.undone_timestamp,
sl.transaction_type,
sl.spoiled,
sl.amount,
sl.location_id,
l.name AS location_name,
p.name AS product_name,
qu.name AS qu_name,
qu.name_plural AS qu_name_plural,
u.display_name AS user_display_name,
p.id AS product_id
FROM stock_log sl
JOIN users_dto u
ON sl.user_id = u.id
JOIN products p
ON sl.product_id = p.id
JOIN locations l
ON sl.location_id = l.id
JOIN quantity_units qu
ON p.qu_id_stock = qu.id;

View File

@@ -270,7 +270,6 @@ a:not([href]) {
padding: 0; padding: 0;
word-wrap: break-word; word-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
color: inherit;
} }
/* Barcodescanner Quagga */ /* Barcodescanner Quagga */
@@ -318,6 +317,10 @@ a:not([href]) {
border-radius: 0.2rem; border-radius: 0.2rem;
} }
.ls-n1 {
letter-spacing: -0.1rem;
}
.text-larger { .text-larger {
font-size: 125%; font-size: 125%;
} }

View File

@@ -681,7 +681,7 @@ $("textarea.wysiwyg-editor").summernote({
function LoadImagesLazy() function LoadImagesLazy()
{ {
$(".lazy").Lazy({ $(".lazy:visible").Lazy({
enableThrottle: true, enableThrottle: true,
throttle: 500 throttle: 500
}); });
@@ -801,13 +801,6 @@ $.extend(true, $.fn.dataTable.defaults, {
if ("dataSrc" in rowGroup) if ("dataSrc" in rowGroup)
{ {
api.rowGroup().dataSrc(rowGroup.dataSrc); api.rowGroup().dataSrc(rowGroup.dataSrc);
// Apply fixed order for group column
var fixedOrder = {
pre: [rowGroup.dataSrc, 'asc']
};
api.order.fixed(fixedOrder);
} }
} }
} }
@@ -1105,7 +1098,7 @@ $(document).on("click", ".change-table-columns-rowgroup-toggle", function()
dataTable.rowGroup().enable(false); dataTable.rowGroup().enable(false);
// Remove fixed order // Remove fixed order
dataTable.order.fixed({}); //dataTable.order.fixed({});
} }
else else
{ {
@@ -1116,12 +1109,6 @@ $(document).on("click", ".change-table-columns-rowgroup-toggle", function()
dataTable.rowGroup().enable(true); dataTable.rowGroup().enable(true);
dataTable.rowGroup().dataSrc(columnIndex); dataTable.rowGroup().dataSrc(columnIndex);
// Apply fixed order for group column
var fixedOrder = {
pre: [columnIndex, 'asc']
};
dataTable.order.fixed(fixedOrder);
} }
var settingKey = 'datatables_rowGroup_' + dataTable.settings()[0].sTableId; var settingKey = 'datatables_rowGroup_' + dataTable.settings()[0].sTableId;

View File

@@ -1,5 +1,4 @@
var batteriesJournalTable = $('#batteries-journal-table').DataTable({ var batteriesJournalTable = $('#batteries-journal-table').DataTable({
'paginate': true,
'order': [[2, 'desc']], 'order': [[2, 'desc']],
'columnDefs': [ 'columnDefs': [
{ 'orderable': false, 'targets': 0 }, { 'orderable': false, 'targets': 0 },
@@ -12,13 +11,16 @@ batteriesJournalTable.columns.adjust().draw();
$("#battery-filter").on("change", function() $("#battery-filter").on("change", function()
{ {
var value = $(this).val(); var value = $(this).val();
var text = $("#battery-filter option:selected").text();
if (value === "all") if (value === "all")
{ {
text = ""; RemoveUriParam("battery");
}
else
{
UpdateUriParam("battery", value);
} }
batteriesJournalTable.column(1).search(text).draw(); window.location.reload();
}); });
$("#search").on("keyup", Delay(function() $("#search").on("keyup", Delay(function()
@@ -36,14 +38,27 @@ $("#clear-filter-button").on("click", function()
{ {
$("#search").val(""); $("#search").val("");
$("#battery-filter").val("all"); $("#battery-filter").val("all");
batteriesJournalTable.column(1).search("").draw(); $("#daterange-filter").val("24");
batteriesJournalTable.search("").draw();
RemoveUriParam("months");
RemoveUriParam("battery");
window.location.reload();
});
$("#daterange-filter").on("change", function()
{
UpdateUriParam("months", $(this).val());
window.location.reload();
}); });
if (typeof GetUriParam("battery") !== "undefined") if (typeof GetUriParam("battery") !== "undefined")
{ {
$("#battery-filter").val(GetUriParam("battery")); $("#battery-filter").val(GetUriParam("battery"));
$("#battery-filter").trigger("change"); }
if (typeof GetUriParam("months") !== "undefined")
{
$("#daterange-filter").val(GetUriParam("months"));
} }
$(document).on('click', '.undo-battery-execution-button', function(e) $(document).on('click', '.undo-battery-execution-button', function(e)

View File

@@ -122,6 +122,21 @@ $(document).on("click", ".battery-name-cell", function(e)
$("#batteriesoverview-batterycard-modal").modal("show"); $("#batteriesoverview-batterycard-modal").modal("show");
}); });
$(document).on('click', '.battery-grocycode-label-print', function(e)
{
e.preventDefault();
document.activeElement.blur();
var batteryId = $(e.currentTarget).attr('data-battery-id');
Grocy.Api.Get('batteries/' + batteryId + '/printlabel', function(labelData)
{
if (Grocy.Webhooks.labelprinter !== undefined)
{
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
}
});
});
function RefreshStatistics() function RefreshStatistics()
{ {
var nextXDays = $("#info-due-batteries").data("next-x-days"); var nextXDays = $("#info-due-batteries").data("next-x-days");

View File

@@ -83,6 +83,21 @@ $('#battery-form input').keydown(function(event)
} }
}); });
$(document).on('click', '.battery-grocycode-label-print', function(e)
{
e.preventDefault();
document.activeElement.blur();
var batteryId = $(e.currentTarget).attr('data-chore-id');
Grocy.Api.Get('batteries/' + batteryId + '/printlabel', function(labelData)
{
if (Grocy.Webhooks.labelprinter !== undefined)
{
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
}
});
});
Grocy.Components.UserfieldsForm.Load(); Grocy.Components.UserfieldsForm.Load();
$('#name').focus(); $('#name').focus();
Grocy.FrontendHelpers.ValidateForm('battery-form'); Grocy.FrontendHelpers.ValidateForm('battery-form');

View File

@@ -60,7 +60,8 @@ $('#battery_id').on('change', function(e)
$('.combobox').combobox({ $('.combobox').combobox({
appendId: '_text_input', appendId: '_text_input',
bsVersion: '4' bsVersion: '4',
clearIfNoMatch: false
}); });
$('#battery_id').val(''); $('#battery_id').val('');
@@ -97,6 +98,16 @@ $('#tracked_time').find('input').on('keypress', function(e)
Grocy.FrontendHelpers.ValidateForm('batterytracking-form'); Grocy.FrontendHelpers.ValidateForm('batterytracking-form');
}); });
$(document).on("Grocy.BarcodeScanned", function(e, barcode, target)
{
if (!(target == "@batterypicker" || target == "undefined" || target == undefined)) // Default target
{
return;
}
$('#battery_id_text_input').val(barcode).trigger('change');
});
function UndoChargeCycle(chargeCycleId) function UndoChargeCycle(chargeCycleId)
{ {
Grocy.Api.Post('batteries/charge-cycles/' + chargeCycleId.toString() + '/undo', {}, Grocy.Api.Post('batteries/charge-cycles/' + chargeCycleId.toString() + '/undo', {},
@@ -110,3 +121,38 @@ function UndoChargeCycle(chargeCycleId)
} }
); );
}; };
$('#battery_id_text_input').on('blur', function(e)
{
if ($('#battery_id').hasClass("combobox-menu-visible"))
{
return;
}
var input = $('#battery_id_text_input').val().toString();
var possibleOptionElement = [];
// grocycode handling
if (input.startsWith("grcy"))
{
var gc = input.split(":");
if (gc[1] == "b")
{
possibleOptionElement = $("#battery_id option[value=\"" + gc[2] + "\"]").first();
}
}
if (possibleOptionElement.length > 0)
{
$('#battery_id').val(possibleOptionElement.val());
$('#battery_id').data('combobox').refresh();
$('#battery_id').trigger('change');
}
else
{
$('#battery_id').val(null);
$('#battery_id_text_input').val("");
$('#battery_id').data('combobox').refresh();
$('#battery_id').trigger('change');
}
});

View File

@@ -237,3 +237,18 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e)
); );
} }
}); });
$(document).on('click', '.chore-grocycode-label-print', function(e)
{
e.preventDefault();
document.activeElement.blur();
var choreId = $(e.currentTarget).attr('data-chore-id');
Grocy.Api.Get('chores/' + choreId + '/printlabel', function(labelData)
{
if (Grocy.Webhooks.labelprinter !== undefined)
{
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
}
});
});

View File

@@ -1,5 +1,4 @@
var choresJournalTable = $('#chores-journal-table').DataTable({ var choresJournalTable = $('#chores-journal-table').DataTable({
'paginate': true,
'order': [[2, 'desc']], 'order': [[2, 'desc']],
'columnDefs': [ 'columnDefs': [
{ 'orderable': false, 'targets': 0 }, { 'orderable': false, 'targets': 0 },
@@ -12,13 +11,22 @@ choresJournalTable.columns.adjust().draw();
$("#chore-filter").on("change", function() $("#chore-filter").on("change", function()
{ {
var value = $(this).val(); var value = $(this).val();
var text = $("#chore-filter option:selected").text();
if (value === "all") if (value === "all")
{ {
text = ""; RemoveUriParam("chore");
}
else
{
UpdateUriParam("chore", value);
} }
choresJournalTable.column(1).search(text).draw(); window.location.reload();
});
$("#daterange-filter").on("change", function()
{
UpdateUriParam("months", $(this).val());
window.location.reload();
}); });
$("#search").on("keyup", Delay(function() $("#search").on("keyup", Delay(function()
@@ -36,14 +44,21 @@ $("#clear-filter-button").on("click", function()
{ {
$("#search").val(""); $("#search").val("");
$("#chore-filter").val("all"); $("#chore-filter").val("all");
choresJournalTable.column(1).search("").draw(); $("#daterange-filter").val("24");
choresJournalTable.search("").draw();
RemoveUriParam("months");
RemoveUriParam("chore");
window.location.reload();
}); });
if (typeof GetUriParam("chore") !== "undefined") if (typeof GetUriParam("chore") !== "undefined")
{ {
$("#chore-filter").val(GetUriParam("chore")); $("#chore-filter").val(GetUriParam("chore"));
$("#chore-filter").trigger("change"); }
if (typeof GetUriParam("months") !== "undefined")
{
$("#daterange-filter").val(GetUriParam("months"));
} }
$(document).on('click', '.undo-chore-execution-button', function(e) $(document).on('click', '.undo-chore-execution-button', function(e)

View File

@@ -185,6 +185,21 @@ $(document).on("click", ".chore-name-cell", function(e)
$("#choresoverview-chorecard-modal").modal("show"); $("#choresoverview-chorecard-modal").modal("show");
}); });
$(document).on('click', '.chore-grocycode-label-print', function(e)
{
e.preventDefault();
document.activeElement.blur();
var choreId = $(e.currentTarget).attr('data-chore-id');
Grocy.Api.Get('chores/' + choreId + '/printlabel', function(labelData)
{
if (Grocy.Webhooks.labelprinter !== undefined)
{
Grocy.FrontendHelpers.RunWebhook(Grocy.Webhooks.labelprinter, labelData);
}
});
});
function RefreshStatistics() function RefreshStatistics()
{ {
var nextXDays = $("#info-due-chores").data("next-x-days"); var nextXDays = $("#info-due-chores").data("next-x-days");

View File

@@ -83,7 +83,8 @@ $('#chore_id').on('change', function(e)
$('.combobox').combobox({ $('.combobox').combobox({
appendId: '_text_input', appendId: '_text_input',
bsVersion: '4' bsVersion: '4',
clearIfNoMatch: false
}); });
$('#chore_id_text_input').focus(); $('#chore_id_text_input').focus();
@@ -113,6 +114,16 @@ $('#choretracking-form input').keydown(function(event)
} }
}); });
$(document).on("Grocy.BarcodeScanned", function(e, barcode, target)
{
if (!(target == "@chorepicker" || target == "undefined" || target == undefined)) // Default target
{
return;
}
$('#chore_id_text_input').val(barcode).trigger('change');
});
Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e) Grocy.Components.DateTimePicker.GetInputElement().on('keypress', function(e)
{ {
Grocy.FrontendHelpers.ValidateForm('choretracking-form'); Grocy.FrontendHelpers.ValidateForm('choretracking-form');
@@ -131,3 +142,38 @@ function UndoChoreExecution(executionId)
} }
); );
}; };
$('#chore_id_text_input').on('blur', function(e)
{
if ($('#chore_id').hasClass("combobox-menu-visible"))
{
return;
}
var input = $('#chore_id_text_input').val().toString();
var possibleOptionElement = [];
// grocycode handling
if (input.startsWith("grcy"))
{
var gc = input.split(":");
if (gc[1] == "c")
{
possibleOptionElement = $("#chore_id option[value=\"" + gc[2] + "\"]").first();
}
}
if (possibleOptionElement.length > 0)
{
$('#chore_id').val(possibleOptionElement.val());
$('#chore_id').data('combobox').refresh();
$('#chore_id').trigger('change');
}
else
{
$('#chore_id').val(null);
$('#chore_id_text_input').val("");
$('#chore_id').data('combobox').refresh();
$('#chore_id').trigger('change');
}
});

View File

@@ -92,3 +92,20 @@ $(".numberpicker").on("keydown", function(e)
$(this).parent().find(".numberpicker-down-button").click(); $(this).parent().find(".numberpicker-down-button").click();
} }
}); });
$(".numberpicker.locale-number-input.locale-number-currency").on("blur", function()
{
if (BoolVal(Grocy.UserSettings.stock_auto_decimal_separator_prices))
{
var value = this.value.toString();
var decimalPlaces = parseInt(Grocy.UserSettings.stock_decimal_places_prices);
if (value.length <= decimalPlaces)
{
return;
}
var valueNew = parseFloat(value.substring(0, value.length - decimalPlaces) + '.' + value.slice(decimalPlaces * -1)).toLocaleString(undefined, { minimumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces });
$(this).val(valueNew);
}
});

View File

@@ -84,7 +84,7 @@ Grocy.Components.ProductCard.Refresh = function(productId)
if (productDetails.last_price !== null) if (productDetails.last_price !== null)
{ {
$('#productcard-product-last-price').text(__t("%1$s per %2$s", Number.parseFloat(productDetails.last_price).toLocaleString() + ' ' + Grocy.Currency, productDetails.quantity_unit_stock.name)); $('#productcard-product-last-price').text(__t("%1$s per %2$s", Number.parseFloat(productDetails.last_price).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices }), productDetails.quantity_unit_stock.name));
} }
else else
{ {
@@ -93,7 +93,7 @@ Grocy.Components.ProductCard.Refresh = function(productId)
if (productDetails.avg_price !== null) if (productDetails.avg_price !== null)
{ {
$('#productcard-product-average-price').text(__t("%1$s per %2$s", Number.parseFloat(productDetails.avg_price).toLocaleString() + ' ' + Grocy.Currency, productDetails.quantity_unit_stock.name)); $('#productcard-product-average-price').text(__t("%1$s per %2$s", Number.parseFloat(productDetails.avg_price).toLocaleString(undefined, { style: "currency", currency: Grocy.Currency, minimumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices, maximumFractionDigits: Grocy.UserSettings.stock_decimal_places_prices }), productDetails.quantity_unit_stock.name));
} }
else else
{ {

View File

@@ -149,18 +149,17 @@ $('#product_id_text_input').on('blur', function(e)
var input = $('#product_id_text_input').val().toString(); var input = $('#product_id_text_input').val().toString();
var possibleOptionElement = []; var possibleOptionElement = [];
// did we enter a grocycode? // grocycode handling
if (input.startsWith("grcy")) if (input.startsWith("grcy"))
{ {
var gc = input.split(":"); var gc = input.split(":");
if (gc[1] == "p") if (gc[1] == "p")
{ {
// find product id
possibleOptionElement = $("#product_id option[value=\"" + gc[2] + "\"]").first(); possibleOptionElement = $("#product_id option[value=\"" + gc[2] + "\"]").first();
$("#product_id").data("grocycode", true); $("#product_id").data("grocycode", true);
} }
} }
else // process barcode as usual else // Normal product barcode handling
{ {
possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first(); possibleOptionElement = $("#product_id option[data-additional-searchdata*=\"" + input + ",\"]").first();
} }

View File

@@ -32,10 +32,10 @@ var calendar = $("#calendar").fullCalendar({
<div class="btn-group mr-2 my-1"> \ <div class="btn-group mr-2 my-1"> \
<button type="button" class="btn btn-outline-dark btn-xs add-recipe-button" data-toggle="tooltip" title="' + __t('Add recipe') + '"><i class="fas fa-plus"></i></a></button> \ <button type="button" class="btn btn-outline-dark btn-xs add-recipe-button" data-toggle="tooltip" title="' + __t('Add recipe') + '"><i class="fas fa-plus"></i></a></button> \
<button type="button" class="btn btn-outline-dark btn-xs dropdown-toggle dropdown-toggle-split" data-toggle="dropdown"></button> \ <button type="button" class="btn btn-outline-dark btn-xs dropdown-toggle dropdown-toggle-split" data-toggle="dropdown"></button> \
<div class="dropdown-menu"> \ <div class="table-inline-menu dropdown-menu"> \
<a class="dropdown-item add-note-button" href="#">' + __t('Add note') + '</a> \ <a class="dropdown-item add-note-button" href="#"><span class="dropdown-item-text">' + __t('Add note') + '</span></a> \
<a class="dropdown-item add-product-button" href="#">' + __t('Add product') + '</a> \ <a class="dropdown-item add-product-button" href="#"><span class="dropdown-item-text">' + __t('Add product') + '</span></a> \
<a class="dropdown-item copy-day-button" href="#">' + __t('Copy this day') + '</a> \ <a class="dropdown-item copy-day-button" href="#"><span class="dropdown-item-text">' + __t('Copy this day') + '</span></a> \
</div> \ </div> \
</div>'); </div>');
@@ -121,11 +121,11 @@ var calendar = $("#calendar").fullCalendar({
var costsAndCaloriesPerServing = "" var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{ {
costsAndCaloriesPerServing = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + resolvedRecipe.costs + '</span> / <span class="locale-number locale-number-generic">' + resolvedRecipe.calories + '</span> kcal ' + __t('per serving') + '<h5>'; costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-currency">' + resolvedRecipe.costs + '</span> / <span class="locale-number locale-number-generic">' + resolvedRecipe.calories + '</span> kcal ' + __t('per serving') + '</h5>';
} }
else else
{ {
costsAndCaloriesPerServing = '<h5 class="small text-truncate"><span class="locale-number locale-number-generic">' + resolvedRecipe.calories + '</span> kcal ' + __t('per serving') + '<h5>'; costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-generic">' + resolvedRecipe.calories + '</span> kcal ' + __t('per serving') + '</h5>';
} }
if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK) if (!Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK)
@@ -136,16 +136,15 @@ var calendar = $("#calendar").fullCalendar({
element.html('\ element.html('\
<div> \ <div> \
<h5 class="text-truncate ' + additionalTitleCssClasses + '">' + recipe.name + '<h5> \ <h5 class="text-truncate mb-1 cursor-link display-recipe-button ' + additionalTitleCssClasses + '" data-toggle="tooltip" title="' + __t("Display recipe") + '" data-recipe-id="' + recipe.id.toString() + '" data-recipe-name="' + recipe.name + '" data-mealplan-servings="' + mealPlanEntry.recipe_servings + '" data-recipe-type="' + recipe.type + '">' + recipe.name + '</h5> \
<h5 class="small text-truncate">' + __n(mealPlanEntry.recipe_servings, "%s serving", "%s servings") + '</h5> \ <h5 class="small text-truncate mb-1">' + __n(mealPlanEntry.recipe_servings, "%s serving", "%s servings") + '</h5> \
<h5 class="small timeago-contextual text-truncate">' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '</h5> \ <h5 class="small timeago-contextual text-truncate mb-1">' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '</h5> \
' + costsAndCaloriesPerServing + ' \ ' + costsAndCaloriesPerServing + ' \
<h5> \ <h5> \
<a class="ml-1 btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Edit this item") + '"><i class="fas fa-edit"></i></a> \ <a class="ml-1 btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Edit this item") + '"><i class="fas fa-edit"></i></a> \
<a class="btn btn-outline-danger btn-xs remove-recipe-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \ <a class="btn btn-outline-danger btn-xs remove-recipe-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \
<a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + recipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + recipe.id.toString() + '" data-mealplan-servings="' + mealPlanEntry.recipe_servings + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-cart-plus"></i></a> \ <a class="ml-1 btn btn-outline-primary btn-xs recipe-order-missing-button ' + recipeOrderMissingButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Put missing products on shopping list") + '" data-recipe-id="' + recipe.id.toString() + '" data-mealplan-servings="' + mealPlanEntry.recipe_servings + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-cart-plus"></i></a> \
<a class="btn btn-outline-success btn-xs recipe-consume-button ' + recipeConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this recipe") + '" data-recipe-id="' + internalShadowRecipe.id.toString() + '" data-mealplan-entry-id="' + mealPlanEntry.id.toString() + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-utensils"></i></a> \ <a class="btn btn-outline-success btn-xs recipe-consume-button ' + recipeConsumeButtonDisabledClasses + '" href="#" data-toggle="tooltip" title="' + __t("Consume all ingredients needed by this recipe") + '" data-recipe-id="' + internalShadowRecipe.id.toString() + '" data-mealplan-entry-id="' + mealPlanEntry.id.toString() + '" data-recipe-name="' + recipe.name + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-utensils"></i></a> \
<a class="ml-1 btn btn-outline-secondary btn-xs recipe-popup-button" href="#" data-toggle="tooltip" title="' + __t("Display recipe") + '" data-recipe-id="' + recipe.id.toString() + '" data-recipe-name="' + recipe.name + '" data-mealplan-servings="' + mealPlanEntry.recipe_servings + '" data-recipe-type="' + recipe.type + '"><i class="fas fa-eye"></i></a> \
' + doneButtonHtml + ' \ ' + doneButtonHtml + ' \
</h5> \ </h5> \
</div>'); </div>');
@@ -166,11 +165,11 @@ var calendar = $("#calendar").fullCalendar({
var costsAndCaloriesPerDay = "" var costsAndCaloriesPerDay = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{ {
costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + dayRecipeResolved.costs + '</span> / <span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '<h5>'; costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + dayRecipeResolved.costs + '</span> / <span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '</h5>';
} }
else else
{ {
costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '<h5>'; costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '</h5>';
} }
$(".fc-day-header[data-date='" + dayRecipeName + "']").append('<h5 id="day-summary-' + dayRecipeName + '" class="small text-truncate border-top pt-1 pb-0">' + costsAndCaloriesPerDay + '</h5>'); $(".fc-day-header[data-date='" + dayRecipeName + "']").append('<h5 id="day-summary-' + dayRecipeName + '" class="small text-truncate border-top pt-1 pb-0">' + costsAndCaloriesPerDay + '</h5>');
} }
@@ -214,18 +213,18 @@ var calendar = $("#calendar").fullCalendar({
var costsAndCaloriesPerServing = "" var costsAndCaloriesPerServing = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{ {
costsAndCaloriesPerServing = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + productDetails.last_price * mealPlanEntry.product_amount + '</span> / <span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '<h5>'; costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-currency">' + productDetails.last_price * mealPlanEntry.product_amount + '</span> / <span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '</h5>';
} }
else else
{ {
costsAndCaloriesPerServing = '<h5 class="small text-truncate"><span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '<h5>'; costsAndCaloriesPerServing = '<h5 class="small text-truncate mb-1"><span class="locale-number locale-number-generic">' + productDetails.product.calories * mealPlanEntry.product_amount + '</span> kcal ' + '</h5>';
} }
element.html('\ element.html('\
<div> \ <div> \
<h5 class="text-truncate ' + additionalTitleCssClasses + '">' + productDetails.product.name + '<h5> \ <h5 class="text-truncate mb-1 cursor-link display-product-button ' + additionalTitleCssClasses + '" data-toggle="tooltip" title="' + __t("Display product") + '" data-product-id="' + productDetails.product.id.toString() + '">' + productDetails.product.name + '</h5> \
<h5 class="small text-truncate"><span class="locale-number locale-number-quantity-amount">' + mealPlanEntry.product_amount + "</span> " + __n(mealPlanEntry.product_amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural) + '</h5> \ <h5 class="small text-truncate mb-1"><span class="locale-number locale-number-quantity-amount">' + mealPlanEntry.product_amount + "</span> " + __n(mealPlanEntry.product_amount, productDetails.quantity_unit_stock.name, productDetails.quantity_unit_stock.name_plural) + '</h5> \
<h5 class="small timeago-contextual text-truncate">' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '</h5> \ <h5 class="small timeago-contextual text-truncate mb-1">' + fulfillmentIconHtml + " " + fulfillmentInfoHtml + '</h5> \
' + costsAndCaloriesPerServing + ' \ ' + costsAndCaloriesPerServing + ' \
<h5> \ <h5> \
<a class="ml-1 btn btn-outline-danger btn-xs remove-product-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \ <a class="ml-1 btn btn-outline-danger btn-xs remove-product-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \
@@ -252,11 +251,11 @@ var calendar = $("#calendar").fullCalendar({
var costsAndCaloriesPerDay = "" var costsAndCaloriesPerDay = ""
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
{ {
costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + dayRecipeResolved.costs + '</span> / <span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '<h5>'; costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-currency">' + dayRecipeResolved.costs + '</span> / <span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '</h5>';
} }
else else
{ {
costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '<h5>'; costsAndCaloriesPerDay = '<h5 class="small text-truncate"><span class="locale-number locale-number-generic">' + dayRecipeResolved.calories + '</span> kcal ' + __t('per day') + '</h5>';
} }
$(".fc-day-header[data-date='" + dayRecipeName + "']").append('<h5 id="day-summary-' + dayRecipeName + '" class="small text-truncate border-top pt-1 pb-0">' + costsAndCaloriesPerDay + '</h5>'); $(".fc-day-header[data-date='" + dayRecipeName + "']").append('<h5 id="day-summary-' + dayRecipeName + '" class="small text-truncate border-top pt-1 pb-0">' + costsAndCaloriesPerDay + '</h5>');
} }
@@ -266,7 +265,7 @@ var calendar = $("#calendar").fullCalendar({
{ {
element.html('\ element.html('\
<div> \ <div> \
<h5 class="text-wrap text-break ' + additionalTitleCssClasses + '">' + mealPlanEntry.note + '<h5> \ <h5 class="text-wrap text-break mb-1 ' + additionalTitleCssClasses + '">' + mealPlanEntry.note + '</h5> \
<h5> \ <h5> \
<a class="ml-1 btn btn-outline-danger btn-xs remove-note-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \ <a class="ml-1 btn btn-outline-danger btn-xs remove-note-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-trash"></i></a> \
<a class="btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-edit"></i></a> \ <a class="btn btn-outline-info btn-xs edit-meal-plan-entry-button" href="#" data-toggle="tooltip" title="' + __t("Delete this item") + '"><i class="fas fa-edit"></i></a> \
@@ -831,7 +830,7 @@ $(document).on('click', '.recipe-consume-button', function(e)
}); });
}); });
$(document).on("click", ".recipe-popup-button", function(e) $(document).on("click", ".display-recipe-button", function(e)
{ {
// Remove the focus from the current button // Remove the focus from the current button
// to prevent that the tooltip stays until clicked anywhere else // to prevent that the tooltip stays until clicked anywhere else
@@ -868,6 +867,16 @@ $(document).on("click", ".recipe-popup-button", function(e)
); );
}); });
$(document).on("click", ".display-product-button", function(e)
{
// Remove the focus from the current button
// to prevent that the tooltip stays until clicked anywhere else
document.activeElement.blur();
Grocy.Components.ProductCard.Refresh($(e.currentTarget).attr('data-product-id'));
$("#mealplan-productcard-modal").modal("show");
});
$(document).on("click", ".mealplan-entry-done-button", function(e) $(document).on("click", ".mealplan-entry-done-button", function(e)
{ {
e.preventDefault(); e.preventDefault();

View File

@@ -21,7 +21,8 @@ const swaggerUi = SwaggerUIBundle({
], ],
layout: 'StandaloneLayout', layout: 'StandaloneLayout',
docExpansion: "list", docExpansion: "list",
defaultModelsExpandDepth: -1 defaultModelsExpandDepth: -1,
validatorUrl: false
}); });
window.ui = swaggerUi; window.ui = swaggerUi;

View File

@@ -269,7 +269,6 @@ $("#delete-current-product-picture-button").on("click", function(e)
var quConversionsTable = $('#qu-conversions-table-products').DataTable({ var quConversionsTable = $('#qu-conversions-table-products').DataTable({
'order': [[1, 'asc']], 'order': [[1, 'asc']],
"orderFixed": [[4, 'asc']],
'columnDefs': [ 'columnDefs': [
{ 'orderable': false, 'targets': 0 }, { 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }, { 'searchable': false, "targets": 0 },
@@ -285,7 +284,6 @@ quConversionsTable.columns.adjust().draw();
var barcodeTable = $('#barcode-table').DataTable({ var barcodeTable = $('#barcode-table').DataTable({
'order': [[1, 'asc']], 'order': [[1, 'asc']],
"orderFixed": [[1, 'asc']],
'columnDefs': [ 'columnDefs': [
{ 'orderable': false, 'targets': 0 }, { 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }, { 'searchable': false, "targets": 0 },
@@ -302,7 +300,7 @@ $('#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) $(document).on('click', '.product-grocycode-label-print', function(e)
{ {
e.preventDefault(); e.preventDefault();
document.activeElement.blur(); document.activeElement.blur();

View File

@@ -117,7 +117,7 @@ $('#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.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER)
{ {
if (Grocy.Webhooks.labelprinter !== undefined) if (Grocy.Webhooks.labelprinter !== undefined)
{ {
@@ -126,7 +126,7 @@ $('#save-purchase-button').on('click', function(e)
post_data.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + result[0].stock_id post_data.grocycode = 'grcy:p:' + jsonForm.product_id + ":" + result[0].stock_id
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{ {
post_data.duedate = __t('DD') + ': ' + result[0].best_before_date post_data.due_date = __t('DD') + ': ' + result[0].best_before_date
} }
if (jsonForm.print_stock_label > 0) if (jsonForm.print_stock_label > 0)
@@ -304,7 +304,7 @@ if (Grocy.Components.ProductPicker !== undefined)
} }
} }
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABELPRINTER) if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_LABEL_PRINTER)
{ {
$("#print_stock_label").val(productDetails.product.default_print_stock_label); $("#print_stock_label").val(productDetails.product.default_print_stock_label);
if (productDetails.product.allow_label_per_unit) if (productDetails.product.allow_label_per_unit)

View File

@@ -77,7 +77,6 @@ $('.save-recipe').on('click', function(e)
var recipesPosTables = $('#recipes-pos-table').DataTable({ var recipesPosTables = $('#recipes-pos-table').DataTable({
'order': [[1, 'asc']], 'order': [[1, 'asc']],
"orderFixed": [[4, 'asc']],
'columnDefs': [ 'columnDefs': [
{ 'orderable': false, 'targets': 0 }, { 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }, { 'searchable': false, "targets": 0 },

View File

@@ -61,6 +61,7 @@ $("a[data-toggle='tab']").on("shown.bs.tab", function(e)
{ {
var tabId = $(e.target).attr("id"); var tabId = $(e.target).attr("id");
window.localStorage.setItem("recipes_last_tab_id", tabId); window.localStorage.setItem("recipes_last_tab_id", tabId);
LoadImagesLazy();
}); });
$("#search").on("keyup", Delay(function() $("#search").on("keyup", Delay(function()
@@ -166,6 +167,24 @@ $(".recipe-delete").on('click', function(e)
}); });
}); });
$(".recipe-copy").on('click', function(e)
{
e.preventDefault();
var objectId = $(e.currentTarget).attr('data-recipe-id');
Grocy.Api.Post("recipes/" + objectId.toString() + "/copy", {},
function(result)
{
window.location.href = U('/recipes?recipe=' + result.created_object_id.toString());
},
function(xhr)
{
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response);
}
);
});
$(document).on('click', '.recipe-shopping-list', function(e) $(document).on('click', '.recipe-shopping-list', function(e)
{ {
var objectName = $(e.currentTarget).attr('data-recipe-name'); var objectName = $(e.currentTarget).attr('data-recipe-name');
@@ -346,3 +365,5 @@ if (window.location.hash === "#fullscreen")
{ {
$("#selectedRecipeToggleFullscreenButton").click(); $("#selectedRecipeToggleFullscreenButton").click();
} }
LoadImagesLazy();

View File

@@ -1,6 +1,5 @@
var shoppingListTable = $('#shoppinglist-table').DataTable({ var shoppingListTable = $('#shoppinglist-table').DataTable({
'order': [[1, 'asc']], 'order': [[1, 'asc']],
"orderFixed": [[3, 'asc']],
'columnDefs': [ 'columnDefs': [
{ 'orderable': false, 'targets': 0 }, { 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }, { 'searchable': false, "targets": 0 },

View File

@@ -125,7 +125,7 @@ $(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) $(document).on('click', '.stockentry-grocycode-label-print', function(e)
{ {
e.preventDefault(); e.preventDefault();
document.activeElement.blur(); document.activeElement.blur();

View File

@@ -1,5 +1,4 @@
var stockJournalTable = $('#stock-journal-table').DataTable({ var stockJournalTable = $('#stock-journal-table').DataTable({
'paginate': true,
'order': [[3, 'desc']], 'order': [[3, 'desc']],
'columnDefs': [ 'columnDefs': [
{ 'orderable': false, 'targets': 0 }, { 'orderable': false, 'targets': 0 },
@@ -12,15 +11,16 @@ stockJournalTable.columns.adjust().draw();
$("#product-filter").on("change", function() $("#product-filter").on("change", function()
{ {
var value = $(this).val(); var value = $(this).val();
var text = $("#product-filter option:selected").text();
if (value === "all") if (value === "all")
{ {
stockJournalTable.column(1).search("").draw(); RemoveUriParam("product");
} }
else else
{ {
stockJournalTable.column(1).search(">" + text + "<").draw(); UpdateUriParam("product", value);
} }
window.location.reload();
}); });
$("#transaction-type-filter").on("change", function() $("#transaction-type-filter").on("change", function()
@@ -59,6 +59,12 @@ $("#user-filter").on("change", function()
stockJournalTable.column(6).search(text).draw(); stockJournalTable.column(6).search(text).draw();
}); });
$("#daterange-filter").on("change", function()
{
UpdateUriParam("months", $(this).val());
window.location.reload();
});
$("#search").on("keyup", Delay(function() $("#search").on("keyup", Delay(function()
{ {
var value = $(this).val(); var value = $(this).val();
@@ -77,17 +83,21 @@ $("#clear-filter-button").on("click", function()
$("#location-filter").val("all"); $("#location-filter").val("all");
$("#user-filter").val("all"); $("#user-filter").val("all");
$("#product-filter").val("all"); $("#product-filter").val("all");
stockJournalTable.column(1).search("").draw(); $("#daterange-filter").val("6");
stockJournalTable.column(4).search("").draw();
stockJournalTable.column(5).search("").draw(); RemoveUriParam("months");
stockJournalTable.column(6).search("").draw(); RemoveUriParam("product");
stockJournalTable.search("").draw(); window.location.reload();
}); });
if (typeof GetUriParam("product") !== "undefined") if (typeof GetUriParam("product") !== "undefined")
{ {
$("#product-filter").val(GetUriParam("product")); $("#product-filter").val(GetUriParam("product"));
$("#product-filter").trigger("change"); }
if (typeof GetUriParam("months") !== "undefined")
{
$("#daterange-filter").val(GetUriParam("months"));
} }
$(document).on('click', '.undo-stock-booking-button', function(e) $(document).on('click', '.undo-stock-booking-button', function(e)

View File

@@ -1,5 +1,4 @@
var journalSummaryTable = $('#stock-journal-summary-table').DataTable({ var journalSummaryTable = $('#stock-journal-summary-table').DataTable({
'paginate': true,
'order': [[1, 'asc']], 'order': [[1, 'asc']],
'columnDefs': [ 'columnDefs': [
{ 'orderable': false, 'targets': 0 }, { 'orderable': false, 'targets': 0 },

View File

@@ -17,6 +17,7 @@
{ 'visible': false, 'targets': 14 }, { 'visible': false, 'targets': 14 },
{ 'visible': false, 'targets': 15 }, { 'visible': false, 'targets': 15 },
{ 'visible': false, 'targets': 16 }, { 'visible': false, 'targets': 16 },
{ 'visible': false, 'targets': 17 },
{ "type": "num", "targets": 3 }, { "type": "num", "targets": 3 },
{ "type": "html-num-fmt", "targets": 9 }, { "type": "html-num-fmt", "targets": 9 },
{ "type": "html-num-fmt", "targets": 10 }, { "type": "html-num-fmt", "targets": 10 },
@@ -29,6 +30,7 @@
$('#stock-overview-table tbody').removeClass("d-none"); $('#stock-overview-table tbody').removeClass("d-none");
stockOverviewTable.columns.adjust().draw(); stockOverviewTable.columns.adjust().draw();
LoadImagesLazy();
$("#location-filter").on("change", function() $("#location-filter").on("change", function()
{ {
@@ -104,7 +106,7 @@ $("#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) $(document).on('click', '.product-grocycode-label-print', function(e)
{ {
e.preventDefault(); e.preventDefault();
document.activeElement.blur(); document.activeElement.blur();

View File

@@ -27,6 +27,11 @@ if (BoolVal(Grocy.UserSettings.stock_default_consume_amount_use_quick_consume_am
$("#stock_default_consume_amount").attr("disabled", ""); $("#stock_default_consume_amount").attr("disabled", "");
} }
if (BoolVal(Grocy.UserSettings.stock_auto_decimal_separator_prices))
{
$("#stock_auto_decimal_separator_prices").prop("checked", true);
}
RefreshLocaleNumberInput(); RefreshLocaleNumberInput();
$("#stock_default_consume_amount_use_quick_consume_amount").on("click", function() $("#stock_default_consume_amount_use_quick_consume_amount").on("click", function()

View File

@@ -99,6 +99,7 @@ $app->group('', function (RouteCollectorProxy $group) {
$group->get('/chores', '\Grocy\Controllers\ChoresController:ChoresList'); $group->get('/chores', '\Grocy\Controllers\ChoresController:ChoresList');
$group->get('/chore/{choreId}', '\Grocy\Controllers\ChoresController:ChoreEditForm'); $group->get('/chore/{choreId}', '\Grocy\Controllers\ChoresController:ChoreEditForm');
$group->get('/choressettings', '\Grocy\Controllers\ChoresController:ChoresSettings'); $group->get('/choressettings', '\Grocy\Controllers\ChoresController:ChoresSettings');
$group->get('/chore/{choreId}/grocycode', '\Grocy\Controllers\ChoresController:ChoreGrocycodeImage');
} }
// Battery routes // Battery routes
@@ -110,6 +111,7 @@ $app->group('', function (RouteCollectorProxy $group) {
$group->get('/batteries', '\Grocy\Controllers\BatteriesController:BatteriesList'); $group->get('/batteries', '\Grocy\Controllers\BatteriesController:BatteriesList');
$group->get('/battery/{batteryId}', '\Grocy\Controllers\BatteriesController:BatteryEditForm'); $group->get('/battery/{batteryId}', '\Grocy\Controllers\BatteriesController:BatteryEditForm');
$group->get('/batteriessettings', '\Grocy\Controllers\BatteriesController:BatteriesSettings'); $group->get('/batteriessettings', '\Grocy\Controllers\BatteriesController:BatteriesSettings');
$group->get('/battery/{batteryId}/grocycode', '\Grocy\Controllers\BatteriesController:BatteryGrocycodeImage');
} }
// Task routes // Task routes
@@ -225,6 +227,7 @@ $app->group('/api', function (RouteCollectorProxy $group) {
$group->get('/recipes/{recipeId}/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment'); $group->get('/recipes/{recipeId}/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment');
$group->post('/recipes/{recipeId}/consume', '\Grocy\Controllers\RecipesApiController:ConsumeRecipe'); $group->post('/recipes/{recipeId}/consume', '\Grocy\Controllers\RecipesApiController:ConsumeRecipe');
$group->get('/recipes/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment'); $group->get('/recipes/fulfillment', '\Grocy\Controllers\RecipesApiController:GetRecipeFulfillment');
$group->Post('/recipes/{recipeId}/copy', '\Grocy\Controllers\RecipesApiController:CopyRecipe');
// Chores // Chores
$group->get('/chores', '\Grocy\Controllers\ChoresApiController:Current'); $group->get('/chores', '\Grocy\Controllers\ChoresApiController:Current');
@@ -232,6 +235,7 @@ $app->group('/api', function (RouteCollectorProxy $group) {
$group->post('/chores/{choreId}/execute', '\Grocy\Controllers\ChoresApiController:TrackChoreExecution'); $group->post('/chores/{choreId}/execute', '\Grocy\Controllers\ChoresApiController:TrackChoreExecution');
$group->post('/chores/executions/{executionId}/undo', '\Grocy\Controllers\ChoresApiController:UndoChoreExecution'); $group->post('/chores/executions/{executionId}/undo', '\Grocy\Controllers\ChoresApiController:UndoChoreExecution');
$group->post('/chores/executions/calculate-next-assignments', '\Grocy\Controllers\ChoresApiController:CalculateNextExecutionAssignments'); $group->post('/chores/executions/calculate-next-assignments', '\Grocy\Controllers\ChoresApiController:CalculateNextExecutionAssignments');
$group->get('/chores/{choreId}/printlabel', '\Grocy\Controllers\ChoresApiController:ChorePrintLabel');
//Printing //Printing
$group->get('/print/shoppinglist/thermal', '\Grocy\Controllers\PrintApiController:PrintShoppingListThermal'); $group->get('/print/shoppinglist/thermal', '\Grocy\Controllers\PrintApiController:PrintShoppingListThermal');
@@ -241,6 +245,7 @@ $app->group('/api', function (RouteCollectorProxy $group) {
$group->get('/batteries/{batteryId}', '\Grocy\Controllers\BatteriesApiController:BatteryDetails'); $group->get('/batteries/{batteryId}', '\Grocy\Controllers\BatteriesApiController:BatteryDetails');
$group->post('/batteries/{batteryId}/charge', '\Grocy\Controllers\BatteriesApiController:TrackChargeCycle'); $group->post('/batteries/{batteryId}/charge', '\Grocy\Controllers\BatteriesApiController:TrackChargeCycle');
$group->post('/batteries/charge-cycles/{chargeCycleId}/undo', '\Grocy\Controllers\BatteriesApiController:UndoChargeCycle'); $group->post('/batteries/charge-cycles/{chargeCycleId}/undo', '\Grocy\Controllers\BatteriesApiController:UndoChargeCycle');
$group->get('/batteries/{batteryId}/printlabel', '\Grocy\Controllers\BatteriesApiController:BatteryPrintLabel');
// Tasks // Tasks
$group->get('/tasks', '\Grocy\Controllers\TasksApiController:Current'); $group->get('/tasks', '\Grocy\Controllers\TasksApiController:Current');

View File

@@ -145,6 +145,7 @@ class DemoDataGeneratorService extends BaseService
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$saturdayThisWeek}', 2); INSERT INTO meal_plan(day, recipe_id) VALUES ('{$saturdayThisWeek}', 2);
INSERT INTO meal_plan(day, recipe_id) VALUES ('{$sundayThisWeek}', 4); INSERT INTO meal_plan(day, recipe_id) VALUES ('{$sundayThisWeek}', 4);
INSERT INTO meal_plan(day, type, note) VALUES ('{$tuesdayThisWeek}', 'note', '{$this->__t_sql('This is a note')}'); INSERT INTO meal_plan(day, type, note) VALUES ('{$tuesdayThisWeek}', 'note', '{$this->__t_sql('This is a note')}');
INSERT INTO meal_plan(day, type, product_id, product_amount) VALUES (DATE('{$mondayThisWeek}', '-1 days'), 'product', 3, 1);
INSERT INTO chores (name, period_type, period_days) VALUES ('{$this->__t_sql('Changed towels in the bathroom')}', 'manually', 5); --1 INSERT INTO chores (name, period_type, period_days) VALUES ('{$this->__t_sql('Changed towels in the bathroom')}', 'manually', 5); --1
INSERT INTO chores (name, period_type, period_days, assignment_type, assignment_config, next_execution_assigned_to_user_id) VALUES ('{$this->__t_sql('Cleaned the kitchen floor')}', 'dynamic-regular', 7, 'random', '1,2,3,4', 1); --2 INSERT INTO chores (name, period_type, period_days, assignment_type, assignment_config, next_execution_assigned_to_user_id) VALUES ('{$this->__t_sql('Cleaned the kitchen floor')}', 'dynamic-regular', 7, 'random', '1,2,3,4', 1); --2

View File

@@ -112,6 +112,23 @@ class RecipesService extends BaseService
} }
} }
public function CopyRecipe($recipeId)
{
if (!$this->RecipeExists($recipeId))
{
throw new \Exception('Recipe does not exist');
}
$newName = $this->getLocalizationService()->__t('Copy of %s', $this->getDataBase()->recipes($recipeId)->name);
$this->getDatabaseService()->ExecuteDbStatement('INSERT INTO recipes (name, description, picture_file_name, base_servings, desired_servings, not_check_shoppinglist, type, product_id) SELECT \'' . $newName . '\', description, picture_file_name, base_servings, desired_servings, not_check_shoppinglist, type, product_id FROM recipes WHERE id = ' . $recipeId);
$lastInsertId = $this->getDatabase()->lastInsertId();
$this->getDatabaseService()->ExecuteDbStatement('INSERT INTO recipes_pos (recipe_id, product_id, amount, note, qu_id, only_check_single_unit_in_stock, ingredient_group, not_check_stock_fulfillment, variable_amount, price_factor) SELECT ' . $lastInsertId . ', product_id, amount, note, qu_id, only_check_single_unit_in_stock, ingredient_group, not_check_stock_fulfillment, variable_amount, price_factor FROM recipes_pos WHERE recipe_id = ' . $recipeId);
$this->getDatabaseService()->ExecuteDbStatement('INSERT INTO recipes_nestings (recipe_id, includes_recipe_id, servings) SELECT ' . $lastInsertId . ', includes_recipe_id, servings FROM recipes_nestings WHERE recipe_id = ' . $recipeId);
return $lastInsertId;
}
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();

View File

@@ -193,7 +193,7 @@ class StockService extends BaseService
]); ]);
$stockRow->save(); $stockRow->save();
if (GROCY_FEATURE_FLAG_LABELPRINTER && GROCY_LABEL_PRINTER_RUN_SERVER && $runWebhook) if (GROCY_FEATURE_FLAG_LABEL_PRINTER && GROCY_LABEL_PRINTER_RUN_SERVER && $runWebhook)
{ {
$reps = 1; $reps = 1;
if ($runWebhook == 2) if ($runWebhook == 2)
@@ -208,7 +208,7 @@ class StockService extends BaseService
if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING) if (GROCY_FEATURE_FLAG_STOCK_BEST_BEFORE_DATE_TRACKING)
{ {
$webhookData['duedate'] = $this->getLocalizationService()->__t('DD') . ': ' . $bestBeforeDate; $webhookData['due_date'] = $this->getLocalizationService()->__t('DD') . ': ' . $bestBeforeDate;
} }
$runner = new WebhookRunner(); $runner = new WebhookRunner();

View File

@@ -48,6 +48,22 @@
</select> </select>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-clock"></i>&nbsp;{{ $__t('Date range') }}</span>
</div>
<select class="custom-control custom-select"
id="daterange-filter">
<option value="1">{{ $__n(1, '%s month', '%s months') }}</option>
<option value="6">{{ $__n(6, '%s month', '%s months') }}</option>
<option value="12">{{ $__n(1, '%s year', '%s years') }}</option>
<option value="24"
selected>{{ $__n(2, '%s month', '%s years') }}</option>
<option value="9999">{{ $__t('All') }}</option>
</select>
</div>
</div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button"
@@ -81,7 +97,7 @@
<tr id="charge-cycle-{{ $chargeCycleEntry->id }}-row" <tr id="charge-cycle-{{ $chargeCycleEntry->id }}-row"
class="@if($chargeCycleEntry->undone == 1) text-muted @endif"> class="@if($chargeCycleEntry->undone == 1) text-muted @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-secondary btn-sm undo-battery-execution-button @if($chargeCycleEntry->undone == 1) disabled @endif permission-BATTERIES_UNDO_CHARGE_CYCLE" <a class="btn btn-secondary btn-xs undo-battery-execution-button @if($chargeCycleEntry->undone == 1) disabled @endif permission-BATTERIES_UNDO_CHARGE_CYCLE"
href="#" href="#"
data-charge-cycle-id="{{ $chargeCycleEntry->id }}" data-charge-cycle-id="{{ $chargeCycleEntry->id }}"
data-toggle="tooltip" data-toggle="tooltip"

View File

@@ -145,6 +145,20 @@
href="{{ $U('/battery/') }}{{ $currentBatteryEntry->battery_id }}?embedded"> href="{{ $U('/battery/') }}{{ $currentBatteryEntry->battery_id }}?embedded">
<span class="dropdown-item-text">{{ $__t('Edit battery') }}</span> <span class="dropdown-item-text">{{ $__t('Edit battery') }}</span>
</a> </a>
<div class="dropdown-divider"></div>
<a class="dropdown-item stockentry-grocycode-link"
type="button"
href="{{ $U('/battery/' . $currentBatteryEntry->battery_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Battery'))) !!}
</a>
@if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item battery-grocycode-label-print"
data-battery-id="{{ $currentBatteryEntry->battery_id }}"
type="button"
href="#">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Battery'))) !!}
</a>
@endif
</div> </div>
</div> </div>
</td> </td>

View File

@@ -95,4 +95,35 @@
</form> </form>
</div> </div>
</div> </div>
<div class="row mt-2 border-top">
<div class="col clearfix mt-2">
<div class="title-related-links">
<h4>
<span class="ls-n1">{{ $__t('grocycode') }}</span>
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode', $__t('Battery')) }}"></i>
</h4>
<p>
@if($mode == 'edit')
<img src="{{ $U('/battery/' . $battery->id . '/grocycode?size=60') }}"
class="float-lg-left">
@endif
</p>
<p>
<a class="btn btn-outline-primary btn-sm"
href="{{ $U('/battery/' . $battery->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a>
@if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="btn btn-outline-primary btn-sm battery-grocycode-label-print"
data-battery-id="{{ $battery->id }}"
href="#">
{{ $__t('Print on label printer') }}
</a>
@endif
</p>
</div>
</div>
</div>
@stop @stop

View File

@@ -15,11 +15,17 @@
novalidate> novalidate>
<div class="form-group"> <div class="form-group">
<label for="battery_id">{{ $__t('Battery') }}</label> <label class="w-100"
<select class="form-control combobox" for="battery_id">
{{ $__t('Battery') }}
<i id="barcode-lookup-hint"
class="fas fa-barcode float-right mt-1"></i>
</label>
<select class="form-control combobox barcodescanner-input"
id="battery_id" id="battery_id"
name="battery_id" name="battery_id"
required> required
data-target="@batterypicker">
<option value=""></option> <option value=""></option>
@foreach($batteries as $battery) @foreach($batteries as $battery)
<option value="{{ $battery->id }}">{{ $battery->name }}</option> <option value="{{ $battery->id }}">{{ $battery->name }}</option>
@@ -48,4 +54,6 @@
@include('components.batterycard') @include('components.batterycard')
</div> </div>
</div> </div>
@include('components.barcodescanner')
@stop @stop

View File

@@ -289,5 +289,38 @@
</form> </form>
</div> </div>
<div class="col-lg-6 col-12 @if($mode == 'create') d-none @endif">
<div class="row">
<div class="col clearfix">
<div class="title-related-links">
<h4>
<span class="ls-n1">{{ $__t('grocycode') }}</span>
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode', $__t('Chore')) }}"></i>
</h4>
<p>
@if($mode == 'edit')
<img src="{{ $U('/chore/' . $chore->id . '/grocycode?size=60') }}"
class="float-lg-left">
@endif
</p>
<p>
<a class="btn btn-outline-primary btn-sm"
href="{{ $U('/chore/' . $chore->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a>
@if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="btn btn-outline-primary btn-sm chore-grocycode-label-print"
data-chore-id="{{ $chore->id }}"
href="#">
{{ $__t('Print on label printer') }}
</a>
@endif
</p>
</div>
</div>
</div>
</div>
</div> </div>
@stop @stop

View File

@@ -48,6 +48,22 @@
</select> </select>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-clock"></i>&nbsp;{{ $__t('Date range') }}</span>
</div>
<select class="custom-control custom-select"
id="daterange-filter">
<option value="1">{{ $__n(1, '%s month', '%s months') }}</option>
<option value="6">{{ $__n(6, '%s month', '%s months') }}</option>
<option value="12"
selected>{{ $__n(1, '%s year', '%s years') }}</option>
<option value="24">{{ $__n(2, '%s month', '%s years') }}</option>
<option value="9999">{{ $__t('All') }}</option>
</select>
</div>
</div>
<div class="col"> <div class="col">
<div class="float-right"> <div class="float-right">
<a id="clear-filter-button" <a id="clear-filter-button"
@@ -88,7 +104,7 @@
<tr id="chore-execution-{{ $choreLogEntry->id }}-row" <tr id="chore-execution-{{ $choreLogEntry->id }}-row"
class="@if($choreLogEntry->undone == 1) text-muted @endif"> class="@if($choreLogEntry->undone == 1) text-muted @endif">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-secondary btn-sm undo-chore-execution-button permission-CHORE_UNDO_EXECUTION @if($choreLogEntry->undone == 1) disabled @endif" <a class="btn btn-secondary btn-xs undo-chore-execution-button permission-CHORE_UNDO_EXECUTION @if($choreLogEntry->undone == 1) disabled @endif"
href="#" href="#"
data-execution-id="{{ $choreLogEntry->id }}" data-execution-id="{{ $choreLogEntry->id }}"
data-toggle="tooltip" data-toggle="tooltip"

View File

@@ -168,6 +168,20 @@
href="{{ $U('/chore/') }}{{ $curentChoreEntry->chore_id }}"> href="{{ $U('/chore/') }}{{ $curentChoreEntry->chore_id }}">
<span class="dropdown-item-text">{{ $__t('Edit chore') }}</span> <span class="dropdown-item-text">{{ $__t('Edit chore') }}</span>
</a> </a>
<div class="dropdown-divider"></div>
<a class="dropdown-item stockentry-grocycode-link"
type="button"
href="{{ $U('/chore/' . $curentChoreEntry->chore_id . '/grocycode?download=true') }}">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Chore'))) !!}
</a>
@if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item chore-grocycode-label-print"
data-chore-id="{{ $curentChoreEntry->chore_id }}"
type="button"
href="#">
{!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Chore'))) !!}
</a>
@endif
</div> </div>
</div> </div>
</td> </td>

View File

@@ -15,11 +15,17 @@
novalidate> novalidate>
<div class="form-group"> <div class="form-group">
<label for="chore_id">{{ $__t('Chore') }}</label> <label class="w-100"
<select class="form-control combobox" for="chore_id">
{{ $__t('Chore') }}
<i id="barcode-lookup-hint"
class="fas fa-barcode float-right mt-1"></i>
</label>
<select class="form-control combobox barcodescanner-input"
id="chore_id" id="chore_id"
name="chore_id" name="chore_id"
required> required
data-target="@chorepicker">
<option value=""></option> <option value=""></option>
@foreach($chores as $chore) @foreach($chores as $chore)
<option value="{{ $chore->id }}">{{ $chore->name }}</option> <option value="{{ $chore->id }}">{{ $chore->name }}</option>
@@ -67,4 +73,6 @@
@include('components.chorecard') @include('components.chorecard')
</div> </div>
</div> </div>
@include('components.barcodescanner')
@stop @stop

View File

@@ -18,8 +18,7 @@
@php if(empty($additionalGroupCssClasses)) { $additionalGroupCssClasses = ''; } @endphp @php if(empty($additionalGroupCssClasses)) { $additionalGroupCssClasses = ''; } @endphp
@php if(empty($activateNumberPad)) { $activateNumberPad = false; } @endphp @php if(empty($activateNumberPad)) { $activateNumberPad = false; } @endphp
<div class="datetimepicker-wrapper" <div class="datetimepicker-wrapper form-group {{ $additionalGroupCssClasses }}">
class="form-group {{ $additionalGroupCssClasses }}">
<label for="{{ $id }}">{{ $__t($label) }} <label for="{{ $id }}">{{ $__t($label) }}
@if(!empty($hint)) @if(!empty($hint))
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted"

View File

@@ -17,8 +17,7 @@
@php if(empty($additionalAttributes)) { $additionalAttributes = ''; } @endphp @php if(empty($additionalAttributes)) { $additionalAttributes = ''; } @endphp
@php if(empty($additionalGroupCssClasses)) { $additionalGroupCssClasses = ''; } @endphp @php if(empty($additionalGroupCssClasses)) { $additionalGroupCssClasses = ''; } @endphp
<div class="datetimepicker2-wrapper" <div class="datetimepicker2-wrapper form-group {{ $additionalGroupCssClasses }}">
class="form-group {{ $additionalGroupCssClasses }}">
<label for="{{ $id }}">{{ $__t($label) }} <label for="{{ $id }}">{{ $__t($label) }}
@if(!empty($hint)) @if(!empty($hint))
&nbsp;<i class="fas fa-question-circle text-muted" &nbsp;<i class="fas fa-question-circle text-muted"

View File

@@ -85,14 +85,22 @@
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-sm btn-danger hide-when-embedded hide-on-fullscreen-card equipment-delete-button" <div class="dropdown d-inline-block">
href="#" <button class="btn btn-sm btn-light text-secondary"
data-equipment-id="{{ $equipmentItem->id }}" type="button"
data-equipment-name="{{ $equipmentItem->name }}" data-toggle="dropdown">
data-toggle="tooltip" <i class="fas fa-ellipsis-v"></i>
title="{{ $__t('Delete this item') }}"> </button>
<i class="fas fa-trash"></i> <div class="table-inline-menu dropdown-menu dropdown-menu-right hide-on-fullscreen-card hide-when-embedded">
</a> <a class="dropdown-item equipment-delete-button"
type="button"
href="#"
data-equipment-id="{{ $equipmentItem->id }}"
data-equipment-name="{{ $equipmentItem->name }}">
<span class="dropdown-item-text">{{ $__t('Delete this item') }}</span>
</a>
</div>
</div>
</td> </td>
<td> <td>
{{ $equipmentItem->name }} {{ $equipmentItem->name }}

View File

@@ -97,7 +97,7 @@
Grocy.LocalizationStrings = {!! $LocalizationStrings !!}; Grocy.LocalizationStrings = {!! $LocalizationStrings !!};
Grocy.FeatureFlags = {!! json_encode($featureFlags) !!}; Grocy.FeatureFlags = {!! json_encode($featureFlags) !!};
Grocy.Webhooks = { Grocy.Webhooks = {
@if(GROCY_FEATURE_FLAG_LABELPRINTER && !GROCY_LABEL_PRINTER_RUN_SERVER) @if(GROCY_FEATURE_FLAG_LABEL_PRINTER && !GROCY_LABEL_PRINTER_RUN_SERVER)
"labelprinter" : { "labelprinter" : {
"hook" : "{{ GROCY_LABEL_PRINTER_WEBHOOK}}", "hook" : "{{ GROCY_LABEL_PRINTER_WEBHOOK}}",
"extra_data" : {!! json_encode(GROCY_LABEL_PRINTER_PARAMS) !!} "extra_data" : {!! json_encode(GROCY_LABEL_PRINTER_PARAMS) !!}

View File

@@ -21,6 +21,13 @@
.img-fluid { .img-fluid {
max-width: 90%; max-width: 90%;
max-height: 140px;
}
@media (min-width: 400px) {
.table-inline-menu.dropdown-menu {
width: 200px !important;
}
} }
</style> </style>
@@ -220,4 +227,21 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade"
id="mealplan-productcard-modal"
tabindex="-1">
<div class="modal-dialog">
<div class="modal-content text-center">
<div class="modal-body">
@include('components.productcard')
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Close') }}</button>
</div>
</div>
</div>
</div>
@stop @stop

View File

@@ -425,7 +425,7 @@
'entity' => 'products' 'entity' => 'products'
)) ))
@if(GROCY_FEATURE_FLAG_LABELPRINTER) @if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<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'
@@ -616,11 +616,11 @@
<div class="col clearfix"> <div class="col clearfix">
<div class="title-related-links"> <div class="title-related-links">
<h4> <h4>
{{ $__t('grocycode') }} <span class="ls-n1">{{ $__t('grocycode') }}</span>
<i class="fas fa-question-circle text-muted" <i class="fas fa-question-circle text-muted"
data-toggle="tooltip" data-toggle="tooltip"
data-trigger="hover click" data-trigger="hover click"
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> title="{{ $__t('grocycode is a unique referer to this %s in your grocy instance - print it onto a label and scan it like any other barcode', $__t('Product')) }}"></i>
</h4> </h4>
<p> <p>
@if($mode == 'edit') @if($mode == 'edit')
@@ -631,8 +631,8 @@
<p> <p>
<a class="btn btn-outline-primary btn-sm" <a class="btn btn-outline-primary btn-sm"
href="{{ $U('/product/' . $product->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a> href="{{ $U('/product/' . $product->id . '/grocycode?download=true') }}">{{ $__t('Download') }}</a>
@if(GROCY_FEATURE_FLAG_LABELPRINTER) @if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="btn btn-outline-primary btn-sm stockentry-grocycode-product-label-print" <a class="btn btn-outline-primary btn-sm product-grocycode-label-print"
data-product-id="{{ $product->id }}" data-product-id="{{ $product->id }}"
href="#"> href="#">
{{ $__t('Print on label printer') }} {{ $__t('Print on label printer') }}

View File

@@ -148,7 +148,7 @@
)) ))
@endif @endif
@if(GROCY_FEATURE_FLAG_LABELPRINTER) @if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<div class="form-group"> <div class="form-group">
<label for="print_stock_label">{{ $__t('Stock entry label') }}</label> <label for="print_stock_label">{{ $__t('Stock entry label') }}</label>
<select class="form-control" <select class="form-control"

View File

@@ -142,14 +142,28 @@
title="{{ $__t('Edit this item') }}"> title="{{ $__t('Edit this item') }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-sm btn-danger hide-when-embedded hide-on-fullscreen-card recipe-delete" <div class="dropdown d-inline-block">
href="#" <button class="btn btn-sm btn-light text-secondary"
data-recipe-id="{{ $recipe->id }}" type="button"
data-recipe-name="{{ $recipe->name }}" data-toggle="dropdown">
data-toggle="tooltip" <i class="fas fa-ellipsis-v"></i>
title="{{ $__t('Delete this item') }}"> </button>
<i class="fas fa-trash"></i> <div class="table-inline-menu dropdown-menu dropdown-menu-right hide-on-fullscreen-card hide-when-embedded">
</a> <a class="dropdown-item recipe-delete"
type="button"
href="#"
data-recipe-id="{{ $recipe->id }}"
data-recipe-name="{{ $recipe->name }}">
<span class="dropdown-item-text">{{ $__t('Delete this item') }}</span>
</a>
<a class="dropdown-item recipe-copy"
type="button"
href="#"
data-recipe-id="{{ $recipe->id }}">
<span class="dropdown-item-text">{{ $__t('Copy recipe') }}</span>
</a>
</div>
</div>
</td> </td>
<td> <td>
{{ $recipe->name }} {{ $recipe->name }}

View File

@@ -21,6 +21,11 @@
<h2 class="title mr-2 order-0"> <h2 class="title mr-2 order-0">
@yield('title') @yield('title')
</h2> </h2>
@if(GROCY_FEATURE_FLAG_STOCK_PRICE_TRACKING)
<h2 class="mb-0 mr-auto order-3 order-md-1 width-xs-sm-100">
<span class="text-muted small">{!! $__t('%s total value', '<span class="locale-number locale-number-currency">' . SumArrayValue($listItems, 'last_price_total') . '</span>') !!}</span>
</h2>
@endif
<div class="float-right"> <div class="float-right">
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3"
type="button" type="button"
@@ -313,7 +318,7 @@
class="d-none mr-auto"></span> class="d-none mr-auto"></span>
<button id="shopping-list-stock-add-workflow-skip-button" <button id="shopping-list-stock-add-workflow-skip-button"
type="button" type="button"
class="btn btn-primary"><i class="fas fa-angle-double-right"></i> {{ $__t('Skip') }}</button> class="btn btn-primary">{{ $__t('Skip') }}</button>
<button type="button" <button type="button"
class="btn btn-secondary" class="btn btn-secondary"
data-dismiss="modal">{{ $__t('Close') }}</button> data-dismiss="modal">{{ $__t('Close') }}</button>

View File

@@ -202,21 +202,21 @@
<a class="dropdown-item stockentry-grocycode-link" <a class="dropdown-item stockentry-grocycode-link"
type="button" type="button"
href="{{ $U('/stockentry/' . $stockEntry->id . '/grocycode?download=true') }}"> href="{{ $U('/stockentry/' . $stockEntry->id . '/grocycode?download=true') }}">
{{ $__t('Download stock entry grocycode') }} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Stock entry'))) !!}
</a> </a>
@if(GROCY_FEATURE_FLAG_LABELPRINTER) @if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item stockentry-grocycode-stockentry-label-print" <a class="dropdown-item stockentry-grocycode-label-print"
data-stock-id="{{ $stockEntry->id }}" data-stock-id="{{ $stockEntry->id }}"
type="button" type="button"
href="#"> href="#">
{{ $__t('Print stock entry grocycode on label printer') }} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Stock entry'))) !!}
</a> </a>
@endif @endif
<a class="dropdown-item stockentry-label-link" <a class="dropdown-item stockentry-label-link"
type="button" type="button"
target="_blank" target="_blank"
href="{{ $U('/stockentry/' . $stockEntry->id . '/label') }}"> href="{{ $U('/stockentry/' . $stockEntry->id . '/label') }}">
{{ $__t('Open stock entry print label in new window') }} {{ $__t('Open stock entry label in new window') }}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -14,7 +14,7 @@
data-target="#table-filter-row"> data-target="#table-filter-row">
<i class="fas fa-filter"></i> <i class="fas fa-filter"></i>
</button> </button>
<button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3" <button class="btn btn-outline-dark d-md-none mt-2 order-1 order-md-3 hide-when-embedded"
type="button" type="button"
data-toggle="collapse" data-toggle="collapse"
data-target="#related-links"> data-target="#related-links">
@@ -45,7 +45,7 @@
placeholder="{{ $__t('Search') }}"> placeholder="{{ $__t('Search') }}">
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-2"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Product') }}</span>
@@ -73,7 +73,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-2"> <div class="col-12 col-md-6 col-xl-3">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Location') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('Location') }}</span>
@@ -87,7 +87,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-2"> <div class="col-12 col-md-6 col-xl-2 mt-1">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('User') }}</span> <span class="input-group-text"><i class="fas fa-filter"></i>&nbsp;{{ $__t('User') }}</span>
@@ -101,6 +101,22 @@
</select> </select>
</div> </div>
</div> </div>
<div class="col-12 col-md-6 col-xl-3 mt-1">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-clock"></i>&nbsp;{{ $__t('Date range') }}</span>
</div>
<select class="custom-control custom-select"
id="daterange-filter">
<option value="1">{{ $__n(1, '%s month', '%s months') }}</option>
<option value="6"
selected>{{ $__n(6, '%s month', '%s months') }}</option>
<option value="12">{{ $__n(1, '%s year', '%s years') }}</option>
<option value="24">{{ $__n(2, '%s month', '%s years') }}</option>
<option value="9999">{{ $__t('All') }}</option>
</select>
</div>
</div>
<div class="col"> <div class="col">
<div class="float-right mt-1"> <div class="float-right mt-1">
<a id="clear-filter-button" <a id="clear-filter-button"
@@ -139,7 +155,7 @@
class="@if($stockLogEntry->undone == 1) text-muted @endif stock-booking-correlation-{{ $stockLogEntry->correlation_id }}" class="@if($stockLogEntry->undone == 1) text-muted @endif stock-booking-correlation-{{ $stockLogEntry->correlation_id }}"
data-correlation-id="{{ $stockLogEntry->correlation_id }}"> data-correlation-id="{{ $stockLogEntry->correlation_id }}">
<td class="fit-content border-right"> <td class="fit-content border-right">
<a class="btn btn-secondary btn-sm undo-stock-booking-button @if($stockLogEntry->undone == 1) disabled @endif" <a class="btn btn-secondary btn-xs undo-stock-booking-button @if($stockLogEntry->undone == 1) disabled @endif"
href="#" href="#"
data-booking-id="{{ $stockLogEntry->id }}" data-booking-id="{{ $stockLogEntry->id }}"
data-toggle="tooltip" data-toggle="tooltip"

View File

@@ -173,6 +173,7 @@
<th>{{ $__t('Product description') }}</th> <th>{{ $__t('Product description') }}</th>
<th>{{ $__t('Parent product') }}</th> <th>{{ $__t('Parent product') }}</th>
<th>{{ $__t('Default location') }}</th> <th>{{ $__t('Default location') }}</th>
<th>{{ $__t('Product picture') }}</th>
@include('components.userfields_thead', array( @include('components.userfields_thead', array(
'userfields' => $userfields 'userfields' => $userfields
@@ -306,14 +307,14 @@
<a class="dropdown-item stockentry-grocycode-link" <a class="dropdown-item stockentry-grocycode-link"
type="button" type="button"
href="{{ $U('/product/' . $currentStockEntry->product_id . '/grocycode?download=true') }}"> href="{{ $U('/product/' . $currentStockEntry->product_id . '/grocycode?download=true') }}">
{{ $__t('Download product grocycode') }} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Download %s grocycode', $__t('Product'))) !!}
</a> </a>
@if(GROCY_FEATURE_FLAG_LABELPRINTER) @if(GROCY_FEATURE_FLAG_LABEL_PRINTER)
<a class="dropdown-item stockentry-grocycode-product-label-print" <a class="dropdown-item product-grocycode-label-print"
data-product-id="{{ $currentStockEntry->product_id }}" data-product-id="{{ $currentStockEntry->product_id }}"
type="button" type="button"
href="#"> href="#">
{{ $__t('Print product grocycode on label printer') }} {!! str_replace('grocycode', '<span class="ls-n1">grocycode</span>', $__t('Print %s grocycode on label printer', $__t('Product'))) !!}
</a> </a>
@endif @endif
</div> </div>
@@ -414,6 +415,12 @@
<td> <td>
{{ $currentStockEntry->product_default_location_name }} {{ $currentStockEntry->product_default_location_name }}
</td> </td>
<td>
@if(!empty($currentStockEntry->product_picture_file_name))
<img data-src="{{ $U('/api/files/productpictures/' . base64_encode($currentStockEntry->product_picture_file_name) . '?force_serve_as=picture&best_fit_width=64&best_fit_height=64') }}"
class="lazy">
@endif
</td>
@include('components.userfields_tbody', array( @include('components.userfields_tbody', array(
'userfields' => $userfields, 'userfields' => $userfields,

View File

@@ -157,6 +157,23 @@
'min' => 0, 'min' => 0,
'additionalCssClasses' => 'user-setting-control' 'additionalCssClasses' => 'user-setting-control'
)) ))
<div class="form-group mt-n3">
<div class="custom-control custom-checkbox">
<input type="checkbox"
class="form-check-input custom-control-input user-setting-control"
id="stock_auto_decimal_separator_prices"
data-setting-key="stock_auto_decimal_separator_prices">
<label class="form-check-label custom-control-label"
for="stock_auto_decimal_separator_prices">
{{ $__t('Add decimal separator automatically for price inputs') }}
<i class="fas fa-question-circle text-muted"
data-toggle="tooltip"
data-trigger="hover click"
title="{{ $__t('When enabled, you always have to enter the value including decimal places, the decimal separator will be automatically added based on the amount of allowed decimal places') }}"></i>
</label>
</div>
</div>
@endif @endif
<a href="{{ $U('/stockoverview') }}" <a href="{{ $U('/stockoverview') }}"