Add FQL and rework /transaction endpoint

This commit is contained in:
2020-03-14 23:44:43 +01:00
parent 1cc7fdf052
commit d4fef75903
85 changed files with 2257 additions and 472 deletions

View File

@@ -4,8 +4,6 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@SpringBootApplication
public class FinancerApplication extends SpringBootServletInitializer {

View File

@@ -1,55 +0,0 @@
package de.financer;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
public enum ResponseReason {
OK(HttpStatus.OK),
UNKNOWN_ERROR(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_ACCOUNT_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
FROM_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_DATE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_DATE(HttpStatus.INTERNAL_SERVER_ERROR),
AMOUNT_ZERO(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_AMOUNT(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_BOOKING_ACCOUNTS(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_INTERVAL_TYPE(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_FIRST_OCCURRENCE(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_RECURRING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
INVALID_TRANSACTION_ID(HttpStatus.INTERNAL_SERVER_ERROR),
TRANSACTION_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
ACCOUNT_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR),
DUPLICATE_ACCOUNT_KEY(HttpStatus.INTERNAL_SERVER_ERROR),
DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.INTERNAL_SERVER_ERROR),
ACCOUNT_GROUP_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR);
private HttpStatus httpStatus;
ResponseReason(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
}
public ResponseEntity toResponseEntity() {
return new ResponseEntity<>(this.name(), this.httpStatus);
}
public static ResponseReason fromResponseEntity(ResponseEntity<String> entity) {
for (ResponseReason reason : values()) {
if (reason.name().equals(entity.getBody())) {
return reason;
}
}
return UNKNOWN_ERROR;
}
}

View File

@@ -12,18 +12,18 @@ public enum ChartType {
EXPENSE_PERIOD_TOTALS_CURRENT_YEAR(false),
EXPENSES_ALL_PERIODS_INLINE(true);
private boolean inline;
private final boolean inline;
ChartType(boolean inline) {
this.inline = inline;
}
public static List<String> valueList(boolean filterInline) {
return Arrays.stream(ChartType.values()).filter((ct) -> filterInline ? !ct.inline : true).map(ChartType::name)
return Arrays.stream(ChartType.values()).filter((ct) -> !filterInline || !ct.inline).map(ChartType::name)
.collect(Collectors.toList());
}
public static ChartType getByValue(String value) {
return Arrays.asList(values()).stream().filter((ct) -> ct.name().equals(value)).findFirst().get();
return Arrays.stream(values()).filter((ct) -> ct.name().equals(value)).findFirst().get();
}
}

View File

@@ -2,10 +2,8 @@ package de.financer.chart.impl.expense;
import de.financer.config.FinancerConfig;
import de.financer.dto.AccountExpense;
import de.financer.dto.AccountGroupExpense;
import de.financer.template.GetAccountExpensesCurrentExpensePeriodTemplate;
import de.financer.template.GetAccountExpensesTemplate;
import de.financer.template.GetAccountGroupExpensesTemplate;
import org.apache.commons.collections4.IterableUtils;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;
@@ -24,7 +22,7 @@ public class AccountExpensesGenerator extends AbstractExpensesGenerator {
final DefaultPieDataset dataSet = new DefaultPieDataset();
IterableUtils.toList(expenses).stream()
IterableUtils.toList(expenses)
.forEach((ex) -> dataSet.setValue(ex.getAccount().getKey(), (ex.getExpense() / 100D)));
return dataSet;

View File

@@ -22,7 +22,7 @@ public class AccountGroupExpensesGenerator extends AbstractExpensesGenerator {
final DefaultPieDataset dataSet = new DefaultPieDataset();
IterableUtils.toList(expenses).stream()
IterableUtils.toList(expenses)
.forEach((ex) -> dataSet.setValue(ex.getAccountGroup().getName(), (ex.getExpense() / 100D)));
return dataSet;

View File

@@ -39,7 +39,7 @@ public class ExpensesAllPeriodsGenerator extends AbstractChartGenerator<EmptyPar
final List<Long> totalData = new GetExpensesAllPeriodsTemplate().exchange(this.getFinancerConfig()).getBody();
final AtomicInteger counter = new AtomicInteger();
totalData.stream().forEach((l) -> result.addValue(l, "c", "r" + counter.incrementAndGet()));
totalData.forEach((l) -> result.addValue(l, "c", "r" + counter.incrementAndGet()));
return result;
}

View File

@@ -56,7 +56,7 @@ public class PeriodTotalGenerator extends AbstractChartGenerator<PeriodTotalPara
IterableUtils.toList(totalData).stream()
.sorted(Comparator.comparing((ExpensePeriodTotal ept) -> ept.getPeriod().getStart())
.thenComparing((ExpensePeriodTotal ept) -> ept.getType()))
.thenComparing(ExpensePeriodTotal::getType))
.forEach((ept) -> result.addValue((ept.getTotal() / 100D),
this.getMessage("financer.account-type." + ept.getType()),
formatDateY(ept.getPeriod())));

View File

@@ -3,12 +3,14 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.decorator.AccountDecorator;
import de.financer.template.*;
import de.financer.dto.Order;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.form.NewAccountForm;
import de.financer.model.*;
import de.financer.template.*;
import de.financer.template.exception.FinancerRestException;
import de.financer.util.ControllerUtils;
import de.financer.util.TransactionUtils;
import de.financer.util.comparator.TransactionByDateByIdDescComparator;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -57,7 +59,7 @@ public class AccountController {
}
private Iterable<AccountDecorator> decorateAccounts(List<Account> accounts) {
return accounts.stream().map((a) -> new AccountDecorator(a)).collect(Collectors.toList());
return accounts.stream().map(AccountDecorator::new).collect(Collectors.toList());
}
@GetMapping("/newAccount")
@@ -102,21 +104,40 @@ public class AccountController {
return "redirect:/accountOverview";
}
public static void _accountDetails(String key, Model model, FinancerConfig financerConfig) {
try {
final Iterable<SearchTransactionsResponseDto> response =
new SearchTransactionsTemplate()
.exchangeGet(financerConfig, key, key, null, null,
Order.TRANSACTIONS_BY_DATE_DESC, null, false);
final List<SearchTransactionsResponseDto> transactions = IterableUtils.toList(response);
final Account account = IterableUtils.toList(response).stream()
.filter(t -> t.getFromAccount().getKey().equals(key))
.findFirst()
.map(SearchTransactionsResponseDto::getFromAccount)
.orElseGet(() -> transactions
.stream()
.filter(t -> t.getToAccount().getKey().equals(key))
.findFirst()
.map(SearchTransactionsResponseDto::getToAccount).get());
transactions.forEach((t) -> TransactionUtils.adjustAmount(t, account));
model.addAttribute("account", account);
model.addAttribute("transactions", transactions);
model.addAttribute("showActions", true);
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
}
catch(FinancerRestException e) {
// TODO probably leads to follow-up exceptions during thymeleaf parsing as relevant information are missing
}
}
@GetMapping("/accountDetails")
public String accountDetails(String key, Model model) {
final ResponseEntity<Account> response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
final Account account = response.getBody();
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
.exchange(this.financerConfig, account.getKey());
_accountDetails(key, model, this.financerConfig);
List<Transaction> transactions = IterableUtils.toList(transactionResponse.getBody());
transactions.sort(new TransactionByDateByIdDescComparator());
transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
model.addAttribute("account", account);
model.addAttribute("transactions", transactions);
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
@@ -134,19 +155,8 @@ public class AccountController {
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
if (!ResponseReason.OK.equals(responseReason)) {
final ResponseEntity<Account> response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
final Account account = response.getBody();
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
.exchange(this.financerConfig, account.getKey());
_accountDetails(key, model, this.financerConfig);
List<Transaction> transactions = IterableUtils.toList(transactionResponse.getBody());
transactions.sort(new TransactionByDateByIdDescComparator());
transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
model.addAttribute("account", account);
model.addAttribute("transactions", transactions);
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
model.addAttribute("errorMessage", responseReason.name());
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
@@ -168,19 +178,8 @@ public class AccountController {
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
if (!ResponseReason.OK.equals(responseReason)) {
final ResponseEntity<Account> response = new GetAccountByKeyTemplate().exchange(this.financerConfig, key);
final Account account = response.getBody();
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
.exchange(this.financerConfig, account.getKey());
_accountDetails(key, model, this.financerConfig);
List<Transaction> transactions = IterableUtils.toList(transactionResponse.getBody());
transactions.sort(new TransactionByDateByIdDescComparator());
transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
model.addAttribute("account", account);
model.addAttribute("transactions", transactions);
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
model.addAttribute("errorMessage", responseReason.name());
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);

View File

@@ -15,10 +15,10 @@ public enum Function {
ACC_GP_GET_ACC_GP_EXPENSES("accountGroups/getAccountGroupExpenses"),
ACC_GP_GET_ACC_GP_EXPENSES_CURRENT_EXPENSE_PERIOD("accountGroups/getAccountGroupExpensesCurrentExpensePeriod"),
TR_GET_ALL("transactions/getAll"),
TR_GET_ALL_FOR_ACCOUNT("transactions/getAllForAccount"),
TR_CREATE_TRANSACTION("transactions/createTransaction"),
TR_DELETE_TRANSACTION("transactions/deleteTransaction"),
TR_SEARCH("transactions"),
TR_SEARCH_BY_FQL("transactionsByFql"),
TR_CREATE_TRANSACTION("transactions"),
TR_DELETE_TRANSACTION("transactions/"),
TR_EXPENSES_CURRENT_PERIOD("transactions/getExpensesCurrentPeriod"),
TR_EXPENSE_PERIOD_TOTALS("transactions/getExpensePeriodTotals"),
TR_EXPENSES_ALL_PERIODS("transactions/getExpensesAllPeriods"),
@@ -34,7 +34,7 @@ public enum Function {
P_GET_CURRENT_EXPENSE_PERIOD("periods/getCurrentExpensePeriod"),
P_CLOSE_CURRENT_EXPENSE_PERIOD("periods/closeCurrentExpensePeriod");
private String path;
private final String path;
Function(String path) {
this.path = path;

View File

@@ -2,21 +2,17 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dto.SaveTransactionRequestDto;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.form.SearchTransactionsForm;
import de.financer.model.AccountType;
import de.financer.template.GetAccountByKeyTemplate;
import de.financer.template.GetAllAccountsTemplate;
import de.financer.template.GetAllTransactionsForAccountTemplate;
import de.financer.template.StringTemplate;
import de.financer.template.*;
import de.financer.form.NewTransactionForm;
import de.financer.model.Account;
import de.financer.model.AccountStatus;
import de.financer.model.Transaction;
import de.financer.template.exception.FinancerRestException;
import de.financer.util.ControllerUtils;
import de.financer.util.TransactionUtils;
import de.financer.util.comparator.TransactionByDateByIdDescComparator;
import org.apache.commons.collections4.IterableUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@@ -24,6 +20,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Controller
@@ -32,20 +29,77 @@ public class TransactionController {
@Autowired
private FinancerConfig financerConfig;
@GetMapping("/searchTransactions")
public String searchTransaction(Model model) {
model.addAttribute("form", new SearchTransactionsForm());
model.addAttribute("showActions", false);
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
return "transaction/searchTransactions";
}
@PostMapping("/searchTransactions")
public String searchTransactions(SearchTransactionsForm form, Model model) {
try {
final UriComponentsBuilder transactionBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_SEARCH_BY_FQL));
transactionBuilder.queryParam("fql", form.getFql());
final Iterable<SearchTransactionsResponseDto> trxs = FinancerRestTemplate.exchangeGet(transactionBuilder,
new ParameterizedTypeReference<Iterable<SearchTransactionsResponseDto>>() {
});
model.addAttribute("transactions", trxs);
}
catch(FinancerRestException e) {
// TODO
model.addAttribute("errorMessage", e.getResponseReason().name());
}
model.addAttribute("form", form);
model.addAttribute("showActions", false);
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
return "transaction/searchTransactions";
}
@GetMapping("/newTransaction")
public String newTransaction(Model model) {
final ResponseEntity<Iterable<Account>> response = new GetAllAccountsTemplate().exchange(this.financerConfig);
final List<Account> fromAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream()
.filter((a) -> a.getType() != AccountType.EXPENSE)
.collect(Collectors.toList());
final List<Account> toAccounts = ControllerUtils.filterAndSortAccounts(response.getBody()).stream()
.filter((a) -> a.getType() != AccountType.INCOME && a
.getType() != AccountType.START)
.collect(Collectors.toList());
return _newTransaction(model, new NewTransactionForm(), Optional.empty());
}
private String _newTransaction(Model model, NewTransactionForm form, Optional<ResponseReason> responseReason) {
try {
final Iterable<Account> allAccounts =
FinancerRestTemplate.exchangeGet(this.financerConfig,
Function.ACC_GET_ALL,
new ParameterizedTypeReference<Iterable<Account>>() {});
final List<Account> fromAccounts = ControllerUtils.filterAndSortAccounts(allAccounts).stream()
.filter((a) -> a.getType() != AccountType.EXPENSE)
.collect(Collectors.toList());
final List<Account> toAccounts = ControllerUtils.filterAndSortAccounts(allAccounts).stream()
.filter((a) -> a.getType() != AccountType.INCOME && a
.getType() != AccountType.START)
.collect(Collectors.toList());
model.addAttribute("fromAccounts", fromAccounts);
model.addAttribute("toAccounts", toAccounts);
}
catch(FinancerRestException e) {
// Nothing to do
// This is very unlikely to happen and if it happens the account selection stays empty, so the user
// cannot create a transaction anyway and is forced to reload the page or navigate back
}
model.addAttribute("form", form);
responseReason.ifPresent(rr -> model.addAttribute("errorMessage", rr.name()));
model.addAttribute("fromAccounts", fromAccounts);
model.addAttribute("toAccounts", toAccounts);
model.addAttribute("form", new NewTransactionForm());
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
@@ -54,36 +108,20 @@ public class TransactionController {
@PostMapping("/saveTransaction")
public String saveTransaction(NewTransactionForm form, Model model) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_CREATE_TRANSACTION))
.queryParam("fromAccountKey", form.getFromAccountKey())
.queryParam("toAccountKey", form.getToAccountKey())
.queryParam("amount", form.getAmount())
.queryParam("date", ControllerUtils.formatDate(this.financerConfig, form.getDate()))
.queryParam("description", form.getDescription())
.queryParam("taxRelevant", form.getTaxRelevant());
final SaveTransactionRequestDto requestDto = new SaveTransactionRequestDto();
final ResponseEntity<String> response = new StringTemplate().exchange(builder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
requestDto.setFromAccountKey(form.getFromAccountKey());
requestDto.setToAccountKey(form.getToAccountKey());
requestDto.setAmount(form.getAmount());
requestDto.setDate(ControllerUtils.formatDate(this.financerConfig, form.getDate()));
requestDto.setDescription(form.getDescription());
requestDto.setTaxRelevant(form.getTaxRelevant());
if (!ResponseReason.OK.equals(responseReason)) {
final ResponseEntity<Iterable<Account>> accRes = new GetAllAccountsTemplate().exchange(this.financerConfig);
final List<Account> fromAccounts = ControllerUtils.filterAndSortAccounts(accRes.getBody()).stream()
.filter((a) -> a.getType() != AccountType.EXPENSE)
.collect(Collectors.toList());
final List<Account> toAccounts = ControllerUtils.filterAndSortAccounts(accRes.getBody()).stream()
.filter((a) -> a.getType() != AccountType.INCOME && a
.getType() != AccountType.START)
.collect(Collectors.toList());
final ResponseReason responseReason = FinancerRestTemplate
.exchangePost(this.financerConfig, Function.TR_CREATE_TRANSACTION, requestDto);
model.addAttribute("fromAccounts", fromAccounts);
model.addAttribute("toAccounts", toAccounts);
model.addAttribute("form", form);
model.addAttribute("errorMessage", responseReason.name());
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
return "transaction/newTransaction";
if (!ResponseReason.CREATED.equals(responseReason)) {
return _newTransaction(model, form, Optional.of(responseReason));
}
return "redirect:/accountOverview";
@@ -91,34 +129,16 @@ public class TransactionController {
@GetMapping("/deleteTransaction")
public String deleteTransaction(String transactionId, String accountKey, Model model) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.TR_DELETE_TRANSACTION))
.queryParam("transactionId", transactionId);
final ResponseReason responseReason = FinancerRestTemplate
.exchangeDelete(this.financerConfig, Function.TR_DELETE_TRANSACTION, transactionId);
final ResponseEntity<String> response = new StringTemplate().exchange(builder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
AccountController._accountDetails(accountKey, model, this.financerConfig);
final ResponseEntity<Account> accountResponse = new GetAccountByKeyTemplate()
.exchange(this.financerConfig, accountKey);
final Account account = accountResponse.getBody();
final ResponseEntity<Iterable<Transaction>> transactionResponse = new GetAllTransactionsForAccountTemplate()
.exchange(this.financerConfig, account.getKey());
List<Transaction> transactions = IterableUtils.toList(transactionResponse.getBody());
transactions.sort(new TransactionByDateByIdDescComparator());
transactions.stream().forEach((t) -> TransactionUtils.adjustAmount(t, account));
model.addAttribute("account", account);
model.addAttribute("transactions", transactions);
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
if (!ResponseReason.OK.equals(responseReason)) {
model.addAttribute("errorMessage", responseReason.name());
return "account/accountDetails";
}
return "account/accountDetails";

View File

@@ -1,18 +0,0 @@
package de.financer.controller.handler;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import java.io.IOException;
public class NoExceptionResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
}

View File

@@ -41,7 +41,7 @@ public class AccountDecorator {
.findFirst()
.orElseGet(() -> {
AccountStatistic as = new AccountStatistic();
as.setTransactionCountTo(0l);
as.setTransactionCountTo(0L);
return as;
})
.getSpendingTotalTo();
@@ -55,9 +55,9 @@ public class AccountDecorator {
return Math.round(this.account.getAccountStatistics().stream()
.filter((as) -> as.getPeriod().getType().equals(PeriodType.EXPENSE) && as.getPeriod()
.getEnd() != null)
.mapToLong((as) -> as.getSpendingTotalTo())
.mapToLong(AccountStatistic::getSpendingTotalTo)
.average()
.orElseGet(() -> Double.valueOf(0d)));
.orElseGet(() -> 0d));
}
return null;

View File

@@ -0,0 +1,13 @@
package de.financer.form;
public class SearchTransactionsForm {
private String fql;
public String getFql() {
return fql;
}
public void setFql(String fql) {
this.fql = fql;
}
}

View File

@@ -1,19 +1,133 @@
package de.financer.template;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.financer.controller.handler.NoExceptionResponseErrorHandler;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.template.exception.FinancerRestException;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
public class FinancerRestTemplate<T> {
public ResponseEntity<T> exchange(String url, ParameterizedTypeReference<T> type) {
// Requests that result in the return of a String allow simplified error handling
private static <R, B> ResponseEntity<R> _exchangePost(FinancerConfig financerConfig,
Function endpoint,
B body,
Class<R> type) throws FinancerRestException {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint));
final RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new NoExceptionResponseErrorHandler());
try {
return restTemplate.exchange(builder.toUriString(), HttpMethod.POST, new HttpEntity<>(body), type);
} catch (HttpStatusCodeException e) {
final ResponseEntity<String> exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e
.getStatusCode());
throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse));
}
}
public static <R, B> R exchangePost(FinancerConfig financerConfig,
Function endpoint,
B body,
Class<R> type) throws FinancerRestException {
return FinancerRestTemplate._exchangePost(financerConfig, endpoint, body, type).getBody();
}
public static <B> ResponseReason exchangePost(FinancerConfig financerConfig,
Function endpoint,
B body) {
ResponseReason response;
try {
final ResponseEntity<String> responseEntity = FinancerRestTemplate
._exchangePost(financerConfig, endpoint, body, String.class);
response = ResponseReason.fromResponseEntity(responseEntity);
} catch (FinancerRestException e) {
response = e.getResponseReason();
}
return response;
}
public static ResponseReason exchangeDelete(FinancerConfig financerConfig,
Function endpoint,
String id
) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint, id));
final RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response;
try {
response = restTemplate.exchange(builder.toUriString(), HttpMethod.DELETE, null, String.class);
} catch (HttpStatusCodeException e) {
response = new ResponseEntity<>(e.getResponseBodyAsString(), e.getStatusCode());
}
return ResponseReason.fromResponseEntity(response);
}
public static <R> R exchangeGet(FinancerConfig financerConfig,
Function endpoint,
Class<R> type) throws FinancerRestException {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint));
final RestTemplate restTemplate = new RestTemplate();
try {
return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody();
} catch (HttpStatusCodeException e) {
final ResponseEntity<String> exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e
.getStatusCode());
throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse));
}
}
public static <R> R exchangeGet(FinancerConfig financerConfig,
Function endpoint,
ParameterizedTypeReference<R> type) throws FinancerRestException {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, endpoint));
final RestTemplate restTemplate = new RestTemplate();
try {
return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody();
} catch (HttpStatusCodeException e) {
final ResponseEntity<String> exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e
.getStatusCode());
throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse));
}
}
public static <R> R exchangeGet(UriComponentsBuilder builder,
ParameterizedTypeReference<R> type) throws FinancerRestException {
final RestTemplate restTemplate = new RestTemplate();
try {
return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody();
} catch (HttpStatusCodeException e) {
final ResponseEntity<String> exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e
.getStatusCode());
throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse));
}
}
// ---------------------------------- LEGACY
public ResponseEntity<T> exchange(String url, ParameterizedTypeReference<T> type) {
final RestTemplate restTemplate = new RestTemplate();
return restTemplate.exchange(url, HttpMethod.GET, null, type);
}
@@ -21,22 +135,6 @@ public class FinancerRestTemplate<T> {
public ResponseEntity<T> exchange(String url, Class<T> type) {
final RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new NoExceptionResponseErrorHandler());
return restTemplate.exchange(url, HttpMethod.GET, null, type);
}
// The spring.jackson. properties are not picked up by the RestTemplate and its converter,
// so we need to overwrite it here
private MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jsonConverter.setObjectMapper(objectMapper);
return jsonConverter;
}
}

