#25 Account edit mask

This commit is contained in:
2021-09-01 15:45:59 +02:00
parent 2cb7589b96
commit 6a3359ea5c
12 changed files with 224 additions and 17 deletions

View File

@@ -54,6 +54,24 @@ public class AccountController {
return responseReason.toResponseEntity(); return responseReason.toResponseEntity();
} }
@RequestMapping("editAccount")
public ResponseEntity editAccount(Long id, String key, String accountGroupName) {
final String decoded = ControllerUtil.urlDecode(key);
final String decodedGroup = ControllerUtil.urlDecode(accountGroupName);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/accounts/editAccount got parameters: %s, %s, %s", id, decoded, decodedGroup));
}
final ResponseReason responseReason = this.accountService.editAccount(id, decoded, decodedGroup);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/accounts/editAccount returns with %s", responseReason.name()));
}
return responseReason.toResponseEntity();
}
@RequestMapping("closeAccount") @RequestMapping("closeAccount")
public ResponseEntity closeAccount(String key) { public ResponseEntity closeAccount(String key) {
final String decoded = ControllerUtil.urlDecode(key); final String decoded = ControllerUtil.urlDecode(key);

View File

@@ -72,12 +72,12 @@ public class AccountService {
* *
* @param key the key of the new account * @param key the key of the new account
* @param type the type of the new account. Must be one of {@link AccountType}. * @param type the type of the new account. Must be one of {@link AccountType}.
* @param accountGroupName the name of the account group to use, can be <code>null</code> * @param accountGroupName the name of the account group to use
* *
* @return {@link ResponseReason#INVALID_ACCOUNT_TYPE} if the given type is not a valid {@link AccountType}, {@link * @return {@link ResponseReason#INVALID_ACCOUNT_TYPE} if the given type is not a valid {@link AccountType}, {@link
* ResponseReason#UNKNOWN_ERROR} if an unexpected error occurs, {@link ResponseReason#OK} if the operation completed * ResponseReason#UNKNOWN_ERROR} if an unexpected error occurs, {@link ResponseReason#OK} if the operation completed
* successfully, {@link ResponseReason#DUPLICATE_ACCOUNT_KEY} if an account with the given key already exists and * successfully, {@link ResponseReason#DUPLICATE_ACCOUNT_KEY} if an account with the given key already exists and
* {@link ResponseReason#ACCOUNT_GROUP_NOT_FOUND} if the optional parameter * {@link ResponseReason#ACCOUNT_GROUP_NOT_FOUND} if the parameter
* <code>accountGroupName</code> does not identify a valid account group. Never returns <code>null</code>. * <code>accountGroupName</code> does not identify a valid account group. Never returns <code>null</code>.
*/ */
@Transactional(propagation = Propagation.SUPPORTS) @Transactional(propagation = Propagation.SUPPORTS)
@@ -87,17 +87,14 @@ public class AccountService {
} }
final Account account = new Account(); final Account account = new Account();
final AccountGroup accountGroup = this.accountGroupService.getAccountGroupByName(accountGroupName);
if (StringUtils.isNotEmpty(accountGroupName)) { if (accountGroup == null) {
final AccountGroup accountGroup = this.accountGroupService.getAccountGroupByName(accountGroupName); return ResponseReason.ACCOUNT_GROUP_NOT_FOUND; // early return
if (accountGroup == null) {
return ResponseReason.ACCOUNT_GROUP_NOT_FOUND; // early return
}
account.setAccountGroup(accountGroup);
} }
account.setAccountGroup(accountGroup);
account.setKey(key); account.setKey(key);
account.setType(AccountType.valueOf(type)); account.setType(AccountType.valueOf(type));
// If we create an account it's implicitly open // If we create an account it's implicitly open
@@ -120,6 +117,51 @@ public class AccountService {
return ResponseReason.OK; return ResponseReason.OK;
} }
/**
* This method edits the account with the given id.
*
* @param id the id of the account to edit
* @param key the new key of the account
* @param accountGroupName the new name of the account group to use
*
* @return {@link ResponseReason#OK} if the operation completed successfully, {@link ResponseReason#UNKNOWN_ERROR}
* if an unexpected error occurs, {@link ResponseReason#DUPLICATE_ACCOUNT_KEY} if an account with the given key
* already exists and {@link ResponseReason#ACCOUNT_GROUP_NOT_FOUND} if the parameter
* <code>accountGroupName</code> does not identify a valid account group or {@link ResponseReason#ACCOUNT_NOT_FOUND}
* if the given id does not identify a valid account. Never returns <code>null</code>.
*/
@Transactional(propagation = Propagation.REQUIRED)
public ResponseReason editAccount(Long id, String key, String accountGroupName) {
final Account account = this.accountRepository.findById(id).orElse(null);
if(account == null) {
return ResponseReason.ACCOUNT_NOT_FOUND;
}
final AccountGroup accountGroup = this.accountGroupService.getAccountGroupByName(accountGroupName);
if (accountGroup == null) {
return ResponseReason.ACCOUNT_GROUP_NOT_FOUND;
}
account.setKey(key);
account.setAccountGroup(accountGroup);
try {
this.accountRepository.save(account);
} catch (DataIntegrityViolationException dive) {
LOGGER.error(String.format("Duplicate key! %s|%s", key, accountGroupName), dive);
return ResponseReason.DUPLICATE_ACCOUNT_KEY;
} catch (Exception e) {
LOGGER.error(String.format("Could not save account %s|%s", key, accountGroupName), e);
return ResponseReason.UNKNOWN_ERROR;
}
return ResponseReason.OK;
}
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
public ResponseReason closeAccount(String key) { public ResponseReason closeAccount(String key) {
return setAccountStatus(key, AccountStatus.CLOSED); return setAccountStatus(key, AccountStatus.CLOSED);

View File

@@ -3,6 +3,7 @@ package de.financer.service;
import de.financer.ResponseReason; import de.financer.ResponseReason;
import de.financer.dba.AccountRepository; import de.financer.dba.AccountRepository;
import de.financer.model.Account; import de.financer.model.Account;
import de.financer.model.AccountGroup;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -40,9 +41,11 @@ public class AccountService_createAccountTest {
public void test_createAccount_UNKNOWN_ERROR() { public void test_createAccount_UNKNOWN_ERROR() {
// Arrange // Arrange
Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class)); Mockito.doThrow(new NullPointerException()).when(this.accountRepository).save(Mockito.any(Account.class));
Mockito.when(this.accountGroupService.getAccountGroupByName(Mockito.anyString()))
.thenReturn(Mockito.mock(AccountGroup.class));
// Act // Act
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null); ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
// Assert // Assert
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response); Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
@@ -51,10 +54,11 @@ public class AccountService_createAccountTest {
@Test @Test
public void test_createAccount_OK() { public void test_createAccount_OK() {
// Arrange // Arrange
// Nothing to do Mockito.when(this.accountGroupService.getAccountGroupByName(Mockito.anyString()))
.thenReturn(Mockito.mock(AccountGroup.class));
// Act // Act
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null); ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
// Assert // Assert
Assert.assertEquals(ResponseReason.OK, response); Assert.assertEquals(ResponseReason.OK, response);
@@ -79,9 +83,11 @@ public class AccountService_createAccountTest {
public void test_createAccount_DUPLICATE_ACCOUNT_KEY() { public void test_createAccount_DUPLICATE_ACCOUNT_KEY() {
// Arrange // Arrange
Mockito.doThrow(new DataIntegrityViolationException("DIVE")).when(this.accountRepository).save(Mockito.any(Account.class)); Mockito.doThrow(new DataIntegrityViolationException("DIVE")).when(this.accountRepository).save(Mockito.any(Account.class));
Mockito.when(this.accountGroupService.getAccountGroupByName(Mockito.anyString()))
.thenReturn(Mockito.mock(AccountGroup.class));
// Act // Act
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null); ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
// Assert // Assert
Assert.assertEquals(ResponseReason.DUPLICATE_ACCOUNT_KEY, response); Assert.assertEquals(ResponseReason.DUPLICATE_ACCOUNT_KEY, response);

View File

@@ -5,6 +5,7 @@ import de.financer.config.FinancerConfig;
import de.financer.decorator.AccountDecorator; import de.financer.decorator.AccountDecorator;
import de.financer.dto.Order; import de.financer.dto.Order;
import de.financer.dto.SearchTransactionsResponseDto; import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.form.EditAccountForm;
import de.financer.form.NewAccountForm; import de.financer.form.NewAccountForm;
import de.financer.model.*; import de.financer.model.*;
import de.financer.notification.Notification; import de.financer.notification.Notification;
@@ -17,6 +18,7 @@ import de.financer.util.TransactionUtils;
import org.apache.commons.collections4.IterableUtils; import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jfree.data.resources.DataPackageResources_es;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
@@ -30,6 +32,7 @@ import java.net.MalformedURLException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Controller @Controller
@@ -87,7 +90,7 @@ public class AccountController {
} }
@PostMapping("/saveAccount") @PostMapping("/saveAccount")
public String saveAccont(NewAccountForm form, Model model) { public String saveAccount(NewAccountForm form, Model model) {
final UriComponentsBuilder builder = UriComponentsBuilder final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CREATE_ACCOUNT)) .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CREATE_ACCOUNT))
.queryParam("key", form.getKey()) .queryParam("key", form.getKey())
@@ -225,6 +228,56 @@ public class AccountController {
return "redirect:" + navigateTo; return "redirect:" + navigateTo;
} }
@GetMapping("/editAccount")
public String editAccount(Model model, String key) {
_editAccount(model, key, Optional.empty(), Optional.empty());
return "account/editAccount";
}
private void _editAccount(Model model, String originalKey, Optional<String> newKey, Optional<String> newGroup) {
final ResponseEntity<Account> exchange = new GetAccountByKeyTemplate().exchange(this.financerConfig, originalKey);
final ResponseEntity<Iterable<AccountGroup>> accountGroupResponse = new GetAllAccountGroupsTemplate()
.exchange(this.financerConfig);
final Account account = exchange.getBody();
model.addAttribute("accountGroups", ControllerUtils.sortAccountGroups(accountGroupResponse.getBody()));
final EditAccountForm form = new EditAccountForm();
form.setKey(newKey.orElse(account.getKey()));
form.setGroup(newGroup.orElse(Optional.ofNullable(account.getAccountGroup()).map(AccountGroup::getName).orElse(null)));
form.setId(account.getId().toString());
form.setOriginalKey(originalKey);
model.addAttribute("form", form);
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
ControllerUtils.addDarkMode(model, this.financerConfig);
}
@PostMapping("/editAccount")
public String editAccount(Model model, EditAccountForm form) {
final UriComponentsBuilder editBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_EDIT_ACCOUNT))
.queryParam("id", form.getId())
.queryParam("key", form.getKey())
.queryParam("accountGroupName", form.getGroup());
final ResponseEntity<String> closeResponse = new StringTemplate().exchange(editBuilder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
if (!ResponseReason.OK.equals(responseReason)) {
_editAccount(model, form.getOriginalKey(), Optional.of(form.getKey()), Optional.of(form.getGroup()));
return "account/editAccount";
}
return "redirect:/accountOverview";
}
// --------------------------------------------- // ---------------------------------------------
@Autowired @Autowired

View File

@@ -9,6 +9,7 @@ public enum Function {
ACC_CURRENT_ASSETS("accounts/getCurrentAssets"), ACC_CURRENT_ASSETS("accounts/getCurrentAssets"),
ACC_GET_ACC_EXPENSES("accounts/getAccountExpenses"), ACC_GET_ACC_EXPENSES("accounts/getAccountExpenses"),
ACC_GET_ACC_EXPENSES_CURRENT_EXPENSE_PERIOD("accounts/getAccountExpensesCurrentExpensePeriod"), ACC_GET_ACC_EXPENSES_CURRENT_EXPENSE_PERIOD("accounts/getAccountExpensesCurrentExpensePeriod"),
ACC_EDIT_ACCOUNT("accounts/editAccount"),
ACC_GP_CREATE_ACCOUNT_GROUP("accountGroups/createAccountGroup"), ACC_GP_CREATE_ACCOUNT_GROUP("accountGroups/createAccountGroup"),
ACC_GP_GET_ALL("accountGroups/getAll"), ACC_GP_GET_ALL("accountGroups/getAll"),

View File

@@ -0,0 +1,40 @@
package de.financer.form;
public class EditAccountForm {
private String key;
private String group;
private String id;
private String originalKey;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getGroup() {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getOriginalKey() {
return originalKey;
}
public void setOriginalKey(String originalKey) {
this.originalKey = originalKey;
}
}

View File

@@ -36,6 +36,11 @@ financer.account-new.label.type=Type\:
financer.account-new.label.group=Group\: financer.account-new.label.group=Group\:
financer.account-new.submit=Create account financer.account-new.submit=Create account
financer.account-edit.title=financer\: edit account
financer.account-edit.label.key=Key\:
financer.account-edit.label.group=Group\:
financer.account-edit.submit=Edit account
financer.account-group-new.title=financer\: create new account group financer.account-group-new.title=financer\: create new account group
financer.account-group-new.label.name=Name\: financer.account-group-new.label.name=Name\:
financer.account-group-new.submit=Create account group financer.account-group-new.submit=Create account group
@@ -104,6 +109,7 @@ financer.account-details.available-actions.close-account=Close account
financer.account-details.available-actions.open-account=Open account financer.account-details.available-actions.open-account=Open account
financer.account-details.available-actions.back-to-overview=Back to overview financer.account-details.available-actions.back-to-overview=Back to overview
financer.account-details.available-actions.create-transaction=Create new transaction financer.account-details.available-actions.create-transaction=Create new transaction
financer.account-details.available-actions.edit-account=Edit account
financer.transaction-list.table-header.id=ID financer.transaction-list.table-header.id=ID
financer.transaction-list.table-header.fromAccount=From account financer.transaction-list.table-header.fromAccount=From account
financer.transaction-list.table-header.toAccount=To account financer.transaction-list.table-header.toAccount=To account
@@ -262,6 +268,7 @@ financer.heading.recurring-transaction-calendar=financer\: recurring transaction
financer.heading.period-overview=financer\: period overview financer.heading.period-overview=financer\: period overview
financer.heading.upload-transactions=financer\: upload transactions financer.heading.upload-transactions=financer\: upload transactions
financer.heading.create-upload-transactions=financer\: create uploaded transactions financer.heading.create-upload-transactions=financer\: create uploaded transactions
financer.heading.account-edit=financer\: edit account
financer.cancel-back-to-overview=Cancel and back to overview financer.cancel-back-to-overview=Cancel and back to overview
financer.back-to-overview=Back to overview financer.back-to-overview=Back to overview

View File

@@ -40,6 +40,11 @@ financer.account-group-new.title=financer\: Neue Konto-Gruppe erstellen
financer.account-group-new.label.name=Name\: financer.account-group-new.label.name=Name\:
financer.account-group-new.submit=Konto-Gruppe erstellen financer.account-group-new.submit=Konto-Gruppe erstellen
financer.account-edit.title=financer\: Bearbeite Konto
financer.account-edit.label.key=Schl\u00FCssel\:
financer.account-edit.label.group=Gruppe\:
financer.account-edit.submit=Konto bearbeiten
financer.transaction-new.title=financer\: Neue Buchung erstellen financer.transaction-new.title=financer\: Neue Buchung erstellen
financer.transaction-new.label.from-account=Von Konto\: financer.transaction-new.label.from-account=Von Konto\:
financer.transaction-new.label.to-account=An Konto\: financer.transaction-new.label.to-account=An Konto\:
@@ -104,6 +109,7 @@ financer.account-details.available-actions.close-account=Konto schlie\u00DFen
financer.account-details.available-actions.open-account=Konto \u00F6ffnen financer.account-details.available-actions.open-account=Konto \u00F6ffnen
financer.account-details.available-actions.back-to-overview=Zur\u00FCck zur \u00DCbersicht financer.account-details.available-actions.back-to-overview=Zur\u00FCck zur \u00DCbersicht
financer.account-details.available-actions.create-transaction=Neue Buchung erstellen financer.account-details.available-actions.create-transaction=Neue Buchung erstellen
financer.account-details.available-actions.edit-account=Konto bearbeiten
financer.transaction-list.table-header.id=ID financer.transaction-list.table-header.id=ID
financer.transaction-list.table-header.fromAccount=Von Konto financer.transaction-list.table-header.fromAccount=Von Konto
financer.transaction-list.table-header.toAccount=An Konto financer.transaction-list.table-header.toAccount=An Konto
@@ -261,6 +267,7 @@ financer.heading.recurring-transaction-calendar=financer\: Kalender wiederkehren
financer.heading.period-overview=financer\: Perioden\u00FCbersicht financer.heading.period-overview=financer\: Perioden\u00FCbersicht
financer.heading.upload-transactions=financer\: Buchungen hochladen financer.heading.upload-transactions=financer\: Buchungen hochladen
financer.heading.create-upload-transactions=financer\: Erstelle hochgeladene Buchungen financer.heading.create-upload-transactions=financer\: Erstelle hochgeladene Buchungen
financer.heading.account-edit=financer\: Bearbeite Konto
financer.cancel-back-to-overview=Abbrechen und zur\u00FCck zur \u00DCbersicht financer.cancel-back-to-overview=Abbrechen und zur\u00FCck zur \u00DCbersicht
financer.back-to-overview=Zur\u00FCck zur \u00DCbersicht financer.back-to-overview=Zur\u00FCck zur \u00DCbersicht

View File

@@ -2,9 +2,10 @@ v48 -> v49:
- #27 The recurring transaction selection during transaction import now contains all recurring transaction instead of - #27 The recurring transaction selection during transaction import now contains all recurring transaction instead of
only the active ones only the active ones
- #11 It is now possible to specify a date during creation of a transaction from a recurring transaction - #11 It is now possible to specify a date during creation of a transaction from a recurring transaction
- #25 Added the possibility to edit accounts
v47 -> v48: v47 -> v48:
- Added new property 'transaction type' to a transaction, denoting the type of the transaction, e.g. asset swap, - #20 Added new property 'transaction type' to a transaction, denoting the type of the transaction, e.g. asset swap,
expense, liability or income. This can also be queried via FQL expense, liability or income. This can also be queried via FQL
- #22 Added new feature to upload a file that contains transactions, e.g. a file export from online banking. Currently - #22 Added new feature to upload a file that contains transactions, e.g. a file export from online banking. Currently
supported is the MT940_CSV format supported is the MT940_CSV format

View File

@@ -175,7 +175,8 @@ tr:hover {
#chart-config-account-group-expenses-for-period-form *, #chart-config-account-group-expenses-for-period-form *,
#chart-config-account-expenses-for-period-form *, #chart-config-account-expenses-for-period-form *,
#search-transactions-form *, #search-transactions-form *,
#upload-transactions-form * { #upload-transactions-form *,
#edit-account-form * {
display: block; display: block;
margin-top: 1em; margin-top: 1em;
width: 20em; width: 20em;

View File

@@ -28,6 +28,8 @@
</div> </div>
<div id="account-details-action-container"> <div id="account-details-action-container">
<span th:text="#{financer.account-details.available-actions}"/> <span th:text="#{financer.account-details.available-actions}"/>
<a th:if="${!isClosed}" th:href="@{/editAccount(key=${account.key})}"
th:text="#{financer.account-details.available-actions.edit-account}"/>
<a th:if="${!isClosed}" th:href="@{/closeAccount(key=${account.key})}" <a th:if="${!isClosed}" th:href="@{/closeAccount(key=${account.key})}"
th:text="#{financer.account-details.available-actions.close-account}"/> th:text="#{financer.account-details.available-actions.close-account}"/>
<a th:if="${isClosed}" th:href="@{/openAccount(key=${account.key})}" <a th:if="${isClosed}" th:href="@{/openAccount(key=${account.key})}"

View File

@@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{financer.account-edit.title}"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}" />
<link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}" />
<link rel="stylesheet" th:href="@{/css/main.css}">
<link rel="shortcut icon" th:href="@{/favicon.ico}" />
</head>
<body>
<h1 th:text="#{financer.heading.account-edit}" />
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<a th:href="@{/accountOverview}" th:text="#{financer.cancel-back-to-overview}"/>
<form id="edit-account-form" action="#" th:action="@{/editAccount}" th:object="${form}" method="post">
<label for="inputKey" th:text="#{financer.account-edit.label.key}"/>
<input type="text" id="inputKey" th:field="*{key}" />
<label for="selectGroup" th:text="#{financer.account-edit.label.group}"/>
<select id="selectGroup" th:field="*{group}">
<option th:each="group : ${accountGroups}" th:value="${group.name}" th:text="${group.name}"/>
</select>
<input type="hidden" id="inputId" th:field="*{id}"/>
<input type="hidden" id="originalKey" th:field="*{originalKey}"/>
<input type="submit" th:value="#{financer.account-edit.submit}" />
</form>
<div th:replace="includes/footer :: footer"/>
</body>
</html>