Add charts (based on JFreeChart)

Currently only an expense report is supported, but more will come.
Also moved rest templates into own toplevel package.
This commit is contained in:
2019-06-30 03:03:36 +02:00
parent 53e46a418c
commit 477d3c203a
29 changed files with 229 additions and 32 deletions

View File

@@ -0,0 +1,33 @@
package de.financer.dto;
import de.financer.model.AccountGroup;
public class AccountGroupExpense {
private AccountGroup accountGroup;
private Long expense;
public AccountGroupExpense() {
// nothing to do
}
public AccountGroupExpense(AccountGroup accountGroup, Long expense) {
this.accountGroup = accountGroup;
this.expense = expense;
}
public AccountGroup getAccountGroup() {
return accountGroup;
}
public void setAccountGroup(AccountGroup accountGroup) {
this.accountGroup = accountGroup;
}
public Long getExpense() {
return expense;
}
public void setExpense(Long expense) {
this.expense = expense;
}
}

View File

@@ -1,6 +1,7 @@
package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.dto.AccountGroupExpense;
import de.financer.model.AccountGroup;
import de.financer.service.AccountGroupService;
import org.slf4j.Logger;
@@ -50,4 +51,13 @@ public class AccountGroupController {
return responseReason.toResponseEntity();
}
@RequestMapping("getAccountGroupExpenses")
public Iterable<AccountGroupExpense> getAccountGroupExpenses(String periodStart, String periodEnd) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/accountGroups/getAccountGroupExpenses got parameters: %s|%s", periodStart, periodEnd));
}
return this.accountGroupService.getAccountGroupExpenses(periodStart, periodEnd);
}
}

View File

@@ -1,11 +1,19 @@
package de.financer.dba;
import de.financer.dto.AccountGroupExpense;
import de.financer.model.AccountGroup;
import de.financer.model.AccountType;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@Transactional(propagation = Propagation.REQUIRED)
public interface AccountGroupRepository extends CrudRepository<AccountGroup, Long> {
AccountGroup findByName(String name);
@Query("SELECT new de.financer.dto.AccountGroupExpense(ag, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a JOIN a.accountGroup ag WHERE a.type in :expenseTypes AND t.date BETWEEN :startDate AND :startEnd GROUP BY ag")
Iterable<AccountGroupExpense> getAccountGroupExpenses(LocalDate startDate, LocalDate startEnd, AccountType... expenseTypes);
}

View File