View File

@@ -3,7 +3,6 @@ package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.dto.AccountExpense;
import de.financer.dto.AccountGroupExpense;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;

View File

@@ -3,8 +3,6 @@ package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.dto.AccountGroupExpense;
import de.financer.model.Account;
import de.financer.model.Transaction;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;

View File

@@ -1,21 +0,0 @@
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.model.Transaction;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;
public class GetAllTransactionsForAccountTemplate {
public ResponseEntity<Iterable<Transaction>> exchange(FinancerConfig financerConfig, String accountKey) {
final UriComponentsBuilder transactionBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_GET_ALL_FOR_ACCOUNT))
.queryParam("accountKey", accountKey);
return new FinancerRestTemplate<Iterable<Transaction>>()
.exchange(transactionBuilder.toUriString(), new ParameterizedTypeReference<Iterable<Transaction>>() {
});
}
}

View File

@@ -2,7 +2,6 @@ package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.dto.ExpensePeriodTotal;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;

View File

@@ -0,0 +1,52 @@
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.dto.Order;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.template.exception.FinancerRestException;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.web.util.UriComponentsBuilder;
public class SearchTransactionsTemplate {
public Iterable<SearchTransactionsResponseDto> exchangeGet(FinancerConfig financerConfig,
String fromAccountKey,
String toAccountKey,
Long periodId,
Integer limit,
Order order,
Boolean taxRelevant,
boolean accountsAnd) throws FinancerRestException {
final UriComponentsBuilder transactionBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_SEARCH));
if (fromAccountKey != null) {
transactionBuilder.queryParam("fromAccountKey", fromAccountKey);
}
if (fromAccountKey != null) {
transactionBuilder.queryParam("toAccountKey", toAccountKey);
}
if (periodId != null) {
transactionBuilder.queryParam("periodId", periodId);
}
if (limit != null) {
transactionBuilder.queryParam("limit", limit);
}
if (order != null) {
transactionBuilder.queryParam("order", order.name());
}
if (taxRelevant != null) {
transactionBuilder.queryParam("taxRelevant", taxRelevant);
}
transactionBuilder.queryParam("accountsAnd", accountsAnd);
return FinancerRestTemplate.exchangeGet(transactionBuilder, new ParameterizedTypeReference<Iterable<SearchTransactionsResponseDto>>() {});
}
}

