#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();
}
@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")
public ResponseEntity closeAccount(String 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 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
* 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
* {@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>.
*/
@Transactional(propagation = Propagation.SUPPORTS)
@@ -87,17 +87,14 @@ public class AccountService {
}
final Account account = new Account();
final AccountGroup accountGroup = this.accountGroupService.getAccountGroupByName(accountGroupName);
if (StringUtils.isNotEmpty(accountGroupName)) {
final AccountGroup accountGroup = this.accountGroupService.getAccountGroupByName(accountGroupName);
if (accountGroup == null) {
return ResponseReason.ACCOUNT_GROUP_NOT_FOUND; // early return
}
account.setAccountGroup(accountGroup);
if (accountGroup == null) {
return ResponseReason.ACCOUNT_GROUP_NOT_FOUND; // early return
}
account.setAccountGroup(accountGroup);
account.setKey(key);
account.setType(AccountType.valueOf(type));
// If we create an account it's implicitly open
@@ -120,6 +117,51 @@ public class AccountService {
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)
public ResponseReason closeAccount(String key) {
return setAccountStatus(key, AccountStatus.CLOSED);

View File

@@ -3,6 +3,7 @@ package de.financer.service;
import de.financer.ResponseReason;
import de.financer.dba.AccountRepository;
import de.financer.model.Account;
import de.financer.model.AccountGroup;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -40,9 +41,11 @@ public class AccountService_createAccountTest {
public void test_createAccount_UNKNOWN_ERROR() {
// Arrange
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
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null);
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
// Assert
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
@@ -51,10 +54,11 @@ public class AccountService_createAccountTest {
@Test
public void test_createAccount_OK() {
// Arrange
// Nothing to do
Mockito.when(this.accountGroupService.getAccountGroupByName(Mockito.anyString()))
.thenReturn(Mockito.mock(AccountGroup.class));
// Act
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null);
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
// Assert
Assert.assertEquals(ResponseReason.OK, response);
@@ -79,9 +83,11 @@ public class AccountService_createAccountTest {
public void test_createAccount_DUPLICATE_ACCOUNT_KEY() {
// Arrange
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
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", null);
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
// Assert
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.dto.Order;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.form.EditAccountForm;
import de.financer.form.NewAccountForm;
import de.financer.model.*;
import de.financer.notification.Notification;
@@ -17,6 +18,7 @@ import de.financer.util.TransactionUtils;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.jfree.data.resources.DataPackageResources_es;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
@@ -30,6 +32,7 @@ import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Controller
@@ -87,7 +90,7 @@ public class AccountController {
}
@PostMapping("/saveAccount")
public String saveAccont(NewAccountForm form, Model model) {
public String saveAccount(NewAccountForm form, Model model) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CREATE_ACCOUNT))
.queryParam("key", form.getKey())
@@ -225,6 +228,56 @@ public class AccountController {
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

View File

@@ -9,6 +9,7 @@ public enum Function {
ACC_CURRENT_ASSETS("accounts/getCurrentAssets"),
ACC_GET_ACC_EXPENSES("accounts/getAccountExpenses"),
ACC_GET_ACC_EXPENSES_CURRENT_EXPENSE_PERIOD("accounts/getAccountExpensesCurrentExpensePeriod"),
ACC_EDIT_ACCOUNT("accounts/editAccount"),
ACC_GP_CREATE_ACCOUNT_GROUP("accountGroups/createAccountGroup"),
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.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.label.name=Name\:
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.back-to-overview=Back to overview
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.fromAccount=From 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.upload-transactions=financer\: upload 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.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.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.label.from-account=Von 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.back-to-overview=Zur\u00FCck zur \u00DCbersicht
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.fromAccount=Von 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.upload-transactions=financer\: Buchungen hochladen
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.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
only the active ones
- #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:
- 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
- #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

View File

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

View File

@@ -28,6 +28,8 @@
</div>
<div id="account-details-action-container">
<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})}"
th:text="#{financer.account-details.available-actions.close-account}"/>
<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>