Introduce periods (and other smaller stuff)

This commit is contained in:
2019-09-28 21:47:07 +02:00
parent f6cdec638e
commit e052a5a98a
58 changed files with 953 additions and 246 deletions

View File

@@ -28,6 +28,34 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<exclusions>
<exclusion>
<artifactId>xml-apis</artifactId>
<groupId>xml-apis</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Dependencies for meta model generation of Hibernate entities -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
</dependency>
<!-- Misc dependencies -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -1,19 +1,20 @@
package de.financer.dto;
import de.financer.model.AccountType;
import de.financer.model.Period;
public class ExpensePeriodTotal {
private Long total;
private String periodShortCode;
private Period period;
private AccountType type;
public ExpensePeriodTotal() {
// nothing to do
}
public ExpensePeriodTotal(String periodShortCode, Long total, AccountType type) {
public ExpensePeriodTotal(Period period, Long total, AccountType type) {
this.total = total;
this.periodShortCode = periodShortCode;
this.period = period;
this.type = type;
}
@@ -25,12 +26,12 @@ public class ExpensePeriodTotal {
this.total = total;
}
public String getPeriodShortCode() {
return periodShortCode;
public Period getPeriod() {
return period;
}
public void setPeriodShortCode(String periodShortCode) {
this.periodShortCode = periodShortCode;
public void setPeriod(Period period) {
this.period = period;
}
public AccountType getType() {

View File

@@ -1,6 +1,7 @@
package de.financer.model;
import javax.persistence.*;
import java.util.Set;
@Entity
public class Account {
@@ -16,6 +17,9 @@ public class Account {
private Long currentBalance;
@ManyToOne
private AccountGroup accountGroup;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "account_id")
private Set<AccountStatistic> accountStatistics;
public Long getId() {
return id;
@@ -60,4 +64,12 @@ public class Account {
public void setAccountGroup(AccountGroup accountGroup) {
this.accountGroup = accountGroup;
}
public Set<AccountStatistic> getAccountStatistics() {
return accountStatistics;
}
public void setAccountStatistics(Set<AccountStatistic> accountStatistics) {
this.accountStatistics = accountStatistics;
}
}

View File

@@ -0,0 +1,76 @@
package de.financer.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
@Entity
public class AccountStatistic {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
// We need to ignore this field during serialization as otherwise
// the stack will overflow because of the circular dependency
// between Account and AccountStatistic
@JsonIgnore
private Account account;
@OneToOne(fetch = FetchType.EAGER)
private Period period;
private long spendingTotalFrom;
private long transactionCountFrom;
private long spendingTotalTo;
private long transactionCountTo;
public Long getId() {
return id;
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public Period getPeriod() {
return period;
}
public void setPeriod(Period period) {
this.period = period;
}
public long getSpendingTotalFrom() {
return spendingTotalFrom;
}
public void setSpendingTotalFrom(long spendingTotalFrom) {
this.spendingTotalFrom = spendingTotalFrom;
}
public long getTransactionCountFrom() {
return transactionCountFrom;
}
public void setTransactionCountFrom(long transactionCountFrom) {
this.transactionCountFrom = transactionCountFrom;
}
public long getSpendingTotalTo() {
return spendingTotalTo;
}
public void setSpendingTotalTo(long spendingTotalTo) {
this.spendingTotalTo = spendingTotalTo;
}
public long getTransactionCountTo() {
return transactionCountTo;
}
public void setTransactionCountTo(long transactionCountTo) {
this.transactionCountTo = transactionCountTo;
}
}

View File

@@ -0,0 +1,44 @@
package de.financer.model;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
public class Period {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
private PeriodType type;
private LocalDateTime start;
@Column(name = "\"end\"")
private LocalDateTime end;
public Long getId() {
return id;
}
public PeriodType getType() {
return type;
}
public void setType(PeriodType type) {
this.type = type;
}
public LocalDateTime getStart() {
return start;
}
public void setStart(LocalDateTime start) {
this.start = start;
}
public LocalDateTime getEnd() {
return end;
}
public void setEnd(LocalDateTime end) {
this.end = end;
}
}

View File

@@ -0,0 +1,24 @@
package de.financer.model;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public enum PeriodType {
/** User defined start and end */
EXPENSE;
/**
* This method validates whether the given string represents a valid period type.
*
* @param type to check
* @return whether the given type represents a valid period type
*/
public static boolean isValidType(String type) {
return Arrays.stream(PeriodType.values()).anyMatch((periodType) -> periodType.name().equals(type));
}
public static List<String> valueList() {
return Arrays.stream(PeriodType.values()).map(PeriodType::name).collect(Collectors.toList());
}
}

View File

@@ -2,6 +2,7 @@ package de.financer.model;
import javax.persistence.*;
import java.time.LocalDate;
import java.util.Set;
@Entity
@Table(name = "\"transaction\"")
@@ -16,9 +17,16 @@ public class Transaction {
@Column(name = "\"date\"")
private LocalDate date;
private String description;
private Long amount;
private long amount;
@ManyToOne(fetch = FetchType.EAGER)
private RecurringTransaction recurringTransaction;
@OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.EAGER)
//@formatter:off
@JoinTable(name = "link_transaction_period",
joinColumns = @JoinColumn(name = "transaction_id"),
inverseJoinColumns = @JoinColumn(name = "period_id"))
//@formatter:on
private Set<Period> periods;
public Long getId() {
return id;
@@ -56,11 +64,11 @@ public class Transaction {
this.description = description;
}
public Long getAmount() {
public long getAmount() {
return amount;
}
public void setAmount(Long amount) {
public void setAmount(long amount) {
this.amount = amount;
}
@@ -71,4 +79,12 @@ public class Transaction {
public void setRecurringTransaction(RecurringTransaction recurringTransaction) {
this.recurringTransaction = recurringTransaction;
}
public Set<Period> getPeriods() {
return periods;
}
public void setPeriods(Set<Period> periods) {
this.periods = periods;
}
}

View File

@@ -1,100 +0,0 @@
package de.financer.util;
import com.google.common.collect.Streams;
import java.time.LocalDate;
import java.time.Month;
import java.time.Period;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ExpensePeriod {
private LocalDate start;
private LocalDate end;
private ExpensePeriod(LocalDate start, LocalDate end) {
this.start = start;
this.end = end;
}
public static final ExpensePeriod getCurrentExpensePeriod(int periodStartDay) {
LocalDate periodStart;
LocalDate periodEnd;
if (LocalDate.now().getDayOfMonth() < periodStartDay) {
// If the current day of month is less than the configured period start day
// we need to go back one month in time and then set the day to the configured
// start day. For example:
// configured start day of month for period = 15
// now = 2019-06-10
// => 10 < 15
// now - one month = 2019-05-10
// set the day = 2019-05-15 = start
// end = start + one month - one day = 2019-06-14
// Period from 2019-05-15 to 2019-06-14
periodStart = LocalDate.now().minusMonths(1).withDayOfMonth(periodStartDay);
periodEnd = periodStart.plusMonths(1).minusDays(1);
} else {
// Else, the current day of month is greater or equals the configured period
// start day, just reset the day of month to the configured day, for example:
// configured start day of month for period = 15
// now = 2019-06-26
// => 26 > 15
// set the day = 2019-06-15 = start
// end = start + one month - one day = 2019-07-14
// Period from 2019-06-15 to 2019-07-14
periodStart = LocalDate.now().withDayOfMonth(periodStartDay);
periodEnd = periodStart.plusMonths(1).minusDays(1);
}
return new ExpensePeriod(periodStart, periodEnd);
}
public static final List<ExpensePeriod> generateExpensePeriodsForYear(int periodStartDay, int year) {
Stream<LocalDate> localDateStreamStart;
Stream<LocalDate> localDateStreamEnd;
if (periodStartDay == 1) {
localDateStreamStart = LocalDate.of(year, Month.JANUARY, 1)
.datesUntil(LocalDate.of(year, Month.DECEMBER, 2), Period
.ofMonths(1));
localDateStreamEnd = LocalDate.of(year, Month.JANUARY, 31)
.datesUntil(LocalDate.of(year + 1, Month.JANUARY, 1), Period
.ofMonths(1));
}
// If the start of the period is not the 1st we need to look to the previous year as well
// because the period in January actually started in December last year
else {
localDateStreamStart = LocalDate.of(year, Month.JANUARY, periodStartDay).minusMonths(1)
.datesUntil(LocalDate.of(year, Month.DECEMBER, periodStartDay + 1).plusDays(1), Period
.ofMonths(1));
localDateStreamEnd = LocalDate.of(year, Month.JANUARY, periodStartDay - 1)
.datesUntil(LocalDate.of(year, Month.DECEMBER, periodStartDay + 1).plusDays(1), Period
.ofMonths(1));
}
return Streams
.zip(localDateStreamStart, localDateStreamEnd, (start, end) -> new ExpensePeriod(start, end))
.collect(Collectors.toList());
}
public String generatePeriodShortCode() {
return String.format("%s/%s", this.start.getYear(), this.start.getMonthValue());
}
@Override
public String toString() {
return String.format("Start[%s], End[%s], ShortCode[%s]", this.start, this.end, this.generatePeriodShortCode());
}
public LocalDate getStart() {
return start;
}
public LocalDate getEnd() {
return end;
}
}

View File

@@ -12,7 +12,8 @@
<artifactId>financer-server</artifactId>
<packaging>${packaging.type}</packaging>
<description>The server part of the financer application - a simple app to manage your personal finances</description>
<description>The server part of the financer application - a simple app to manage your personal finances
</description>
<name>financer-server</name>
<properties>

View File

@@ -6,32 +6,32 @@ 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);
INVALID_ACCOUNT_TYPE(HttpStatus.BAD_REQUEST),
FROM_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
TO_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
FROM_AND_TO_ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
INVALID_DATE_FORMAT(HttpStatus.BAD_REQUEST),
MISSING_DATE(HttpStatus.BAD_REQUEST),
AMOUNT_ZERO(HttpStatus.BAD_REQUEST),
MISSING_AMOUNT(HttpStatus.BAD_REQUEST),
INVALID_BOOKING_ACCOUNTS(HttpStatus.BAD_REQUEST),
MISSING_HOLIDAY_WEEKEND_TYPE(HttpStatus.BAD_REQUEST),
INVALID_HOLIDAY_WEEKEND_TYPE(HttpStatus.BAD_REQUEST),
MISSING_INTERVAL_TYPE(HttpStatus.BAD_REQUEST),
INVALID_INTERVAL_TYPE(HttpStatus.BAD_REQUEST),
MISSING_FIRST_OCCURRENCE(HttpStatus.BAD_REQUEST),
INVALID_FIRST_OCCURRENCE_FORMAT(HttpStatus.BAD_REQUEST),
INVALID_LAST_OCCURRENCE_FORMAT(HttpStatus.BAD_REQUEST),
MISSING_RECURRING_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
INVALID_RECURRING_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
RECURRING_TRANSACTION_NOT_FOUND(HttpStatus.BAD_REQUEST),
MISSING_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
INVALID_TRANSACTION_ID(HttpStatus.BAD_REQUEST),
TRANSACTION_NOT_FOUND(HttpStatus.BAD_REQUEST),
ACCOUNT_NOT_FOUND(HttpStatus.BAD_REQUEST),
DUPLICATE_ACCOUNT_KEY(HttpStatus.BAD_REQUEST),
DUPLICATE_ACCOUNT_GROUP_NAME(HttpStatus.BAD_REQUEST),
ACCOUNT_GROUP_NOT_FOUND(HttpStatus.BAD_REQUEST);
private HttpStatus httpStatus;

View File

@@ -111,4 +111,13 @@ public class AccountController {
return this.accountService.getAccountExpenses(periodStart, periodEnd);
}
@RequestMapping("getAccountExpensesCurrentExpensePeriod")
public Iterable<AccountExpense> getAccountExpensesCurrentExpensePeriod() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("/accounts/getAccountExpensesCurrentExpensePeriod called");
}
return this.accountService.getAccountExpensesCurrentExpensePeriod();
}
}

View File

@@ -60,4 +60,13 @@ public class AccountGroupController {
return this.accountGroupService.getAccountGroupExpenses(periodStart, periodEnd);
}
@RequestMapping("getAccountGroupExpensesCurrentExpensePeriod")
public Iterable<AccountGroupExpense> getAccountGroupExpensesCurrentExpensePeriod() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("/accountGroups/getAccountGroupExpenses called");
}
return this.accountGroupService.getAccountGroupExpensesCurrentExpensePeriod();
}
}

View File

@@ -0,0 +1,49 @@
package de.financer.controller;
import de.financer.model.Period;
import de.financer.service.PeriodService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("periods")
public class PeriodController {
private static final Logger LOGGER = LoggerFactory.getLogger(PeriodController.class);
@Autowired
private PeriodService periodService;
@RequestMapping("closeCurrentExpensePeriod")
public ResponseEntity closeCurrentExpensePeriod() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("/periods/closeCurrentExpensePeriod called");
}
final ResponseEntity responseEntity = this.periodService.closeCurrentExpensePeriod().toResponseEntity();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/periods/closeCurrentExpensePeriod returns with %s", responseEntity));
}
return responseEntity;
}
@RequestMapping("getCurrentExpensePeriod")
public Period getCurrentExpensePeriod() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("/periods/getCurrentExpensePeriod called");
}
final Period currentExpensePeriod = this.periodService.getCurrentExpensePeriod();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/periods/getCurrentExpensePeriod returns with %s", currentExpensePeriod));
}
return currentExpensePeriod;
}
}