View File

@@ -0,0 +1,15 @@
package de.financer.template.exception;
import de.financer.ResponseReason;
public class FinancerRestException extends Exception {
private final ResponseReason responseReason;
public FinancerRestException(ResponseReason responseReason) {
this.responseReason = responseReason;
}
public ResponseReason getResponseReason() {
return this.responseReason;
}
}

View File

@@ -24,6 +24,10 @@ public class ControllerUtils {
return String.format("%s%s", financerConfig.getServerUrl(), function.getPath());
}
public static String buildUrl(FinancerConfig financerConfig, Function function, String id) {
return String.format("%s%s%s", financerConfig.getServerUrl(), function.getPath(), id);
}
public static List<Account> filterAndSortAccounts(Iterable<Account> accounts) {
return filterAndSortAccounts(accounts, false);
}

View File

@@ -1,13 +1,13 @@
package de.financer.util;
import de.financer.dto.SearchTransactionsResponseDto;
import de.financer.model.Account;
import de.financer.model.AccountType;
import de.financer.model.Transaction;
import static de.financer.model.AccountType.*;
public class TransactionUtils {
public static final void adjustAmount(Transaction t, Account account) {
public static void adjustAmount(SearchTransactionsResponseDto t, Account account) {
boolean isFrom = t.getFromAccount().getKey().equals(account.getKey());
if (AccountType.START.equals(t.getFromAccount().getType()) && AccountType.LIABILITY.equals(t.getToAccount().getType())) {

View File

@@ -4,6 +4,7 @@ financer.account-overview.available-actions.show-closed=Show closed accounts
financer.account-overview.available-actions.hide-closed=Hide closed accounts
financer.account-overview.available-actions.create-account=Create new account
financer.account-overview.available-actions.create-transaction=Create new transaction
financer.account-overview.available-actions.search-transactions=Search transactions
financer.account-overview.available-actions.create-recurring-transaction=Create new recurring transaction
financer.account-overview.available-actions.recurring-transaction-all=Show all recurring transactions
financer.account-overview.available-actions.create-account-group=Create new account group
@@ -98,28 +99,34 @@ 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.table-header.id=ID
financer.account-details.table-header.fromAccount=From account
financer.account-details.table-header.toAccount=To account
financer.account-details.table-header.date=Date
financer.account-details.table-header.amount=Amount
financer.account-details.table-header.description=Description
financer.account-details.table-header.byRecurring=Recurring
financer.account-details.table-header.taxRelevant=Tax relevant
financer.transaction-list.table-header.id=ID
financer.transaction-list.table-header.fromAccount=From account
financer.transaction-list.table-header.toAccount=To account
financer.transaction-list.table-header.date=Date
financer.transaction-list.table-header.amount=Amount
financer.transaction-list.table-header.description=Description
financer.transaction-list.table-header.byRecurring=Recurring
financer.transaction-list.table-header.taxRelevant=Tax relevant
financer.account-details.details.type=Type\:
financer.account-details.details.balance=Current balance\:
financer.account-details.details.group=Group\:
financer.account-details.table-header.actions=Actions
financer.account-details.table.actions.deleteTransaction=Delete
financer.account-details.table.recurring.yes=Yes
financer.account-details.table.recurring.no=No
financer.account-details.table.taxRelevant.true=Yes
financer.account-details.table.taxRelevant.false=No
financer.transaction-list.table-header.actions=Actions
financer.transaction-list.table.actions.deleteTransaction=Delete
financer.transaction-list.table.recurring.yes=Yes
financer.transaction-list.table.recurring.no=No
financer.transaction-list.table.taxRelevant.true=Yes
financer.transaction-list.table.taxRelevant.false=No
financer.recurring-to-transaction-with-amount.title=financer\: create transaction from recurring with amount
financer.recurring-to-transaction-with-amount.label.amount=Amount\:
financer.recurring-to-transaction-with-amount.submit=Create
financer.search-transactions.title=financer\: search transactions
financer.search-transactions.back-to-overview=Back to overview
financer.search-transactions.label.fql=FQL expression\:
financer.search-transactions.submit=Search
financer.search-transactions.show-query-options=Show query options
financer.chart-select.title=Select a chart to generate
financer.chart-select.submit=Select
@@ -166,6 +173,7 @@ financer.heading.recurring-to-transaction-with-amount=financer\: create transact
financer.heading.chart-select=financer\: select a chart to generate
financer.heading.chart-config-account-group-expenses-for-period=financer\: configure account group expenses for period chart
financer.heading.chart-config-account-expenses-for-period=financer\: configure account expenses for period chart
financer.heading.search-transactions=financer\: search transactions
financer.cancel-back-to-overview=Cancel and back to overview

View File

@@ -4,6 +4,7 @@ financer.account-overview.available-actions.show-closed=Zeige auch geschlossene
financer.account-overview.available-actions.hide-closed=Verstecke geschlossene Konten
financer.account-overview.available-actions.create-account=Neues Konto erstellen
financer.account-overview.available-actions.create-transaction=Neue Buchung erstellen
financer.account-overview.available-actions.search-transactions=Buchungen suchen
financer.account-overview.available-actions.create-recurring-transaction=Neue wiederkehrende Buchung erstellen
financer.account-overview.available-actions.recurring-transaction-all=Zeige alle wiederkehrende Buchungen
financer.account-overview.available-actions.create-account-group=Neue Konto-Gruppe erstellen
@@ -18,6 +19,8 @@ financer.account-overview.table-header.id=ID
financer.account-overview.table-header.key=Konto
financer.account-overview.table-header.group=Gruppe
financer.account-overview.table-header.balance=Kontostand
financer.account-overview.table-header.spending-current-period=Ausgaben aktuelle Periode
financer.account-overview.table-header.average-spending-period=Durchschnittliche Ausgaben
financer.account-overview.table-header.type=Typ
financer.account-overview.table-header.status=Status
financer.account-overview.tooltip.status.current-expenses=Periode ab {0}. Durch Klicken des Betrags kann eine grafische \u00DCbersicht über die Ausgaben gruppiert nach Konto-Gruppe angezeigt werden
@@ -96,28 +99,34 @@ 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.table-header.id=ID
financer.account-details.table-header.fromAccount=Von Konto
financer.account-details.table-header.toAccount=An Konto
financer.account-details.table-header.date=Datum
financer.account-details.table-header.amount=Betrag
financer.account-details.table-header.description=Beschreibung
financer.account-details.table-header.byRecurring=Wiederkehrend
financer.account-details.table-header.taxRelevant=Relevant f\u00FCr Steuererkl\u00E4rung
financer.transaction-list.table-header.id=ID
financer.transaction-list.table-header.fromAccount=Von Konto
financer.transaction-list.table-header.toAccount=An Konto
financer.transaction-list.table-header.date=Datum
financer.transaction-list.table-header.amount=Betrag
financer.transaction-list.table-header.description=Beschreibung
financer.transaction-list.table-header.byRecurring=Wiederkehrend
financer.transaction-list.table-header.taxRelevant=Relevant f\u00FCr Steuererkl\u00E4rung
financer.account-details.details.type=Typ\:
financer.account-details.details.balance=Kontostand\:
financer.account-details.details.group=Gruppe\:
financer.account-details.table-header.actions=Aktionen
financer.account-details.table.actions.deleteTransaction=L\u00F6schen
financer.account-details.table.recurring.yes=Ja
financer.account-details.table.recurring.no=Nein
financer.account-details.table.taxRelevant.true=Ja
financer.account-details.table.taxRelevant.false=Nein
financer.transaction-list.table-header.actions=Aktionen
financer.transaction-list.table.actions.deleteTransaction=L\u00F6schen
financer.transaction-list.table.recurring.yes=Ja
financer.transaction-list.table.recurring.no=Nein
financer.transaction-list.table.taxRelevant.true=Ja
financer.transaction-list.table.taxRelevant.false=Nein
financer.recurring-to-transaction-with-amount.title=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen
financer.recurring-to-transaction-with-amount.label.amount=Betrag\:
financer.recurring-to-transaction-with-amount.submit=Erstellen
financer.search-transactions.title=financer\: Buchungen suchen
financer.search-transactions.back-to-overview=Zur\u00FCck zur \u00DCbersicht
financer.search-transactions.label.fql=FQL Ausdruck\:
financer.search-transactions.submit=Suchen
financer.search-transactions.show-query-options=Suchoptionen anzeigen
financer.chart-select.title=Ein Diagramm zum Erzeugen ausw\u00E4hlen
financer.chart-select.submit=Ausw\u00E4hlen
@@ -157,12 +166,13 @@ financer.heading.account-new=financer\: Neues Konto erstellen
financer.heading.account-group-new=financer\: Neue Konto-Gruppe erstellen
financer.heading.account-overview=financer\: \u00DCbersicht
financer.heading.account-details=financer\: Kontodetails f\u00FCr {0}
financer.heading.recurring-transaction-list.dueToday=financer\: wiederkehrende Buchungen heute f\u00E4llig
financer.heading.recurring-transaction-list.active=financer\: aktive wiederkehrende Buchungen
financer.heading.recurring-transaction-list.all=financer\: alle wiederkehrende Buchungen
financer.heading.recurring-transaction-list.dueToday=financer\: Wiederkehrende Buchungen heute f\u00E4llig
financer.heading.recurring-transaction-list.active=financer\: Aktive wiederkehrende Buchungen
financer.heading.recurring-transaction-list.all=financer\: Alle wiederkehrende Buchungen
financer.heading.recurring-to-transaction-with-amount=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen
financer.heading.chart-select=financer\: ein Diagramm zum Erzeugen ausw\u00E4hlen
financer.heading.chart-config-account-group-expenses-for-period=financer\: konfigurieren von Ausgaben f\u00FCr Periode gruppiert nach Konto-Gruppe Diagramm
financer.heading.chart-select=financer\: Ein Diagramm zum Erzeugen ausw\u00E4hlen
financer.heading.chart-config-account-group-expenses-for-period=financer\: Konfigurieren von Ausgaben f\u00FCr Periode gruppiert nach Konto-Gruppe Diagramm
financer.heading.search-transactions=financer\: Buchungen suchen
financer.cancel-back-to-overview=Abbrechen und zur \u00DCbersicht

View File

@@ -2,7 +2,10 @@ v26 -> v27:
- Changed sort order of accounts in overview page. The accounts are now sorted by the account type first (BCILES), then
by the account group name and then by the account ID, leading to an overall more organic order of accounts
- Add tax relevance flag to transaction and recurring transaction creation. This flag denotes whether a transaction or
the instances of a recurring transaction are relevant for a tax declaration. This is preparation for extended reports.
the instances of a recurring transaction are relevant for a tax declaration. This is preparation for extended reports
- Add searching of transactions via FQL (Financer Query Language)
- Rework /transaction end point to better adhere to REST API requirements (proper HTTP return codes and HTTP method
usage)
v25 -> v26:
- Close of the current expense period now creates null statistic entries for accounts that have not been used in

View File

@@ -12,7 +12,7 @@
/* --------------------- */
#account-overview-table,
#account-transaction-table,
#transaction-table,
#recurring-transaction-list-table {
width: 100%;
border-collapse: collapse;
@@ -23,8 +23,8 @@
#account-overview-table th,
#account-overview-table td,
#account-transaction-table th,
#account-transaction-table td,
#transaction-table th,
#transaction-table td,
#recurring-transaction-list-table th,
#recurring-transaction-list-table td {
border-bottom: 1px solid #ddd;
@@ -33,7 +33,7 @@
}
#account-overview-table th,
#account-transaction-table th,
#transaction-table th,
#recurring-transaction-list-table th {
position: sticky;
top: 0px;
@@ -107,13 +107,18 @@ tr:hover {
#recurring-to-transaction-with-amount-form *,
#new-account-group-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 * {
display: block;
margin-top: 1em;
width: 20em;
box-sizing: border-box;
}
#search-transactions-form > input[type=text] {
width: 100% !important;
}
#chart-select-form div {
width: 20em;
margin-top: 1em;
@@ -187,4 +192,15 @@ input[type=submit] {
#type-row {
width: var(--type-row-width);
padding: 0px !important
}
#search-transactions-fql-detail {
border: 1px solid grey;
border-radius: 0.5em;
padding-inline: 0.3em;
background-color: lightgrey;
}
#search-transactions-fql-detail > * {
font-size: 0.7em;
}

