diff --git a/financer-common/src/main/java/de/financer/model/PeriodType.java b/financer-common/src/main/java/de/financer/model/PeriodType.java index 5f4f24d..7653dd6 100644 --- a/financer-common/src/main/java/de/financer/model/PeriodType.java +++ b/financer-common/src/main/java/de/financer/model/PeriodType.java @@ -6,7 +6,19 @@ import java.util.stream.Collectors; public enum PeriodType { /** User defined start and end */ - EXPENSE; + EXPENSE, + + /** + * Like {@link PeriodType#EXPENSE} but for a whole year - this may not be a calendar year, + * if e.g. the EXPENSE periods do not start at the first of every month. + * This includes all periods that start the in the year. + */ + EXPENSE_YEAR, + + /** + * Denotes a continuous, cumulative period, starting with the inception of the financer app, without an end. + */ + GRAND_TOTAL; /** * This method validates whether the given string represents a valid period type. diff --git a/financer-server/src/main/java/database/common/V26_0_0__createGrandTotalPeriod.java b/financer-server/src/main/java/database/common/V26_0_0__createGrandTotalPeriod.java new file mode 100644 index 0000000..dfeab37 --- /dev/null +++ b/financer-server/src/main/java/database/common/V26_0_0__createGrandTotalPeriod.java @@ -0,0 +1,177 @@ +package database.common; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Date; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class V26_0_0__createGrandTotalPeriod extends BaseJavaMigration { + private static class TransactionAccountsContainer { + public Long transactionAmount; + public Long fromAccountId; + public Long toAccountId; + public Long id; + + public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) { + this.transactionAmount = transactionAmount; + this.fromAccountId = fromAccountId; + this.toAccountId = toAccountId; + this.id = id; + } + } + + private static final String GET_CONTAINERS_SQL = "SELECT t.amount, t.from_account_id, t.to_account_id, t.id FROM \"transaction\" t"; + + private static final String ACCOUNT_STATISTIC_EXISTS_SQL = "SELECT id FROM account_statistic WHERE account_id = ? AND period_id = ?"; + + private static final String ACCOUNT_STATISTIC_UPDATE_FROM_SQL = "UPDATE account_statistic SET spending_total_from = spending_total_from + ?, transaction_count_from = transaction_count_from + 1 WHERE id = ?"; + private static final String ACCOUNT_STATISTIC_UPDATE_TO_SQL = "UPDATE account_statistic SET spending_total_to = spending_total_to + ?, transaction_count_to = transaction_count_to + 1 WHERE id = ?"; + + private static final String ACCOUNT_STATISTIC_INSERT_FROM_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, ?, 1, 0, 0)"; + private static final String ACCOUNT_STATISTIC_INSERT_TO_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, 0, 0, ?, 1)"; + + private static final String SELECT_DATE_OLDEST_TRANSACTION_SQL = "SELECT \"date\" FROM \"transaction\" ORDER BY \"date\" ASC"; + + private static final String INSERT_GRAND_TOTAL_PERIOD_SQL = "INSERT INTO period (type, start) VALUES ('GRAND_TOTAL', ?)"; + private static final String SELECT_GRAND_TOTAL_PERIOD_SQL = "SELECT id FROM period WHERE type = 'GRAND_TOTAL'"; + + private static final String INSERT_LINK_TRANSACTION_PERIOD_SQL = "INSERT INTO link_transaction_period (transaction_id, period_id) VALUES (?, ?)"; + + @Override + public void migrate(Context context) throws Exception { + final long grandTotalPeriodId = createGrandTotalPeriod(context); + final List cons = getContainers(context); + + for (TransactionAccountsContainer con : cons) { + handleAccount(con, context, grandTotalPeriodId, true); + handleAccount(con, context, grandTotalPeriodId, false); + + handleTransaction(con, context, grandTotalPeriodId); + } + } + + private long createGrandTotalPeriod(Context context) throws Exception { + // Check whether there are transactions from which we can derive the inception date + // If so, use the date of the oldest transaction to create the GRAND_TOTAL period + // If not, use the current date to create the GRAND_TOTAL period + + try (PreparedStatement statement = + context + .getConnection() + .prepareStatement(SELECT_DATE_OLDEST_TRANSACTION_SQL)) { + statement.setMaxRows(1); + + final ResultSet rs = statement.executeQuery(); + final Date inceptionDate; + + // The ResultSet contains only one entry as specified with the setMaxRows(1) call above + if (rs.next()) { + inceptionDate = rs.getDate(1); + } + else { + inceptionDate = new Date(System.currentTimeMillis()); + } + + try (PreparedStatement statementInsert = + context + .getConnection() + .prepareStatement(INSERT_GRAND_TOTAL_PERIOD_SQL)) { + statementInsert.setDate(1, inceptionDate); + + statementInsert.execute(); + + try (PreparedStatement statementSelect = + context + .getConnection() + .prepareStatement(SELECT_GRAND_TOTAL_PERIOD_SQL)) { + final ResultSet rsSelect = statementSelect.executeQuery(); + + // We know it's there as we just created it + rsSelect.next(); + + return rsSelect.getLong(1); + } + } + } + } + + private List getContainers(Context context) throws SQLException { + try (PreparedStatement statement = + context + .getConnection() + .prepareStatement(GET_CONTAINERS_SQL)) { + final ResultSet rs = statement.executeQuery(); + final List cons = new ArrayList<>(); + + while(rs.next()) { + cons.add(new TransactionAccountsContainer( + rs.getLong(1), // transaction amount + rs.getLong(2), // from account ID + rs.getLong(3), // to account ID + rs.getLong(4) // ID + )); + } + + return cons; + } + } + + private void handleAccount(TransactionAccountsContainer con, Context context, long grandTotalPeriodId, boolean from) throws SQLException { + try (PreparedStatement statement = + context + .getConnection() + .prepareStatement(ACCOUNT_STATISTIC_EXISTS_SQL)) { + statement.setLong(1, from ? con.fromAccountId : con.toAccountId); + statement.setLong(2, grandTotalPeriodId); + + final ResultSet rs = statement.executeQuery(); + boolean found = false; + + // Only one possible entry because of the unique constraint + while(rs.next()) { + found = true; + + try (PreparedStatement statementUpdate = + context + .getConnection() + .prepareStatement(from ? ACCOUNT_STATISTIC_UPDATE_FROM_SQL : ACCOUNT_STATISTIC_UPDATE_TO_SQL)) { + statementUpdate.setLong(1, con.transactionAmount); + statementUpdate.setLong(2, rs.getLong(1)); + + statementUpdate.executeUpdate(); + } + } + + // We need to create a new account_statistic entry for the tuple {account_id;period_id} + if (!found) { + try (PreparedStatement statementInsert = + context + .getConnection() + .prepareStatement(from ? ACCOUNT_STATISTIC_INSERT_FROM_SQL : ACCOUNT_STATISTIC_INSERT_TO_SQL)) { + statementInsert.setLong(1, from ? con.fromAccountId : con.toAccountId); + statementInsert.setLong(2, grandTotalPeriodId); + statementInsert.setLong(3, con.transactionAmount); + + statementInsert.execute(); + } + } + } + } + + private void handleTransaction(TransactionAccountsContainer con, Context context, long grandTotalPeriodId) throws SQLException { + try (PreparedStatement statementInsert = + context + .getConnection() + .prepareStatement(INSERT_LINK_TRANSACTION_PERIOD_SQL)) { + statementInsert.setLong(1, con.id); + statementInsert.setLong(2, grandTotalPeriodId); + + statementInsert.execute(); + } + } +} \ No newline at end of file diff --git a/financer-server/src/main/java/database/common/V26_0_1__createExpenseYearPeriod_2019.java b/financer-server/src/main/java/database/common/V26_0_1__createExpenseYearPeriod_2019.java new file mode 100644 index 0000000..3f8eaab --- /dev/null +++ b/financer-server/src/main/java/database/common/V26_0_1__createExpenseYearPeriod_2019.java @@ -0,0 +1,200 @@ +package database.common; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; + +import java.sql.*; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; + +public class V26_0_1__createExpenseYearPeriod_2019 extends BaseJavaMigration { + + private static class TransactionAccountsContainer { + + + public Long transactionAmount; + public Long fromAccountId; + public Long toAccountId; + public Long id; + public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) { + this.transactionAmount = transactionAmount; + this.fromAccountId = fromAccountId; + this.toAccountId = toAccountId; + this.id = id; + } + + } + + private static final String GET_CONTAINERS_SQL = "SELECT t.amount, t.from_account_id, t.to_account_id, t.id FROM \"transaction\" t INNER JOIN link_transaction_period ltp ON ltp.transaction_id = t.id INNER JOIN period p ON p.id = ltp.period_id WHERE p.type = 'EXPENSE' AND t.\"date\" >= ? AND t.\"date\" < ?"; + + private static final String ACCOUNT_STATISTIC_EXISTS_SQL = "SELECT id FROM account_statistic WHERE account_id = ? AND period_id = ?"; + + private static final String ACCOUNT_STATISTIC_UPDATE_FROM_SQL = "UPDATE account_statistic SET spending_total_from = spending_total_from + ?, transaction_count_from = transaction_count_from + 1 WHERE id = ?"; + private static final String ACCOUNT_STATISTIC_UPDATE_TO_SQL = "UPDATE account_statistic SET spending_total_to = spending_total_to + ?, transaction_count_to = transaction_count_to + 1 WHERE id = ?"; + + private static final String ACCOUNT_STATISTIC_INSERT_FROM_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, ?, 1, 0, 0)"; + private static final String ACCOUNT_STATISTIC_INSERT_TO_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, 0, 0, ?, 1)"; + + private static final String SELECT_FIRST_PERIOD_SQL = "SELECT start FROM period WHERE type = 'EXPENSE' AND start BETWEEN '2019-01-01' AND '2019-12-31' ORDER BY start ASC"; + private static final String SELECT_LAST_PERIOD_SQL = "SELECT \"end\" FROM period WHERE type = 'EXPENSE' AND start BETWEEN '2019-01-01' AND '2019-12-31' ORDER BY start DESC"; + + private static final String INSERT_EXPENSE_YEAR_PERIOD_SQL = "INSERT INTO period (type, start, \"end\") VALUES ('EXPENSE_YEAR', ?, ?)"; + private static final String SELECT_EXPENSE_YEAR_PERIOD_SQL = "SELECT id FROM period WHERE type = 'EXPENSE_YEAR'"; + + private static final String INSERT_LINK_TRANSACTION_PERIOD_SQL = "INSERT INTO link_transaction_period (transaction_id, period_id) VALUES (?, ?)"; + + private Timestamp firstPeriodStart; + private Timestamp lastPeriodEnd; + + @Override + public void migrate(Context context) throws Exception { + final long expenseYearPeriodId = createExpenseYearPeriod(context); + + if (expenseYearPeriodId == -1) { + return; + } + + final List cons = getContainers(context); + + for (TransactionAccountsContainer con : cons) { + handleAccount(con, context, expenseYearPeriodId, true); + handleAccount(con, context, expenseYearPeriodId, false); + + handleTransaction(con, context, expenseYearPeriodId); + } + } + + private long createExpenseYearPeriod(Context context) throws Exception { + try (PreparedStatement statement = + context + .getConnection() + .prepareStatement(SELECT_FIRST_PERIOD_SQL)) { + statement.setMaxRows(1); + + final ResultSet rs = statement.executeQuery(); + + // The ResultSet contains only one entry as specified with the setMaxRows(1) call above + if (!rs.next()) { + return -1; // it may happen that no period for 2019 exists + } + + firstPeriodStart = rs.getTimestamp(1); + + try (PreparedStatement statementLast = + context + .getConnection() + .prepareStatement(SELECT_LAST_PERIOD_SQL)) { + statementLast.setMaxRows(1); + + final ResultSet rsLast = statementLast.executeQuery(); + + // The ResultSet contains only one entry as specified with the setMaxRows(1) call above + rsLast.next(); + + lastPeriodEnd = rsLast.getTimestamp(1); + + try (PreparedStatement statementInsert = + context + .getConnection() + .prepareStatement(INSERT_EXPENSE_YEAR_PERIOD_SQL)) { + statementInsert.setTimestamp(1, firstPeriodStart); + statementInsert.setTimestamp(2, lastPeriodEnd); + + statementInsert.execute(); + + try (PreparedStatement statementSelect = + context + .getConnection() + .prepareStatement(SELECT_EXPENSE_YEAR_PERIOD_SQL)) { + final ResultSet rsSelect = statementSelect.executeQuery(); + + // We know it's there as we just created it + rsSelect.next(); + + return rsSelect.getLong(1); + } + } + } + } + } + + private List getContainers(Context context) throws SQLException { + try (PreparedStatement statement = + context + .getConnection() + .prepareStatement(GET_CONTAINERS_SQL)) { + statement.setDate(1, new Date(firstPeriodStart.toLocalDateTime().toInstant(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS).toEpochMilli())); + statement.setDate(2, new Date(lastPeriodEnd.toLocalDateTime().toInstant(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS).toEpochMilli())); + + final ResultSet rs = statement.executeQuery(); + final List cons = new ArrayList<>(); + + while(rs.next()) { + cons.add(new TransactionAccountsContainer( + rs.getLong(1), // transaction amount + rs.getLong(2), // from account ID + rs.getLong(3), // to account ID + rs.getLong(4) // ID + )); + } + + return cons; + } + } + + private void handleAccount(TransactionAccountsContainer con, Context context, long expenseYearPeriodId, boolean from) throws SQLException { + try (PreparedStatement statement = + context + .getConnection() + .prepareStatement(ACCOUNT_STATISTIC_EXISTS_SQL)) { + statement.setLong(1, from ? con.fromAccountId : con.toAccountId); + statement.setLong(2, expenseYearPeriodId); + + final ResultSet rs = statement.executeQuery(); + boolean found = false; + + // Only one possible entry because of the unique constraint + while(rs.next()) { + found = true; + + try (PreparedStatement statementUpdate = + context + .getConnection() + .prepareStatement(from ? ACCOUNT_STATISTIC_UPDATE_FROM_SQL : ACCOUNT_STATISTIC_UPDATE_TO_SQL)) { + statementUpdate.setLong(1, con.transactionAmount); + statementUpdate.setLong(2, rs.getLong(1)); + + statementUpdate.executeUpdate(); + } + } + + // We need to create a new account_statistic entry for the tuple {account_id;period_id} + if (!found) { + try (PreparedStatement statementInsert = + context + .getConnection() + .prepareStatement(from ? ACCOUNT_STATISTIC_INSERT_FROM_SQL : ACCOUNT_STATISTIC_INSERT_TO_SQL)) { + statementInsert.setLong(1, from ? con.fromAccountId : con.toAccountId); + statementInsert.setLong(2, expenseYearPeriodId); + statementInsert.setLong(3, con.transactionAmount); + + statementInsert.execute(); + } + } + } + } + + private void handleTransaction(TransactionAccountsContainer con, Context context, long expenseYearPeriodId) throws SQLException { + try (PreparedStatement statementInsert = + context + .getConnection() + .prepareStatement(INSERT_LINK_TRANSACTION_PERIOD_SQL)) { + statementInsert.setLong(1, con.id); + statementInsert.setLong(2, expenseYearPeriodId); + + statementInsert.execute(); + } + } +} \ No newline at end of file diff --git a/financer-server/src/main/java/database/common/V26_0_2__createExpenseYearPeriod_2020.java b/financer-server/src/main/java/database/common/V26_0_2__createExpenseYearPeriod_2020.java new file mode 100644 index 0000000..ea641e7 --- /dev/null +++ b/financer-server/src/main/java/database/common/V26_0_2__createExpenseYearPeriod_2020.java @@ -0,0 +1,183 @@ +package database.common; + +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; + +import java.sql.*; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalField; +import java.util.ArrayList; +import java.util.List; + +public class V26_0_2__createExpenseYearPeriod_2020 extends BaseJavaMigration { + private static class TransactionAccountsContainer { + + + public Long transactionAmount; + public Long fromAccountId; + public Long toAccountId; + public Long id; + public TransactionAccountsContainer(Long transactionAmount, Long fromAccountId, Long toAccountId, Long id) { + this.transactionAmount = transactionAmount; + this.fromAccountId = fromAccountId; + this.toAccountId = toAccountId; + this.id = id; + } + + } + + private static final String GET_CONTAINERS_SQL = "SELECT t.amount, t.from_account_id, t.to_account_id, t.id FROM \"transaction\" t INNER JOIN link_transaction_period ltp ON ltp.transaction_id = t.id INNER JOIN period p ON p.id = ltp.period_id WHERE p.type = 'EXPENSE' AND t.\"date\" >= ?"; + + private static final String ACCOUNT_STATISTIC_EXISTS_SQL = "SELECT id FROM account_statistic WHERE account_id = ? AND period_id = ?"; + + private static final String ACCOUNT_STATISTIC_UPDATE_FROM_SQL = "UPDATE account_statistic SET spending_total_from = spending_total_from + ?, transaction_count_from = transaction_count_from + 1 WHERE id = ?"; + private static final String ACCOUNT_STATISTIC_UPDATE_TO_SQL = "UPDATE account_statistic SET spending_total_to = spending_total_to + ?, transaction_count_to = transaction_count_to + 1 WHERE id = ?"; + + private static final String ACCOUNT_STATISTIC_INSERT_FROM_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, ?, 1, 0, 0)"; + private static final String ACCOUNT_STATISTIC_INSERT_TO_SQL = "INSERT INTO account_statistic(account_id, period_id, spending_total_from, transaction_count_from, spending_total_to, transaction_count_to) VALUES(?, ?, 0, 0, ?, 1)"; + + private static final String SELECT_FIRST_PERIOD_SQL = "SELECT start FROM period WHERE type = 'EXPENSE' AND start > '2020-01-01' ORDER BY start ASC"; + + private static final String INSERT_EXPENSE_YEAR_PERIOD_SQL = "INSERT INTO period (type, start) VALUES ('EXPENSE_YEAR', ?)"; + private static final String SELECT_EXPENSE_YEAR_PERIOD_SQL = "SELECT id FROM period WHERE type = 'EXPENSE_YEAR' AND start BETWEEN '2020-01-01' AND '2020-12-31'"; + + private static final String INSERT_LINK_TRANSACTION_PERIOD_SQL = "INSERT INTO link_transaction_period (transaction_id, period_id) VALUES (?, ?)"; + + private Timestamp firstPeriodStart; + + @Override + public void migrate(Context context) throws Exception { + final long expenseYearPeriodId = createExpenseYearPeriod(context); + + if (expenseYearPeriodId == -1) { + return; + } + + final List cons = getContainers(context); + + for (TransactionAccountsContainer con : cons) { + handleAccount(con, context, expenseYearPeriodId, true); + handleAccount(con, context, expenseYearPeriodId, false); + + handleTransaction(con, context, expenseYearPeriodId); + } + } + + private long createExpenseYearPeriod(Context context) throws Exception { + try (PreparedStatement statement = + context + .getConnection() + .prepareStatement(SELECT_FIRST_PERIOD_SQL)) { + statement.setMaxRows(1); + + final ResultSet rs = statement.executeQuery(); + + // The ResultSet contains only one entry as specified with the setMaxRows(1) call above + if (!rs.next()) { + return -1; // it may happen that no period for 2020 exists + } + + firstPeriodStart = rs.getTimestamp(1); + + try (PreparedStatement statementInsert = + context + .getConnection() + .prepareStatement(INSERT_EXPENSE_YEAR_PERIOD_SQL)) { + statementInsert.setTimestamp(1, firstPeriodStart); + + statementInsert.execute(); + + try (PreparedStatement statementSelect = + context + .getConnection() + .prepareStatement(SELECT_EXPENSE_YEAR_PERIOD_SQL)) { + final ResultSet rsSelect = statementSelect.executeQuery(); + + // We know it's there as we just created it + rsSelect.next(); + + return rsSelect.getLong(1); + } + } + } + } + + private List getContainers(Context context) throws SQLException { + try (PreparedStatement statement = + context + .getConnection() + .prepareStatement(GET_CONTAINERS_SQL)) { + + statement.setDate(1, new Date(firstPeriodStart.toLocalDateTime().toInstant(ZoneOffset.UTC).truncatedTo(ChronoUnit.DAYS).toEpochMilli())); + + final ResultSet rs = statement.executeQuery(); + final List cons = new ArrayList<>(); + + while(rs.next()) { + cons.add(new TransactionAccountsContainer( + rs.getLong(1), // transaction amount + rs.getLong(2), // from account ID + rs.getLong(3), // to account ID + rs.getLong(4) // ID + )); + } + + return cons; + } + } + + private void handleAccount(TransactionAccountsContainer con, Context context, long expenseYearPeriodId, boolean from) throws SQLException { + try (PreparedStatement statement = + context + .getConnection() + .prepareStatement(ACCOUNT_STATISTIC_EXISTS_SQL)) { + statement.setLong(1, from ? con.fromAccountId : con.toAccountId); + statement.setLong(2, expenseYearPeriodId); + + final ResultSet rs = statement.executeQuery(); + boolean found = false; + + // Only one possible entry because of the unique constraint + while(rs.next()) { + found = true; + + try (PreparedStatement statementUpdate = + context + .getConnection() + .prepareStatement(from ? ACCOUNT_STATISTIC_UPDATE_FROM_SQL : ACCOUNT_STATISTIC_UPDATE_TO_SQL)) { + statementUpdate.setLong(1, con.transactionAmount); + statementUpdate.setLong(2, rs.getLong(1)); + + statementUpdate.executeUpdate(); + } + } + + // We need to create a new account_statistic entry for the tuple {account_id;period_id} + if (!found) { + try (PreparedStatement statementInsert = + context + .getConnection() + .prepareStatement(from ? ACCOUNT_STATISTIC_INSERT_FROM_SQL : ACCOUNT_STATISTIC_INSERT_TO_SQL)) { + statementInsert.setLong(1, from ? con.fromAccountId : con.toAccountId); + statementInsert.setLong(2, expenseYearPeriodId); + statementInsert.setLong(3, con.transactionAmount); + + statementInsert.execute(); + } + } + } + } + + private void handleTransaction(TransactionAccountsContainer con, Context context, long expenseYearPeriodId) throws SQLException { + try (PreparedStatement statementInsert = + context + .getConnection() + .prepareStatement(INSERT_LINK_TRANSACTION_PERIOD_SQL)) { + statementInsert.setLong(1, con.id); + statementInsert.setLong(2, expenseYearPeriodId); + + statementInsert.execute(); + } + } +} \ No newline at end of file diff --git a/financer-server/src/main/java/de/financer/dba/PeriodRepository.java b/financer-server/src/main/java/de/financer/dba/PeriodRepository.java index ac2a107..27deeef 100644 --- a/financer-server/src/main/java/de/financer/dba/PeriodRepository.java +++ b/financer-server/src/main/java/de/financer/dba/PeriodRepository.java @@ -7,11 +7,23 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; +import java.time.LocalDateTime; + @Transactional(propagation = Propagation.REQUIRED) public interface PeriodRepository extends CrudRepository { @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") + Period findGrandTotalPeriod(PeriodType type); + + @Query("SELECT p FROM Period p WHERE p.type = :type AND YEAR(p.start) = :year") + Period findExpenseYearPeriod(PeriodType type, Integer year); + @Query("SELECT p FROM Period p WHERE p.type = :type AND YEAR(p.start) = :year") Iterable getAllPeriodsForYear(PeriodType type, Integer year); + + @Query("SELECT p FROM Period p WHERE p.type = :type AND p.start <= :date AND p.end >= :date") + Period findPeriodForDate(PeriodType type, LocalDateTime date); } diff --git a/financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java b/financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java index 712bff2..07683be 100644 --- a/financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java +++ b/financer-server/src/main/java/de/financer/dba/RecurringTransactionRepository.java @@ -15,7 +15,7 @@ public interface RecurringTransactionRepository extends CrudRepository findRecurringTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount); @Query("SELECT rt FROM RecurringTransaction rt WHERE rt.deleted = false AND (rt.lastOccurrence IS NULL OR rt.lastOccurrence >= :lastOccurrence)") - Iterable findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate lastOccurrence); + Iterable findAllActive(LocalDate lastOccurrence); Iterable findByDeletedFalse(); } diff --git a/financer-server/src/main/java/de/financer/dba/TransactionRepository.java b/financer-server/src/main/java/de/financer/dba/TransactionRepository.java index e46644f..5097b2c 100644 --- a/financer-server/src/main/java/de/financer/dba/TransactionRepository.java +++ b/financer-server/src/main/java/de/financer/dba/TransactionRepository.java @@ -1,10 +1,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 de.financer.model.*; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.transaction.annotation.Propagation; @@ -15,13 +12,14 @@ import java.util.List; @Transactional(propagation = Propagation.REQUIRED) public interface TransactionRepository extends CrudRepository { + @Query("SELECT t FROM Transaction t WHERE t.toAccount = :toAccount OR t.fromAccount = :fromAccount") Iterable findTransactionsByFromAccountOrToAccount(Account fromAccount, Account toAccount); @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); - @Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a JOIN t.fromAccount f WHERE a.type IN :expenseTypes AND f.type != :startType GROUP BY p ORDER BY p.start ASC") - List getExpensesForAllPeriods(AccountType startType, AccountType... expenseTypes); + @Query("SELECT SUM(t.amount) FROM Transaction t JOIN t.periods p JOIN t.toAccount a JOIN t.fromAccount f WHERE a.type IN :expenseTypes AND f.type != :startType AND p.type = :type GROUP BY p ORDER BY p.start ASC") + List getExpensesForAllPeriods(PeriodType type, AccountType startType, 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 diff --git a/financer-server/src/main/java/de/financer/service/PeriodService.java b/financer-server/src/main/java/de/financer/service/PeriodService.java index e145264..e94818f 100644 --- a/financer-server/src/main/java/de/financer/service/PeriodService.java +++ b/financer-server/src/main/java/de/financer/service/PeriodService.java @@ -4,6 +4,7 @@ import de.financer.ResponseReason; import de.financer.dba.PeriodRepository; import de.financer.model.Period; import de.financer.model.PeriodType; +import de.financer.model.Transaction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.slf4j.Logger; @@ -11,7 +12,10 @@ import org.slf4j.LoggerFactory; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; @Service public class PeriodService { @@ -24,16 +28,24 @@ public class PeriodService { private AccountStatisticService accountStatisticService; /** - * @return the currently open expense period + * @return the currently open {@link PeriodType#EXPENSE expense} period */ public Period getCurrentExpensePeriod() { return this.periodRepository.findCurrentExpensePeriod(PeriodType.EXPENSE); } + /** + * @return the {@link PeriodType#GRAND_TOTAL grand total} period + */ + public Period getGrandTotalPeriod() { + return this.periodRepository.findGrandTotalPeriod(PeriodType.GRAND_TOTAL); + } + /** * This method closes the current expense period and opens a new one. * - * @return {@link ResponseReason#OK} if the operation succeeded, {@link ResponseReason#OK} + * @return {@link ResponseReason#OK} if the operation succeeded, {@link ResponseReason#UNKNOWN_ERROR} if an + * unexpected exception occurred. */ @Transactional(propagation = Propagation.REQUIRED) public ResponseReason closeCurrentExpensePeriod() { @@ -51,6 +63,8 @@ public class PeriodService { this.periodRepository.save(currentPeriod); this.periodRepository.save(nextPeriod); + handleExpenseYearPeriod(currentPeriod, nextPeriod); + this.accountStatisticService.generateNullStatisticsForUnusedAccounts(currentPeriod); response = ResponseReason.OK; @@ -60,10 +74,62 @@ public class PeriodService { response = ResponseReason.UNKNOWN_ERROR; } - return response; + return response; + } + + private void handleExpenseYearPeriod(Period currentPeriod, Period nextPeriod) { + final Period expenseYearPeriod = this.periodRepository + .findExpenseYearPeriod(PeriodType.EXPENSE_YEAR, currentPeriod.getStart().getYear()); + + if (expenseYearPeriod.getStart().getYear() != nextPeriod.getStart().getYear()) { + expenseYearPeriod.setEnd(currentPeriod.getEnd()); + + final Period nextExpenseYearPeriod = new Period(); + + nextExpenseYearPeriod.setType(PeriodType.EXPENSE_YEAR); + nextExpenseYearPeriod.setStart(nextPeriod.getStart()); + + this.periodRepository.save(expenseYearPeriod); + this.periodRepository.save(nextExpenseYearPeriod); + } + } + + /** + * @return the {@link PeriodType#EXPENSE_YEAR expense year} period for the given expense period + */ + public Period getExpenseYearPeriod(Period expensePeriod) { + return this.periodRepository + .findExpenseYearPeriod(PeriodType.EXPENSE_YEAR, expensePeriod.getStart().getYear()); } public Iterable getAllExpensePeriodsForYear(Integer year) { return this.periodRepository.getAllPeriodsForYear(PeriodType.EXPENSE, year); } + + /** + *

+ * This method gets the {@link Period} for the date of the given transaction. It is intended to handle the edge case + * of late booking a transaction after the corresponding period has already been closed. So most of the time + * it returns the {@link PeriodService#getCurrentExpensePeriod() current expense period}. + *

+ *

+ * It is important to note this method also returns the current expense period for transactions which have a date in + * the future. + *

+ * + * @param transaction to get the period for + * + * @return either the current expense period or one of the past + */ + public Period getExpensePeriodForTransaction(Transaction transaction) { + // Using LocalTime.now() here is kinda hacky. We know that we are only called from transaction creation. + Period period = this.periodRepository + .findPeriodForDate(PeriodType.EXPENSE, LocalDateTime.of(transaction.getDate(), LocalTime.now())); + + if (period == null) { + period = this.getCurrentExpensePeriod(); + } + + return period; + } } diff --git a/financer-server/src/main/java/de/financer/service/RecurringTransactionService.java b/financer-server/src/main/java/de/financer/service/RecurringTransactionService.java index 23d330c..e65bfe9 100644 --- a/financer-server/src/main/java/de/financer/service/RecurringTransactionService.java +++ b/financer-server/src/main/java/de/financer/service/RecurringTransactionService.java @@ -3,10 +3,7 @@ package de.financer.service; import de.financer.ResponseReason; import de.financer.config.FinancerConfig; import de.financer.dba.RecurringTransactionRepository; -import de.financer.model.Account; -import de.financer.model.HolidayWeekendType; -import de.financer.model.IntervalType; -import de.financer.model.RecurringTransaction; +import de.financer.model.*; import org.apache.commons.collections4.IterableUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; @@ -45,13 +42,15 @@ public class RecurringTransactionService { @Autowired private TransactionService transactionService; + @Autowired + private PeriodService periodService; + public Iterable getAll() { return this.recurringTransactionRepository.findByDeletedFalse(); } public Iterable getAllActive() { - return this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(LocalDate.now()); + return this.recurringTransactionRepository.findAllActive(LocalDate.now()); } public Iterable getAllForAccount(String accountKey) { @@ -85,7 +84,7 @@ public class RecurringTransactionService { // there would never be a reminder about them. On the actual due date the reminder is deferred because of the // HWT and for later runs it's not grabbed because of the condition '...LastOccurrenceGreaterThanEqual(now)' final Iterable allRecurringTransactions = this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(now.minusDays(7)); + .findAllActive(now.minusDays(7)); LOGGER.debug(String.format("Found %s candidate recurring transactions. Checking which are due", IterableUtils.size(allRecurringTransactions))); diff --git a/financer-server/src/main/java/de/financer/service/TransactionService.java b/financer-server/src/main/java/de/financer/service/TransactionService.java index 8fcaf10..6472cd3 100644 --- a/financer-server/src/main/java/de/financer/service/TransactionService.java +++ b/financer-server/src/main/java/de/financer/service/TransactionService.java @@ -17,9 +17,7 @@ import org.springframework.transaction.annotation.Transactional; 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; +import java.util.*; @Service public class TransactionService { @@ -92,7 +90,7 @@ public class TransactionService { try { final Transaction transaction = buildTransaction(fromAccount, toAccount, amount, description, date, recurringTransaction); - transaction.setPeriods(Collections.singleton(this.periodService.getCurrentExpensePeriod())); + transaction.setPeriods(getRelevantPeriods(transaction)); fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService .getMultiplierFromAccount(fromAccount) * amount)); @@ -124,6 +122,17 @@ public class TransactionService { return response; } + private Set getRelevantPeriods(Transaction transaction) { + final Set periods = new HashSet<>(); + final Period expensePeriod = this.periodService.getExpensePeriodForTransaction(transaction); + + periods.add(expensePeriod); + periods.add(this.periodService.getExpenseYearPeriod(expensePeriod)); + periods.add(this.periodService.getGrandTotalPeriod()); + + return periods; + } + /** * This method builds the actual transaction object with the given values. * @@ -252,13 +261,14 @@ public class TransactionService { } /** - * This method gets all sums of expenses of all periods, ordered from oldest to newest. + * This method gets all sums of expenses of all expense periods, ordered from oldest to newest. * * @return the sums of expenses - may be empty * @see TransactionService#getExpensesCurrentPeriod() */ public List getExpensesAllPeriods() { - return this.transactionRepository.getExpensesForAllPeriods(AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY); + return this.transactionRepository + .getExpensesForAllPeriods(PeriodType.EXPENSE, AccountType.START, AccountType.EXPENSE, AccountType.LIABILITY); } /** diff --git a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java index 0471644..daab99f 100644 --- a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java +++ b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest.java @@ -54,7 +54,7 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { // Arrange // Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false) Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-3))); final LocalDate now = LocalDate.now(); @@ -74,7 +74,7 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { // Arrange // Implicitly: ruleService.isWeekend().return(false) Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(0))); // Today is a holiday, but yesterday was not Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE); @@ -96,7 +96,7 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { // Arrange // Implicitly: ruleService.isHoliday().return(false) Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(0))); // Today is a weekend day, but yesterday was not Mockito.when(this.ruleService.isWeekend(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE); @@ -118,7 +118,7 @@ public class RecurringTransactionService_getAllDueToday_DAILY_NEXT_WORKDAYTest { // Arrange // Implicitly: ruleService.isHoliday().return(false) and ruleService.isWeekend().return(false) Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(1))); final LocalDate now = LocalDate.now(); diff --git a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java index 83733ba..6cbb980 100644 --- a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java +++ b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest.java @@ -44,7 +44,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest public void test_getAllDueToday_duePast_holiday() { // Arrange Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-1))); // Today is not a holiday but yesterday was Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE); @@ -82,7 +82,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); // The transaction occurs on a friday Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 2)))); // First False for the dueToday check, 2x True for actual weekend, second False for Friday Mockito.when(this.ruleService.isWeekend(Mockito.any())) @@ -116,7 +116,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); // The transaction occurs on a sunday Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue())))); // First False for the dueToday check, 2x True for actual weekend, second False for Friday Mockito.when(this.ruleService.isWeekend(Mockito.any())) @@ -143,7 +143,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest final LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); // The transaction occurs on a saturday Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-(now.getDayOfWeek().getValue() + 1)))); // First False for the dueToday check, 2x True for actual weekend, second False for Friday Mockito.when(this.ruleService.isWeekend(Mockito.any())) @@ -167,7 +167,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest // Arrange final LocalDate now = LocalDate.of(2019, 5, 19); // A sunday Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(LocalDate.of(2019, 5, 18)))); // First False for the dueToday check, 2x True for actual weekend, second False for Friday Mockito.when(this.ruleService.isWeekend(Mockito.any())) @@ -193,7 +193,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_NEXT_WORKDAYTest recurringTransaction.setIntervalType(IntervalType.MONTHLY); Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(recurringTransaction)); Mockito.when(this.ruleService.isWeekend(Mockito.any())) .thenReturn(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE); diff --git a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java index 0227e75..59b64fa 100644 --- a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java +++ b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAYTest.java @@ -43,7 +43,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAY public void test_getAllDueToday_dueFuture_holiday() { // Arrange Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(1))); // Today is not a holiday but tomorrow is Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.FALSE, Boolean.TRUE); @@ -71,7 +71,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAY recurringTransaction.setIntervalType(IntervalType.MONTHLY); Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(recurringTransaction)); Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE); Mockito.when(this.ruleService.isWeekend(Mockito.any())).thenReturn(Boolean.TRUE, Boolean.FALSE); @@ -100,7 +100,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_PREVIOUS_WORKDAY recurringTransaction.setIntervalType(IntervalType.MONTHLY); Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(recurringTransaction)); Mockito.when(this.ruleService.isWeekend(Mockito.any())) .thenReturn(Boolean.FALSE, Boolean.TRUE, Boolean.TRUE, Boolean.FALSE); diff --git a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java index e04ef6a..50f8b52 100644 --- a/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java +++ b/financer-server/src/test/java/de/financer/service/RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest.java @@ -43,7 +43,7 @@ public class RecurringTransactionService_getAllDueToday_MONTHLY_SAME_DAYTest { public void test_getAllDueToday_duePast_holiday() { // Arrange Mockito.when(this.recurringTransactionRepository - .findByDeletedFalseAndLastOccurrenceIsNullOrLastOccurrenceGreaterThanEqual(Mockito.any())) + .findAllActive(Mockito.any())) .thenReturn(Collections.singletonList(createRecurringTransaction(-1))); // Today is not a holiday but yesterday was Mockito.when(this.ruleService.isHoliday(Mockito.any())).thenReturn(Boolean.FALSE, Boolean.TRUE); diff --git a/financer-web-client/src/main/resources/favicon.ico b/financer-web-client/src/main/resources/favicon.ico index ecd593e..f33d840 100644 Binary files a/financer-web-client/src/main/resources/favicon.ico and b/financer-web-client/src/main/resources/favicon.ico differ diff --git a/financer-web-client/src/main/resources/static/changelog.txt b/financer-web-client/src/main/resources/static/changelog.txt index 1afeab3..8fb45db 100644 --- a/financer-web-client/src/main/resources/static/changelog.txt +++ b/financer-web-client/src/main/resources/static/changelog.txt @@ -1,6 +1,13 @@ v25 -> v26: - Close of the current expense period now creates null statistic entries for accounts that have not been used in bookings in the period to close. This way the average spending better reflects the period average +- Introduce period type GRAND TOTAL, that denotes a continuous, cumulative period, starting with the inception of the + financer app, without an end +- Introduce period type EXPENSE YEAR, that acts as a group for all expense periods in a year. This may not be a + calendar year, if e.g. the EXPENSE periods do not start at the first of every month. This includes all periods that + start the in the year. This is a preparation for extended reports and year-based booking +- Transactions with a date in the past are now assigned to the proper EXPENSE and EXPENSE YEAR periods +- Add a fav icon v24 -> v25: - Add color column in account overview diff --git a/financer-web-client/src/main/resources/templates/account/accountDetails.html b/financer-web-client/src/main/resources/templates/account/accountDetails.html index a2657dc..55b3005 100644 --- a/financer-web-client/src/main/resources/templates/account/accountDetails.html +++ b/financer-web-client/src/main/resources/templates/account/accountDetails.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/account/accountOverview.html b/financer-web-client/src/main/resources/templates/account/accountOverview.html index 093720e..1a929db 100644 --- a/financer-web-client/src/main/resources/templates/account/accountOverview.html +++ b/financer-web-client/src/main/resources/templates/account/accountOverview.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/account/newAccount.html b/financer-web-client/src/main/resources/templates/account/newAccount.html index d053612..fa34b1c 100644 --- a/financer-web-client/src/main/resources/templates/account/newAccount.html +++ b/financer-web-client/src/main/resources/templates/account/newAccount.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/accountGroup/newAccountGroup.html b/financer-web-client/src/main/resources/templates/accountGroup/newAccountGroup.html index 79c3e9c..22bcc1b 100644 --- a/financer-web-client/src/main/resources/templates/accountGroup/newAccountGroup.html +++ b/financer-web-client/src/main/resources/templates/accountGroup/newAccountGroup.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html b/financer-web-client/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html index b4824bd..dee11f1 100644 --- a/financer-web-client/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html +++ b/financer-web-client/src/main/resources/templates/recurringTransaction/newRecurringTransaction.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/recurringTransaction/recurringToTransactionWithAmount.html b/financer-web-client/src/main/resources/templates/recurringTransaction/recurringToTransactionWithAmount.html index 76c0356..3d42c1e 100644 --- a/financer-web-client/src/main/resources/templates/recurringTransaction/recurringToTransactionWithAmount.html +++ b/financer-web-client/src/main/resources/templates/recurringTransaction/recurringToTransactionWithAmount.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/recurringTransaction/recurringTransactionList.html b/financer-web-client/src/main/resources/templates/recurringTransaction/recurringTransactionList.html index 2a049d5..fee3066 100644 --- a/financer-web-client/src/main/resources/templates/recurringTransaction/recurringTransactionList.html +++ b/financer-web-client/src/main/resources/templates/recurringTransaction/recurringTransactionList.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/report/configureAccountExpensesForPeriod.html b/financer-web-client/src/main/resources/templates/report/configureAccountExpensesForPeriod.html index 278aa5b..2a53df1 100644 --- a/financer-web-client/src/main/resources/templates/report/configureAccountExpensesForPeriod.html +++ b/financer-web-client/src/main/resources/templates/report/configureAccountExpensesForPeriod.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/report/configureAccountGroupExpensesForPeriod.html b/financer-web-client/src/main/resources/templates/report/configureAccountGroupExpensesForPeriod.html index 1208d4d..eeecc8f 100644 --- a/financer-web-client/src/main/resources/templates/report/configureAccountGroupExpensesForPeriod.html +++ b/financer-web-client/src/main/resources/templates/report/configureAccountGroupExpensesForPeriod.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/report/selectChart.html b/financer-web-client/src/main/resources/templates/report/selectChart.html index d53a74f..1a23b36 100644 --- a/financer-web-client/src/main/resources/templates/report/selectChart.html +++ b/financer-web-client/src/main/resources/templates/report/selectChart.html @@ -5,6 +5,7 @@ +

diff --git a/financer-web-client/src/main/resources/templates/transaction/newTransaction.html b/financer-web-client/src/main/resources/templates/transaction/newTransaction.html index 7959d76..bfaf557 100644 --- a/financer-web-client/src/main/resources/templates/transaction/newTransaction.html +++ b/financer-web-client/src/main/resources/templates/transaction/newTransaction.html @@ -5,6 +5,7 @@ +