View File

@@ -79,12 +79,12 @@ public class TransactionController {
}
@RequestMapping("getExpensesCurrentPeriod")
public Long getExpensesCurrentPeriod(Integer monthPeriodStartDay) {
public Long getExpensesCurrentPeriod() {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod got parameters: %s", monthPeriodStartDay));
LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod called"));
}
final Long response = this.transactionService.getExpensesCurrentPeriod(monthPeriodStartDay);
final Long response = this.transactionService.getExpensesCurrentPeriod();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensesCurrentPeriod returns with %s", response));
@@ -94,13 +94,13 @@ public class TransactionController {
}
@RequestMapping("getExpensePeriodTotals")
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer monthPeriodStartDay, Integer year) {
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer year) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensePeriodTotals got parameters: %s, %s", monthPeriodStartDay, year));
LOGGER.debug(String.format("/transactions/getExpensePeriodTotals got parameters: %s", year));
}
final Iterable<ExpensePeriodTotal> expensePeriodTotals = this.transactionService
.getExpensePeriodTotals(monthPeriodStartDay, year);
.getExpensePeriodTotals(year);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/transactions/getExpensePeriodTotals returns with %s", expensePeriodTotals));

View File

@@ -3,6 +3,7 @@ package de.financer.dba;
import de.financer.dto.AccountGroupExpense;
import de.financer.model.AccountGroup;
import de.financer.model.AccountType;
import de.financer.model.Period;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
@@ -16,4 +17,7 @@ public interface AccountGroupRepository extends CrudRepository<AccountGroup, Lon
@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);
@Query("SELECT new de.financer.dto.AccountGroupExpense(ag, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a JOIN a.accountGroup ag JOIN t.periods p WHERE a.type in :expenseTypes AND p = :period GROUP BY ag")
Iterable<AccountGroupExpense> getAccountGroupExpensesCurrentExpensePeriod(Period period, AccountType... expenseTypes);
}

View File

@@ -3,6 +3,7 @@ package de.financer.dba;
import de.financer.dto.AccountExpense;
import de.financer.model.Account;
import de.financer.model.AccountType;
import de.financer.model.Period;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Propagation;
@@ -19,4 +20,7 @@ public interface AccountRepository extends CrudRepository<Account, Long> {
@Query("SELECT new de.financer.dto.AccountExpense(a, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a WHERE a.type in :expenseTypes AND t.date BETWEEN :startDate AND :startEnd GROUP BY a")
Iterable<AccountExpense> getAccountExpenses(LocalDate startDate, LocalDate startEnd, AccountType... expenseTypes);
@Query("SELECT new de.financer.dto.AccountExpense(a, SUM(t.amount)) FROM Transaction t JOIN t.toAccount a JOIN t.periods p WHERE a.type in :expenseTypes AND p = :period GROUP BY a")
Iterable<AccountExpense> getAccountExpenses(Period period, AccountType... expenseTypes);
}

View File

@@ -0,0 +1,15 @@
package de.financer.dba;
import de.financer.model.Account;
import de.financer.model.AccountStatistic;
import de.financer.model.Period;
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;
@Transactional(propagation = Propagation.REQUIRED)
public interface AccountStatisticRepository extends CrudRepository<AccountStatistic, Long> {
@Query("SELECT accStat FROM AccountStatistic accStat WHERE accStat.account = :account AND accStat.period = :period")
AccountStatistic findForAccountAndPeriod(Account account, Period period);
}

View File