View File

@@ -14,8 +14,9 @@
7. Transactions
8. Recurring transactions
9. Reporting
10. Setup
11. Planned features
10. FQL
11. Setup
12. Planned features
1. About
========
@@ -170,12 +171,15 @@
9. Reporting
============
10. Setup
10. FQL
=======
11. Setup
=========
This chapter explains how to setup a financer instance. It requires PostgreSQL as a database backend and a Java
Servlet Container (e.g. Apache Tomcat) as a runtime environment.
10.1 Database setup
11.1 Database setup
-------------------
First install PostgreSQL. Then create a user for financer:
sudo -iu postgres
@@ -191,7 +195,7 @@
\q
exit
11. Planned features
12. Planned features
====================
This chapter lists planned features. The list is in no particular order:
- Transaction import from online banking (file based)

View File

@@ -35,36 +35,7 @@
<a th:href="@{/accountOverview}"
th:text="#{financer.account-details.available-actions.back-to-overview}"/>
</div>
<table id="account-transaction-table">
<tr>
<th class="hideable-column" th:text="#{financer.account-details.table-header.id}"/>
<th th:text="#{financer.account-details.table-header.fromAccount}"/>
<th th:text="#{financer.account-details.table-header.toAccount}"/>
<th th:text="#{financer.account-details.table-header.date}"/>
<th th:text="#{financer.account-details.table-header.amount}"/>
<th th:text="#{financer.account-details.table-header.description}"/>
<th th:text="#{financer.account-details.table-header.byRecurring}"/>
<th th:text="#{financer.account-details.table-header.taxRelevant}"/>
<th th:text="#{financer.account-details.table-header.actions}"/>
</tr>
<tr th:each="transaction : ${transactions}">
<td class="hideable-column" th:text="${transaction.id}"/>
<td th:text="${transaction.fromAccount.key}" />
<td th:text="${transaction.toAccount.key}" />
<td th:text="${#temporals.format(transaction.date)}" />
<td th:text="${#numbers.formatDecimal(transaction.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<td th:text="${transaction.description}" />
<td th:if="${transaction.recurringTransaction != null}" th:text="#{financer.account-details.table.recurring.yes}" />
<td th:if="${transaction.recurringTransaction == null}" th:text="#{financer.account-details.table.recurring.no}" />
<td th:text="#{'financer.account-details.table.taxRelevant.' + ${transaction.taxRelevant}}" />
<td>
<div id="account-transaction-table-actions-container">
<a th:href="@{/deleteTransaction(transactionId=${transaction.id}, accountKey=${account.key})}"
th:text="#{financer.account-details.table.actions.deleteTransaction}"/>
</div>
</td>
</tr>
</table>
<div th:replace="transaction/transactionList :: transaction-list"/>
<div th:replace="includes/footer :: footer"/>
</body>
</html>

