From caf8bdfe61715373f943a7237014fa11ed65dc96 Mon Sep 17 00:00:00 2001
From: Robert Goldmann <deadlocker@gmx.de>
Date: Sun, 30 Jan 2022 12:46:45 +0100
Subject: [PATCH] #546 - import/export database: importing template groups is
 optional

---
 .../database/InternalDatabase.java            |  1 +
 .../budgetmaster/services/EntityType.java     |  2 +-
 .../budgetmaster/services/ImportService.java  | 21 ++++++--
 .../settings/SettingsController.java          |  8 ++-
 .../resources/languages/base_de.properties    |  6 ++-
 .../resources/languages/base_en.properties    |  4 +-
 ...tabaseParser_v8_convertToInternalTest.java |  2 +-
 .../unit/database/ImportServiceTest.java      | 50 ++++++++++++++-----
 8 files changed, 71 insertions(+), 23 deletions(-)

diff --git a/src/main/java/de/deadlocker8/budgetmaster/database/InternalDatabase.java b/src/main/java/de/deadlocker8/budgetmaster/database/InternalDatabase.java
index 6437bc5da..9fcb7495f 100644
--- a/src/main/java/de/deadlocker8/budgetmaster/database/InternalDatabase.java
+++ b/src/main/java/de/deadlocker8/budgetmaster/database/InternalDatabase.java
@@ -96,6 +96,7 @@ public class InternalDatabase
 
 		numberOfEntitiesByType.put(EntityType.TRANSACTION, transactions.size());
 		numberOfEntitiesByType.put(EntityType.TEMPLATE, templates.size());
+		numberOfEntitiesByType.put(EntityType.TEMPLATE_GROUP, templateGroups.size());
 		numberOfEntitiesByType.put(EntityType.IMAGE, images.size());
 		numberOfEntitiesByType.put(EntityType.CHART, charts.size());
 		return numberOfEntitiesByType;
diff --git a/src/main/java/de/deadlocker8/budgetmaster/services/EntityType.java b/src/main/java/de/deadlocker8/budgetmaster/services/EntityType.java
index 28f35c47e..ee0a10e30 100644
--- a/src/main/java/de/deadlocker8/budgetmaster/services/EntityType.java
+++ b/src/main/java/de/deadlocker8/budgetmaster/services/EntityType.java
@@ -17,7 +17,7 @@ public enum EntityType implements LocalizedEnum
 	IMAGE("image", "background-grey", ImportRequired.REQUIRED),
 	HOTKEYS("keyboard", "background-grey", ImportRequired.NONE),
 	ABOUT("info", "background-grey", ImportRequired.NONE),
-	TEMPLATE_GROUP("folder", "background-orange-dark", ImportRequired.REQUIRED);
+	TEMPLATE_GROUP("folder", "background-orange-dark", ImportRequired.OPTIONAL);
 
 
 	public enum ImportRequired
diff --git a/src/main/java/de/deadlocker8/budgetmaster/services/ImportService.java b/src/main/java/de/deadlocker8/budgetmaster/services/ImportService.java
index 95c8612a6..31f395868 100644
--- a/src/main/java/de/deadlocker8/budgetmaster/services/ImportService.java
+++ b/src/main/java/de/deadlocker8/budgetmaster/services/ImportService.java
@@ -72,7 +72,7 @@ public class ImportService
 		this.iconService = iconService;
 	}
 
-	public List<ImportResultItem> importDatabase(InternalDatabase database, AccountMatchList accountMatchList, Boolean importTemplates, Boolean importCharts)
+	public List<ImportResultItem> importDatabase(InternalDatabase database, AccountMatchList accountMatchList, Boolean importTemplateGroups, Boolean importTemplates, Boolean importCharts)
 	{
 		this.database = database;
 		this.collectedErrorMessages = new ArrayList<>();
@@ -86,14 +86,21 @@ public class ImportService
 		importResultItems.add(importAccounts(accountMatchList));
 		importResultItems.add(importTransactions());
 
-		if(importTemplates)
+		if(importTemplateGroups)
 		{
 			importResultItems.add(importTemplateGroups());
-			importResultItems.add(importTemplates());
 		}
 		else
 		{
 			importResultItems.add(new ImportResultItem(EntityType.TEMPLATE_GROUP, 0, 0));
+		}
+
+		if(importTemplates)
+		{
+			importResultItems.add(importTemplates(importTemplateGroups));
+		}
+		else
+		{
 			importResultItems.add(new ImportResultItem(EntityType.TEMPLATE, 0, 0));
 		}
 
@@ -471,7 +478,7 @@ public class ImportService
 		return updatedItems;
 	}
 