@@ -0,0 +1,17 @@
package de.financer.dba;
import de.financer.model.Period;
import de.financer.model.PeriodType;
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;
@Transactional(propagation = Propagation.REQUIRED)
public interface PeriodRepository extends CrudRepository<Period, Long> {
@Query("SELECT p FROM Period p WHERE p.type = :type AND p.end IS NULL")
Period findCurrentExpensePeriod(PeriodType type);
@Query("SELECT p FROM Period p WHERE p.type = :type AND YEAR(p.start) = :year")
Iterable<Period> getAllPeriodsForYear(PeriodType type, Integer year);
}

View File

@@ -3,6 +3,7 @@ package de.financer.dba;
import de.financer.dto.ExpensePeriodTotal;
import de.financer.model.Account;
import de.financer.model.AccountType;
import de.financer.model.Period;
import de.financer.model.Transaction;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
@@ -16,13 +17,12 @@ import java.util.List;
public interface TransactionRepository extends CrudRepository<Transaction, Long> {
Iterable<Transaction> findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount);
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.toAccount a WHERE t.date BETWEEN :periodStart AND :periodEnd AND a.type IN :expenseTypes")
Long getExpensesCurrentPeriod(LocalDate periodStart, LocalDate periodEnd, AccountType... expenseTypes);
@Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a WHERE a.type IN :expenseTypes AND p = :period")
Long getExpensesForPeriod(Period period, AccountType... expenseTypes);
// The HQL contains a hack because Hibernate can't resolve the alias of the CASE column in the GROUP BY clause
// That's why the generated alias is used directly in the HQL. It will break if the columns in the SELECT clause get reordered
// col_0_0_ instead of periodShortCode
// col_2_0_ instead of AccType
@Query("SELECT new de.financer.dto.ExpensePeriodTotal(CASE WHEN EXTRACT(DAY FROM t.date) >= :startDay THEN CONCAT(CAST(EXTRACT(YEAR FROM t.date) AS string), '/', CAST(EXTRACT(MONTH FROM t.date) AS string)) ELSE CASE WHEN EXTRACT(MONTH FROM t.date) = 1 THEN CONCAT(CAST(EXTRACT(YEAR FROM t.date) - 1 AS string), '/12') ELSE CONCAT(CAST(EXTRACT(YEAR FROM t.date) AS string), '/', CAST(EXTRACT(MONTH FROM t.date) - 1 AS string)) END END AS periodShortCode, SUM(t.amount), CASE WHEN fa.type = :incomeType THEN fa.type ELSE ta.type END AS AccType) FROM Transaction t JOIN t.toAccount ta JOIN t.fromAccount fa WHERE ((ta.type IN :expenseTypes AND fa.type <> :startType) OR (fa.type = :incomeType)) AND t.date BETWEEN :lowerBound AND :upperBound GROUP BY col_0_0_, col_2_0_ ORDER BY periodShortCode, AccType ASC")
List<ExpensePeriodTotal> getAccountExpenseTotals(int startDay, LocalDate lowerBound, LocalDate upperBound, AccountType incomeType, AccountType startType, AccountType... expenseTypes);
@Query("SELECT new de.financer.dto.ExpensePeriodTotal(p, SUM(t.amount), CASE WHEN fa.type = :incomeType THEN fa.type ELSE ta.type END AS AccType) FROM Transaction t JOIN t.toAccount ta JOIN t.fromAccount fa JOIN t.periods p WHERE ((ta.type IN :expenseTypes AND fa.type <> :startType) OR (fa.type = :incomeType)) AND p IN :periods GROUP BY p, col_2_0_")
List<ExpensePeriodTotal> getAccountExpenseTotals(Iterable<Period> periods, AccountType incomeType, AccountType startType, AccountType... expenseTypes);
}

View File

@@ -6,6 +6,7 @@ import de.financer.dba.AccountGroupRepository;
import de.financer.dto.AccountGroupExpense;
import de.financer.model.AccountGroup;
import de.financer.model.AccountType;
import de.financer.model.Period;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -26,6 +27,9 @@ public class AccountGroupService {
@Autowired
private AccountGroupRepository accountGroupRepository;
@Autowired
private PeriodService periodService;
@Autowired
private FinancerConfig financerConfig;
@@ -94,4 +98,10 @@ public class AccountGroupService {
return this.accountGroupRepository.getAccountGroupExpenses(startDate, endDate, AccountType.LIABILITY, AccountType.EXPENSE);
}
public Iterable<AccountGroupExpense> getAccountGroupExpensesCurrentExpensePeriod() {
final Period period = this.periodService.getCurrentExpensePeriod();
return this.accountGroupRepository.getAccountGroupExpensesCurrentExpensePeriod(period, AccountType.LIABILITY, AccountType.EXPENSE);
}
}

View File