View File

@@ -49,6 +49,7 @@
</div>
<div id="action-container-sub-transactions">
<a th:href="@{/newTransaction}" th:text="#{financer.account-overview.available-actions.create-transaction}"/>
<a th:href="@{/searchTransactions}" th:text="#{financer.account-overview.available-actions.search-transactions}"/>
</div>
<div id="action-container-sub-recurring-transactions">
<a th:href="@{/newRecurringTransaction}"

View File

@@ -0,0 +1,61 @@
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{financer.search-transactions.title}"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" th:href="@{/css/main.css}">
<link rel="shortcut icon" th:href="@{/favicon.ico}" />
</head>
<body>
<h1 th:text="#{financer.heading.search-transactions}" />
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<a th:href="@{/accountOverview}" th:text="#{financer.search-transactions.back-to-overview}"/>
<form id="search-transactions-form" action="#" th:action="@{/searchTransactions}" th:object="${form}"
method="post">
<label for="inputFql" th:text="#{financer.search-transactions.label.fql}"/>
<input type="text" id="inputFql" th:field="*{fql}"/>
<input type="submit" th:value="#{financer.search-transactions.submit}"/>
</form>
<details>
<summary th:text="#{financer.search-transactions.show-query-options}"/>
<div id="search-transactions-fql-detail">
<p>
<div>Available fields:</div>
<ul>
<li>amount: the amount of a transaction</li>
<li>fromAccount: the key of the from account</li>
<li>toAccount: the key of the to account</li>
<li>fromAccountGroup: the name of the account group of the from account</li>
<li>toAccountGroup: the name of the account group of the to account</li>
<li>date: the date of the transaction in the format yyyy-mm-dd</li>
<li>recurring: whether the transaction has been created from a recurring transaction</li>
<li>taxRelevant: whether the transaction is relevant for tax declaration</li>
<li>period: the period the transaction is assigned to
<ul>
<li>CURRENT: denotes the current expense period</li>
<li>LAST: denotes the last expense period</li>
<li>CURRENT_YEAR: denotes the current year period</li>
<li>LAST_YEAR: denotes the last year period</li>
<li>GRAND_TOTAL: denotes the grand total period</li>
</ul>
</li>
</ul>
</p>
<p>
<div>General</div>
<ul>
<li>Conjunctions with OR, AND</li>
<li>Grouping of expressions with parenthesis</li>
<li>Strings enclosed in single quotes</li>
<li>Case-insensitive</li>
<li>Possible operators =, >, >=, <, <=, !=</li>
<li>Ordering for a single field ASC, DESC via ORDER BY</li>
</ul>
</p>
</div>
</details>
<div th:replace="transaction/transactionList :: transaction-list"/>
<div th:replace="includes/footer :: footer"/>
</body>
</html>