@@ -1,8 +1,11 @@
package de.financer.service;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dba.AccountGroupRepository;
import de.financer.dto.AccountGroupExpense;
import de.financer.model.AccountGroup;
import de.financer.model.AccountType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -11,6 +14,11 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collections;
@Service
public class AccountGroupService {
private static final Logger LOGGER = LoggerFactory.getLogger(AccountGroupService.class);
@@ -18,6 +26,9 @@ public class AccountGroupService {
@Autowired
private AccountGroupRepository accountGroupRepository;
@Autowired
private FinancerConfig financerConfig;
/**
* @return all existing account groups
*/
@@ -29,6 +40,7 @@ public class AccountGroupService {
* This method returns the account group with the given name.
*
* @param name the name to get the account group for
*
* @return the account group or <code>null</code> if no account group with the given name can be found
*/
public AccountGroup getAccountGroupByName(String name) {
@@ -39,10 +51,10 @@ public class AccountGroupService {
* This method creates a new account group with the given name.
*
* @param name the name of the new account group
* @return {@link ResponseReason#DUPLICATE_ACCOUNT_GROUP_NAME} if an account group with the given name already exists,
* {@link ResponseReason#UNKNOWN_ERROR} if an unknown error occurs,
* {@link ResponseReason#OK} if the operation completed successfully.
* Never returns <code>null</code>.
*
* @return {@link ResponseReason#DUPLICATE_ACCOUNT_GROUP_NAME} if an account group with the given name already
* exists, {@link ResponseReason#UNKNOWN_ERROR} if an unknown error occurs, {@link ResponseReason#OK} if the
* operation completed successfully. Never returns <code>null</code>.
*/
@Transactional(propagation = Propagation.SUPPORTS)
public ResponseReason createAccountGroup(String name) {
@@ -52,13 +64,11 @@ public class AccountGroupService {
try {
this.accountGroupRepository.save(accountGroup);
}
catch (DataIntegrityViolationException dive) {
} catch (DataIntegrityViolationException dive) {
LOGGER.error(String.format("Duplicate account group name! %s", name), dive);
return ResponseReason.DUPLICATE_ACCOUNT_GROUP_NAME;
}
catch (Exception e) {
} catch (Exception e) {
LOGGER.error(String.format("Could not save account group %s", name), e);
return ResponseReason.UNKNOWN_ERROR;
@@ -66,4 +76,22 @@ public class AccountGroupService {
return ResponseReason.OK;
}
public Iterable<AccountGroupExpense> getAccountGroupExpenses(String periodStart, String periodEnd) {
LocalDate startDate;
LocalDate endDate;
try {
startDate = LocalDate.parse(periodStart, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
endDate = LocalDate.parse(periodEnd, DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat()));
} catch (DateTimeParseException dtpe) {
LOGGER.error(String
.format("Could not parse periodStart (%s) or periodEnd (%s): %s", periodStart, periodEnd, dtpe
.getMessage()));
return Collections.emptyList();
}
return this.accountGroupRepository.getAccountGroupExpenses(startDate, endDate, AccountType.LIABILITY, AccountType.EXPENSE);
}
}

View File

@@ -0,0 +1,3 @@
UPDATE account SET account_group_id = (SELECT id FROM account_group WHERE name = 'Car') WHERE "key" IN ('Car', 'Gas')
UPDATE account SET account_group_id = (SELECT id FROM account_group WHERE name = 'Housing') WHERE "key" IN ('Rent', 'FVS', 'Electricity/Water')

View File

@@ -12,7 +12,8 @@
<artifactId>financer-web-client</artifactId>
<packaging>${packaging.type}</packaging>
<description>The web client part of the financer application - a simple app to manage your personal finances</description>
<description>The web client part of the financer application - a simple app to manage your personal finances
</description>
<name>financer-web-client</name>
<properties>
@@ -47,6 +48,11 @@
<artifactId>commons-collections4</artifactId>
</dependency>
<!-- Misc dependencies -->
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.0</version>
</dependency>
<!-- Financer dependencies -->
<dependency>

View File

@@ -0,0 +1,40 @@
package de.financer.chart;
import de.financer.config.FinancerConfig;
import de.financer.dto.AccountGroupExpense;
import de.financer.template.GetAccountGroupExpensesTemplate;
import de.financer.util.ControllerUtils;
import org.apache.commons.collections4.IterableUtils;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import java.time.LocalDate;
public class AccountGroupExpensesChartGenerator {
public JFreeChart generateChart(FinancerConfig financerConfig, LocalDate periodStart, LocalDate periodEnd, MessageSource messageSource) {
final String start = ControllerUtils.formatDate(financerConfig, periodStart);
final String end = ControllerUtils.formatDate(financerConfig, periodEnd);
final Iterable<AccountGroupExpense> expenses = new GetAccountGroupExpensesTemplate()
.exchange(financerConfig, start, end).getBody();
final PieDataset dataSet = createDataSet(expenses);
return ChartFactory.createPieChart(messageSource
.getMessage("financer.chart.account-group-expenses-current-period.title", null, LocaleContextHolder
.getLocale()), dataSet);
}
private PieDataset createDataSet(Iterable<AccountGroupExpense> data) {
final DefaultPieDataset dataSet = new DefaultPieDataset();
IterableUtils.toList(data).stream()
.forEach((ex) -> dataSet.setValue(ex.getAccountGroup().getName(), ex.getExpense()));
return dataSet;
}
}

View File

@@ -2,7 +2,7 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.controller.template.*;
import de.financer.template.*;
import de.financer.form.NewAccountForm;
import de.financer.model.*;
import de.financer.util.ControllerUtils;
@@ -19,7 +19,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder;
import java.time.LocalDate;
import java.util.List;
@Controller

View File

@@ -2,7 +2,7 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.controller.template.StringTemplate;
import de.financer.template.StringTemplate;
import de.financer.form.NewAccountGroupForm;
import de.financer.util.ControllerUtils;
import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -0,0 +1,40 @@
package de.financer.controller;
import de.financer.chart.AccountGroupExpensesChartGenerator;
import de.financer.config.FinancerConfig;
import de.financer.util.ExpensePeriod;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Controller
public class ChartController {
@Autowired
private FinancerConfig financerConfig;
@Autowired
private MessageSource messageSource;
@GetMapping("/getAccountGroupExpensesCurrentPeriod")
public void getAccountGroupExpensesCurrentPeriod(HttpServletResponse response) {
final ExpensePeriod period = ExpensePeriod
.getCurrentExpensePeriod(this.financerConfig.getMonthPeriodStartDay());
JFreeChart chart = new AccountGroupExpensesChartGenerator()
.generateChart(this.financerConfig, period.getStart(), period.getEnd(), this.messageSource);
response.setContentType("image/png");
try {
ChartUtils.writeChartAsPNG(response.getOutputStream(), chart, 1024, 768);
} catch (IOException ioe) {
// TODO
}
}
}

View File

@@ -10,6 +10,7 @@ public enum Function {
ACC_GP_CREATE_ACCOUNT_GROUP("accountGroups/createAccountGroup"),
ACC_GP_GET_ALL("accountGroups/getAll"),
ACC_GP_GET_ACC_GP_EXPENSES("accountGroups/getAccountGroupExpenses"),
TR_GET_ALL("transactions/getAll"),
TR_GET_ALL_FOR_ACCOUNT("transactions/getAllForAccount"),

View File

@@ -2,7 +2,7 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.controller.template.*;
import de.financer.template.*;
import de.financer.form.NewRecurringTransactionForm;
import de.financer.form.RecurringToTransactionWithAmountForm;
import de.financer.model.Account;

View File

@@ -2,10 +2,10 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.controller.template.GetAccountByKeyTemplate;
import de.financer.controller.template.GetAllAccountsTemplate;
import de.financer.controller.template.GetAllTransactionsForAccountTemplate;
import de.financer.controller.template.StringTemplate;
import de.financer.template.GetAccountByKeyTemplate;
import de.financer.template.GetAllAccountsTemplate;
import de.financer.template.GetAllTransactionsForAccountTemplate;
import de.financer.template.StringTemplate;
import de.financer.form.NewTransactionForm;
import de.financer.model.Account;
import de.financer.model.AccountStatus;

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;

View File

@@ -0,0 +1,24 @@
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;
import org.springframework.web.util.UriComponentsBuilder;
public class GetAccountGroupExpensesTemplate {
public ResponseEntity<Iterable<AccountGroupExpense>> exchange(FinancerConfig financerConfig, String start, String end) {
final UriComponentsBuilder expensesBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.ACC_GP_GET_ACC_GP_EXPENSES))
.queryParam("periodStart", start)
.queryParam("periodEnd", end);
return new FinancerRestTemplate<Iterable<AccountGroupExpense>>()
.exchange(expensesBuilder.toUriString(), new ParameterizedTypeReference<Iterable<AccountGroupExpense>>() {
});
}
}

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;

View File

@@ -1,8 +1,7 @@
package de.financer.controller.template;
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;

View File

@@ -1,4 +1,4 @@
package de.financer.controller.template;
package de.financer.template;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;

View File

@@ -136,6 +136,8 @@ financer.heading.recurring-to-transaction-with-amount=financer\: create transact
financer.cancel-back-to-overview=Cancel and back to overview
financer.chart.account-group-expenses-current-period.title=Expenses of the current period grouped by account group
financer.error-message.UNKNOWN_ERROR=An unknown error occurred!
financer.error-message.INVALID_ACCOUNT_TYPE=The selected account type is not valid!
financer.error-message.FROM_ACCOUNT_NOT_FOUND=The specified from account has not been found!

View File

@@ -135,3 +135,5 @@ financer.heading.recurring-transaction-list.all=financer\: alle wiederkehrende B
financer.heading.recurring-to-transaction-with-amount=financer\: Buchung mit Betrag aus wiederkehrender Buchung erstellen
financer.cancel-back-to-overview=Abbrechen und zur \u00DCbersicht
financer.chart.account-group-expenses-current-period.title=Ausgaben in der aktuellen Periode gruppiert nach Konto-Gruppe

View File

@@ -16,7 +16,9 @@
</div>
<div th:title="#{'financer.account-overview.tooltip.status.current-expenses'(${#temporals.format(periodStart)}, ${#temporals.format(periodEnd)})}">
<span th:text="#{financer.account-overview.status.current-expenses}"/>
<span th:text="${#numbers.formatDecimal(currentExpenses/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<a th:href="@{/getAccountGroupExpensesCurrentPeriod}">
<span th:text="${#numbers.formatDecimal(currentExpenses/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}" />
</a>
</div>
<div>
<span th:text="#{financer.account-overview.status.recurring-transaction-due-today}"/>