Compare commits

...

27 Commits

Author SHA1 Message Date
Bernd Bestel
e2ebc037f2 Prepared next release 2023-08-06 15:29:28 +02:00
Bernd Bestel
550aa5565b Pulled translations from Transifex 2023-08-06 15:17:38 +02:00
Bernd Bestel
c105ebc979 Enabled Composer optimize-autoloader 2023-08-06 15:11:27 +02:00
Bernd Bestel
7ef744a995 Relate prices on the shopping to the there selected QU (closes #2294) 2023-08-06 14:31:38 +02:00
Bernd Bestel
ee4a082c74 Fixed handling of edited stock entries when calculating a product's average price (references #2292) 2023-08-06 14:05:35 +02:00
Bernd Bestel
1d7f7b2992 Cache expensive stock data calculations 2023-08-06 13:28:14 +02:00
Bernd Bestel
7689356a57 Changed manifest title 2023-08-06 09:28:41 +02:00
Bernd Bestel
1401ed5c00 Updated dependencies 2023-08-06 09:27:41 +02:00
Bernd Bestel
bae4a7f04c Added missing semicolon 2023-08-05 21:51:13 +02:00
Bernd Bestel
a44d746176 Fixed stock_edited_entries view (references #2292) 2023-08-05 21:44:22 +02:00
Bernd Bestel
3afb9643c4 Fix handling when there are no single edited stock entries at all (references #2292) 2023-08-05 18:28:49 +02:00
Bernd Bestel
61a3a4329b Unified edited stock transactions handling (fixes #2292) 2023-08-05 09:58:21 +02:00
Bernd Bestel
491ad8c791 Optimized indentation 2023-08-04 15:44:14 +02:00
Bernd Bestel
339a1ebffc Mention supported browsers in README 2023-08-04 15:41:44 +02:00
Bernd Bestel
1c35fecc85 Added the possibility to skip demo data generation in dev/demo/prerelease mode 2023-08-02 21:10:03 +02:00
Bernd Bestel
d006436d49 Upgraded PHP-CS-Fixer / applied optimized rules 2023-08-02 18:44:30 +02:00
Bernd Bestel
6c4cc00fd5 Added PHP 8.2 support 2023-08-01 21:23:59 +02:00
Bernd Bestel
847337443d Optimized /shoppinglist performance 2023-08-01 20:47:47 +02:00
Bernd Bestel
b74fbddd94 Use a dynamic (title / URL) manifest 2023-08-01 17:12:35 +02:00
Bernd Bestel
8b444a03e5 Simplified initial /mealplan start date handling (fixes #2286) 2023-07-31 21:29:28 +02:00
Bernd Bestel
e946ec79d5 Fixed typo 2023-07-31 18:27:53 +02:00
Bernd Bestel
ca740e8cee Improved product average shelf life calculation (references #2283) 2023-07-31 17:41:36 +02:00
Bernd Bestel
5d48b02b37 Added the possibility to log executed SQL statements (DEV mode only) 2023-07-31 17:08:55 +02:00
Bernd Bestel
73ad9d39ab Workaround for crap product specific QU conversions for migration 0207 (fixes #2285) 2023-07-31 16:58:41 +02:00
Bernd Bestel
fd7e24b7d1 Optimized StockService->GetProductDetails performance (fixes #2283) 2023-07-31 16:54:58 +02:00
Bernd Bestel
57ccb8645e Updated screenshots 2023-07-30 17:18:59 +02:00
Bernd Bestel
e8d6d455f4 grocy-desktop => Grocy Desktop 2023-07-29 21:17:59 +02:00
55 changed files with 1340 additions and 566 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

After

Width:  |  Height:  |  Size: 476 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -4,8 +4,7 @@ $finder = PhpCsFixer\Finder::create()
->exclude(['packages'])
->ignoreVCSIgnored(true)
->files()->name('*.php')
->in(__DIR__)
;
->in(__DIR__);
$cfg = new PhpCsFixer\Config();
return $cfg
@@ -15,81 +14,41 @@ return $cfg
'array_syntax' => ['syntax' => 'short'],
'combine_consecutive_unsets' => true,
'class_attributes_separation' => true,
'class_attributes_separation' => ['elements' => ['const' => 'none', 'property' => 'none']],
'multiline_whitespace_before_semicolons' => false,
'single_quote' => true,
// 'blank_line_after_opening_tag' => true,
// 'blank_line_before_return' => true,
'braces' => [
'allow_single_line_closure' => true,
'position_after_anonymous_constructs' => 'same',
'position_after_control_structures' => 'next',
'position_after_functions_and_oop_constructs' => 'next',
'blank_line_after_opening_tag' => true,
'curly_braces_position' => [
'control_structures_opening_brace' => 'next_line_unless_newline_at_signature_end',
'anonymous_functions_opening_brace' => 'next_line_unless_newline_at_signature_end'
],
'control_structure_continuation_position' => [
'position' => 'next_line'
],
'cast_spaces' => [
'space' => 'none'
],
// 'cast_spaces' => true,
// 'class_definition' => array('singleLine' => true),
'concat_space' => ['spacing' => 'one'],
'declare_equal_normalize' => true,
'function_typehint_space' => true,
'type_declaration_spaces' => true,
'single_line_comment_style' => ['comment_types' => ['hash']],
'include' => true,
'lowercase_cast' => true,
// 'native_function_casing' => true,
// 'new_with_braces' => true,
// 'no_blank_lines_after_class_opening' => true,
// 'no_blank_lines_after_phpdoc' => true,
// 'no_empty_comment' => true,
// 'no_empty_phpdoc' => true,
// 'no_empty_statement' => true,
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
// 'no_mixed_echo_print' => array('use' => 'echo'),
'no_multiline_whitespace_around_double_arrow' => true,
// 'no_short_bool_cast' => true,
// 'no_singleline_whitespace_before_semicolons' => true,
'no_spaces_around_offset' => true,
// 'no_trailing_comma_in_list_call' => true,
// 'no_trailing_comma_in_singleline_array' => true,
// 'no_unneeded_control_parentheses' => true,
// 'no_unused_imports' => true,
'no_whitespace_before_comma_in_array' => true,
'no_whitespace_in_blank_line' => true,
// 'normalize_index_brace' => true,
'object_operator_without_whitespace' => true,
// 'php_unit_fqcn_annotation' => true,
// 'phpdoc_align' => true,
// 'phpdoc_annotation_without_dot' => true,
// 'phpdoc_indent' => true,
// 'phpdoc_inline_tag' => true,
// 'phpdoc_no_access' => true,
// 'phpdoc_no_alias_tag' => true,
// 'phpdoc_no_empty_return' => true,
// 'phpdoc_no_package' => true,
// 'phpdoc_no_useless_inheritdoc' => true,
// 'phpdoc_return_self_reference' => true,
// 'phpdoc_scalar' => true,
// 'phpdoc_separation' => true,
// 'phpdoc_single_line_var_spacing' => true,
// 'phpdoc_summary' => true,
// 'phpdoc_to_comment' => true,
// 'phpdoc_trim' => true,
// 'phpdoc_types' => true,
// 'phpdoc_var_without_name' => true,
// 'pre_increment' => true,
// 'return_type_declaration' => true,
// 'self_accessor' => true,
// 'short_scalar_cast' => true,
'single_blank_line_before_namespace' => true,
// 'single_class_element_per_statement' => true,
// 'space_after_semicolon' => true,
// 'standardize_not_equals' => true,
'blank_lines_before_namespace' => true,
'ternary_operator_spaces' => true,
// 'trailing_comma_in_multiline_array' => true,
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'whitespace_after_comma_in_array' => true,
'no_trailing_comma_in_singleline' => true
])
->setIndent("\t")
->setLineEnding("\n")
->setUsingCache(false)
->setFinder($finder)
;
->setFinder($finder);

View File

@@ -28,7 +28,7 @@ See the website for a list of community contributed Add-ons / Tools. → [htt
## How to install
> Checkout [grocy-desktop](https://github.com/grocy/grocy-desktop), if you want to run Grocy without having to manage a webserver just like a normal (Windows) desktop application.
> Checkout [Grocy Desktop](https://github.com/grocy/grocy-desktop), if you want to run Grocy without having to manage a webserver just like a normal (Windows) desktop application.
>
> Directly download the [latest release](https://releases.grocy.info/latest-desktop) - the installation is nothing more than just clicking 2 times "next".
@@ -47,9 +47,10 @@ Alternatively clone this repository (the `release` branch always references the
### Platform support
- PHP 8.1 (with SQLite 3.34.0+)
- Required PHP extensions: `fileinfo`, `pdo_sqlite`, `gd`, `ctype`, `json`, `intl`, `zlib`, `mbstring`
- _Recommendation: Benchmark tests showed that e.g. unit conversion handling is up to 5 times faster when using a more recent (3.39.4+) SQLite version._
- PHP 8.1 or 8.2 (with SQLite 3.34.0+)
- Required PHP extensions: `fileinfo`, `pdo_sqlite`, `gd`, `ctype`, `json`, `intl`, `zlib`, `mbstring`
- _Recommendation: Benchmark tests showed that e.g. unit conversion handling is up to 5 times faster when using a more recent (3.39.4+) SQLite version._
- Recent Firefox, Chrome or Edge
## How to run using Docker
@@ -142,11 +143,11 @@ If you don't use certain feature sets of Grocy (for example if you don't need "C
### Demo mode
When the `MODE` setting is set to `dev`, `demo` or `prerelease`, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration.
When the `MODE` setting is set to `dev`, `demo` or `prerelease`, the application will work in a demo mode which means authentication is disabled and some demo data will be generated during the database schema migration (pass the query parameter `nodemodata`, e.g. `https://grocy.example.com/?nodemodata` to skip that).
### Embedded mode
When the file `embedded.txt` exists, it must contain a valid and writable path which will be used as the data directory instead of `data` and authentication will be disabled (used in [grocy-desktop](https://github.com/grocy/grocy-desktop)).
When the file `embedded.txt` exists, it must contain a valid and writable path which will be used as the data directory instead of `data` and authentication will be disabled (used in [Grocy Desktop](https://github.com/grocy/grocy-desktop)).
In embedded mode, settings can be overridden by text files in `data/settingoverrides`, the file name must be `<SettingName>.txt` (e. g. `BASE_URL.txt`) and the content must be the setting value (normally one single line).

View File

@@ -62,15 +62,18 @@ AppFactory::setContainer(new DI\Container());
$app = AppFactory::create();
$container = $app->getContainer();
$container->set('view', function (Container $container) {
$container->set('view', function (Container $container)
{
return new Blade(__DIR__ . '/views', GROCY_DATAPATH . '/viewcache');
});
$container->set('UrlManager', function (Container $container) {
$container->set('UrlManager', function (Container $container)
{
return new UrlManager(GROCY_BASE_URL);
});
$container->set('ApiKeyHeaderName', function (Container $container) {
$container->set('ApiKeyHeaderName', function (Container $container)
{
return 'GROCY-API-KEY';
});

View File

@@ -0,0 +1,19 @@
> 💡 PHP 8.2 is from now on (additionally to PHP 8.1) supported.
### Stock
- Fixed performance issues affecting all places where quantity unit conversions / prices are involved
- Fixed that the upgrade failed when having improperly defined product specific quantity unit conversions
- Fixed that edited stock entries were not considered in some cases (affecting the product's last price, average price, the price history and the stock reports)
### Shopping list
- Changed that prices on the shopping list (table columns "Last price (Unit)" and "Last price (Total)") are now related to the there selected quantity unit (instead of to the product's QU stock as before)
### Meal plan
- Fixed that the meal plan did initially not display the current week when the settings `MEAL_PLAN_FIRST_DAY_OF_WEEK` and `CALENDAR_FIRST_DAY_OF_WEEK` were set to different values
### API
- Fixed performance issues on the endpoint `/stock/products/{productId}`

View File

@@ -2,6 +2,8 @@
> ❗ xxxImportant upgrade informationXXX
> 💡 xxxMinor upgrade informationXXX
### New feature: xxxx
- xxx

View File

@@ -6,7 +6,7 @@
"slim/http": "^1.0",
"php-di/php-di": "^7.0.3",
"berrnd/slim-blade-view": "^1.0.0",
"morris/lessql": "dev-php81",
"morris/lessql": "dev-php82",
"gettext/gettext": "dev-php81",
"eluceo/ical": "^2.2.0",
"erusev/parsedown": "^1.7",
@@ -39,6 +39,7 @@
},
"config": {
"vendor-dir": "packages",
"platform-check": false
"platform-check": false,
"optimize-autoloader": true
}
}

560
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,8 +14,9 @@
// The settings defined here below
// Either "production", "dev", "demo" or "prerelease"
// When not "production", authentication will be disabled and
// demo data will be populated during database migrations
// When not "production", the application will work in a demo mode which means
// authentication is disabled and some demo data will be generated during the database schema migration
// (pass the query parameter "nodemodata", e.g. https://grocy.example.com/?nodemodata to skip that)
Setting('MODE', 'production');
// The directory name of one of the available localization folders

View File

@@ -8,9 +8,7 @@ use Psr\Http\Message\ResponseInterface as Response;
class BaseApiController extends BaseController
{
const PATTERN_FIELD = '[A-Za-z_][A-Za-z0-9_]+';
const PATTERN_OPERATOR = '!?((>=)|(<=)|=|~|<|>|(§))';
const PATTERN_VALUE = '[A-Za-z\p{L}\p{M}0-9*_.$#^| -\\\]+';
protected $OpenApiSpec = null;

View File

@@ -29,6 +29,7 @@ class BaseController
}
protected $AppContainer;
private $View;
protected function getApiKeyService()
{
@@ -123,10 +124,12 @@ class BaseController
$this->View->set('version', $versionInfo->Version);
$localizationService = $this->getLocalizationService();
$this->View->set('__t', function (string $text, ...$placeholderValues) use ($localizationService) {
$this->View->set('__t', function (string $text, ...$placeholderValues) use ($localizationService)
{
return $localizationService->__t($text, $placeholderValues);
});
$this->View->set('__n', function ($number, $singularForm, $pluralForm, $isQu = false) use ($localizationService) {
$this->View->set('__n', function ($number, $singularForm, $pluralForm, $isQu = false) use ($localizationService)
{
return $localizationService->__n($number, $singularForm, $pluralForm, $isQu);
});
$this->View->set('LocalizationStrings', $localizationService->GetPoAsJsonString());
@@ -140,7 +143,8 @@ class BaseController
}
$this->View->set('dir', $dir);
$this->View->set('U', function ($relativePath, $isResource = false) use ($container) {
$this->View->set('U', function ($relativePath, $isResource = false) use ($container)
{
return $container->get('UrlManager')->ConstructUrl($relativePath, $isResource);
});

View File

@@ -16,11 +16,11 @@ trait GrocycodeTrait
if (GROCY_GROCYCODE_TYPE == '2D')
{
$png = (new DatamatrixFactory())->setCode((string) $grocycode)->setSize($size)->getDatamatrixPngData();
$png = (new DatamatrixFactory())->setCode((string)$grocycode)->setSize($size)->getDatamatrixPngData();
}
else
{
$png = (new BarcodeFactory())->setType('C128')->setCode((string) $grocycode)->setHeight($size)->getBarcodePngData();
$png = (new BarcodeFactory())->setType('C128')->setCode((string)$grocycode)->setHeight($size)->getBarcodePngData();
}
$isDownload = $request->getQueryParam('download', false);

View File

@@ -64,7 +64,7 @@ class RecipesController extends BaseController
'recipesResolved' => $this->getRecipesService()->GetRecipesResolved("recipe_id IN (SELECT recipe_id FROM meal_plan_internal_recipe_relation WHERE $mealPlanWhereTimespan)"),
'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved(),
'mealplanSections' => $this->getDatabase()->meal_plan_sections()->orderBy('sort_number'),
'usedMealplanSections' => $this->getDatabase()->meal_plan_sections()->where("id IN (SELECT section_id FROM meal_plan WHERE $mealPlanWhereTimespan)")->orderBy('sort_number'),
'weekRecipe' => $this->getDatabase()->recipes()->where("type = 'mealplan-week' AND name = LTRIM(STRFTIME('%Y-%W', DATE('$start')), '0')")->fetch()
@@ -107,7 +107,7 @@ class RecipesController extends BaseController
'quantityUnits' => $this->getDatabase()->quantity_units(),
'userfields' => $this->getUserfieldsService()->GetFields('recipes'),
'userfieldValues' => $this->getUserfieldsService()->GetAllValues('recipes'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved(),
'selectedRecipeTotalCosts' => $totalCosts,
'selectedRecipeTotalCalories' => $totalCalories,
'mealplanSections' => $this->getDatabase()->meal_plan_sections()->orderBy('sort_number')
@@ -162,7 +162,7 @@ class RecipesController extends BaseController
'recipes' => $this->getDatabase()->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->orderBy('name', 'COLLATE NOCASE'),
'recipeNestings' => $this->getDatabase()->recipes_nestings()->where('recipe_id', $recipeId),
'userfields' => $this->getUserfieldsService()->GetFields('recipes'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved()
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved()
]);
}
@@ -176,7 +176,7 @@ class RecipesController extends BaseController
'recipePos' => new \stdClass(),
'products' => $this->getDatabase()->products()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved()
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved()
]);
}
else
@@ -187,7 +187,7 @@ class RecipesController extends BaseController
'recipePos' => $this->getDatabase()->recipes_pos($args['recipePosId']),
'products' => $this->getDatabase()->products()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved()
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved()
]);
}
}

View File

@@ -19,7 +19,7 @@ class StockController extends BaseController
'recipes' => $this->getDatabase()->recipes()->where('type', RecipesService::RECIPE_TYPE_NORMAL)->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved()
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved()
]);
}
@@ -31,7 +31,7 @@ class StockController extends BaseController
'shoppinglocations' => $this->getDatabase()->shopping_locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('stock')
]);
}
@@ -147,7 +147,7 @@ class StockController extends BaseController
'product' => $product,
'shoppinglocations' => $this->getDatabase()->shopping_locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('product_barcodes')
]);
}
@@ -159,7 +159,7 @@ class StockController extends BaseController
'product' => $product,
'shoppinglocations' => $this->getDatabase()->shopping_locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('product_barcodes')
]);
}
@@ -192,8 +192,8 @@ class StockController extends BaseController
'locations' => $this->getDatabase()->locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'barcodes' => $this->getDatabase()->product_barcodes()->orderBy('barcode'),
'quantityunits' => $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsStock' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1) OR NOT EXISTS(SELECT 1 FROM stock_log WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'referencedQuantityunits' => $this->getDatabase()->quantity_units()->where('active = 1')->where('id IN (SELECT to_qu_id FROM quantity_unit_conversions_resolved WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'quantityunitsStock' => $this->getDatabase()->quantity_units()->where('id IN (SELECT to_qu_id FROM cache__quantity_unit_conversions_resolved WHERE product_id = :1) OR NOT EXISTS(SELECT 1 FROM stock_log WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'referencedQuantityunits' => $this->getDatabase()->quantity_units()->where('active = 1')->where('id IN (SELECT to_qu_id FROM cache__quantity_unit_conversions_resolved WHERE product_id = :1)', $product->id)->orderBy('name', 'COLLATE NOCASE'),
'shoppinglocations' => $this->getDatabase()->shopping_locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'productgroups' => $this->getDatabase()->product_groups()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'userfields' => $this->getUserfieldsService()->GetFields('products'),
@@ -289,7 +289,7 @@ class StockController extends BaseController
'shoppinglocations' => $this->getDatabase()->shopping_locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'locations' => $this->getDatabase()->locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('stock')
]);
}
@@ -399,7 +399,7 @@ class StockController extends BaseController
'missingProducts' => $this->getStockService()->GetMissingProducts(),
'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name', 'COLLATE NOCASE'),
'selectedShoppingListId' => $listId,
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved(),
'productUserfields' => $this->getUserfieldsService()->GetFields('products'),
'productUserfieldValues' => $this->getUserfieldsService()->GetAllValues('products'),
'productGroupUserfields' => $this->getUserfieldsService()->GetFields('product_groups'),
@@ -438,7 +438,7 @@ class StockController extends BaseController
'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name', 'COLLATE NOCASE'),
'mode' => 'create',
'quantityUnits' => $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('shopping_list')
]);
}
@@ -451,7 +451,7 @@ class StockController extends BaseController
'shoppingLists' => $this->getDatabase()->shopping_lists()->orderBy('name', 'COLLATE NOCASE'),
'mode' => 'edit',
'quantityUnits' => $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved(),
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved(),
'userfields' => $this->getUserfieldsService()->GetFields('shopping_list')
]);
}
@@ -564,7 +564,7 @@ class StockController extends BaseController
'barcodes' => $this->getDatabase()->product_barcodes_comma_separated(),
'locations' => $this->getDatabase()->locations()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnits' => $this->getDatabase()->quantity_units()->where('active = 1')->orderBy('name', 'COLLATE NOCASE'),
'quantityUnitConversionsResolved' => $this->getDatabase()->quantity_unit_conversions_resolved()
'quantityUnitConversionsResolved' => $this->getDatabase()->cache__quantity_unit_conversions_resolved()
]);
}
@@ -599,11 +599,11 @@ class StockController extends BaseController
if (isset($request->getQueryParams()['product']))
{
$product = $this->getDatabase()->products($request->getQueryParams()['product']);
$quantityUnitConversionsResolved = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id', $product->id);
$quantityUnitConversionsResolved = $this->getDatabase()->cache__quantity_unit_conversions_resolved()->where('product_id', $product->id);
}
else
{
$quantityUnitConversionsResolved = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id IS NULL');
$quantityUnitConversionsResolved = $this->getDatabase()->cache__quantity_unit_conversions_resolved()->where('product_id IS NULL');
}
return $this->renderPage($response, 'quantityunitconversionsresolved', [

View File

@@ -29,7 +29,7 @@ class StockReportsController extends BaseController
pg.id AS id,
pg.name AS name,
SUM(pph.amount * pph.price) AS total
FROM product_price_history pph
FROM products_price_history pph
JOIN products p
ON pph.product_id = p.id
JOIN product_groups pg
@@ -53,7 +53,7 @@ class StockReportsController extends BaseController
pg.id AS group_id,
pg.name AS group_name,
SUM(pph.amount * pph.price) AS total
FROM product_price_history pph
FROM products_price_history pph
JOIN products p
ON pph.product_id = p.id
JOIN product_groups pg

View File

@@ -32,12 +32,34 @@ class SystemController extends BaseController
if (GROCY_MODE === 'dev' || GROCY_MODE === 'demo' || GROCY_MODE === 'prerelease')
{
$demoDataGeneratorService = DemoDataGeneratorService::getInstance();
$demoDataGeneratorService->PopulateDemoData();
$demoDataGeneratorService->PopulateDemoData(isset($request->getQueryParams()['nodemodata']));
}
return $response->withRedirect($this->AppContainer->get('UrlManager')->ConstructUrl($this->GetEntryPageRelative()));
}
public function Manifest(Request $request, Response $response, array $args)
{
$data = explode('#', base64_decode($request->getQueryParams()['data']));
$manifest = [
'name' => 'Grocy ' . $data[0],
'short_name' => 'Grocy ' . $data[0],
'icons' => [[
'src' => './img/icon-1024.png',
'sizes'=> '1024x1024',
'type' => 'image/png'
]],
'start_url' => $data[1],
'background_color' => '#333131',
'theme_color' => '#333131',
'display' => 'standalone'
];
$response->getBody()->write(json_encode($manifest));
return $response->withHeader('Content-Type', 'application/json');
}
private function GetEntryPageRelative()
{
if (defined('GROCY_ENTRY_PAGE'))

View File

@@ -8,63 +8,34 @@ use LessQL\Result;
class User
{
const PERMISSION_ADMIN = 'ADMIN';
const PERMISSION_BATTERIES = 'BATTERIES';
const PERMISSION_BATTERIES_TRACK_CHARGE_CYCLE = 'BATTERIES_TRACK_CHARGE_CYCLE';
const PERMISSION_BATTERIES_UNDO_CHARGE_CYCLE = 'BATTERIES_UNDO_CHARGE_CYCLE';
const PERMISSION_CALENDAR = 'CALENDAR';
const PERMISSION_CHORES = 'CHORES';
const PERMISSION_CHORE_TRACK_EXECUTION = 'CHORE_TRACK_EXECUTION';
const PERMISSION_CHORE_UNDO_EXECUTION = 'CHORE_UNDO_EXECUTION';
const PERMISSION_EQUIPMENT = 'EQUIPMENT';
const PERMISSION_MASTER_DATA_EDIT = 'MASTER_DATA_EDIT';
const PERMISSION_RECIPES = 'RECIPES';
const PERMISSION_RECIPES_MEALPLAN = 'RECIPES_MEALPLAN';
const PERMISSION_SHOPPINGLIST = 'SHOPPINGLIST';
const PERMISSION_SHOPPINGLIST_ITEMS_ADD = 'SHOPPINGLIST_ITEMS_ADD';
const PERMISSION_SHOPPINGLIST_ITEMS_DELETE = 'SHOPPINGLIST_ITEMS_DELETE';
const PERMISSION_STOCK = 'STOCK';
const PERMISSION_STOCK_CONSUME = 'STOCK_CONSUME';
const PERMISSION_STOCK_EDIT = 'STOCK_EDIT';
const PERMISSION_STOCK_INVENTORY = 'STOCK_INVENTORY';
const PERMISSION_STOCK_OPEN = 'STOCK_OPEN';
const PERMISSION_STOCK_PURCHASE = 'STOCK_PURCHASE';
const PERMISSION_STOCK_TRANSFER = 'STOCK_TRANSFER';
const PERMISSION_TASKS = 'TASKS';
const PERMISSION_TASKS_MARK_COMPLETED = 'TASKS_MARK_COMPLETED';
const PERMISSION_TASKS_UNDO_EXECUTION = 'TASKS_UNDO_EXECUTION';
const PERMISSION_USERS = 'USERS';
const PERMISSION_USERS_CREATE = 'USERS_CREATE';
const PERMISSION_USERS_EDIT = 'USERS_EDIT';
const PERMISSION_USERS_EDIT_SELF = 'USERS_EDIT_SELF';
const PERMISSION_USERS_READ = 'USERS_READ';
public function __construct()

View File

@@ -51,7 +51,8 @@ class UsersController extends BaseController
public function UserSettings(Request $request, Response $response, array $args)
{
return $this->renderPage($response, 'usersettings', [
'languages' => array_filter(scandir(__DIR__ . '/../localization'), function ($item) {
'languages' => array_filter(scandir(__DIR__ . '/../localization'), function ($item)
{
if ($item == '.' || $item == '..')
{
return false;

View File

@@ -11,7 +11,6 @@ abstract class BaseBarcodeLookupPlugin
}
protected $Locations;
protected $QuantityUnits;
final public function Lookup($barcode)

View File

@@ -18,13 +18,9 @@ namespace Grocy\Helpers;
class Grocycode
{
public const PRODUCT = 'p';
public const BATTERY = 'b';
public const CHORE = 'c';
public const RECIPE = 'r';
public const MAGIC = 'grcy';
public function __construct(...$args)
@@ -49,11 +45,8 @@ class Grocycode
}
public static $Items = [self::PRODUCT, self::BATTERY, self::CHORE, self::RECIPE];
private $type;
private $id;
private $extra_data = [];
public static function Validate(string $code)

View File

@@ -10,6 +10,7 @@
# Moltivie Denied <major2015usa@gmail.com>, 2022
# Martino Falorni, 2023
# Davide Casella, 2023
# Saul il, 2023
#
msgid ""
msgstr ""
@@ -17,7 +18,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n"
"PO-Revision-Date: 2019-05-01 17:42+0000\n"
"Last-Translator: Davide Casella, 2023\n"
"Last-Translator: Saul il, 2023\n"
"Language-Team: Italian (https://app.transifex.com/grocy/teams/93189/it/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -2768,7 +2769,7 @@ msgid "This is the default quantity unit used when consuming this product"
msgstr ""
msgid "Add to meal plan"
msgstr ""
msgstr "Aggiungi al piano alimentare"
msgid "Successfully added the recipe to the meal plan"
msgstr ""

View File

@@ -7,9 +7,9 @@
# unwarkz <git@unwar.kz>, 2021
# J K <su1ka.box@gmail.com>, 2021
# Pavel Pletenev <cpp.create@gmail.com>, 2021
# Sergey Kodolov, 2022
# Dmitry Galyshev, 2022
# Юрий Куклин, 2022
# Sergey Kodolov, 2023
#
msgid ""
msgstr ""
@@ -17,7 +17,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-05-01T17:59:17+00:00\n"
"PO-Revision-Date: 2019-05-01 17:42+0000\n"
"Last-Translator: Юрий Куклин, 2022\n"
"Last-Translator: Sergey Kodolov, 2023\n"
"Language-Team: Russian (https://app.transifex.com/grocy/teams/93189/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -376,7 +376,7 @@ msgid "Removed %1$s of %2$s from stock"
msgstr "Убрано %1$s из %2$s из запаса"
msgid "About Grocy"
msgstr ""
msgstr "О Grocy"
msgid "Close"
msgstr "Закрыть"
@@ -1535,7 +1535,7 @@ msgid "Price factor"
msgstr "Коэффициент цены"
msgid "Do you find Grocy useful?"
msgstr ""
msgstr "Вы находите Grocy полезным?"
msgid "Say thanks"
msgstr "Сказать спасибо"
@@ -1553,7 +1553,7 @@ msgid "Output"
msgstr "Вывод"
msgid "Energy"
msgstr ""
msgstr "Энергетическая ценность"
msgid "Per stock quantity unit"
msgstr "На единицу измерения в запасе"
@@ -1668,6 +1668,9 @@ msgid ""
"Camera access is only possible when supported and allowed by your browser "
"and when Grocy is served via a secure (https://) connection"
msgstr ""
"Доступ к камере возможен только тогда, когда это поддерживается и разрешено "
"вашим браузером и когда Grocy обслуживается через безопасное (https://) "
"соединение."
msgid "Keep screen on"
msgstr "Держать экран включенным"
@@ -1874,16 +1877,16 @@ msgid "Unauthorized"
msgstr "Не авторизированы"
msgid "Error source"
msgstr ""
msgstr "Источник ошибок"
msgid "Error message"
msgstr "Сообщение об ошибке"
msgid "Stack trace"
msgstr ""
msgstr "Трассировки стека"
msgid "Easy error info copy & paste (for reporting)"
msgstr ""
msgstr "Простое копирование и вставка информации об ошибках (для отчетности)"
msgid "This page does not exist"
msgstr "Эта страница не существует"

View File

@@ -8,7 +8,6 @@ use DI\Container;
class BaseMiddleware
{
protected $AppContainer;
protected $ApplicationService;
public function __construct(Container $container)

View File

@@ -35,9 +35,10 @@ class LocaleMiddleware extends BaseMiddleware
// Src: https://gist.github.com/spolischook/0cde9c6286415cddc088
$prefLocales = array_reduce(
explode(',', $langs),
function ($res, $el) {
function ($res, $el)
{
list($l, $q) = array_merge(explode(';q=', $el), [1]);
$res[$l] = (float) $q;
$res[$l] = (float)$q;
return $res;
},
[]

View File

@@ -3,7 +3,7 @@ DROP VIEW products_current_price;
CREATE VIEW products_last_purchased
AS
select
SELECT
1 AS id, -- Dummy, LessQL needs an id column
sl.product_id,
sl.amount,
@@ -14,20 +14,20 @@ select
sl.shopping_location_id
FROM stock_log sl
JOIN (
SELECT
s1.product_id,
MAX(s1.id) max_stock_id
FROM stock_log s1
JOIN (
SELECT
s.product_id,
MAX(s.purchased_date) max_purchased_date
FROM stock_log s
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s.product_id) sp2
ON s1.product_id = sp2.product_id
AND s1.purchased_date = sp2.max_purchased_date
SELECT
s1.product_id,
MAX(s1.id) max_stock_id
FROM stock_log s1
JOIN (
SELECT
s.product_id,
MAX(s.purchased_date) max_purchased_date
FROM stock_log s
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s.product_id) sp2
ON s1.product_id = sp2.product_id
AND s1.purchased_date = sp2.max_purchased_date
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s1.product_id) sp3

View File

@@ -3,13 +3,14 @@ AS
/*
Returns stock_id's which have been edited manually
*/
SELECT DISTINCT sl_add.stock_id
fROM stock_log sl_add
SELECT sl_add.stock_id
FROM stock_log sl_add
JOIN stock_log sl_edit
ON sl_add.stock_id = sl_edit.stock_id
AND sl_edit.transaction_type = 'stock-edit-new'
WHERE sl_add.transaction_type IN ('purchase', 'inventory-correction', 'self-production')
AND sl_add.amount > 0;
AND sl_add.amount > 0
GROUP BY sl_add.stock_id;
DROP VIEW stock_average_product_shelf_life;
CREATE VIEW stock_average_product_shelf_life
@@ -23,12 +24,11 @@ LEFT JOIN (
sl_p.product_id,
JULIANDAY(sl_p.best_before_date) - JULIANDAY(sl_p.purchased_date) AS shelf_life_days
FROM stock_log sl_p
WHERE (
(sl_p.transaction_type IN ('purchase', 'inventory-correction', 'self-production') AND sl_p.stock_id NOT IN (SELECT stock_id FROM stock_edited_entries))
OR (sl_p.transaction_type = 'stock-edit-new' AND sl_p.stock_id IN (SELECT stock_id FROM stock_edited_entries))
WHERE sl_p.undone = 0
AND (
(sl_p.transaction_type IN ('purchase', 'inventory-correction', 'self-production') AND sl_p.stock_id NOT IN (SELECT stock_id FROM stock_edited_entries))
OR (sl_p.transaction_type = 'stock-edit-new' AND sl_p.stock_id IN (SELECT stock_id FROM stock_edited_entries))
)
AND sl_p.undone = 0
) x
ON p.id = x.product_id
GROUP BY p.id;

View File

@@ -40,8 +40,9 @@ INSERT INTO quantity_unit_conversions
(from_qu_id, to_qu_id, factor, product_id)
SELECT p.qu_id_purchase, p.qu_id_stock, IFNULL(p.qu_factor_purchase_to_stock, 1.0), p.id
FROM products p
WHERE p.qu_id_stock != qu_id_purchase
AND NOT EXISTS(SELECT 1 FROM quantity_unit_conversions WHERE product_id = p.id AND from_qu_id = p.qu_id_stock AND to_qu_id = p.qu_id_purchase);
WHERE p.qu_id_stock != p.qu_id_purchase
AND NOT EXISTS(SELECT 1 FROM quantity_unit_conversions WHERE product_id = p.id AND from_qu_id = p.qu_id_stock AND to_qu_id = p.qu_id_purchase)
AND NOT EXISTS(SELECT 1 FROM quantity_unit_conversions WHERE product_id = p.id AND from_qu_id = p.qu_id_purchase AND to_qu_id = p.qu_id_stock);
-- ALTER TABLE DROP COLUMN is only available in SQLite >= 3.35.0 (we require 3.34.0 as of now), so can't be used
PRAGMA legacy_alter_table = ON;

View File

@@ -115,7 +115,6 @@ AS (
JOIN default_conversions s
ON c.path LIKE ('%/' || s.from_qu_id || '/' || s.to_qu_id || '/%') -- the conversion has been used as part of another path ...
WHERE NOT EXISTS(SELECT 1 FROM conversion_factors ci WHERE ci.product_id = c.product_id AND ci.from_qu_id = s.from_qu_id AND ci.to_qu_id = s.to_qu_id) -- ... and is itself new
)
SELECT DISTINCT
@@ -135,6 +134,5 @@ JOIN quantity_units qu_from
JOIN quantity_units qu_to
ON c.to_qu_id = qu_to.id
GROUP BY product_id, from_qu_id, to_qu_id
WINDOW win
AS (PARTITION BY product_id, from_qu_id, to_qu_id ORDER BY depth ASC)
ORDER BY product_id, from_qu_id, to_qu_id;
WINDOW win AS (PARTITION BY product_id, from_qu_id, to_qu_id ORDER BY depth ASC)
ORDER BY product_id, from_qu_id, to_qu_id;

View File

@@ -15,7 +15,7 @@ GROUP BY s.product_id;
DROP VIEW products_last_purchased;
CREATE VIEW products_last_purchased
AS
select
SELECT
1 AS id, -- Dummy, LessQL needs an id column
sl.product_id,
sl.amount,
@@ -26,20 +26,20 @@ select
sl.shopping_location_id
FROM stock_log sl
JOIN (
SELECT
s1.product_id,
MAX(s1.id) max_stock_id
FROM stock_log s1
JOIN (
SELECT
s.product_id,
MAX(s.purchased_date) max_purchased_date
FROM stock_log s
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s.product_id) sp2
ON s1.product_id = sp2.product_id
AND s1.purchased_date = sp2.max_purchased_date
SELECT
s1.product_id,
MAX(s1.id) max_stock_id
FROM stock_log s1
JOIN (
SELECT
s.product_id,
MAX(s.purchased_date) max_purchased_date
FROM stock_log s
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s.product_id) sp2
ON s1.product_id = sp2.product_id
AND s1.purchased_date = sp2.max_purchased_date
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s1.product_id) sp3

48
migrations/0221.sql Normal file
View File

@@ -0,0 +1,48 @@
CREATE VIEW products_last_price
AS
SELECT
product_id,
MAX(purchased_date) AS purchased_date,
price -- Bare column, ref https://www.sqlite.org/lang_select.html#bare_columns_in_an_aggregate_query
FROM stock_log
WHERE transaction_type IN ('purchase', 'stock-edit-new', 'inventory-correction')
AND IFNULL(price, 0) > 0
AND IFNULL(amount, 0) > 0
AND undone = 0
GROUP BY product_id;
DROP VIEW products_last_purchased;
CREATE VIEW products_last_purchased
AS
SELECT
1 AS id, -- Dummy, LessQL needs an id column
sl.product_id,
sl.amount,
sl.best_before_date,
sl.purchased_date,
IFNULL(plp.price, 0) AS price,
sl.location_id,
sl.shopping_location_id
FROM stock_log sl
JOIN (
SELECT
s1.product_id,
MAX(s1.id) max_stock_id
FROM stock_log s1
JOIN (
SELECT
s.product_id,
MAX(s.purchased_date) max_purchased_date
FROM stock_log s
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s.product_id) sp2
ON s1.product_id = sp2.product_id
AND s1.purchased_date = sp2.max_purchased_date
WHERE undone = 0
AND transaction_type in ('purchase', 'stock-edit-new', 'inventory-correction')
GROUP BY s1.product_id) sp3
ON sl.product_id = sp3.product_id
AND sl.id = sp3.max_stock_id
LEFT JOIN products_last_price plp
ON sl.product_id = plp.product_id;

14
migrations/0222.sql Normal file
View File

@@ -0,0 +1,14 @@
CREATE INDEX ix_stock_log_performance1 ON stock_log (
stock_id,
transaction_type,
amount
);
CREATE INDEX ix_stock_log_performance2 ON stock_log (
product_id,
best_before_date,
purchased_date,
transaction_type,
stock_id,
undone
);

27
migrations/0223.sql Normal file
View File

@@ -0,0 +1,27 @@
DROP VIEW uihelper_shopping_list;
CREATE VIEW uihelper_shopping_list
AS
SELECT
sl.*,
p.name AS product_name,
plp.price AS last_price_unit,
plp.price * sl.amount AS last_price_total,
st.name AS default_shopping_location_name,
qu.name AS qu_name,
qu.name_plural AS qu_name_plural,
pg.id AS product_group_id,
pg.name AS product_group_name,
pbcs.barcodes AS product_barcodes
FROM shopping_list sl
LEFT JOIN products p
ON sl.product_id = p.id
LEFT JOIN products_last_price plp
ON sl.product_id = plp.product_id
LEFT JOIN shopping_locations st
ON p.shopping_location_id = st.id
LEFT JOIN quantity_units qu
ON sl.qu_id = qu.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN product_barcodes_comma_separated pbcs
ON sl.product_id = pbcs.product_id;

133
migrations/0224.sql Normal file
View File

@@ -0,0 +1,133 @@
DROP VIEW products_last_price;
DROP VIEW stock_edited_entries;
CREATE VIEW stock_edited_entries
AS
/*
Returns stock_id's which have been edited manually
*/
SELECT
sl_add.stock_id,
MAX(sl_edit.id) AS stock_log_id_of_newest_edited_entry
FROM stock_log sl_add
JOIN stock_log sl_edit
ON sl_add.stock_id = sl_edit.stock_id
AND sl_edit.transaction_type = 'stock-edit-new'
WHERE sl_add.transaction_type IN ('purchase', 'inventory-correction', 'self-production')
AND sl_add.amount > 0
GROUP BY sl_add.stock_id;
DROP VIEW products_last_purchased;
CREATE VIEW products_last_purchased
AS
SELECT
1 AS id, -- Dummy, LessQL needs an id column
sl.product_id,
sl.amount,
sl.best_before_date,
sl.purchased_date,
IFNULL(sl.price, 0) AS price,
sl.location_id,
sl.shopping_location_id
FROM stock_log sl
JOIN (
/*
This subquery gets the ID of the stock_log row (per product) which referes to the last purchase transaction,
while taking undone and edited transactions into account
*/
SELECT
sl1.product_id,
MAX(sl1.id) stock_log_id_of_last_purchase
FROM stock_log sl1
JOIN (
/*
This subquery finds the last purchased date per product,
there can be multiple purchase transactions per day, therefore a JOIN by purchased_date
for the outer query on this and then take MAX id of stock_log (of that day)
*/
SELECT
sl2.product_id,
MAX(sl2.purchased_date) AS last_purchased_date
FROM stock_log sl2
WHERE sl2.undone = 0
AND (
(sl2.transaction_type IN ('purchase', 'inventory-correction', 'self-production') AND sl2.stock_id NOT IN (SELECT stock_id FROM stock_edited_entries))
OR (sl2.transaction_type = 'stock-edit-new' AND sl2.stock_id IN (SELECT stock_id FROM stock_edited_entries) AND sl2.id IN (SELECT stock_log_id_of_newest_edited_entry FROM stock_edited_entries))
)
GROUP BY sl2.product_id
) x2
ON sl1.product_id = x2.product_id
AND sl1.purchased_date = x2.last_purchased_date
WHERE sl1.undone = 0
AND (
(sl1.transaction_type IN ('purchase', 'inventory-correction', 'self-production') AND sl1.stock_id NOT IN (SELECT stock_id FROM stock_edited_entries))
OR (sl1.transaction_type = 'stock-edit-new' AND sl1.stock_id IN (SELECT stock_id FROM stock_edited_entries) AND sl1.id IN (SELECT stock_log_id_of_newest_edited_entry FROM stock_edited_entries))
)
GROUP BY sl1.product_id
) x
ON sl.product_id = x.product_id
AND sl.id = x.stock_log_id_of_last_purchase;
DROP VIEW uihelper_shopping_list;
CREATE VIEW uihelper_shopping_list
AS
SELECT
sl.*,
p.name AS product_name,
plp.price AS last_price_unit,
plp.price * sl.amount AS last_price_total,
st.name AS default_shopping_location_name,
qu.name AS qu_name,
qu.name_plural AS qu_name_plural,
pg.id AS product_group_id,
pg.name AS product_group_name,
pbcs.barcodes AS product_barcodes
FROM shopping_list sl
LEFT JOIN products p
ON sl.product_id = p.id
LEFT JOIN products_last_purchased plp
ON sl.product_id = plp.product_id
LEFT JOIN shopping_locations st
ON p.shopping_location_id = st.id
LEFT JOIN quantity_units qu
ON sl.qu_id = qu.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN product_barcodes_comma_separated pbcs
ON sl.product_id = pbcs.product_id;
DROP VIEW products_average_price;
CREATE VIEW products_average_price
AS
SELECT
1 AS id, -- Dummy, LessQL needs an id column
s.product_id,
SUM(s.amount * s.price) / SUM(s.amount) as price
FROM stock_log s
WHERE s.undone = 0
AND (
(s.transaction_type IN ('purchase', 'inventory-correction', 'self-production') AND s.stock_id NOT IN (SELECT stock_id FROM stock_edited_entries))
OR (s.transaction_type = 'stock-edit-new' AND s.stock_id IN (SELECT stock_id FROM stock_edited_entries) AND s.id IN (SELECT stock_log_id_of_newest_edited_entry FROM stock_edited_entries))
)
AND IFNULL(s.price, 0) > 0
AND IFNULL(s.amount, 0) > 0
GROUP BY s.product_id;
DROP VIEW product_price_history;
CREATE VIEW products_price_history
AS
SELECT
sl.product_id AS id, -- Dummy, LessQL needs an id column
sl.product_id,
sl.price,
sl.amount,
sl.purchased_date,
sl.shopping_location_id
FROM stock_log sl
WHERE sl.undone = 0
AND (
(sl.transaction_type IN ('purchase', 'inventory-correction', 'self-production') AND sl.stock_id NOT IN (SELECT stock_id FROM stock_edited_entries))
OR (sl.transaction_type = 'stock-edit-new' AND sl.stock_id IN (SELECT stock_id FROM stock_edited_entries) AND sl.id IN (SELECT stock_log_id_of_newest_edited_entry FROM stock_edited_entries))
)
AND IFNULL(sl.price, 0) > 0
AND IFNULL(sl.amount, 0) > 0;

284
migrations/0225.sql Normal file
View File

@@ -0,0 +1,284 @@
CREATE TABLE cache__quantity_unit_conversions_resolved (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INT,
from_qu_id INT,
from_qu_name TEXT,
from_qu_name_plural TEXT,
to_qu_id INT,
to_qu_name TEXT,
to_qu_name_plural TEXT,
factor TEXT,
path TEXT
);
INSERT INTO cache__quantity_unit_conversions_resolved
(product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path)
SELECT product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path
FROM quantity_unit_conversions_resolved;
CREATE INDEX ix_cache__quantity_unit_conversions_resolved_performance1 ON cache__quantity_unit_conversions_resolved (
product_id,
from_qu_id,
to_qu_id
);
DROP TRIGGER qu_conversions_inverse_INS;
CREATE TRIGGER quantity_unit_conversions_INS AFTER INSERT ON quantity_unit_conversions
BEGIN
-- Create the inverse QU conversion
INSERT OR REPLACE INTO quantity_unit_conversions
(from_qu_id, to_qu_id, factor, product_id)
VALUES
(NEW.to_qu_id, NEW.from_qu_id, 1 / IFNULL(NEW.factor, 1), NEW.product_id);
-- Update quantity_unit_conversions_resolved cache
DELETE FROM cache__quantity_unit_conversions_resolved
WHERE path LIKE '%/' || NEW.to_qu_id || '/%'
OR path LIKE '%/' || NEW.from_qu_id || '/%';
INSERT INTO cache__quantity_unit_conversions_resolved
(product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path)
SELECT product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path
FROM quantity_unit_conversions_resolved
WHERE path LIKE '%/' || NEW.to_qu_id || '/%'
OR path LIKE '%/' || NEW.from_qu_id || '/%';
END;
DROP TRIGGER qu_conversions_inverse_UPD;
CREATE TRIGGER quantity_unit_conversions_UPD AFTER UPDATE ON quantity_unit_conversions
BEGIN
-- Update the inverse QU conversion
UPDATE quantity_unit_conversions
SET factor = 1 / IFNULL(NEW.factor, 1),
from_qu_id = NEW.to_qu_id,
to_qu_id = NEW.from_qu_id
WHERE from_qu_id = OLD.to_qu_id
AND to_qu_id = OLD.from_qu_id
AND IFNULL(product_id, -1) = IFNULL(NEW.product_id, -1);
-- Update quantity_unit_conversions_resolved cache
DELETE FROM cache__quantity_unit_conversions_resolved
WHERE path LIKE '%/' || NEW.to_qu_id || '/%'
OR path LIKE '%/' || NEW.from_qu_id || '/%';
INSERT INTO cache__quantity_unit_conversions_resolved
(product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path)
SELECT product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path
FROM quantity_unit_conversions_resolved
WHERE path LIKE '%/' || NEW.to_qu_id || '/%'
OR path LIKE '%/' || NEW.from_qu_id || '/%';
END;
DROP TRIGGER qu_conversions_inverse_DEL;
CREATE TRIGGER quantity_unit_conversions_DEL AFTER DELETE ON quantity_unit_conversions
BEGIN
-- Delete the inverse QU conversion
DELETE FROM quantity_unit_conversions
WHERE from_qu_id = OLD.to_qu_id
AND to_qu_id = OLD.from_qu_id
AND IFNULL(product_id, -1) = IFNULL(OLD.product_id, -1);
-- Update quantity_unit_conversions_resolved cache
DELETE FROM cache__quantity_unit_conversions_resolved
WHERE path LIKE '%/' || OLD.to_qu_id || '/%'
OR path LIKE '%/' || OLD.from_qu_id || '/%';
INSERT INTO cache__quantity_unit_conversions_resolved
(product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path)
SELECT product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path
FROM quantity_unit_conversions_resolved
WHERE path LIKE '%/' || OLD.to_qu_id || '/%'
OR path LIKE '%/' || OLD.from_qu_id || '/%';
END;
CREATE TRIGGER products_INS AFTER INSERT ON products
BEGIN
-- Update quantity_unit_conversions_resolved cache
DELETE FROM cache__quantity_unit_conversions_resolved
WHERE product_id = NEW.id;
INSERT INTO cache__quantity_unit_conversions_resolved
(product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path)
SELECT product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path
FROM quantity_unit_conversions_resolved
WHERE product_id = NEW.id;
END;
CREATE TRIGGER products_UPD AFTER UPDATE ON products
BEGIN
-- Update quantity_unit_conversions_resolved cache
DELETE FROM cache__quantity_unit_conversions_resolved
WHERE product_id = NEW.id;
INSERT INTO cache__quantity_unit_conversions_resolved
(product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path)
SELECT product_id, from_qu_id, from_qu_name, from_qu_name_plural, to_qu_id, to_qu_name, to_qu_name_plural, factor, path
FROM quantity_unit_conversions_resolved
WHERE product_id = NEW.id;
END;
CREATE TRIGGER products_DELETE AFTER DELETE ON products
BEGIN
-- Update quantity_unit_conversions_resolved cache
DELETE FROM cache__quantity_unit_conversions_resolved
WHERE product_id = OLD.id;
END;
DROP VIEW recipes_pos_resolved;
CREATE VIEW recipes_pos_resolved
AS
-- Multiplication by 1.0 to force conversion to float (REAL)
SELECT
r.id AS recipe_id,
rp.id AS recipe_pos_id,
rp.product_id AS product_id,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END AS recipe_amount,
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
CASE WHEN IFNULL(sc.amount_aggregated, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END THEN 1 ELSE 0 END AS need_fulfilled,
CASE WHEN IFNULL(sc.amount_aggregated, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END < 0 THEN ABS(IFNULL(sc.amount_aggregated, 0) - (CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END)) ELSE 0 END AS missing_amount,
IFNULL(sl.amount, 0) AS amount_on_shopping_list,
CASE WHEN ROUND(IFNULL(sc.amount_aggregated, 0) + CASE WHEN r.not_check_shoppinglist = 1 THEN 0 ELSE IFNULL(sl.amount, 0) END, 2) >= ROUND(CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 0.00000001 ELSE CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END END, 2) THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list,
rp.qu_id,
(r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * rp.amount * IFNULL(pcp.price, 0) * rp.price_factor * IFNULL(qucr.factor, 1) AS costs,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
rp.ingredient_group,
pg.name as product_group,
rp.id, -- Just a dummy id column
r.type as recipe_type,
rnr.includes_recipe_id as child_recipe_id,
rp.note,
rp.variable_amount AS recipe_variable_amount,
rp.only_check_single_unit_in_stock,
rp.amount / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p_effective.calories, 0) * IFNULL(qucr.factor, 1) AS calories,
p.active AS product_active,
CASE pvs.current_due_status
WHEN 'ok' THEN 0
WHEN 'due_soon' THEN 1
WHEN 'overdue' THEN 10
WHEN 'expired' THEN 20
END AS due_score,
IFNULL(pcs.product_id_effective, rp.product_id) AS product_id_effective,
p.name AS product_name
FROM recipes r
JOIN recipes_nestings_resolved rnr
ON r.id = rnr.recipe_id
JOIN recipes rnrr
ON rnr.includes_recipe_id = rnrr.id
JOIN recipes_pos rp
ON rnr.includes_recipe_id = rp.recipe_id
JOIN products p
ON rp.product_id = p.id
JOIN products_volatile_status pvs
ON rp.product_id = pvs.product_id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN (
SELECT product_id, SUM(amount) AS amount
FROM shopping_list
GROUP BY product_id) sl
ON rp.product_id = sl.product_id
LEFT JOIN stock_current sc
ON rp.product_id = sc.product_id
LEFT JOIN products_current_substitutions pcs
ON rp.product_id = pcs.parent_product_id
LEFT JOIN products_current_price pcp
ON IFNULL(pcs.product_id_effective, rp.product_id) = pcp.product_id
LEFT JOIN products p_effective
ON IFNULL(pcs.product_id_effective, rp.product_id) = p_effective.id
LEFT JOIN cache__quantity_unit_conversions_resolved qucr
ON IFNULL(pcs.product_id_effective, rp.product_id) = qucr.product_id
AND CASE WHEN rp.product_id != p_effective.id THEN p.qu_id_stock ELSE rp.qu_id END = qucr.from_qu_id
AND IFNULL(p_effective.qu_id_stock, p.qu_id_stock) = qucr.to_qu_id
WHERE rp.not_check_stock_fulfillment = 0
UNION
-- Just add all recipe positions which should not be checked against stock with fulfilled need
SELECT
r.id AS recipe_id,
rp.id AS recipe_pos_id,
rp.product_id AS product_id,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) ELSE rp.amount * ((r.desired_servings*1.0) / (r.base_servings*1.0)) * ((rnr.includes_servings*1.0) / (rnrr.base_servings*1.0)) END AS recipe_amount,
IFNULL(sc.amount_aggregated, 0) AS stock_amount,
1 AS need_fulfilled,
0 AS missing_amount,
IFNULL(sl.amount, 0) AS amount_on_shopping_list,
1 AS need_fulfilled_with_shopping_list,
rp.qu_id,
(r.desired_servings*1.0 / r.base_servings*1.0) * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * rp.amount * IFNULL(pcp.price, 0) * rp.price_factor * IFNULL(qucr.factor, 1) AS costs,
CASE WHEN rnr.recipe_id = rnr.includes_recipe_id THEN 0 ELSE 1 END AS is_nested_recipe_pos,
rp.ingredient_group,
pg.name as product_group,
rp.id, -- Just a dummy id column
r.type as recipe_type,
rnr.includes_recipe_id as child_recipe_id,
rp.note,
rp.variable_amount AS recipe_variable_amount,
rp.only_check_single_unit_in_stock,
rp.amount / r.base_servings*1.0 * (rnr.includes_servings*1.0 / CASE WHEN rnr.recipe_id != rnr.includes_recipe_id THEN rnrr.base_servings*1.0 ELSE 1 END) * IFNULL(p_effective.calories, 0) * IFNULL(qucr.factor, 1) AS calories,
p.active AS product_active,
CASE pvs.current_due_status
WHEN 'ok' THEN 0
WHEN 'due_soon' THEN 1
WHEN 'overdue' THEN 10
WHEN 'expired' THEN 20
END AS due_score,
IFNULL(pcs.product_id_effective, rp.product_id) AS product_id_effective,
p.name AS product_name
FROM recipes r
JOIN recipes_nestings_resolved rnr
ON r.id = rnr.recipe_id
JOIN recipes rnrr
ON rnr.includes_recipe_id = rnrr.id
JOIN recipes_pos rp
ON rnr.includes_recipe_id = rp.recipe_id
JOIN products p
ON rp.product_id = p.id
JOIN products_volatile_status pvs
ON rp.product_id = pvs.product_id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN (
SELECT product_id, SUM(amount) AS amount
FROM shopping_list
GROUP BY product_id) sl
ON rp.product_id = sl.product_id
LEFT JOIN stock_current sc
ON rp.product_id = sc.product_id
LEFT JOIN products_current_substitutions pcs
ON rp.product_id = pcs.parent_product_id
LEFT JOIN products_current_price pcp
ON IFNULL(pcs.product_id_effective, rp.product_id) = pcp.product_id
LEFT JOIN products p_effective
ON IFNULL(pcs.product_id_effective, rp.product_id) = p_effective.id
LEFT JOIN cache__quantity_unit_conversions_resolved qucr
ON IFNULL(pcs.product_id_effective, rp.product_id) = qucr.product_id
AND CASE WHEN rp.product_id != p_effective.id THEN p.qu_id_stock ELSE rp.qu_id END = qucr.from_qu_id
AND IFNULL(p_effective.qu_id_stock, p.qu_id_stock) = qucr.to_qu_id
WHERE rp.not_check_stock_fulfillment = 1;
DROP VIEW products_view;
CREATE VIEW products_view
AS
SELECT
p.*,
CASE WHEN (SELECT 1 FROM products WHERE parent_product_id = p.id) NOTNULL THEN 1 ELSE 0 END AS has_sub_products,
IFNULL(quc_purchase.factor, 1.0) AS qu_factor_purchase_to_stock,
IFNULL(quc_consume.factor, 1.0) AS qu_factor_consume_to_stock,
IFNULL(quc_price.factor, 1.0) AS qu_factor_price_to_stock
FROM products p
LEFT JOIN cache__quantity_unit_conversions_resolved quc_purchase
ON p.id = quc_purchase.product_id
AND p.qu_id_purchase = quc_purchase.from_qu_id
AND p.qu_id_stock = quc_purchase.to_qu_id
LEFT JOIN cache__quantity_unit_conversions_resolved quc_consume
ON p.id = quc_consume.product_id
AND p.qu_id_consume = quc_consume.from_qu_id
AND p.qu_id_stock = quc_consume.to_qu_id
LEFT JOIN cache__quantity_unit_conversions_resolved quc_price
ON p.id = quc_price.product_id
AND p.qu_id_price = quc_price.from_qu_id
AND p.qu_id_stock = quc_price.to_qu_id;

218
migrations/0226.sql Normal file
View File

@@ -0,0 +1,218 @@
CREATE TABLE cache__products_average_price (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INT,
price DECIMAL(15, 2),
UNIQUE(product_id)
);
INSERT INTO cache__products_average_price
(product_id, price)
SELECT product_id, price
FROM products_average_price;
CREATE TABLE cache__products_last_purchased (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
product_id INT,
amount DECIMAL(15, 2),
best_before_date DATE,
purchased_date DATE,
price DECIMAL(15, 2),
location_id INT,
shopping_location_id INT,
UNIQUE(product_id)
);
INSERT INTO cache__products_last_purchased
(product_id, amount, best_before_date, purchased_date, price, location_id, shopping_location_id)
SELECT product_id, amount, best_before_date, purchased_date, price, location_id, shopping_location_id
FROM products_last_purchased;
CREATE TRIGGER stock_log_INS AFTER INSERT ON stock_log
BEGIN
-- Update products_average_price cache
INSERT OR REPLACE INTO cache__products_average_price
(product_id, price)
SELECT product_id, price
FROM products_average_price
WHERE product_id = NEW.product_id;
-- Update products_last_purchased cache
INSERT OR REPLACE INTO cache__products_last_purchased
(product_id, amount, best_before_date, purchased_date, price, location_id, shopping_location_id)
SELECT product_id, amount, best_before_date, purchased_date, price, location_id, shopping_location_id
FROM products_last_purchased
WHERE product_id = NEW.product_id;
END;
CREATE TRIGGER stock_log_UPD AFTER UPDATE ON stock_log
BEGIN
-- Update products_average_price cache
INSERT OR REPLACE INTO cache__products_average_price
(product_id, price)
SELECT product_id, price
FROM products_average_price
WHERE product_id = NEW.product_id;
-- Update products_last_purchased cache
INSERT OR REPLACE INTO cache__products_last_purchased
(product_id, amount, best_before_date, purchased_date, price, location_id, shopping_location_id)
SELECT product_id, amount, best_before_date, purchased_date, price, location_id, shopping_location_id
FROM products_last_purchased
WHERE product_id = NEW.product_id;
END;
CREATE TRIGGER stock_log_DEL AFTER DELETE ON stock_log
BEGIN
-- Update products_average_price cache
DELETE FROM cache__products_average_price
WHERE product_id = OLD.id;
-- Update products_last_purchased cache
DELETE FROM cache__products_last_purchased
WHERE product_id = OLD.id;
END;
DROP VIEW uihelper_stock_current_overview;
CREATE VIEW uihelper_stock_current_overview
AS
SELECT
p.id,
sc.amount_opened AS amount_opened,
p.tare_weight AS tare_weight,
p.enable_tare_weight_handling AS enable_tare_weight_handling,
sc.amount AS amount,
sc.value as value,
sc.product_id AS product_id,
sc.best_before_date AS best_before_date,
EXISTS(SELECT id FROM stock_missing_products WHERE id = sc.product_id) AS product_missing,
p.name AS product_name,
pg.name AS product_group_name,
EXISTS(SELECT * FROM shopping_list WHERE shopping_list.product_id = sc.product_id) AS on_shopping_list,
qu_stock.name AS qu_stock_name,
qu_stock.name_plural AS qu_stock_name_plural,
qu_purchase.name AS qu_purchase_name,
qu_purchase.name_plural AS qu_purchase_name_plural,
qu_consume.name AS qu_consume_name,
qu_consume.name_plural AS qu_consume_name_plural,
qu_price.name AS qu_price_name,
qu_price.name_plural AS qu_price_name_plural,
sc.is_aggregated_amount,
sc.amount_opened_aggregated,
sc.amount_aggregated,
p.calories AS product_calories,
sc.amount * p.calories AS calories,
sc.amount_aggregated * p.calories AS calories_aggregated,
p.quick_consume_amount,
p.quick_consume_amount / p.qu_factor_consume_to_stock AS quick_consume_amount_qu_consume,
p.quick_open_amount,
p.quick_open_amount / p.qu_factor_consume_to_stock AS quick_open_amount_qu_consume,
p.due_type,
plp.purchased_date AS last_purchased,
plp.price AS last_price,
pap.price as average_price,
p.min_stock_amount,
pbcs.barcodes AS product_barcodes,
p.description AS product_description,
l.name AS product_default_location_name,
p_parent.id AS parent_product_id,
p_parent.name AS parent_product_name,
p.picture_file_name AS product_picture_file_name,
p.no_own_stock AS product_no_own_stock,
p.qu_factor_purchase_to_stock AS product_qu_factor_purchase_to_stock,
p.qu_factor_price_to_stock AS product_qu_factor_price_to_stock
FROM (
SELECT *
FROM stock_current
WHERE best_before_date IS NOT NULL
UNION
SELECT m.id, 0, 0, 0, null, 0, 0, 0, p.due_type
FROM stock_missing_products m
JOIN products p
ON m.id = p.id
WHERE m.id NOT IN (SELECT product_id FROM stock_current)
) sc
JOIN products_view p
ON sc.product_id = p.id
JOIN locations l
ON p.location_id = l.id
JOIN quantity_units qu_stock
ON p.qu_id_stock = qu_stock.id
JOIN quantity_units qu_purchase
ON p.qu_id_purchase = qu_purchase.id
JOIN quantity_units qu_consume
ON p.qu_id_consume = qu_consume.id
JOIN quantity_units qu_price
ON p.qu_id_price = qu_price.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN cache__products_last_purchased plp
ON sc.product_id = plp.product_id
LEFT JOIN cache__products_average_price pap
ON sc.product_id = pap.product_id
LEFT JOIN product_barcodes_comma_separated pbcs
ON sc.product_id = pbcs.product_id
LEFT JOIN products p_parent
ON p.parent_product_id = p_parent.id
WHERE p.hide_on_stock_overview = 0;
DROP VIEW uihelper_shopping_list;
CREATE VIEW uihelper_shopping_list
AS
SELECT
sl.*,
p.name AS product_name,
plp.price * IFNULL(quc.factor, 1.0) AS last_price_unit,
plp.price * (sl.amount / IFNULL(quc.factor, 1.0)) * IFNULL(quc.factor, 1.0) AS last_price_total,
st.name AS default_shopping_location_name,
qu.name AS qu_name,
qu.name_plural AS qu_name_plural,
pg.id AS product_group_id,
pg.name AS product_group_name,
pbcs.barcodes AS product_barcodes
FROM shopping_list sl
LEFT JOIN products p
ON sl.product_id = p.id
LEFT JOIN cache__products_last_purchased plp
ON sl.product_id = plp.product_id
LEFT JOIN shopping_locations st
ON p.shopping_location_id = st.id
LEFT JOIN quantity_units qu
ON sl.qu_id = qu.id
LEFT JOIN product_groups pg
ON p.product_group_id = pg.id
LEFT JOIN cache__quantity_unit_conversions_resolved quc
ON p.id = quc.product_id
AND sl.qu_id = quc.from_qu_id
AND p.qu_id_stock = quc.to_qu_id
LEFT JOIN product_barcodes_comma_separated pbcs
ON sl.product_id = pbcs.product_id;
DROP VIEW products_current_price;
CREATE VIEW products_current_price
AS
/*
Current price per product,
based on the stock entry to use next,
or on the last price if the product is currently not in stock
*/
SELECT
-1 AS id, -- Dummy,
p.id AS product_id,
IFNULL(snu.price, plp.price) AS price
FROM products p
LEFT JOIN (
SELECT
product_id,
MAX(priority),
price -- Bare column, ref https://www.sqlite.org/lang_select.html#bare_columns_in_an_aggregate_query
FROM stock_next_use
GROUP BY product_id
ORDER BY priority DESC, open DESC, best_before_date ASC, purchased_date ASC
) snu
ON p.id = snu.product_id
LEFT JOIN cache__products_last_purchased plp
ON p.id = plp.product_id;

48
migrations/0227.sql Normal file
View File

@@ -0,0 +1,48 @@
DROP VIEW stock_edited_entries;
CREATE VIEW stock_edited_entries
AS
/*
Returns stock_id's which have been edited manually
*/
SELECT
sl_add.stock_id,
MAX(sl_edit.id) AS stock_log_id_of_newest_edited_entry,
sl_origin.id AS stock_log_id_of_origin_entry
FROM stock_log sl_add
JOIN stock_log sl_edit
ON sl_add.stock_id = sl_edit.stock_id
AND sl_edit.transaction_type = 'stock-edit-new'
JOIN stock_log sl_origin
ON sl_add.stock_id = sl_origin.stock_id
AND sl_origin.transaction_type IN ('purchase', 'inventory-correction', 'self-production')
WHERE sl_add.transaction_type IN ('purchase', 'inventory-correction', 'self-production')
AND sl_add.amount > 0
GROUP BY sl_add.stock_id, sl_origin.id;
DROP VIEW products_average_price;
CREATE VIEW products_average_price
AS
SELECT
1 AS id, -- Dummy, LessQL needs an id column
sl.product_id,
SUM(IFNULL(sl_origin.amount, sl.amount) * sl.price) / SUM(IFNULL(sl_origin.amount, sl.amount)) as price
FROM stock_log sl
LEFT JOIN stock_edited_entries see
ON sl.stock_id = see.stock_id
LEFT JOIN stock_log sl_origin
ON sl.stock_id = sl_origin.stock_id
AND see.stock_log_id_of_origin_entry = sl_origin.id
WHERE sl.undone = 0
AND (
(sl.transaction_type IN ('purchase', 'inventory-correction', 'self-production') AND sl.stock_id NOT IN (SELECT stock_id FROM stock_edited_entries))
OR (sl.transaction_type = 'stock-edit-new' AND sl.stock_id IN (SELECT stock_id FROM stock_edited_entries) AND sl.id IN (SELECT stock_log_id_of_newest_edited_entry FROM stock_edited_entries))
)
AND IFNULL(sl.price, 0) > 0
AND IFNULL(sl.amount, 0) > 0
GROUP BY sl.product_id;
-- Update products_average_price cache
INSERT OR REPLACE INTO cache__products_average_price
(product_id, price)
SELECT product_id, price
FROM products_average_price;

View File

@@ -668,8 +668,8 @@ $(document).on("click", ".easy-link-copy-textbox", function()
if (Grocy.CalendarFirstDayOfWeek)
{
moment.updateLocale(moment.locale(), {
week: {
dow: Number.parseInt(Grocy.CalendarFirstDayOfWeek)
"week": {
"dow": Number.parseInt(Grocy.CalendarFirstDayOfWeek)
}
});
}
@@ -772,18 +772,6 @@ $(window).on("message", function(e)
}
});
if (Grocy.FeatureFlags.GROCY_FEATURE_FLAG_RECIPES)
{
if ($(window).width() < 768)
{
$("#meal-plan-nav-link").attr("href", $("#meal-plan-nav-link").attr("href") + "?start=" + moment().format("YYYY-MM-DD") + "&days=0");
}
else
{
$("#meal-plan-nav-link").attr("href", $("#meal-plan-nav-link").attr("href") + "?start=" + moment().startOf("week").format("YYYY-MM-DD"));
}
}
$('[data-toggle="tooltip"][data-html="true"]').on("shown.bs.tooltip", function()
{
RefreshLocaleNumberDisplay(".tooltip");

View File

@@ -1,15 +0,0 @@
{
"name": "Grocy",
"short_name": "Grocy",
"icons": [
{
"src": "./img/icon-1024.png",
"sizes": "1024x1024",
"type": "image/png"
}
],
"start_url": "../",
"background_color": "#333131",
"theme_color": "#333131",
"display": "standalone"
}

View File

@@ -5,10 +5,12 @@ use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Routing\RouteCollectorProxy;
$app->group('', function (RouteCollectorProxy $group) {
$app->group('', function (RouteCollectorProxy $group)
{
// System routes
$group->get('/', '\Grocy\Controllers\SystemController:Root')->setName('root');
$group->get('/about', '\Grocy\Controllers\SystemController:About');
$group->get('/manifest', '\Grocy\Controllers\SystemController:Manifest');
$group->get('/barcodescannertesting', '\Grocy\Controllers\SystemController:BarcodeScannerTesting');
// Login routes
@@ -123,7 +125,8 @@ $app->group('', function (RouteCollectorProxy $group) {
$group->get('/manageapikeys/new', '\Grocy\Controllers\OpenApiController:CreateNewApiKey');
});
$app->group('/api', function (RouteCollectorProxy $group) {
$app->group('/api', function (RouteCollectorProxy $group)
{
// OpenAPI
$group->get('/openapi/specification', '\Grocy\Controllers\OpenApiController:DocumentationSpec');
@@ -242,6 +245,7 @@ $app->group('/api', function (RouteCollectorProxy $group) {
})->add(JsonMiddleware::class);
// Handle CORS preflight OPTIONS requests
$app->options('/api/{routes:.+}', function (Request $request, Response $response): Response {
$app->options('/api/{routes:.+}', function (Request $request, Response $response): Response
{
return $response->withStatus(204);
});

View File

@@ -5,7 +5,6 @@ namespace Grocy\Services;
class ApiKeyService extends BaseService
{
const API_KEY_TYPE_DEFAULT = 'default';
const API_KEY_TYPE_SPECIAL_PURPOSE_CALENDAR_ICAL = 'special-purpose-calendar-ical';
public function CreateApiKey(string $keyType = self::API_KEY_TYPE_DEFAULT, string $description = null)

View File

@@ -33,7 +33,8 @@ class ApplicationService extends BaseService
}
// Sort changelog items to have the changelog descending by newest version
usort($changelogItems, function ($a, $b) {
usort($changelogItems, function ($a, $b)
{
if ($a['release_number'] == $b['release_number'])
{
return 0;

View File

@@ -5,25 +5,15 @@ namespace Grocy\Services;
class ChoresService extends BaseService
{
const CHORE_ASSIGNMENT_TYPE_IN_ALPHABETICAL_ORDER = 'in-alphabetical-order';
const CHORE_ASSIGNMENT_TYPE_NO_ASSIGNMENT = 'no-assignment';
const CHORE_ASSIGNMENT_TYPE_RANDOM = 'random';
const CHORE_ASSIGNMENT_TYPE_WHO_LEAST_DID_FIRST = 'who-least-did-first';
const CHORE_PERIOD_TYPE_HOURLY = 'hourly';
const CHORE_PERIOD_TYPE_DAILY = 'daily';
const CHORE_PERIOD_TYPE_MANUALLY = 'manually';
const CHORE_PERIOD_TYPE_MONTHLY = 'monthly';
const CHORE_PERIOD_TYPE_WEEKLY = 'weekly';
const CHORE_PERIOD_TYPE_YEARLY = 'yearly';
const CHORE_PERIOD_TYPE_ADAPTIVE = 'adaptive';
public function CalculateNextExecutionAssignment($choreId)
@@ -70,7 +60,8 @@ class ChoresService extends BaseService
}
elseif ($chore->assignment_type == self::CHORE_ASSIGNMENT_TYPE_IN_ALPHABETICAL_ORDER)
{
usort($assignedUsers, function ($a, $b) {
usort($assignedUsers, function ($a, $b)
{
return strcmp($a->display_name, $b->display_name);
});

View File

@@ -8,9 +8,7 @@ use LessQL\Database;
class DatabaseService
{
private static $DbConnection = null;
private static $DbConnectionRaw = null;
private static $instance = null;
public function ExecuteDbQuery(string $sql)
@@ -29,9 +27,18 @@ class DatabaseService
{
$pdo = $this->GetDbConnectionRaw();
if (GROCY_MODE === 'dev')
{
$logFilePath = GROCY_DATAPATH . '/sql.log';
if (file_exists($logFilePath))
{
file_put_contents($logFilePath, $sql . PHP_EOL, FILE_APPEND);
}
}
if ($pdo->exec($sql) === false)
{
throw new Exception($pdo->errorInfo());
throw new \Exception($pdo->errorInfo());
}
return true;
@@ -49,6 +56,18 @@ class DatabaseService
self::$DbConnection = new Database($this->GetDbConnectionRaw());
}
if (GROCY_MODE === 'dev')
{
$logFilePath = GROCY_DATAPATH . '/sql.log';
if (file_exists($logFilePath))
{
self::$DbConnection->setQueryCallback(function ($query, $params) use ($logFilePath)
{
file_put_contents($logFilePath, $query . ' #### ' . implode(';', $params) . PHP_EOL, FILE_APPEND);
});
}
}
return self::$DbConnection;
}
@@ -60,12 +79,14 @@ class DatabaseService
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(\PDO::ATTR_ORACLE_NULLS, \PDO::NULL_EMPTY_STRING);
$pdo->sqliteCreateFunction('regexp', function ($pattern, $value) {
$pdo->sqliteCreateFunction('regexp', function ($pattern, $value)
{
mb_regex_encoding('UTF-8');
return (false !== mb_ereg($pattern, $value)) ? 1 : 0;
});
$pdo->sqliteCreateFunction('grocy_user_setting', function ($value) {
$pdo->sqliteCreateFunction('grocy_user_setting', function ($value)
{
$usersService = new UsersService();
return $usersService->GetUserSetting(GROCY_USER_ID, $value);
});

View File

@@ -10,14 +10,19 @@ class DemoDataGeneratorService extends BaseService
}
protected $LocalizationService;
private $LastSupermarketId = 1;
public function PopulateDemoData()
public function PopulateDemoData($skip = false)
{
$rowCount = $this->getDatabaseService()->ExecuteDbQuery('SELECT COUNT(*) FROM migrations WHERE migration = -1')->fetchColumn();
if ($rowCount == 0)
{
if ($skip)
{
$this->getDatabaseService()->ExecuteDbStatement('INSERT INTO migrations (migration) VALUES (-1);');
return;
}
$loremIpsum = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
$loremIpsumWithHtmlFormattings = "<h1>Lorem ipsum</h1><p>Lorem ipsum <b>dolor sit</b> amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur <span style=\"background-color: rgb(255, 255, 0);\">sadipscing elitr</span>, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.</p><ul><li>At vero eos et accusam et justo duo dolores et ea rebum.</li><li>Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</li></ul><h1>Lorem ipsum</h1><p>Lorem ipsum <b>dolor sit</b> amet, consetetur \r\nsadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et \r\ndolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et\r\n justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea \r\ntakimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit \r\namet, consetetur <span style=\"background-color: rgb(255, 255, 0);\">sadipscing elitr</span>,\r\n sed diam nonumy eirmod tempor invidunt ut labore et dolore magna \r\naliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo \r\ndolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus \r\nest Lorem ipsum dolor sit amet.</p>";

View File

@@ -16,15 +16,12 @@ class LocalizationService
}
protected $Po;
protected $PoQu;
protected $Pot;
protected $PotMain;
protected $Translator;
protected $TranslatorQu;
protected $Culture;
private static $instanceMap = [];
public function CheckAndAddMissingTranslationToPot($text)

View File

@@ -3,7 +3,6 @@
namespace Grocy\Services;
use DateTime;
use Exception;
use Mike42\Escpos\PrintConnectors\NetworkPrintConnector;
use Mike42\Escpos\PrintConnectors\FilePrintConnector;
use Mike42\Escpos\Printer;
@@ -15,7 +14,7 @@ class PrintService extends BaseService
$printer = self::getPrinterHandle();
if ($printer === false)
{
throw new Exception('Unable to connect to printer');
throw new \Exception('Unable to connect to printer');
}
if ($printHeader)

View File

@@ -7,11 +7,8 @@ use LessQL\Result;
class RecipesService extends BaseService
{
const RECIPE_TYPE_MEALPLAN_DAY = 'mealplan-day'; // A recipe per meal plan day => name = YYYY-MM-DD
const RECIPE_TYPE_MEALPLAN_WEEK = 'mealplan-week'; // A recipe per meal plan week => name = YYYY-WW (week number)
const RECIPE_TYPE_MEALPLAN_SHADOW = 'mealplan-shadow'; // A recipe per meal plan recipe (for separated stock fulfillment checking) => name = YYYY-MM-DD#<meal_plan.id>
const RECIPE_TYPE_NORMAL = 'normal'; // Normal / manually created recipes
public function AddNotFulfilledProductsToShoppingList($recipeId, $excludedProductIds = null)
@@ -41,7 +38,7 @@ class RecipesService extends BaseService
// => Do the unit conversion here (if any)
if ($recipePosition->only_check_single_unit_in_stock == 1)
{
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $recipePosition->product_id, $recipePosition->qu_id, $product->qu_id_stock)->fetch();
$conversion = $this->getDatabase()->cache__quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $recipePosition->product_id, $recipePosition->qu_id, $product->qu_id_stock)->fetch();
if ($conversion != null)
{
$toOrderAmount = $toOrderAmount * $conversion->factor;

View File

@@ -8,21 +8,13 @@ use Grocy\Helpers\WebhookRunner;
class StockService extends BaseService
{
const TRANSACTION_TYPE_CONSUME = 'consume';
const TRANSACTION_TYPE_INVENTORY_CORRECTION = 'inventory-correction';
const TRANSACTION_TYPE_PRODUCT_OPENED = 'product-opened';
const TRANSACTION_TYPE_PURCHASE = 'purchase';
const TRANSACTION_TYPE_SELF_PRODUCTION = 'self-production';
const TRANSACTION_TYPE_STOCK_EDIT_NEW = 'stock-edit-new';
const TRANSACTION_TYPE_STOCK_EDIT_OLD = 'stock-edit-old';
const TRANSACTION_TYPE_TRANSFER_FROM = 'transfer_from';
const TRANSACTION_TYPE_TRANSFER_TO = 'transfer_to';
public function AddMissingProductsToShoppingList($listId = 1)
@@ -121,7 +113,7 @@ class StockService extends BaseService
throw new \Exception('Product does not exist or is inactive');
}
$productDetails = (object) $this->GetProductDetails($productId);
$productDetails = (object)$this->GetProductDetails($productId);
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
@@ -369,7 +361,7 @@ class StockService extends BaseService
throw new \Exception('Location does not exist');
}
$productDetails = (object) $this->GetProductDetails($productId);
$productDetails = (object)$this->GetProductDetails($productId);
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
@@ -428,7 +420,7 @@ class StockService extends BaseService
{
// A sub product will be used -> use QU conversions
$subProduct = $this->getDatabase()->products($stockEntry->product_id);
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $stockEntry->product_id, $productDetails->product->qu_id_stock, $subProduct->qu_id_stock)->fetch();
$conversion = $this->getDatabase()->cache__quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $stockEntry->product_id, $productDetails->product->qu_id_stock, $subProduct->qu_id_stock)->fetch();
if ($conversion != null)
{
$amount = $amount * $conversion->factor;
@@ -509,7 +501,7 @@ class StockService extends BaseService
}
else
{
throw new Exception("Transaction type $transactionType is not valid (StockService.ConsumeProduct)");
throw new \Exception("Transaction type $transactionType is not valid (StockService.ConsumeProduct)");
}
}
@@ -719,7 +711,7 @@ class StockService extends BaseService
$stockCurrentRow->is_aggregated_amount = 0;
}
$productLastPurchased = $this->getDatabase()->products_last_purchased()->where('product_id', $productId)->fetch();
$productLastPurchased = $this->getDatabase()->cache__products_last_purchased()->where('product_id', $productId)->fetch();
$lastPurchasedDate = null;
$lastPrice = null;
$lastShoppingLocation = null;
@@ -729,7 +721,7 @@ class StockService extends BaseService
$lastPurchasedDate = $productLastPurchased->purchased_date;
$lastPrice = $productLastPurchased->price;
$lastShoppingLocation = $productLastPurchased->shopping_location_id;
$avgPriceRow = $this->getDatabase()->products_average_price()->where('product_id', $productId)->fetch();
$avgPriceRow = $this->getDatabase()->cache__products_average_price()->where('product_id', $productId)->fetch();
if ($avgPriceRow)
{
$avgPrice = $avgPriceRow->price;
@@ -765,7 +757,7 @@ class StockService extends BaseService
$quConversionFactorPurchaseToStock = 1.0;
if ($product->qu_id_stock != $product->qu_id_purchase)
{
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $product->id, $product->qu_id_purchase, $product->qu_id_stock)->fetch();
$conversion = $this->getDatabase()->cache__quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $product->id, $product->qu_id_purchase, $product->qu_id_stock)->fetch();
if ($conversion != null)
{
$quConversionFactorPurchaseToStock = $conversion->factor;
@@ -775,7 +767,7 @@ class StockService extends BaseService
$quConversionFactorPriceToStock = 1.0;
if ($product->qu_id_stock != $product->qu_id_price)
{
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $product->id, $product->qu_id_price, $product->qu_id_stock)->fetch();
$conversion = $this->getDatabase()->cache__quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $product->id, $product->qu_id_price, $product->qu_id_stock)->fetch();
if ($conversion != null)
{
$quConversionFactorPriceToStock = $conversion->factor;
@@ -846,7 +838,7 @@ class StockService extends BaseService
$returnData = [];
$shoppingLocations = $this->getDatabase()->shopping_locations();
$rows = $this->getDatabase()->product_price_history()->where('product_id = :1', $productId)->orderBy('purchased_date', 'DESC');
$rows = $this->getDatabase()->products_price_history()->where('product_id = :1', $productId)->orderBy('purchased_date', 'DESC');
foreach ($rows as $row)
{
$returnData[] = [
@@ -915,7 +907,7 @@ class StockService extends BaseService
throw new \Exception('Product does not exist or is inactive');
}
$productDetails = (object) $this->GetProductDetails($productId);
$productDetails = (object)$this->GetProductDetails($productId);
if ($price === null)
{
@@ -979,7 +971,7 @@ class StockService extends BaseService
throw new \Exception('Product does not exist or is inactive');
}
$productDetails = (object) $this->GetProductDetails($productId);
$productDetails = (object)$this->GetProductDetails($productId);
$productStockAmountUnopened = $productDetails->stock_amount_aggregated - $productDetails->stock_amount_opened_aggregated;
$potentialStockEntries = $this->GetProductStockEntries($productId, true, $allowSubproductSubstitution);
$product = $this->getDatabase()->products($productId);
@@ -1043,7 +1035,7 @@ class StockService extends BaseService
{
// A sub product will be used -> use QU conversions
$subProduct = $this->getDatabase()->products($stockEntry->product_id);
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $stockEntry->product_id, $product->qu_id_stock, $subProduct->qu_id_stock)->fetch();
$conversion = $this->getDatabase()->cache__quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $stockEntry->product_id, $product->qu_id_stock, $subProduct->qu_id_stock)->fetch();
if ($conversion != null)
{
$amount = $amount * $conversion->factor;
@@ -1183,7 +1175,7 @@ class StockService extends BaseService
if ($isValidProduct)
{
$product = $this->getDatabase()->products()->where('id = :1', $row->product_id)->fetch();
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $product->id, $product->qu_id_stock, $row->qu_id)->fetch();
$conversion = $this->getDatabase()->cache__quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $product->id, $product->qu_id_stock, $row->qu_id)->fetch();
$factor = 1.0;
if ($conversion != null)
@@ -1270,7 +1262,7 @@ class StockService extends BaseService
// Tare weight handling
// The given amount is the new total amount including the container weight (gross)
// The amount to be posted needs to be the absolute value of the given amount - stock amount - tare weight
$productDetails = (object) $this->GetProductDetails($productId);
$productDetails = (object)$this->GetProductDetails($productId);
if ($productDetails->product->enable_tare_weight_handling == 1)
{
@@ -1687,7 +1679,7 @@ class StockService extends BaseService
{
$productToKeep = $this->getDatabase()->products($productIdToKeep);
$productToRemove = $this->getDatabase()->products($productIdToRemove);
$conversion = $this->getDatabase()->quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $productToRemove->id, $productToRemove->qu_id_stock, $productToKeep->qu_id_stock)->fetch();
$conversion = $this->getDatabase()->cache__quantity_unit_conversions_resolved()->where('product_id = :1 AND from_qu_id = :2 AND to_qu_id = :3', $productToRemove->id, $productToRemove->qu_id_stock, $productToKeep->qu_id_stock)->fetch();
$factor = 1.0;
if ($conversion != null)
{

View File

@@ -5,29 +5,17 @@ namespace Grocy\Services;
class UserfieldsService extends BaseService
{
const USERFIELD_TYPE_CHECKBOX = 'checkbox';
const USERFIELD_TYPE_DATE = 'date';
const USERFIELD_TYPE_DATETIME = 'datetime';
const USERFIELD_TYPE_DECIMAL_NUMBER = 'number-decimal';
const USERFIELD_TYPE_FILE = 'file';
const USERFIELD_TYPE_IMAGE = 'image';
const USERFIELD_TYPE_INTEGRAL_NUMBER = 'number-integral';
const USERFIELD_TYPE_LINK = 'link';
const USERFIELD_TYPE_LINK_WITH_TITLE = 'link-with-title';
const USERFIELD_TYPE_PRESET_CHECKLIST = 'preset-checklist';
const USERFIELD_TYPE_PRESET_LIST = 'preset-list';
const USERFIELD_TYPE_SINGLE_LINE_TEXT = 'text-single-line';
const USERFIELD_TYPE_SINGLE_MULTILINE_TEXT = 'text-multi-line';
protected $OpenApiSpec = null;

View File

@@ -1,4 +1,4 @@
{
"Version": "4.0.0",
"ReleaseDate": "2023-07-29"
"Version": "4.0.1",
"ReleaseDate": "2023-08-06"
}

View File

@@ -16,7 +16,7 @@
sizes="32x32"
href="{{ $U('/img/icon-32.png?v=', true) }}{{ $version }}">
<link rel="manifest"
href="{{ $U('/manifest.json?v=', true) }}{{ $version }}">
href="{{ $U('/manifest') . '?data=' . base64_encode($__env->yieldContent('title') . '#' . $U($_SERVER['REQUEST_URI'])) }}">
<title>@yield('title') | Grocy</title>

View File

@@ -29,14 +29,14 @@
fsevents "2.3.2"
"@fontsource/open-sans@^5.0.0":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@fontsource/open-sans/-/open-sans-5.0.5.tgz#2a171c159ff2d57251f75b872e24375c279fc133"
integrity sha512-gxFAandXkpbHKysq5gXk1zEpx44izNyaJs82gjnx8Z0Qi2nwCamweowiSGv42JxLZPgM8RB5amztHxPCeKcBJw==
version "5.0.8"
resolved "https://registry.yarnpkg.com/@fontsource/open-sans/-/open-sans-5.0.8.tgz#d2a38bec673e1c5f8115fa8066aa93117d12225d"
integrity sha512-d3Shc6QHoZMzZIL6m4M/geyK0EXHqLvUm9fmlkr1L/AEWgIasoLEBMKEdMczV2e66SHYAYKagIsBwfWnVhmPGQ==
"@fortawesome/fontawesome-free@^6.1.1":
version "6.4.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz#1ee0c174e472c84b23cb46c995154dc383e3b4fe"
integrity sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ==
version "6.4.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz#36b6a9cb5ffbecdf89815c94d0c0ffa489ac5ecb"
integrity sha512-m5cPn3e2+FDCOgi1mz0RexTUvvQibBebOUlUlW0+YrMjDTPkiJ6VTKukA1GRsvRw+12KyJndNjj0O4AgTxm2Pg==
"@types/jquery@^3.5.16":
version "3.5.16"
@@ -115,9 +115,9 @@ bootstrap@^4.5.2, bootstrap@^4.6.1:
integrity sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==
bwip-js@^3.0.1:
version "3.4.4"
resolved "https://registry.yarnpkg.com/bwip-js/-/bwip-js-3.4.4.tgz#d2dba465ebbb9fbbad56883b02e294d3bb8c6f41"
integrity sha512-0TUQrsrActseqwXelzMmrKAdBIuKzqBBzqrWxiMiTt/cL6/Rp5io6WJhyBwelJYRWRBR2s5M8dttV5chn5FfiQ==
version "3.4.5"
resolved "https://registry.yarnpkg.com/bwip-js/-/bwip-js-3.4.5.tgz#e55a9ce5cd13a17b6f2b1370cda367741764d169"
integrity sha512-qeMBeeHkELC+ZTU3UjUFF0mkIQmhqKzo4B5fYfx98hitsdBMytSuAE9oVz2mE02GQ09u3QmgU/kke0qONumuGQ==
caseless@~0.12.0:
version "0.12.0"
@@ -216,9 +216,9 @@ data-uri-to-buffer@0.0.3:
integrity sha512-Cp+jOa8QJef5nXS5hU7M1DWzXPEIoVR3kbV0dQuVGwROZg8bGf1DcCnkmajBTnvghTtSNMUdRrPjgaT6ZQucbw==
datatables.net-bs4@>=1.13.4, datatables.net-bs4@^1.10.22:
version "1.13.5"
resolved "https://registry.yarnpkg.com/datatables.net-bs4/-/datatables.net-bs4-1.13.5.tgz#b4919861a86ab422ac691747d687e9f62e0e1519"
integrity sha512-ii7+yRMkxdEkv4NuzVVsJydTKW24qKIs1m1kX//C2T4eb6cV1cOT+CxV65UchJe6cKLUM883NLxZ0yy7eEaBjw==
version "1.13.6"
resolved "https://registry.yarnpkg.com/datatables.net-bs4/-/datatables.net-bs4-1.13.6.tgz#171ac930cbb68cd1a337e80040bddf537f3845a7"
integrity sha512-+ZYDvpvCf0L0qSXPGKbb17arFPNqnjkyrvAEamR9SGQaGK7PprVaNTLmRfP0Xq2dBxVYr+Y+OD/q63zaDo0cSA==
dependencies:
datatables.net ">=1.13.4"
jquery ">=1.7"
@@ -241,9 +241,9 @@ datatables.net-colreorder@>=1.6.2, datatables.net-colreorder@^1.5.2:
jquery ">=1.7"
datatables.net-plugins@^1.10.20:
version "1.13.5"
resolved "https://registry.yarnpkg.com/datatables.net-plugins/-/datatables.net-plugins-1.13.5.tgz#6ceb96e1a5f813f28280791057608cbf2631c734"
integrity sha512-sMxZZp+EFt5ufS2wRsIiH1jS6oVkXJ/ffpn0MwQeEJbYwMYhWdUQa/FSou6qrNgGbmOtYQ8g355H4biSsMl6bQ==
version "1.13.6"
resolved "https://registry.yarnpkg.com/datatables.net-plugins/-/datatables.net-plugins-1.13.6.tgz#7b0af0675083e2c669ccd09ef7c86878d6c9638d"
integrity sha512-CPLH+09OiEAP3PKbZH7u2qcLajgHhy4fBHCdLzjGWJwKbIkhaPu7tby4jZHQXqoolUznbm3TEpJj4eMI1eqcGw==
dependencies:
"@types/jquery" "^3.5.16"
datatables.net "^1.13.2"
@@ -283,9 +283,9 @@ datatables.net-select@>=1.6.2, datatables.net-select@^1.3.1:
jquery ">=1.7"
datatables.net@>=1.13.4, datatables.net@^1.10.22, datatables.net@^1.13.2:
version "1.13.5"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.13.5.tgz#790a3d70d5e103f5465ed8c52a50eb242e1e2dc4"
integrity sha512-XoCQHkUM5MwbC3Wx7WpVvt4i880J8pIFDA9HIKD4GhvtalryBfmdd+bZvrc/rEbraZS7U4eR2k8/wFY0NeHVqQ==
version "1.13.6"
resolved "https://registry.yarnpkg.com/datatables.net/-/datatables.net-1.13.6.tgz#6e282adbbb2732e8df495611b8bb54e19f7a943e"
integrity sha512-rHNcnW+yEP9me82/KmRcid5eKrqPqW3+I/p1TwqCW3c/7GRYYkDyF6aJQOQ9DNS/pw+nyr4BVpjyJ3yoZXiFPg==
dependencies:
jquery ">=1.7"
@@ -654,9 +654,9 @@ summernote@^0.8.18:
integrity sha512-W9RhjQjsn+b1s9xiJQgJbCiYGJaDAc9CdEqXo+D13WuStG8lCdtKaO5AiNiSSMJsQJN2EfGSwbBQt+SFE2B8Kw==
swagger-ui-dist@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.2.0.tgz#175e112b3aea756fdbbbb035d4cffef26ac579d1"
integrity sha512-rLvJBgualxNZcwKOmTFzy4zF1nHy+3S0pUDDR/ageDRZgi8aITSe7pVYiAy03xGQZtqEifjwEtHQE+eF14gveg==
version "5.3.1"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.3.1.tgz#ae76a74136152d790b06a8b71ca389cac35ab78f"
integrity sha512-El78OvXp9zMasfPrshtkW1CRx8AugAKoZuGGOTW+8llJzOV1RtDJYqQRz/6+2OakjeWWnZuRlN2Qj1Y0ilux3w==
tempusdominus-bootstrap-4@^5.39.2:
version "5.39.2"