@@ -4,10 +4,7 @@ import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dba.AccountRepository;
import de.financer.dto.AccountExpense;
import de.financer.model.Account;
import de.financer.model.AccountGroup;
import de.financer.model.AccountStatus;
import de.financer.model.AccountType;
import de.financer.model.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,6 +29,9 @@ public class AccountService {
@Autowired
private AccountGroupService accountGroupService;
@Autowired
private PeriodService periodService;
@Autowired
private FinancerConfig financerConfig;
@@ -161,6 +161,13 @@ public class AccountService {
return this.accountRepository.getCurrentAssets(AccountType.BANK, AccountType.CASH);
}
/**
* This method calculates the expenses per account in the given arbitrary period.
*
* @param periodStart the start of the arbitrary period
* @param periodEnd the end of the arbitrary period
* @return a mapping of {@link Account}<->its expenses in the given period
*/
public Iterable<AccountExpense> getAccountExpenses(String periodStart, String periodEnd) {
LocalDate startDate;
LocalDate endDate;
@@ -178,4 +185,15 @@ public class AccountService {
return this.accountRepository.getAccountExpenses(startDate, endDate, AccountType.LIABILITY, AccountType.EXPENSE);
}
/**
* This method calculates the expenses per account in the current expense period.
*
* @return a mapping of {@link Account}<->its expenses in the current expense period
*/
public Iterable<AccountExpense> getAccountExpensesCurrentExpensePeriod() {
final Period period = this.periodService.getCurrentExpensePeriod();
return this.accountRepository.getAccountExpenses(period, AccountType.LIABILITY, AccountType.EXPENSE);
}
}

View File

@@ -0,0 +1,77 @@
package de.financer.service;
import de.financer.dba.AccountStatisticRepository;
import de.financer.model.Account;
import de.financer.model.AccountStatistic;
import de.financer.model.Period;
import de.financer.model.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
public class AccountStatisticService {
private static final Logger LOGGER = LoggerFactory.getLogger(PeriodService.class);
@Autowired
private AccountStatisticRepository accountStatisticRepository;
@Transactional(propagation = Propagation.REQUIRED)
public void calculateStatistics(Transaction transaction) {
final Account fromAccount = transaction.getFromAccount();
final Account toAccount = transaction.getToAccount();
final long amount = transaction.getAmount();
final List<AccountStatistic> resultList = new ArrayList<>();
for (final Period period : transaction.getPeriods()) {
resultList.add(calculateInternal(fromAccount, period, amount, true, 1));
resultList.add(calculateInternal(toAccount, period, amount, false, 1));
}
this.accountStatisticRepository.saveAll(resultList);
}
@Transactional(propagation = Propagation.REQUIRED)
public void revertStatistics(Transaction transaction) {
final Account fromAccount = transaction.getFromAccount();
final Account toAccount = transaction.getToAccount();
final long amount = transaction.getAmount();
final List<AccountStatistic> resultList = new ArrayList<>();
for (final Period period : transaction.getPeriods()) {
resultList.add(calculateInternal(fromAccount, period, amount, true, -1));
resultList.add(calculateInternal(toAccount, period, amount, false, -1));
}
this.accountStatisticRepository.saveAll(resultList);
}
private AccountStatistic calculateInternal(Account account, Period period, long amount, boolean from, int multiplier) {
AccountStatistic accountStatistic = this.accountStatisticRepository
.findForAccountAndPeriod(account, period);
if (accountStatistic == null) {
accountStatistic = new AccountStatistic();
accountStatistic.setAccount(account);
accountStatistic.setPeriod(period);
}
if (from) {
accountStatistic.setSpendingTotalFrom(accountStatistic.getSpendingTotalFrom() + amount * multiplier);
accountStatistic.setTransactionCountFrom(accountStatistic.getTransactionCountFrom() + multiplier);
}
else {
accountStatistic.setSpendingTotalTo(accountStatistic.getSpendingTotalTo() + amount * multiplier);
accountStatistic.setTransactionCountTo(accountStatistic.getTransactionCountTo() + multiplier);
}
return accountStatistic;
}
}

View File

@@ -0,0 +1,64 @@
package de.financer.service;
import de.financer.ResponseReason;
import de.financer.dba.PeriodRepository;
import de.financer.model.Period;
import de.financer.model.PeriodType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service
public class PeriodService {
private static final Logger LOGGER = LoggerFactory.getLogger(PeriodService.class);
@Autowired
private PeriodRepository periodRepository;
/**
* @return the currently open expense period
*/
public Period getCurrentExpensePeriod() {
return this.periodRepository.findCurrentExpensePeriod(PeriodType.EXPENSE);
}
/**
* This method closes the current expense period and opens a new one.
*
* @return {@link ResponseReason#OK} if the operation succeeded, {@link ResponseReason#OK}
*/
@Transactional(propagation = Propagation.REQUIRED)
public ResponseReason closeCurrentExpensePeriod() {
final Period currentPeriod = this.getCurrentExpensePeriod();
final Period nextPeriod = new Period();
final LocalDateTime now = LocalDateTime.now();
ResponseReason response;
currentPeriod.setEnd(now);
nextPeriod.setStart(now.plusSeconds(1));
nextPeriod.setType(PeriodType.EXPENSE);
try {
this.periodRepository.save(currentPeriod);
this.periodRepository.save(nextPeriod);
response = ResponseReason.OK;
} catch (Exception e) {
LOGGER.error("Could not close current expense period!", e);
response = ResponseReason.UNKNOWN_ERROR;
}
return response;
}
public Iterable<Period> getAllExpensePeriodsForYear(Integer year) {
return this.periodRepository.getAllPeriodsForYear(PeriodType.EXPENSE, year);
}
}

View File

@@ -1,15 +1,10 @@
package de.financer.service;
import com.google.common.collect.Iterables;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.dba.TransactionRepository;
import de.financer.dto.ExpensePeriodTotal;
import de.financer.model.Account;
import de.financer.model.AccountType;
import de.financer.model.RecurringTransaction;
import de.financer.model.Transaction;
import de.financer.util.ExpensePeriod;
import de.financer.model.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.slf4j.Logger;
@@ -23,7 +18,6 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Service
@@ -36,6 +30,12 @@ public class TransactionService {
@Autowired
private RuleService ruleService;
@Autowired
private PeriodService periodService;
@Autowired
private AccountStatisticService accountStatisticService;
@Autowired
private TransactionRepository transactionRepository;
@@ -91,6 +91,8 @@ public class TransactionService {
try {
final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date, recurringTransaction);
transaction.setPeriods(Collections.singleton(this.periodService.getCurrentExpensePeriod()));
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
.getMultiplierFromAccount(fromAccount) * amount));
@@ -106,6 +108,8 @@ public class TransactionService {
this.transactionRepository.save(transaction);
this.accountStatisticService.calculateStatistics(transaction);
this.accountService.saveAccount(fromAccount);
this.accountService.saveAccount(toAccount);
@@ -215,7 +219,9 @@ public class TransactionService {
.getMultiplierToAccount(toAccount) * amount * -1));
try {
this.transactionRepository.deleteById(Long.valueOf(transactionId));
this.transactionRepository.deleteById(transaction.getId());
this.accountStatisticService.revertStatistics(transaction);
this.accountService.saveAccount(fromAccount);
this.accountService.saveAccount(toAccount);
@@ -228,31 +234,19 @@ public class TransactionService {
return response;
}
public Long getExpensesCurrentPeriod(Integer monthPeriodStartDay) {
if (monthPeriodStartDay == null) {
return Long.valueOf(-1l);
}
final ExpensePeriod expensePeriod = ExpensePeriod
.getCurrentExpensePeriod(monthPeriodStartDay);
public Long getExpensesCurrentPeriod() {
final Period expensePeriod = this.periodService.getCurrentExpensePeriod();
final Long expensesCurrentPeriod = this.transactionRepository
.getExpensesCurrentPeriod(expensePeriod.getStart(), expensePeriod
.getEnd(), AccountType.EXPENSE, AccountType.LIABILITY);
.getExpensesForPeriod(expensePeriod, AccountType.EXPENSE, AccountType.LIABILITY);
return Optional.ofNullable(expensesCurrentPeriod).orElse(Long.valueOf(0l));
}
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer monthPeriodStartDay, Integer year) {
final List<ExpensePeriod> expensePeriods = ExpensePeriod.generateExpensePeriodsForYear(monthPeriodStartDay, year);
public Iterable<ExpensePeriodTotal> getExpensePeriodTotals(Integer year) {
final Iterable<Period> periods = this.periodService.getAllExpensePeriodsForYear(year);
final ExpensePeriod lowerBound = Iterables.get(expensePeriods, 0);
final ExpensePeriod upperBound = Iterables.getLast(expensePeriods);
final List<ExpensePeriodTotal> expensePeriodTotals = this.transactionRepository
.getAccountExpenseTotals(monthPeriodStartDay, lowerBound.getStart(), upperBound
.getEnd(), AccountType.INCOME, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY);
return expensePeriodTotals;
return this.transactionRepository
.getAccountExpenseTotals(periods, AccountType.INCOME, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY);
}
}

View File

@@ -1,2 +1,3 @@
# Hibernate
spring.jpa.show-sql=true
#logging.level.org.hibernate.type=trace

View File

@@ -0,0 +1,4 @@
ALTER TABLE account ALTER COLUMN "key" NVARCHAR(1000);
ALTER TABLE recurring_transaction ALTER COLUMN description NVARCHAR(1000);
ALTER TABLE "transaction" ALTER COLUMN description NVARCHAR(1000);
ALTER TABLE account_group ALTER COLUMN name NVARCHAR(1000);

View File

@@ -0,0 +1,19 @@
-- Account group table
CREATE TABLE period (
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
type VARCHAR(255) NOT NULL,
start DATETIME NOT NULL,
"end" DATETIME
);
CREATE TABLE link_transaction_period (
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
transaction_id BIGINT NOT NULL,
period_id BIGINT NOT NULL,
CONSTRAINT fk_link_transaction_period_transaction FOREIGN KEY (transaction_id) REFERENCES "transaction" (id),
CONSTRAINT fk_link_transaction_period_period FOREIGN KEY (period_id) REFERENCES period (id)
);
INSERT INTO period (type, start)
VALUES ('EXPENSE', NOW());

View File

@@ -0,0 +1,13 @@
-- Account statistic table
CREATE TABLE account_statistic (
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
account_id BIGINT NOT NULL,
period_id BIGINT NOT NULL,
spending_total_from BIGINT NOT NULL,
transaction_count_from BIGINT NOT NULL,
spending_total_to BIGINT NOT NULL,
transaction_count_to BIGINT NOT NULL,
CONSTRAINT fk_account_statistic_account FOREIGN KEY (account_id) REFERENCES account (id),
CONSTRAINT fk_account_statistic_period FOREIGN KEY (period_id) REFERENCES period (id)
)

View File

@@ -0,0 +1,19 @@
-- Account group table
CREATE TABLE period (
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
type VARCHAR(255) NOT NULL,
start DATETIME NOT NULL,
"end" DATETIME
);
CREATE TABLE link_transaction_period (
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
transaction_id BIGINT NOT NULL,
period_id BIGINT NOT NULL,
CONSTRAINT fk_link_transaction_period_transaction FOREIGN KEY (transaction_id) REFERENCES "transaction" (id),
CONSTRAINT fk_link_transaction_period_period FOREIGN KEY (period_id) REFERENCES period (id)
);
INSERT INTO period (type, start)
VALUES ('EXPENSE', NOW());

View File

@@ -0,0 +1,13 @@
-- Account statistic table
CREATE TABLE account_statistic (
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
account_id BIGINT NOT NULL,
period_id BIGINT NOT NULL,
spending_total_from BIGINT NOT NULL,
transaction_count_from BIGINT NOT NULL,
spending_total_to BIGINT NOT NULL,
transaction_count_to BIGINT NOT NULL,
CONSTRAINT fk_account_statistic_account FOREIGN KEY (account_id) REFERENCES account (id),
CONSTRAINT fk_account_statistic_period FOREIGN KEY (period_id) REFERENCES period (id)
)

View File

@@ -0,0 +1 @@
The VARCHAR type on PostgreSQL is already multibyte capable so nothing to do here

View File

@@ -18,7 +18,7 @@ The recurring transaction table is defined like this (at least for postgres):
Note the
deleted BOOLEAN DEFAULT 'TRUE' NOT NULL,
column definition. Not sure why the default is TRUE here is it doesn't make sense.
column definition. Not sure why the default is TRUE here as it doesn't make sense.
It was probably a mistake, however fixing it here _WILL_ break existing installations
as Flyway uses a checksum for scripts. So there is no easy fix, except for effectively
overwriting this default in Java code when creating a new recurring transaction.

View File

@@ -24,6 +24,12 @@ public class TransactionService_createTransactionTest {
@Mock
private RuleService ruleService;
@Mock
private PeriodService periodService;
@Mock
private AccountStatisticService accountStatisticService;
@Mock
private TransactionRepository transactionRepository;

View File

@@ -27,6 +27,9 @@ public class TransactionService_deleteTransactionTest {
@Mock
private RuleService ruleService;
@Mock
private AccountStatisticService accountStatisticService;
@Mock
private TransactionRepository transactionRepository;
@@ -82,7 +85,7 @@ public class TransactionService_deleteTransactionTest {
// Arrange
Mockito.when(this.transactionRepository.findById(Mockito.anyLong()))
.thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE)));
Mockito.doThrow(new NullPointerException()).when(this.transactionRepository).deleteById(Mockito.anyLong());
Mockito.doThrow(new NullPointerException()).when(this.transactionRepository).deleteById(Mockito.any());
// Act
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
@@ -94,8 +97,10 @@ public class TransactionService_deleteTransactionTest {
@Test
public void test_deleteRecurringTransaction_OK() {
// Arrange
final Transaction transaction = createTransaction(AccountType.BANK, AccountType.EXPENSE);
Mockito.when(this.transactionRepository.findById(Mockito.anyLong()))
.thenReturn(Optional.of(createTransaction(AccountType.BANK, AccountType.EXPENSE)));
.thenReturn(Optional.of(transaction));
// Act
final ResponseReason response = this.classUnderTest.deleteTransaction("123");
@@ -103,10 +108,11 @@ public class TransactionService_deleteTransactionTest {
// Assert
Assert.assertEquals(ResponseReason.OK, response);
final InOrder inOrder = Mockito.inOrder(this.accountService);
final InOrder inOrder = Mockito.inOrder(this.accountStatisticService, this.accountService);
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(50000L).equals(arg.getCurrentBalance())));
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((Account arg) -> Long.valueOf(5000L).equals(arg.getCurrentBalance())));
inOrder.verify(this.accountStatisticService).revertStatistics(ArgumentMatchers.eq(transaction));
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((arg) -> Long.valueOf(50000L).equals(arg.getCurrentBalance())));
inOrder.verify(this.accountService).saveAccount(ArgumentMatchers.argThat((arg) -> Long.valueOf(5000L).equals(arg.getCurrentBalance())));
}
private Transaction createTransaction(AccountType fromType, AccountType toType) {

View File

@@ -3,6 +3,7 @@ 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;
@@ -12,8 +13,14 @@ import org.jfree.data.general.PieDataset;
public class AccountExpensesGenerator extends AbstractExpensesGenerator {
@Override
protected PieDataset getDataset(String start, String end, FinancerConfig financerConfig) {
final Iterable<AccountExpense> expenses = new GetAccountExpensesTemplate()
.exchange(financerConfig, start, end).getBody();
Iterable<AccountExpense> expenses;
if (start == null && end == null) {
expenses = new GetAccountExpensesCurrentExpensePeriodTemplate().exchange(financerConfig).getBody();
}
else {
expenses = new GetAccountExpensesTemplate().exchange(financerConfig, start, end).getBody();
}
final DefaultPieDataset dataSet = new DefaultPieDataset();

View File

@@ -2,6 +2,7 @@ package de.financer.chart.impl.expense;
import de.financer.config.FinancerConfig;
import de.financer.dto.AccountGroupExpense;
import de.financer.template.GetAccountGroupExpensesCurrentExpensePeriodTemplate;
import de.financer.template.GetAccountGroupExpensesTemplate;
import org.apache.commons.collections4.IterableUtils;
import org.jfree.data.general.DefaultPieDataset;
@@ -10,8 +11,14 @@ import org.jfree.data.general.PieDataset;
public class AccountGroupExpensesGenerator extends AbstractExpensesGenerator {
@Override
protected PieDataset getDataset(String start, String end, FinancerConfig financerConfig) {
final Iterable<AccountGroupExpense> expenses = new GetAccountGroupExpensesTemplate()
.exchange(financerConfig, start, end).getBody();
Iterable<AccountGroupExpense> expenses;
if (start == null && end == null) {
expenses = new GetAccountGroupExpensesCurrentExpensePeriodTemplate().exchange(financerConfig).getBody();
}
else {
expenses = new GetAccountGroupExpensesTemplate().exchange(financerConfig, start, end).getBody();
}
final DefaultPieDataset dataSet = new DefaultPieDataset();

View File

@@ -2,9 +2,8 @@ package de.financer.chart.impl.total;
import de.financer.chart.AbstractChartGenerator;
import de.financer.dto.ExpensePeriodTotal;
import de.financer.model.Period;
import de.financer.template.GetExpensePeriodTotalsTemplate;
import de.financer.util.ControllerUtils;
import de.financer.util.ExpensePeriod;
import org.apache.commons.collections4.IterableUtils;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
@@ -17,7 +16,7 @@ import org.springframework.context.i18n.LocaleContextHolder;
import java.text.NumberFormat;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.List;
import java.util.Comparator;
public class PeriodTotalGenerator extends AbstractChartGenerator<PeriodTotalParameter> {
@Override
@@ -45,30 +44,36 @@ public class PeriodTotalGenerator extends AbstractChartGenerator<PeriodTotalPara
private CategoryDataset getDataset(PeriodTotalParameter parameter) {
final DefaultCategoryDataset result = new DefaultCategoryDataset();
final List<ExpensePeriod> expensePeriods = ExpensePeriod
.generateExpensePeriodsForYear(this.getFinancerConfig().getMonthPeriodStartDay(), parameter.getYear());
final Iterable<ExpensePeriodTotal> totalData = new GetExpensePeriodTotalsTemplate()
.exchange(this.getFinancerConfig(), parameter.getYear()).getBody();
IterableUtils.toList(totalData).stream()
.sorted(Comparator.comparing((ExpensePeriodTotal ept) -> ept.getPeriod().getStart())
.thenComparing((ExpensePeriodTotal ept) -> ept.getType()))
.forEach((ept) -> result.addValue((ept.getTotal() / 100D),
this.getMessage("financer.account-type." + ept.getType()),
expensePeriods.stream()
.filter((ep) -> ep.generatePeriodShortCode()
.equals(ept.getPeriodShortCode()))
.map((ep) -> formatDateY(ep))
.findFirst().get()));
formatDateY(ept.getPeriod())));
return result;
}
private String formatDateY(ExpensePeriod ep) {
return String.format("%s - %s",
ep.getStart().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(LocaleContextHolder.getLocale())),
ep.getEnd().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(LocaleContextHolder.getLocale())));
private String formatDateY(Period ep) {
final String start = ep.getStart().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(LocaleContextHolder.getLocale()));
String end;
if (ep.getEnd() == null) {
end = this.getMessage("financer.chart.expense-period-totals-current-year.open-period");
}
else {
end = ep.getEnd().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
.withLocale(LocaleContextHolder.getLocale()));
}
return String.format("%s - %s", start, end);
}
}

