Compare commits
	
		
			27 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e2ebc037f2 | ||
|  | 550aa5565b | ||
|  | c105ebc979 | ||
|  | 7ef744a995 | ||
|  | ee4a082c74 | ||
|  | 1d7f7b2992 | ||
|  | 7689356a57 | ||
|  | 1401ed5c00 | ||
|  | bae4a7f04c | ||
|  | a44d746176 | ||
|  | 3afb9643c4 | ||
|  | 61a3a4329b | ||
|  | 491ad8c791 | ||
|  | 339a1ebffc | ||
|  | 1c35fecc85 | ||
|  | d006436d49 | ||
|  | 6c4cc00fd5 | ||
|  | 847337443d | ||
|  | b74fbddd94 | ||
|  | 8b444a03e5 | ||
|  | e946ec79d5 | ||
|  | ca740e8cee | ||
|  | 5d48b02b37 | ||
|  | 73ad9d39ab | ||
|  | fd7e24b7d1 | ||
|  | 57ccb8645e | ||
|  | e8d6d455f4 | 
							
								
								
									
										
											BIN
										
									
								
								.github/publication_assets/chores.png
									
									
									
									
										vendored
									
									
								
							
							
						
						| Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 87 KiB | 
							
								
								
									
										
											BIN
										
									
								
								.github/publication_assets/mealplan.png
									
									
									
									
										vendored
									
									
								
							
							
						
						| Before Width: | Height: | Size: 499 KiB After Width: | Height: | Size: 476 KiB | 
							
								
								
									
										
											BIN
										
									
								
								.github/publication_assets/shoppinglist.png
									
									
									
									
										vendored
									
									
								
							
							
						
						| Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 50 KiB | 
							
								
								
									
										
											BIN
										
									
								
								.github/publication_assets/stock.png
									
									
									
									
										vendored
									
									
								
							
							
						
						| Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 170 KiB | 
| @@ -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); | ||||
|   | ||||
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -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). | ||||
|  | ||||
|   | ||||
							
								
								
									
										9
									
								
								app.php
									
									
									
									
									
								
							
							
						
						| @@ -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'; | ||||
| }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										19
									
								
								changelog/71_4.0.1_2023-08-06.md
									
									
									
									
									
										Normal 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}` | ||||
| @@ -2,6 +2,8 @@ | ||||
|  | ||||
| > ❗ xxxImportant upgrade informationXXX | ||||
|  | ||||
| > 💡 xxxMinor upgrade informationXXX | ||||
|  | ||||
| ### New feature: xxxx | ||||
|  | ||||
| - xxx | ||||
|   | ||||
| @@ -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
									
									
									
								
							
							
						
						| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
| 		}); | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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() | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -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', [ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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')) | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -11,7 +11,6 @@ abstract class BaseBarcodeLookupPlugin | ||||
| 	} | ||||
|  | ||||
| 	protected $Locations; | ||||
|  | ||||
| 	protected $QuantityUnits; | ||||
|  | ||||
| 	final public function Lookup($barcode) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 "" | ||||
|   | ||||
| @@ -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 "Эта страница не существует" | ||||
|   | ||||
| @@ -8,7 +8,6 @@ use DI\Container; | ||||
| class BaseMiddleware | ||||
| { | ||||
| 	protected $AppContainer; | ||||
|  | ||||
| 	protected $ApplicationService; | ||||
|  | ||||
| 	public function __construct(Container $container) | ||||
|   | ||||
| @@ -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; | ||||
| 			}, | ||||
| 			[] | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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; | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -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" | ||||
| } | ||||
							
								
								
									
										10
									
								
								routes.php
									
									
									
									
									
								
							
							
						
						| @@ -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); | ||||
| }); | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
| 				}); | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
| 			}); | ||||
|   | ||||
| @@ -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>"; | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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) | ||||
| 			{ | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| { | ||||
| 	"Version": "4.0.0", | ||||
| 	"ReleaseDate": "2023-07-29" | ||||
| 	"Version": "4.0.1", | ||||
| 	"ReleaseDate": "2023-08-06" | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||
							
								
								
									
										42
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						| @@ -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" | ||||
|   | ||||