View File

@@ -0,0 +1,32 @@
<div id="transaction-list-container" th:fragment="transaction-list">
<table id="transaction-table">
<tr>
<th class="hideable-column" th:text="#{financer.transaction-list.table-header.id}"/>
<th th:text="#{financer.transaction-list.table-header.fromAccount}"/>
<th th:text="#{financer.transaction-list.table-header.toAccount}"/>
<th th:text="#{financer.transaction-list.table-header.date}"/>
<th th:text="#{financer.transaction-list.table-header.amount}"/>
<th th:text="#{financer.transaction-list.table-header.description}"/>
<th th:text="#{financer.transaction-list.table-header.byRecurring}"/>
<th th:text="#{financer.transaction-list.table-header.taxRelevant}"/>
<th th:if="${showActions}" th:text="#{financer.transaction-list.table-header.actions}"/>
</tr>
<tr th:each="transaction : ${transactions}">
<td class="hideable-column" th:text="${transaction.id}"/>
<td th:text="${transaction.fromAccount.key}" />
<td th:text="${transaction.toAccount.key}" />
<td th:text="${#temporals.format(transaction.date)}" />
<td th:text="${#numbers.formatDecimal(transaction.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<td th:text="${transaction.description}" />
<td th:if="${transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.yes}" />
<td th:if="${!transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.no}" />
<td th:text="#{'financer.transaction-list.table.taxRelevant.' + ${transaction.taxRelevant}}" />
<td th:if="${showActions}">
<div id="account-transaction-table-actions-container">
<a th:href="@{/deleteTransaction(transactionId=${transaction.id}, accountKey=${account.key})}"
th:text="#{financer.transaction-list.table.actions.deleteTransaction}"/>
</div>
</td>
</tr>
</table>
</div>