mirror of
				https://github.com/grocy/grocy.git
				synced 2025-10-31 18:49:38 +00:00 
			
		
		
		
	Implement that recipe ingredients can have arbitrary quantity units (references #32)
This commit is contained in:
		| @@ -77,7 +77,8 @@ class RecipesController extends BaseController | ||||
| 			return $this->AppContainer->view->render($response, 'recipeposform', [ | ||||
| 				'mode' => 'create', | ||||
| 				'recipe' => $this->Database->recipes($args['recipeId']), | ||||
| 				'products' =>  $this->Database->products()->orderBy('name') | ||||
| 				'products' => $this->Database->products()->orderBy('name'), | ||||
| 				'quantityUnits' => $this->Database->quantity_units()->orderBy('name') | ||||
| 			]); | ||||
| 		} | ||||
| 		else | ||||
| @@ -86,7 +87,8 @@ class RecipesController extends BaseController | ||||
| 				'mode' => 'edit', | ||||
| 				'recipe' =>  $this->Database->recipes($args['recipeId']), | ||||
| 				'recipePos' => $this->Database->recipes_pos($args['recipePosId']), | ||||
| 				'products' =>  $this->Database->products()->orderBy('name') | ||||
| 				'products' => $this->Database->products()->orderBy('name'), | ||||
| 				'quantityUnits' => $this->Database->quantity_units()->orderBy('name') | ||||
| 			]); | ||||
| 		} | ||||
| 	} | ||||
|   | ||||
| @@ -208,6 +208,8 @@ return array( | ||||
| 	'Never expires' => 'Läuft nie ab', | ||||
| 	'This cannot be lower than #1' => 'Dies darf nicht kleiner als #1 sein', | ||||
| 	'-1 means that this product never expires' => '-1 bedeuet, dass dieses Produkt niemals abläuft', | ||||
| 	'Quantity unit' => 'Mengeneinheit', | ||||
|     'Only check if a single unit is in stock (a different quantity can then be used above)' => 'Nur prüfen, ob eine einzelne Einheit vorrätig ist (eine abweichende Mengeneinheit kann dann oben verwendet werden)', | ||||
| 	 | ||||
| 	//Constants | ||||
| 	'manually' => 'Manuell', | ||||
| @@ -272,5 +274,10 @@ return array( | ||||
| 	'Italian' => 'Italienisch', | ||||
| 	'Demo in different language' => 'Demo in anderer Sprache', | ||||
| 	'This is the note content of the recipe ingredient' => 'Dies ist der Inhalt der Notiz der Zutat', | ||||
| 	'Demo User' => 'Demo Benutzer' | ||||
| 	'Demo User' => 'Demo Benutzer', | ||||
| 	'Gram' => 'Gramm', | ||||
|     'Grams' => 'Gramm', | ||||
|     'Flour' => 'Mehl', | ||||
|     'Pancackes' => 'Pfannkuchen', | ||||
|     'Sugar' => 'Zucker' | ||||
| ); | ||||
|   | ||||
							
								
								
									
										41
									
								
								migrations/0034.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								migrations/0034.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| ALTER TABLE recipes_pos | ||||
| ADD qu_id INTEGER; | ||||
|  | ||||
| UPDATE recipes_pos | ||||
| SET qu_id = (SELECT qu_id_stock FROM products where id = product_id); | ||||
|  | ||||
| CREATE TRIGGER recipes_pos_qu_id_default AFTER INSERT ON recipes_pos | ||||
| BEGIN | ||||
| 	UPDATE recipes_pos | ||||
| 	SET qu_id = (SELECT qu_id_stock FROM products where id = product_id) | ||||
| 	WHERE qu_id IS NULL | ||||
| 		AND id = NEW.id; | ||||
| END; | ||||
|  | ||||
| ALTER TABLE recipes_pos | ||||
| ADD only_check_single_unit_in_stock TINYINT NOT NULL DEFAULT 0; | ||||
|  | ||||
| DROP VIEW recipes_fulfillment; | ||||
| CREATE VIEW recipes_fulfillment | ||||
| AS | ||||
| SELECT | ||||
| 	r.id AS recipe_id, | ||||
| 	rp.id AS recipe_pos_id, | ||||
| 	rp.product_id AS product_id, | ||||
| 	rp.amount AS recipe_amount, | ||||
| 	IFNULL(sc.amount, 0) AS stock_amount, | ||||
| 	CASE WHEN IFNULL(sc.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END THEN 1 ELSE 0 END AS need_fulfilled, | ||||
| 	CASE WHEN IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END < 0 THEN ABS(IFNULL(sc.amount, 0) - CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END) ELSE 0 END AS missing_amount, | ||||
| 	IFNULL(sl.amount, 0) AS amount_on_shopping_list, | ||||
| 	CASE WHEN IFNULL(sc.amount, 0) + IFNULL(sl.amount, 0) >= CASE WHEN rp.only_check_single_unit_in_stock = 1 THEN 1 ELSE IFNULL(rp.amount, 0) END THEN 1 ELSE 0 END AS need_fulfilled_with_shopping_list, | ||||
| 	rp.qu_id | ||||
| FROM recipes r | ||||
| JOIN recipes_pos rp | ||||
| 	ON r.id = rp.recipe_id | ||||
| LEFT JOIN ( | ||||
| 	SELECT product_id, SUM(amount + amount_autoadded) 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; | ||||
| @@ -2,7 +2,7 @@ | ||||
| { | ||||
| 	e.preventDefault(); | ||||
|  | ||||
| 	var jsonData = $('#recipe-pos-form').serializeJSON(); | ||||
| 	var jsonData = $('#recipe-pos-form').serializeJSON({ checkboxUncheckedValue: "0" }); | ||||
| 	jsonData.recipe_id = Grocy.EditObjectParentId; | ||||
| 	console.log(jsonData); | ||||
| 	if (Grocy.EditMode === 'create') | ||||
| @@ -44,7 +44,10 @@ Grocy.Components.ProductPicker.GetPicker().on('change', function(e) | ||||
| 		Grocy.Api.Get('stock/get-product-details/' + productId, | ||||
| 			function (productDetails) | ||||
| 			{ | ||||
| 				$('#amount_qu_unit').text(productDetails.quantity_unit_purchase.name); | ||||
| 				if (!$("#only_check_single_unit_in_stock").is(":checked")) | ||||
| 				{ | ||||
| 					$("#qu_id").val(productDetails.quantity_unit_stock.id); | ||||
| 				} | ||||
| 				$('#amount').focus(); | ||||
| 				Grocy.FrontendHelpers.ValidateForm('recipe-pos-form'); | ||||
| 			}, | ||||
| @@ -96,3 +99,16 @@ $('#recipe-pos-form input').keydown(function (event) | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| $("#only_check_single_unit_in_stock").on("click", function() | ||||
| { | ||||
| 	if (this.checked) | ||||
| 	{ | ||||
| 		$("#qu_id").removeAttr("disabled"); | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		$("#qu_id").attr("disabled", ""); | ||||
| 		Grocy.Components.ProductPicker.GetPicker().trigger("change"); | ||||
| 	} | ||||
| }); | ||||
|   | ||||
| @@ -21,14 +21,15 @@ class DemoDataGeneratorService extends BaseService | ||||
| 				INSERT INTO users (username, password) VALUES ('{$localizationService->Localize('Demo User')} 3', 'x'); | ||||
| 				INSERT INTO users (username, password) VALUES ('{$localizationService->Localize('Demo User')} 4', 'x'); | ||||
|  | ||||
| 				INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Pantry')}'); --2 | ||||
| 				INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Candy cupboard')}'); --3 | ||||
| 				INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Tinned food cupboard')}'); --4 | ||||
| 				INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Pantry')}'); --3 | ||||
| 				INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Candy cupboard')}'); --4 | ||||
| 				INSERT INTO locations (name) VALUES ('{$localizationService->Localize('Tinned food cupboard')}'); --5 | ||||
|  | ||||
| 				INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Glass')}', '{$localizationService->Localize('Glasses')}'); --4 | ||||
| 				INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Tin')}', '{$localizationService->Localize('Tins')}'); --5 | ||||
| 				INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Can')}', '{$localizationService->Localize('Cans')}'); --6 | ||||
| 				INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Bunch')}', '{$localizationService->Localize('Bunches')}'); --7 | ||||
| 				INSERT INTO quantity_units (name, name_plural) VALUES ('{$localizationService->Localize('Gram')}', '{$localizationService->Localize('Grams')}'); --8 | ||||
|  | ||||
| 				DELETE FROM sqlite_sequence WHERE name = 'products'; --Just to keep IDs in order as mentioned here... | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount) VALUES ('{$localizationService->Localize('Cookies')}', 3, 3, 3, 1, 8); --1 | ||||
| @@ -51,6 +52,8 @@ class DemoDataGeneratorService extends BaseService | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Salami')}', 2, 3, 3, 1); --18 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Toast')}', 4, 5, 5, 1); --19 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Minced meat')}', 2, 3, 3, 1); --20 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Flour')}', 2, 3, 3, 1); --21 | ||||
| 				INSERT INTO products (name, location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock) VALUES ('{$localizationService->Localize('Sugar')}', 3, 3, 3, 1); --22 | ||||
|  | ||||
| 				INSERT INTO shopping_list (note, amount) VALUES ('{$localizationService->Localize('Some good snacks')}', 1); | ||||
| 				INSERT INTO shopping_list (product_id, amount) VALUES (20, 1); | ||||
| @@ -59,6 +62,7 @@ class DemoDataGeneratorService extends BaseService | ||||
| 				INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pizza')}', '{$loremIpsum}'); --1 | ||||
| 				INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Spaghetti bolognese')}', '{$loremIpsum}'); --2 | ||||
| 				INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Sandwiches')}', '{$loremIpsum}'); --3 | ||||
| 				INSERT INTO recipes (name, description) VALUES ('{$localizationService->Localize('Pancackes')}', '{$loremIpsum}'); --4 | ||||
|  | ||||
| 				INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 16, 1); | ||||
| 				INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (1, 17, 1); | ||||
| @@ -70,6 +74,9 @@ class DemoDataGeneratorService extends BaseService | ||||
| 				INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (2, 20, 1); | ||||
| 				INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (3, 10, 1); | ||||
| 				INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (3, 11, 1); | ||||
| 				INSERT INTO recipes_pos (recipe_id, product_id, amount) VALUES (4, 5, 4); | ||||
| 				INSERT INTO recipes_pos (recipe_id, product_id, amount, qu_id, only_check_single_unit_in_stock) VALUES (4, 21, 200, 8, 1); | ||||
| 				INSERT INTO recipes_pos (recipe_id, product_id, amount, qu_id, only_check_single_unit_in_stock) VALUES (4, 22, 200, 8, 1); | ||||
|  | ||||
| 				INSERT INTO habits (name, period_type, period_days) VALUES ('{$localizationService->Localize('Changed towels in the bathroom')}', 'manually', 5); --1 | ||||
| 				INSERT INTO habits (name, period_type, period_days) VALUES ('{$localizationService->Localize('Cleaned the kitchen floor')}', 'dynamic-regular', 7); --2 | ||||
| @@ -151,6 +158,10 @@ class DemoDataGeneratorService extends BaseService | ||||
| 			$stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-30 days')), $this->RandomPrice()); | ||||
| 			$stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-40 days')), $this->RandomPrice()); | ||||
| 			$stockService->AddProduct(15, 1, date('Y-m-d', strtotime('-2 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-50 days')), $this->RandomPrice()); | ||||
| 			$stockService->AddProduct(21, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); | ||||
| 			$stockService->AddProduct(21, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); | ||||
| 			$stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-10 days')), $this->RandomPrice()); | ||||
| 			$stockService->AddProduct(22, 1, date('Y-m-d', strtotime('+200 days')), StockService::TRANSACTION_TYPE_PURCHASE, date('Y-m-d', strtotime('-20 days')), $this->RandomPrice()); | ||||
| 			$stockService->AddMissingProductsToShoppingList(); | ||||
|  | ||||
| 			$habitsService = new HabitsService(); | ||||
|   | ||||
| @@ -76,7 +76,7 @@ | ||||
| 						{{ FindObjectInArrayByPropertyValue($products, 'id', $recipePosition->product_id)->name }} | ||||
| 					</td> | ||||
| 					<td> | ||||
| 						{{ $recipePosition->amount }} {{ Pluralize($recipePosition->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $recipePosition->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $recipePosition->product_id)->qu_id_stock)->name_plural) }} | ||||
| 						{{ $recipePosition->amount }} {{ Pluralize($recipePosition->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $recipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $recipePosition->qu_id)->name_plural) }} | ||||
| 						<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $recipePosition->id)->need_fulfilled == 1) {{ $L('Enough in stock') }} @else {{ $L('Not enough in stock, #1 missing, #2 already on shopping list', FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $recipePosition->id)->missing_amount, FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $recipePosition->id)->amount_on_shopping_list) }} @endif</span> | ||||
| 					</td> | ||||
| 					<td class="fit-content"> | ||||
|   | ||||
| @@ -32,11 +32,35 @@ | ||||
| 				'prefillByName' => $prefillByName | ||||
| 			)) | ||||
|  | ||||
| 			<div class="form-group"> | ||||
| 				<label for="amount">{{ $L('Amount') }}  <span id="amount_qu_unit" class="small text-muted"></span></label> | ||||
| 			<div class="form-group row"> | ||||
| 				<div class="col"> | ||||
| 					<div class="row"> | ||||
| 						<div class="form-group col-4"> | ||||
| 							<label for="amount">{{ $L('Amount') }}</label> | ||||
| 							<input type="number" class="form-control" id="amount" name="amount" value="@if($mode == 'edit'){{ $recipePos->amount }}@else{{1}}@endif" min="0" required> | ||||
| 							<div class="invalid-feedback">{{ $L('This cannot be negative') }}</div> | ||||
| 						</div> | ||||
| 						<div class="form-group col-8"> | ||||
| 							<label for="qu_id">{{ $L('Quantity unit') }}</label> | ||||
| 							<select required @if($mode == 'create' || ($mode == 'edit' && $recipePos->only_check_single_unit_in_stock != 1)) disabled @endif class="form-control" id="qu_id" name="qu_id"> | ||||
| 								@foreach($quantityUnits as $quantityunit) | ||||
| 									<option @if($mode == 'edit' && $quantityunit->id == $recipePos->qu_id) selected @endif value="{{ $quantityunit->id }}">{{ $quantityunit->name }}</option> | ||||
| 								@endforeach | ||||
| 							</select> | ||||
| 							<div class="invalid-feedback">{{ $L('A quantity unit is required') }}</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div class="row"> | ||||
| 						<div class="col"> | ||||
| 							<div class="form-check"> | ||||
| 								<input type="hidden" name="only_check_single_unit_in_stock" value="0"> | ||||
| 								<input @if($mode == 'edit' && $recipePos->only_check_single_unit_in_stock == 1) checked @endif class="form-check-input" type="checkbox" id="only_check_single_unit_in_stock" name="only_check_single_unit_in_stock" value="1"> | ||||
| 								<label class="form-check-label" for="only_check_single_unit_in_stock">{{ $L('Only check if a single unit is in stock (a different quantity can then be used above)') }}</label> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | ||||
| 			<div class="form-group"> | ||||
| 				<label for="note">{{ $L('Note') }}</label> | ||||
|   | ||||
| @@ -71,7 +71,7 @@ | ||||
| 			<ul class="list-group list-group-flush"> | ||||
| 				@foreach($selectedRecipePositions as $selectedRecipePosition) | ||||
| 				<li class="list-group-item"> | ||||
| 					{{ $selectedRecipePosition->amount }} {{ Pluralize($selectedRecipePosition->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->qu_id_stock)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }} | ||||
| 					{{ $selectedRecipePosition->amount }} {{ Pluralize($selectedRecipePosition->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', $selectedRecipePosition->qu_id)->name_plural) }} {{ FindObjectInArrayByPropertyValue($products, 'id', $selectedRecipePosition->product_id)->name }} | ||||
| 					<span class="timeago-contextual">@if(FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->need_fulfilled == 1) {{ $L('Enough in stock') }} @else {{ $L('Not enough in stock, #1 missing, #2 already on shopping list', FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->missing_amount, FindObjectInArrayByPropertyValue($recipesFulfillment, 'recipe_pos_id', $selectedRecipePosition->id)->amount_on_shopping_list) }} @endif</span> | ||||
|  | ||||
| 					@if(!empty($selectedRecipePosition->note)) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user