-	private ImportResultItem importTemplates()
+	private ImportResultItem importTemplates(Boolean importTemplateGroups)
 	{
 		List<Template> templates = database.getTemplates();
 		LOGGER.debug(MessageFormat.format("Importing {0} templates...", templates.size()));
@@ -486,6 +493,12 @@ public class ImportService
 				LOGGER.debug(MessageFormat.format("Importing template {0}/{1} (templateName: {2})", i + 1, templates.size(), template.getTemplateName()));
 				updateTagsForItem(template);
 				template.setID(null);
+
+				if(!importTemplateGroups)
+				{
+					template.setTemplateGroup(null);
+				}
+
 				templateRepository.save(template);
 
 				numberOfImportedTemplates++;
diff --git a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java
index 988ef3c95..f580603b1 100644
--- a/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java
+++ b/src/main/java/de/deadlocker8/budgetmaster/settings/SettingsController.java
@@ -80,6 +80,7 @@ public class SettingsController extends BaseController
 	{
 		prepareBasicModel(model, settingsService.getSettings());
 		request.removeAttribute("database", RequestAttributes.SCOPE_SESSION);
+		request.removeAttribute("importTemplatesGroups", RequestAttributes.SCOPE_SESSION);
 		request.removeAttribute("importTemplates", RequestAttributes.SCOPE_SESSION);
 		request.removeAttribute("importCharts", RequestAttributes.SCOPE_SESSION);
 
@@ -316,9 +317,11 @@ public class SettingsController extends BaseController
 	@PostMapping("/database/import/step2")
 	public String importStepTwoPost(WebRequest request, Model model,
 									@RequestParam(value = "TEMPLATE", required = false) boolean importTemplates,
+									@RequestParam(value = "TEMPLATE_GROUP", required = false) boolean importTemplatesGroups,
 									@RequestParam(value = "CHART", required = false) boolean importCharts)
 	{
 		request.setAttribute("importTemplates", importTemplates, RequestAttributes.SCOPE_SESSION);
+		request.setAttribute("importTemplateGroups", importTemplatesGroups, RequestAttributes.SCOPE_SESSION);
 		request.setAttribute("importCharts", importCharts, RequestAttributes.SCOPE_SESSION);
 
 		model.addAttribute("database", request.getAttribute("database", RequestAttributes.SCOPE_SESSION));
@@ -358,12 +361,15 @@ public class SettingsController extends BaseController
 		final Boolean importTemplates = (Boolean) request.getAttribute("importTemplates", RequestAttributes.SCOPE_SESSION);
 		request.removeAttribute("importTemplates", RequestAttributes.SCOPE_SESSION);
 
+		final Boolean importTemplateGroups = (Boolean) request.getAttribute("importTemplateGroups", RequestAttributes.SCOPE_SESSION);
+		request.removeAttribute("importTemplateGroups", RequestAttributes.SCOPE_SESSION);
+
 		final Boolean importCharts = (Boolean) request.getAttribute("importCharts", RequestAttributes.SCOPE_SESSION);
 		request.removeAttribute("importCharts", RequestAttributes.SCOPE_SESSION);
 
 		prepareBasicModel(model, settingsService.getSettings());
 
-		final List<ImportResultItem> importResultItems = importService.importDatabase(database, accountMatchList, importTemplates, importCharts);
+		final List<ImportResultItem> importResultItems = importService.importDatabase(database, accountMatchList, importTemplateGroups, importTemplates, importCharts);
 		model.addAttribute("importResultItems", importResultItems);
 		model.addAttribute("errorMessages", importService.getCollectedErrorMessages());
 
diff --git a/src/main/resources/languages/base_de.properties b/src/main/resources/languages/base_de.properties
index 0114d98da..73091b2ba 100644
--- a/src/main/resources/languages/base_de.properties
+++ b/src/main/resources/languages/base_de.properties
@@ -595,14 +595,16 @@ statistics.first.transaction=Erste Buchung {0}
 entity.account=Konten
 entity.category=Kategorien
 entity.transaction=Buchungen
+entity.template_group=Vorlagengruppen
 entity.template=Vorlagen
 entity.chart=Diagramme
 entity.image=Bilder
 
 import.entity.category=Alle Kategorien werden nacheinander importiert.<br>Wird dabei eine Kategorie mit gleichem Namen und gleicher Farbe in der bestehenden BudgetMaster Datenbank gefunden, so werden alle Buchungen und Vorlagen, die zur Kategorie gehören in diese bestehende Kategorie verschoben.<br>Das Icon der Kategorie wird in diesem Fall nicht angepasst.<br>Wenn keine bestehende Kategorie den gleichen Namen und die gleiche Farbe aufweist, so wird eine neue Kategorie angelegt.
-import.entity.account=Jedes zu importierende Konto wird in den weiteren Schritten des Importvorgangs einem Konto in der bestehenden Datenbank zugeordnet.<br>Die zugehörigen Buchungen und Vorlagen werden dann mit dem zugordneten Konto verknüpft.<br>Falls nicht ausreichend Konten oder kein passendes Konto existiert, können neue Konten während des Importvorgangs angelegt werden.<br>Wenn ein zu importierendes Konto ein Icon gesetzt hat, so wird das Icon im bestehenden Konto ersetzt.
+import.entity.account=Jedes zu importierende Konto wird in den weiteren Schritten des Importvorgangs einem Konto in der bestehenden Datenbank zugeordnet.<br>Die zugehörigen Buchungen und Vorlagen werden dann mit dem zugeordneten Konto verknüpft.<br>Falls nicht ausreichend Konten oder kein passendes Konto existiert, können neue Konten während des Importvorgangs angelegt werden.<br>Wenn ein zu importierendes Konto ein Icon gesetzt hat, so wird das Icon im bestehenden Konto ersetzt.
 import.entity.transaction=Alle Buchungen werden nacheinander importiert und dabei dem jeweils verknüpften Konto zugeordnet.<br>Wiederholende Buchungen werden am Ende des Importvorgangs automatisch bis zum aktuellen Datum aktualisiert.
-import.entity.template=Alle Vorlagen werden nacheinander importiert und dabei die verknüpften Konten neu zugeordnet.<br>Der Import von Vorlagen ist optional.
+import.entity.template_group=Wenn bereits eine Gruppe mit gleichem Namen in der bestehenden BudgetMaster Datenbank existiert, so werden alle Vorlagen, die zur Gruppe gehören in diese bestehende Gruppe verschoben.<br>Wenn keine bestehende Gruppe den gleichen Namen aufweist, so wird eine neue Gruppe angelegt.<br>Der Import von Vorlagengruppen ist optional.<br>Wird der Import deaktiviert, so werden alle Vorlagen, die einer Gruppe zugeordnet sind mit der Standardgruppe verknüpft.
+import.entity.template=Alle Vorlagen werden nacheinander importiert und dabei die verknüpften Konten neu zugeordnet.<br>Der Import von Vorlagen ist optional.<br>Vorlagen verlieren ihre Gruppenzuordnung, wenn der Import von Vorlagengruppen deaktiviert wird. 
 import.entity.image=Icons werden automatisch mit den zugehörigen Konten, Vorlagen und Kategorien importiert.
 import.entity.chart=Es werden nur die benutzerdefinierten Diagramme importiert.<br>Der Import von Diagrammen ist optional.
 
diff --git a/src/main/resources/languages/base_en.properties b/src/main/resources/languages/base_en.properties
index 8cef55684..c3c55f683 100644
--- a/src/main/resources/languages/base_en.properties
+++ b/src/main/resources/languages/base_en.properties
@@ -594,6 +594,7 @@ statistics.first.transaction=First Transaction {0}
 entity.account=Accounts
 entity.category=Categories
 entity.transaction=Transactions
+entity.template_group=Template groups
 entity.template=Templates
 entity.chart=Charts
 entity.image=Images
@@ -601,7 +602,8 @@ entity.image=Images
 import.entity.category=All categories will be imported one by one.<br>If a category with the same name and color already exists in the BudgetMaster database, all transactions and templates belonging to the category will be moved to this existing category.<br>The category icon will not be updated in this case.<br>If no existing category has the same name and color, a new category will be created.
 import.entity.account=Each account to be imported will be assigned to an existing account in the database during the next steps of the import process.<br>The associated transactions and templates will then be linked to the assigned account.<br>If there are not enough accounts or no matching account, new accounts can be created during the import process.<br>If an account has an icon set, the icon in the existing account will be replaced.
 import.entity.transaction=All transactions will be imported one by one, assigning them to the respective linked account.<br>Recurring transactions are automatically updated to the current date at the end of the import process.
-import.entity.template=All templates will be imported one by one, reassigning the linked accounts.<br>The import of templates is optional.
+import.entity.template_group=If a group with the same name already exists in the BudgetMaster database, all templates belonging to the group will be moved to this existing group.<br>f no existing group has the same name, a new group will be created.<br>The import of template groups is optional.<br>If the import is deactivated, all templates assigned to a group will be linked to the default group.
+import.entity.template=All templates will be imported one by one, reassigning the linked accounts.<br>The import of templates is optional.<br>Templates lose their group assignment when template group import is disabled.
 import.entity.image=Icons are automatically imported with their associated accounts, templates and categories.
 import.entity.chart=Only user-defined charts will be imported.<br>The import of charts is optional.
 
diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v8_convertToInternalTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v8_convertToInternalTest.java
index 56dd3b010..8cba7d792 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v8_convertToInternalTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/unit/database/DatabaseParser_v8_convertToInternalTest.java
@@ -182,7 +182,7 @@ class DatabaseParser_v8_convertToInternalTest
 			template.setIconReference(icon);
 			template.setTags(List.of());
 
-			assertThat(database.getTemplates()).hasSize(4)
+			assertThat(database.getTemplates()).hasSize(5)
 					.contains(template);
 			assertThat(database.getTemplates().get(3).getIconReference())
 					.isEqualTo(icon);
diff --git a/src/test/java/de/deadlocker8/budgetmaster/unit/database/ImportServiceTest.java b/src/test/java/de/deadlocker8/budgetmaster/unit/database/ImportServiceTest.java
index 5bad4b468..35b0ebb6b 100644
--- a/src/test/java/de/deadlocker8/budgetmaster/unit/database/ImportServiceTest.java
+++ b/src/test/java/de/deadlocker8/budgetmaster/unit/database/ImportServiceTest.java
@@ -515,7 +515,7 @@ class ImportServiceTest
 
 		Mockito.when(templateGroupRepository.save(Mockito.any())).thenReturn(expectedTemplateGroup);
 
-		importService.importDatabase(database, accountMatchList, true, true);
+		importService.importDatabase(database, accountMatchList, true, true, true);
 		InternalDatabase databaseResult = importService.getDatabase();
 
 		// assert
@@ -553,7 +553,7 @@ class ImportServiceTest
 		final ChartRepository chartRepositoryMock = Mockito.mock(ChartRepository.class);
 		Mockito.when(chartService.getRepository()).thenReturn(chartRepositoryMock);
 
-		importService.importDatabase(database, new AccountMatchList(List.of()), true, true);
+		importService.importDatabase(database, new AccountMatchList(List.of()), true, true, true);
 		InternalDatabase databaseResult = importService.getDatabase();
 
 		// assert
@@ -596,7 +596,7 @@ class ImportServiceTest
 		Mockito.when(imageRepositoryMock.save(Mockito.any())).thenReturn(newImage);
 
 		InternalDatabase database = new InternalDatabase(List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(image), List.of());
-		importService.importDatabase(database, new AccountMatchList(List.of()), true, true);
+		importService.importDatabase(database, new AccountMatchList(List.of()), true, true, true);
 
 		Image expectedImage = new Image(image.getImage(), image.getFileName(), image.getFileExtension());
 		Mockito.verify(imageRepositoryMock, Mockito.atLeast(1)).save(expectedImage);
@@ -616,7 +616,7 @@ class ImportServiceTest
 		Mockito.when(imageRepositoryMock.save(Mockito.any())).thenReturn(newImage);
 
 		InternalDatabase database = new InternalDatabase(List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(image), List.of());
-		importService.importDatabase(database, new AccountMatchList(List.of()), true, true);
+		importService.importDatabase(database, new AccountMatchList(List.of()), true, true, true);
 
 		Image expectedImage = new Image(image.getImage(), image.getFileName(), image.getFileExtension());
 		Mockito.verify(imageRepositoryMock, Mockito.atLeast(1)).save(expectedImage);
@@ -655,7 +655,7 @@ class ImportServiceTest
 		Mockito.when(iconService.getRepository()).thenReturn(iconRepositoryMock);
 		Mockito.when(iconRepositoryMock.save(Mockito.any())).thenReturn(expectedIcon);
 
-		importService.importDatabase(database, new AccountMatchList(List.of(accountMatch)), true, true);
+		importService.importDatabase(database, new AccountMatchList(List.of(accountMatch)), true,true, true);
 
 		Mockito.verify(accountRepository, Mockito.atLeast(1)).save(expectedAccount);
 	}
@@ -663,8 +663,6 @@ class ImportServiceTest
 	@Test
 	void test_skipTemplates()
 	{
-		TemplateGroup templateGroup = new TemplateGroup(1, "My Template Group", TemplateGroupType.CUSTOM);
-
 		Template template = new Template();
 		template.setTemplateName("myTemplate");
 		template.setAmount(200);
@@ -672,14 +670,40 @@ class ImportServiceTest
 		template.setTags(new ArrayList<>());
 
 		// database
-		InternalDatabase database = new InternalDatabase(List.of(), List.of(), List.of(), List.of(templateGroup), List.of(template), List.of(), List.of(), List.of());
+		InternalDatabase database = new InternalDatabase(List.of(), List.of(), List.of(), List.of(), List.of(template), List.of(), List.of(), List.of());
 
 		// act
-		importService.importDatabase(database, new AccountMatchList(List.of()), false, true);
+		importService.importDatabase(database, new AccountMatchList(List.of()), true, false, true);
 
 		// assert
 		Mockito.verify(templateRepository, Mockito.never()).save(Mockito.any());
+	}
+
+	@Test
+	void test_skipTemplateGroups()
+	{
+		TemplateGroup templateGroup = new TemplateGroup(1, "My Template Group", TemplateGroupType.CUSTOM);
+
+		Template templateWithGroup = new Template();
+		templateWithGroup.setTemplateName("myTemplate");
+		templateWithGroup.setTags(new ArrayList<>());
+		templateWithGroup.setTemplateGroup(templateGroup);
+
+		// database
+		InternalDatabase database = new InternalDatabase(List.of(), List.of(), List.of(), List.of(templateGroup), List.of(templateWithGroup), List.of(), List.of(), List.of());
+
+		// act
+		importService.importDatabase(database, new AccountMatchList(List.of()), false, true, true);
+
+		// assert
 		Mockito.verify(templateGroupRepository, Mockito.never()).save(Mockito.any());
+
+		Template expectedTemplate = new Template();
+		expectedTemplate.setTemplateName("myTemplate");
+		expectedTemplate.setTags(new ArrayList<>());
+		expectedTemplate.setTemplateGroup(null);
+
+		Mockito.verify(templateRepository, Mockito.atLeast(1)).save(expectedTemplate);
 	}
 
 	@Test
@@ -699,7 +723,7 @@ class ImportServiceTest
 		Mockito.when(chartService.getRepository()).thenReturn(chartRepositoryMock);
 
 		// act
-		importService.importDatabase(database, new AccountMatchList(List.of()), true, false);
+		importService.importDatabase(database, new AccountMatchList(List.of()), true, true, false);
 
 		// assert
 		Mockito.verify(chartRepositoryMock, Mockito.never()).save(Mockito.any());
@@ -719,7 +743,7 @@ class ImportServiceTest
 		Mockito.when(categoryRepository.findByNameAndColorAndType(Mockito.eq("Category2"), Mockito.any(), Mockito.any())).thenReturn(category2);
 
 		InternalDatabase database = new InternalDatabase(List.of(category1, category2), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of());
-		final List<ImportResultItem> importResultItems = importService.importDatabase(database, new AccountMatchList(List.of()), false, false);
+		final List<ImportResultItem> importResultItems = importService.importDatabase(database, new AccountMatchList(List.of()), false, false, false);
 
 		assertThat(importResultItems).hasSize(7)
 				.contains(new ImportResultItem(EntityType.CATEGORY, 1, 2));
@@ -788,7 +812,7 @@ class ImportServiceTest
 		Mockito.when(templateGroupRepository.save(Mockito.any())).thenReturn(newTemplateGroup);
 
 		InternalDatabase database = new InternalDatabase(List.of(), List.of(), List.of(), List.of(templateGroup), List.of(), List.of(), List.of(), List.of());
-		importService.importDatabase(database, new AccountMatchList(List.of()), true, true);
+		importService.importDatabase(database, new AccountMatchList(List.of()), true, true, true);
 
 		TemplateGroup expectedTemplateGroup = new TemplateGroup(templateGroup.getName(), templateGroup.getType());
 		Mockito.verify(templateGroupRepository, Mockito.atLeast(1)).save(expectedTemplateGroup);
@@ -802,7 +826,7 @@ class ImportServiceTest
 		Mockito.when(templateGroupRepository.findFirstByType(TemplateGroupType.DEFAULT)).thenReturn(templateGroup);
 
 		InternalDatabase database = new InternalDatabase(List.of(), List.of(), List.of(), List.of(templateGroup), List.of(), List.of(), List.of(), List.of());
-		importService.importDatabase(database, new AccountMatchList(List.of()), true, true);
+		importService.importDatabase(database, new AccountMatchList(List.of()), true, true, true);
 
 		Mockito.verify(templateGroupRepository, Mockito.never()).save(Mockito.any());
 	}
-- 
GitLab