View File

@@ -2,11 +2,11 @@ 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.form.NewAccountForm;
import de.financer.model.*;
import de.financer.util.ControllerUtils;
import de.financer.util.ExpensePeriod;
import de.financer.util.TransactionUtils;
import de.financer.util.comparator.TransactionByDateByIdDescComparator;
import org.apache.commons.collections4.IterableUtils;
@@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.List;
import java.util.stream.Collectors;
@Controller
public class AccountController {
@@ -34,18 +35,20 @@ public class AccountController {
final ResponseEntity<Iterable<RecurringTransaction>> rtAllActRes = new GetAllActiveRecurringTransactionsTemplate()
.exchange(this.financerConfig);
final ResponseEntity<Long> currentAssets = new GetCurrentAssetsTemplate().exchange(this.financerConfig);
final ResponseEntity<Long> currentExpenses = new GetExpensesCurrentPeriodTemplate().exchange(this.financerConfig);
final ResponseEntity<Long> currentExpenses = new GetExpensesCurrentPeriodTemplate()
.exchange(this.financerConfig);
final boolean showClosedBoolean = BooleanUtils.toBoolean(showClosed);
final ExpensePeriod expensePeriod = ExpensePeriod
.getCurrentExpensePeriod(this.financerConfig.getMonthPeriodStartDay());
final ResponseEntity<Period> expensePeriodRes = new GetCurrentExpensePeriodTemplate()
.exchange(this.financerConfig);
final Period expensePeriod = expensePeriodRes.getBody();
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody(), showClosedBoolean));
model.addAttribute("accounts", decorateAccounts(ControllerUtils
.filterAndSortAccounts(response.getBody(), showClosedBoolean)));
model.addAttribute("rtDueTodayCount", IterableUtils.size(rtDtRes.getBody()));
model.addAttribute("rtAllActiveCount", IterableUtils.size(rtAllActRes.getBody()));
model.addAttribute("currentAssets", currentAssets.getBody());
model.addAttribute("currentExpenses", currentExpenses.getBody());
model.addAttribute("periodStart", expensePeriod.getStart());
model.addAttribute("periodEnd", expensePeriod.getEnd());
model.addAttribute("showClosed", showClosedBoolean);
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
@@ -53,6 +56,10 @@ public class AccountController {
return "account/accountOverview";
}
private Iterable<AccountDecorator> decorateAccounts(List<Account> accounts) {
return accounts.stream().map((a) -> new AccountDecorator(a)).collect(Collectors.toList());
}
@GetMapping("/newAccount")
public String newAccount(Model model) {
final ResponseEntity<Iterable<AccountGroup>> accountGroupResponse = new GetAllAccountGroupsTemplate()

View File

@@ -1,15 +1,14 @@
package de.financer.controller;
import de.financer.chart.impl.expense.ExpensesParameter;
import de.financer.chart.ChartGenerator;
import de.financer.chart.ChartType;
import de.financer.chart.FinancerChartFactory;
import de.financer.chart.impl.expense.ExpensesParameter;
import de.financer.chart.impl.total.PeriodTotalParameter;
import de.financer.config.FinancerConfig;
import de.financer.form.ConfigAccountExpenseForPeriodForm;
import de.financer.form.ConfigAccountGroupExpenseForPeriodForm;
import de.financer.util.ControllerUtils;
import de.financer.util.ExpensePeriod;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.springframework.beans.factory.annotation.Autowired;
@@ -32,12 +31,8 @@ public class ChartController {
@GetMapping("/getAccountGroupExpensesCurrentPeriod")
public void getAccountGroupExpensesCurrentPeriod(HttpServletResponse response) {
final ExpensePeriod period = ExpensePeriod
.getCurrentExpensePeriod(this.financerConfig.getMonthPeriodStartDay());
final ExpensesParameter parameter = new ExpensesParameter();
parameter.setPeriodStart(period.getStart());
parameter.setPeriodEnd(period.getEnd());
parameter.setTitle("financer.chart.account-group-expenses-current-period.title");
final ChartGenerator<ExpensesParameter> generator =
@@ -70,12 +65,8 @@ public class ChartController {
@GetMapping("/getAccountExpensesCurrentPeriod")
public void getAccountExpensesCurrentPeriod(HttpServletResponse response) {
final ExpensePeriod period = ExpensePeriod
.getCurrentExpensePeriod(this.financerConfig.getMonthPeriodStartDay());
final ExpensesParameter parameter = new ExpensesParameter();
parameter.setPeriodStart(period.getStart());
parameter.setPeriodEnd(period.getEnd());
parameter.setTitle("financer.chart.account-expenses-current-period.title");
final ChartGenerator<ExpensesParameter> generator =

View File

@@ -8,10 +8,12 @@ public enum Function {
ACC_OPEN_ACCOUNT("accounts/openAccount"),
ACC_CURRENT_ASSETS("accounts/getCurrentAssets"),
ACC_GET_ACC_EXPENSES("accounts/getAccountExpenses"),
ACC_GET_ACC_EXPENSES_CURRENT_EXPENSE_PERIOD("accounts/getAccountExpensesCurrentExpensePeriod"),
ACC_GP_CREATE_ACCOUNT_GROUP("accountGroups/createAccountGroup"),
ACC_GP_GET_ALL("accountGroups/getAll"),
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"),
@@ -26,7 +28,10 @@ public enum Function {
RT_GET_ALL_DUE_TODAY("recurringTransactions/getAllDueToday"),
RT_CREATE_RECURRING_TRANSACTION("recurringTransactions/createRecurringTransaction"),
RT_DELETE_RECURRING_TRANSACTION("recurringTransactions/deleteRecurringTransaction"),
RT_CREATE_TRANSACTION("recurringTransactions/createTransaction");
RT_CREATE_TRANSACTION("recurringTransactions/createTransaction"),
P_GET_CURRENT_EXPENSE_PERIOD("periods/getCurrentExpensePeriod"),
P_CLOSE_CURRENT_EXPENSE_PERIOD("periods/closeCurrentExpensePeriod");
private String path;

View File

@@ -0,0 +1,25 @@
package de.financer.controller;
import de.financer.config.FinancerConfig;
import de.financer.template.StringTemplate;
import de.financer.util.ControllerUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.util.UriComponentsBuilder;
@Controller
public class PeriodController {
@Autowired
private FinancerConfig financerConfig;
@GetMapping("/closePeriod")
public String closePeriod() {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.P_CLOSE_CURRENT_EXPENSE_PERIOD));
new StringTemplate().exchange(builder);
return "redirect:/accountOverview";
}
}

View File

@@ -2,13 +2,10 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.model.*;
import de.financer.template.*;
import de.financer.form.NewRecurringTransactionForm;
import de.financer.form.RecurringToTransactionWithAmountForm;
import de.financer.model.Account;
import de.financer.model.HolidayWeekendType;
import de.financer.model.IntervalType;
import de.financer.model.RecurringTransaction;
import de.financer.util.ControllerUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@@ -19,6 +16,8 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Controller
public class RecurringTransactionController {
@@ -30,10 +29,18 @@ public class RecurringTransactionController {
public String newRecurringTransaction(Model model) {
final ResponseEntity<Iterable<Account>> response = new GetAllAccountsTemplate().exchange(this.financerConfig);
final NewRecurringTransactionForm form = new NewRecurringTransactionForm();
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());
form.setRemind(Boolean.TRUE);
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody()));
model.addAttribute("fromAccounts", fromAccounts);
model.addAttribute("toAccounts", toAccounts);
model.addAttribute("intervalTypes", IntervalType.valueList());
model.addAttribute("holidayWeekendTypes", HolidayWeekendType.valueList());
model.addAttribute("form", form);
@@ -64,8 +71,16 @@ public class RecurringTransactionController {
if (!ResponseReason.OK.equals(responseReason)) {
final ResponseEntity<Iterable<Account>> getAllResponse = new GetAllAccountsTemplate()
.exchange(this.financerConfig);
final List<Account> fromAccounts = ControllerUtils.filterAndSortAccounts(getAllResponse.getBody()).stream()
.filter((a) -> a.getType() != AccountType.EXPENSE)
.collect(Collectors.toList());
final List<Account> toAccounts = ControllerUtils.filterAndSortAccounts(getAllResponse.getBody()).stream()
.filter((a) -> a.getType() != AccountType.INCOME && a
.getType() != AccountType.START)
.collect(Collectors.toList());
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(getAllResponse.getBody()));
model.addAttribute("fromAccounts", fromAccounts);
model.addAttribute("toAccounts", toAccounts);
model.addAttribute("intervalTypes", IntervalType.valueList());
model.addAttribute("holidayWeekendTypes", HolidayWeekendType.valueList());
model.addAttribute("form", form);

View File

@@ -2,6 +2,7 @@ package de.financer.controller;
import de.financer.ResponseReason;
import de.financer.config.FinancerConfig;
import de.financer.model.AccountType;
import de.financer.template.GetAccountByKeyTemplate;
import de.financer.template.GetAllAccountsTemplate;
import de.financer.template.GetAllTransactionsForAccountTemplate;
@@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.List;
import java.util.stream.Collectors;
@Controller
public class TransactionController {
@@ -33,8 +35,16 @@ public class TransactionController {
@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());
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(response.getBody()));
model.addAttribute("fromAccounts", fromAccounts);
model.addAttribute("toAccounts", toAccounts);
model.addAttribute("form", new NewTransactionForm());
ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
@@ -57,8 +67,16 @@ public class TransactionController {
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());
model.addAttribute("accounts", ControllerUtils.filterAndSortAccounts(accRes.getBody()));
model.addAttribute("fromAccounts", fromAccounts);
model.addAttribute("toAccounts", toAccounts);
model.addAttribute("form", form);
model.addAttribute("errorMessage", responseReason.name());
ControllerUtils.addVersionAttribute(model, this.financerConfig);
@@ -79,7 +97,8 @@ public class TransactionController {
final ResponseEntity<String> response = new StringTemplate().exchange(builder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
final ResponseEntity<Account> accountResponse = new GetAccountByKeyTemplate().exchange(this.financerConfig, accountKey);
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());

View File

@@ -0,0 +1,65 @@
package de.financer.decorator;
import de.financer.model.*;
public class AccountDecorator {
private final Account account;
public Long getId() {
return account.getId();
}
public String getKey() {
return account.getKey();
}
public AccountType getType() {
return account.getType();
}
public AccountStatus getStatus() {
return account.getStatus();
}
public Long getCurrentBalance() {
return account.getCurrentBalance();
}
public AccountGroup getAccountGroup() {
return account.getAccountGroup();
}
public AccountDecorator(Account account) {
this.account = account;
}
public Long getSpendingCurrentExpensePeriod() {
if (this.account.getType() == AccountType.EXPENSE || this.account.getType() == AccountType.LIABILITY) {
return this.account.getAccountStatistics().stream()
.filter((as) -> as.getPeriod().getType().equals(PeriodType.EXPENSE) && as.getPeriod()
.getEnd() == null)
.findFirst()
.orElseGet(() -> {
AccountStatistic as = new AccountStatistic();
as.setTransactionCountTo(0l);
return as;
})
.getSpendingTotalTo();
}
return null;
}
public Long getAverageSpendingExpensePeriod() {
if (this.account.getType() == AccountType.EXPENSE || this.account.getType() == AccountType.LIABILITY) {
return Math.round(this.account.getAccountStatistics().stream()
.filter((as) -> as.getPeriod().getType().equals(PeriodType.EXPENSE) && as.getPeriod()
.getEnd() != null)
.mapToLong((as) -> as.getSpendingTotalTo())
.average()
.orElseGet(() -> Double.valueOf(0d)));
}
return null;
}
}

View File

@@ -0,0 +1,20 @@
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.dto.AccountExpense;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;
public class GetAccountExpensesCurrentExpensePeriodTemplate {
public ResponseEntity<Iterable<AccountExpense>> exchange(FinancerConfig financerConfig) {
final UriComponentsBuilder expensesBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.ACC_GET_ACC_EXPENSES_CURRENT_EXPENSE_PERIOD));
return new FinancerRestTemplate<Iterable<AccountExpense>>()
.exchange(expensesBuilder.toUriString(), new ParameterizedTypeReference<Iterable<AccountExpense>>() {
});
}
}

View File

@@ -0,0 +1,20 @@
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.dto.AccountGroupExpense;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;
public class GetAccountGroupExpensesCurrentExpensePeriodTemplate {
public ResponseEntity<Iterable<AccountGroupExpense>> exchange(FinancerConfig financerConfig) {
final UriComponentsBuilder expensesBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.ACC_GP_GET_ACC_GP_EXPENSES_CURRENT_EXPENSE_PERIOD));
return new FinancerRestTemplate<Iterable<AccountGroupExpense>>()
.exchange(expensesBuilder.toUriString(), new ParameterizedTypeReference<Iterable<AccountGroupExpense>>() {
});
}
}

View File

@@ -0,0 +1,20 @@
package de.financer.template;
import de.financer.config.FinancerConfig;
import de.financer.controller.Function;
import de.financer.model.Period;
import de.financer.util.ControllerUtils;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.UriComponentsBuilder;
public class GetCurrentExpensePeriodTemplate {
public ResponseEntity<Period> exchange(FinancerConfig financerConfig) {
final UriComponentsBuilder expensesBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.P_GET_CURRENT_EXPENSE_PERIOD));
return new FinancerRestTemplate<Period>()
.exchange(expensesBuilder.toUriString(), new ParameterizedTypeReference<Period>() {
});
}
}

View File

@@ -12,7 +12,6 @@ public class GetExpensePeriodTotalsTemplate {
public ResponseEntity<Iterable<ExpensePeriodTotal>> exchange(FinancerConfig financerConfig, int year) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_EXPENSE_PERIOD_TOTALS))
.queryParam("monthPeriodStartDay", financerConfig.getMonthPeriodStartDay())
.queryParam("year", year);
return new FinancerRestTemplate<Iterable<ExpensePeriodTotal>>()

View File

@@ -10,8 +10,7 @@ import org.springframework.web.util.UriComponentsBuilder;
public class GetExpensesCurrentPeriodTemplate {
public ResponseEntity<Long> exchange(FinancerConfig financerConfig) {
final UriComponentsBuilder transactionBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_EXPENSES_CURRENT_PERIOD))
.queryParam("monthPeriodStartDay", financerConfig.getMonthPeriodStartDay());
.fromHttpUrl(ControllerUtils.buildUrl(financerConfig, Function.TR_EXPENSES_CURRENT_PERIOD));
return new FinancerRestTemplate<Long>()
.exchange(transactionBuilder.toUriString(), new ParameterizedTypeReference<Long>() {

View File

@@ -44,6 +44,10 @@ public class ControllerUtils {
}
public static String formatDate(FinancerConfig financerConfig, LocalDate date) {
if (date == null) {
return null;
}
return date.format(DateTimeFormatter.ofPattern(financerConfig.getDateFormat()));
}

View File

@@ -8,6 +8,7 @@ financer.account-overview.available-actions.create-recurring-transaction=Create
financer.account-overview.available-actions.recurring-transaction-all=Show all recurring transactions
financer.account-overview.available-actions.create-account-group=Create new account group
financer.account-overview.available-actions.select-chart=Generate a chart
financer.account-overview.available-actions.close-current-period=Close the current expense period
financer.account-overview.status=Status\:
financer.account-overview.status.recurring-transaction-due-today=Recurring transactions due today\:
financer.account-overview.status.recurring-transaction-active=Active recurring transactions\:
@@ -16,10 +17,12 @@ financer.account-overview.status.current-expenses=Expenses in the current period
financer.account-overview.table-header.id=ID
financer.account-overview.table-header.key=Account
financer.account-overview.table-header.group=Group
financer.account-overview.table-header.balance=Current Balance
financer.account-overview.table-header.balance=Balance
financer.account-overview.table-header.spending-current-period=Spending current period
financer.account-overview.table-header.average-spending-period=Average spending
financer.account-overview.table-header.type=Type
financer.account-overview.table-header.status=Status
financer.account-overview.tooltip.status.current-expenses=Period from {0} to {1}. Clicking the amount will open a graphical overview about the expenses grouped by account group
financer.account-overview.tooltip.status.current-expenses=Period starting at {0}. Clicking the amount will open a graphical overview about the expenses grouped by account group
financer.account-overview.tooltip.status.current-assets=Assets available at short-notice
financer.account-new.title=financer\: create new account
@@ -88,6 +91,7 @@ financer.account-details.available-actions=Available actions\:
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
@@ -163,6 +167,7 @@ financer.chart.account-expenses-for-period.title=Expenses for period from {0} to
financer.chart.expense-period-totals-current-year.title=Expense period totals for the current year
financer.chart.expense-period-totals-current-year.x=Amount
financer.chart.expense-period-totals-current-year.y=Period
financer.chart.expense-period-totals-current-year.open-period=OPEN
financer.chart.name.ACCOUNT_GROUP_EXPENSES_CURRENT_PERIOD=Expenses of the current period grouped by account group (pie chart)
financer.chart.name.ACCOUNT_GROUP_EXPENSES_FOR_PERIOD=Expenses for a configurable period grouped by account group (pie chart)

View File

@@ -8,6 +8,7 @@ financer.account-overview.available-actions.create-recurring-transaction=Neue wi
financer.account-overview.available-actions.recurring-transaction-all=Zeige alle wiederkehrende Buchungen
financer.account-overview.available-actions.create-account-group=Neue Konto-Gruppe erstellen
financer.account-overview.available-actions.select-chart=Ein Diagramm erzeugen
financer.account-overview.available-actions.close-current-period=Aktuelle Periode schlie\u00DFen
financer.account-overview.status=Status\:
financer.account-overview.status.recurring-transaction-due-today=Wiederkehrende Buchungen heute f\u00E4llig\:
financer.account-overview.status.recurring-transaction-active=Aktive wiederkehrende Buchungen\:
@@ -19,7 +20,7 @@ financer.account-overview.table-header.group=Gruppe
financer.account-overview.table-header.balance=Kontostand
financer.account-overview.table-header.type=Typ
financer.account-overview.table-header.status=Status
financer.account-overview.tooltip.status.current-expenses=Periode ab {0} bis {1}. Durch Klicken des Betrags kann eine grafische \u00DCbersicht über die Ausgaben gruppiert nach Konto-Gruppe angezeigt werden
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
financer.account-overview.tooltip.status.current-assets=Kurzfristig verf\u00FCgbares Verm\u00F6gen
financer.account-new.title=financer\: Neues Konto erstellen
@@ -88,6 +89,7 @@ financer.account-details.available-actions=Verf\u00FCgbare Aktionen\:
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
@@ -162,6 +164,7 @@ financer.chart.account-expenses-for-period.title=Ausgaben in der Periode vom {0}
financer.chart.expense-period-totals-current-year.title=Gesamtbetr\u00E4ge gruppiert nach Periode für das aktuelle Jahr
financer.chart.expense-period-totals-current-year.x=Betrag
financer.chart.expense-period-totals-current-year.y=Periode
financer.chart.expense-period-totals-current-year.open-period=OFFEN
financer.chart.name.ACCOUNT_GROUP_EXPENSES_CURRENT_PERIOD=Ausgaben in der aktuellen Periode gruppiert nach Konto-Gruppe (Kuchendiagramm)
financer.chart.name.ACCOUNT_GROUP_EXPENSES_FOR_PERIOD=Ausgaben f\u00FCr eine konfigurierbare Periode gruppiert nach Konto-Gruppe (Kuchendiagramm)

View File

@@ -1,3 +1,15 @@
v18 -> v19:
- Fix a bug in recurring transaction handling that caused a recurring transaction with Holiday/Weekend type
'previous workday' and a planned occurrence on a Sunday to be due on Friday and Saturday
- Filter from and to account lists when creating a transaction or a recurring transaction by account types that
are allowed in from and to
- Add the create new transaction action to the account detail view
- Rework periods (e.g. expense) so that the underlying technical structures are better defined. This is preparation for
more reports and budgeting functions
- Add action to close the current expense period manually. This also opens a new one
- Add spending in current expense period per account to the account overview
- Add average spending in expense period per account to the account overview
v17 -> v18:
- Add readme to the footer
- Translate error messages to German

View File

@@ -18,9 +18,9 @@
<span th:text="#{financer.account-details.details.balance}"/>
<span th:text="${#numbers.formatDecimal(account.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
</div>
<div id="group-container">
<div id="group-container" th:if="${account.accountGroup != null}">
<span th:text="#{financer.account-details.details.group}"/>
<span th:text="${account.accountGroup?.name}"/>
<span th:text="${account.accountGroup.name}"/>
</div>
</div>
<div id="account-details-action-container">
@@ -29,6 +29,8 @@
th:text="#{financer.account-details.available-actions.close-account}"/>
<a th:if="${isClosed}" th:href="@{/openAccount(key=${account.key})}"
th:text="#{financer.account-details.available-actions.open-account}"/>
<a th:href="@{/newTransaction}"
th:text="#{financer.account-details.available-actions.create-transaction}"/>
<a th:href="@{/accountOverview}"
th:text="#{financer.account-details.available-actions.back-to-overview}"/>
</div>

View File

@@ -8,13 +8,14 @@
</head>
<body>
<h1 th:text="#{financer.heading.account-overview}" />
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<div id="status-container">
<span th:text="#{financer.account-overview.status}"/>
<div th:title="#{financer.account-overview.tooltip.status.current-assets}">
<span th:text="#{financer.account-overview.status.current-assets}"/>
<span th:text="${#numbers.formatDecimal(currentAssets/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
</div>
<div th:title="#{'financer.account-overview.tooltip.status.current-expenses'(${#temporals.format(periodStart)}, ${#temporals.format(periodEnd)})}">
<div th:title="#{'financer.account-overview.tooltip.status.current-expenses'(${#temporals.format(periodStart)})}">
<span th:text="#{financer.account-overview.status.current-expenses}"/>
<a th:href="@{/getAccountGroupExpensesCurrentPeriod}">
<span th:text="${#numbers.formatDecimal(currentExpenses/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}" />
@@ -28,7 +29,6 @@
<span th:text="#{financer.account-overview.status.recurring-transaction-active}"/>
<a th:href="@{/recurringTransactionActive}" th:text="${rtAllActiveCount}"/>
</div>
</div>
<span th:text="#{financer.account-overview.available-actions}"/>
<div id="action-container">
@@ -49,6 +49,10 @@
<a th:href="@{/recurringTransactionAll}"
th:text="#{financer.account-overview.available-actions.recurring-transaction-all}"/>
</div>
<div id="action-container-sub-period">
<a th:href="@{/closePeriod}"
th:text="#{financer.account-overview.available-actions.close-current-period}"/>
</div>
<div id="action-container-sub-reports">
<a th:href="@{/selectChart}"
th:text="#{financer.account-overview.available-actions.select-chart}"/>
@@ -59,6 +63,8 @@
<th class="hideable-column" th:text="#{financer.account-overview.table-header.id}"/>
<th th:text="#{financer.account-overview.table-header.key}"/>
<th th:text="#{financer.account-overview.table-header.balance}"/>
<th class="hideable-column" th:text="#{financer.account-overview.table-header.spending-current-period}"/>
<th class="hideable-column" th:text="#{financer.account-overview.table-header.average-spending-period}"/>
<th class="hideable-column" th:text="#{financer.account-overview.table-header.group}"/>
<th class="hideable-column" th:text="#{financer.account-overview.table-header.type}"/>
<th class="hideable-column" th:text="#{financer.account-overview.table-header.status}"/>
@@ -69,6 +75,12 @@
<a th:href="@{/accountDetails(key=${acc.key})}" th:text="${acc.key}"/>
</td>
<td th:text="${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<td th:if="${acc.spendingCurrentExpensePeriod != null}"
th:text="${#numbers.formatDecimal(acc.spendingCurrentExpensePeriod/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<td th:if="${acc.averageSpendingExpensePeriod != null}"
th:text="${#numbers.formatDecimal(acc.averageSpendingExpensePeriod/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
<td th:if="${acc.spendingCurrentExpensePeriod == null}">-</td>
<td th:if="${acc.averageSpendingExpensePeriod == null}">-</td>
<td class="hideable-column" th:text="${acc.accountGroup?.name}"/>
<td class="hideable-column" th:text="#{'financer.account-type.' + ${acc.type}}"/>
<td class="hideable-column" th:text="#{'financer.account-status.' + ${acc.status}}"/>

View File

@@ -14,12 +14,12 @@
method="post">
<label for="selectFromAccount" th:text="#{financer.recurring-transaction-new.label.from-account}"/>
<select id="selectFromAccount" th:field="*{fromAccountKey}">
<option th:each="acc : ${accounts}" th:value="${acc.key}"
<option th:each="acc : ${fromAccounts}" th:value="${acc.key}"
th:text="#{'financer.recurring-transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')},${currencySymbol})}"/>
</select>
<label for="selectToAccount" th:text="#{financer.recurring-transaction-new.label.to-account}"/>
<select id="selectToAccount" th:field="*{toAccountKey}">
<option th:each="acc : ${accounts}" th:value="${acc.key}"
<option th:each="acc : ${toAccounts}" th:value="${acc.key}"
th:text="#{'financer.recurring-transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')},${currencySymbol})}"/>
</select>
<label for="inputAmount" th:text="#{financer.recurring-transaction-new.label.amount}"/>

View File

@@ -14,12 +14,12 @@
method="post">
<label for="selectFromAccount" th:text="#{financer.transaction-new.label.from-account}"/>
<select id="selectFromAccount" th:field="*{fromAccountKey}">
<option th:each="acc : ${accounts}" th:value="${acc.key}"
<option th:each="acc : ${fromAccounts}" th:value="${acc.key}"
th:text="#{'financer.transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')},${currencySymbol})}"/>
</select>
<label for="selectToAccount" th:text="#{financer.transaction-new.label.to-account}"/>
<select id="selectToAccount" th:field="*{toAccountKey}">
<option th:each="acc : ${accounts}" th:value="${acc.key}"
<option th:each="acc : ${toAccounts}" th:value="${acc.key}"
th:text="#{'financer.transaction-new.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')},${currencySymbol})}"/>
</select>
<label for="inputAmount" th:text="#{financer.transaction-new.label.amount}"/>

11
pom.xml
View File

@@ -35,6 +35,17 @@
<module>financer-common</module>
</modules>
<distributionManagement>
<snapshotRepository>
<id>77zzcx7-snapshots</id>
<url>http://192.168.10.1:8100/repository/77zzcx7-snapshots/</url>
</snapshotRepository>
<repository>
<id>77zzcx7-releases</id>
<url>http://192.168.10.1:8100/repository/77zzcx7-releases/</url>
</repository>
</distributionManagement>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>