From 0bb534c8b49dab2e1c5441e4d486d0f82c12c135 Mon Sep 17 00:00:00 2001 From: MK13 Date: Sun, 8 Aug 2021 23:38:41 +0200 Subject: [PATCH] #20 Add transaction type --- .../java/de/financer/model/Transaction.java | 10 ++ .../de/financer/model/TransactionType.java | 51 +++++++ .../src/main/antlr4/de/financer/fql/FQL.g4 | 4 +- .../V48_0_1__calculateTransactionType.java | 133 ++++++++++++++++++ .../java/de/financer/fql/FieldMapping.java | 5 +- .../AbstractEnumTypeHandler.java | 38 +++++ .../fql/field_handler/AccountTypeHandler.java | 29 +--- .../field_handler/TransactionTypeHandler.java | 9 ++ .../java/de/financer/service/RuleService.java | 68 ++++++++- .../financer/service/TransactionService.java | 2 + .../common/V48_0_0__transactionType.sql | 2 + .../V48_0_2__transactionTypeNotNull.sql | 2 + .../native_queries/period_overview.sql | 79 +++-------- .../financer/controller/PeriodController.java | 8 +- .../main/resources/i18n/message.properties | 1 + .../resources/i18n/message_de_DE.properties | 1 + .../src/main/resources/static/changelog.txt | 7 + .../src/main/resources/static/readme.txt | 27 +++- .../transaction/searchTransactions.html | 1 + 19 files changed, 373 insertions(+), 104 deletions(-) create mode 100644 financer-common/src/main/java/de/financer/model/TransactionType.java create mode 100644 financer-server/src/main/java/database/common/V48_0_1__calculateTransactionType.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/AbstractEnumTypeHandler.java create mode 100644 financer-server/src/main/java/de/financer/fql/field_handler/TransactionTypeHandler.java create mode 100644 financer-server/src/main/resources/database/common/V48_0_0__transactionType.sql create mode 100644 financer-server/src/main/resources/database/common/V48_0_2__transactionTypeNotNull.sql diff --git a/financer-common/src/main/java/de/financer/model/Transaction.java b/financer-common/src/main/java/de/financer/model/Transaction.java index c44942a..8e8da29 100644 --- a/financer-common/src/main/java/de/financer/model/Transaction.java +++ b/financer-common/src/main/java/de/financer/model/Transaction.java @@ -39,6 +39,8 @@ public class Transaction { // the inline expense history chart and the expense/income/liability chart // No UI to set the flag as its use is only for very special cases private boolean expenseNeutral; + @Enumerated(EnumType.STRING) + private TransactionType transactionType; public Long getId() { return id; @@ -123,4 +125,12 @@ public class Transaction { public void setExpenseNeutral(boolean expenseNeutral) { this.expenseNeutral = expenseNeutral; } + + public TransactionType getTransactionType() { + return transactionType; + } + + public void setTransactionType(TransactionType transactionType) { + this.transactionType = transactionType; + } } diff --git a/financer-common/src/main/java/de/financer/model/TransactionType.java b/financer-common/src/main/java/de/financer/model/TransactionType.java new file mode 100644 index 0000000..e7cf63f --- /dev/null +++ b/financer-common/src/main/java/de/financer/model/TransactionType.java @@ -0,0 +1,51 @@ +package de.financer.model; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * This enum specifies constants that specify the type of a transaction. Actual specification which type is used when + * is implemented in financer-server#RuleService + */ +public enum TransactionType { + /** + * Specifies that a transaction swaps money from one asset account (e.g. {@link AccountType#BANK BANK}) + * to another one (e.g. {@link AccountType#CASH CASH}) + */ + ASSET_SWAP, + + /** + * Specifies that a transaction captured the process of buying something + */ + EXPENSE, + + /** + * Specifies that a transaction was made to pay back a liability + */ + LIABILITY, + + /** + * Specifies a transaction that increases the assets + */ + INCOME, + + /** + * Specifies that a transaction was used to book the starting amount to a liability account. + */ + START_LIABILITY; + + /** + * This method validates whether the given string represents a valid transaction type. + * + * @param type to check + * @return whether the given type represents a valid transaction type + */ + public static boolean isValidType(String type) { + return Arrays.stream(TransactionType.values()).anyMatch((accountType) -> accountType.name().equals(type)); + } + + public static List valueList() { + return Arrays.stream(TransactionType.values()).map(TransactionType::name).collect(Collectors.toList()); + } +} diff --git a/financer-server/src/main/antlr4/de/financer/fql/FQL.g4 b/financer-server/src/main/antlr4/de/financer/fql/FQL.g4 index 4af34fb..d0bb0fe 100644 --- a/financer-server/src/main/antlr4/de/financer/fql/FQL.g4 +++ b/financer-server/src/main/antlr4/de/financer/fql/FQL.g4 @@ -23,7 +23,7 @@ orderByExpression regularExpression : (stringExpression | intExpression | booleanExpression | dateExpression) ; stringExpression - : field=IDENTIFIER operator=STRING_OPERATOR value=(STRING_VALUE | ACCOUNT_TYPE_VALUE) ; + : field=IDENTIFIER operator=STRING_OPERATOR value=STRING_VALUE ; intExpression : field=IDENTIFIER operator=(INT_OPERATOR | STRING_OPERATOR) value=INT_VALUE ; @@ -122,7 +122,7 @@ L_PAREN : '(' ; R_PAREN : ')' ; IDENTIFIER : [a-zA-Z]+ ; INT_VALUE : [0-9]+ ; -STRING_VALUE : '\'' [a-zA-Z0-9\-/.&%@$ ]+ '\'' ; +STRING_VALUE : '\'' [a-zA-Z0-9\-/.&%@$_ ]+ '\'' ; DATE_VALUE : [0-9][0-9][0-9][0-9][-][0-9][0-9][-][0-9][0-9] ; // ANTLR does not support regex quantifiers NEWLINE : ('\r'? '\n' | '\r')+ ; diff --git a/financer-server/src/main/java/database/common/V48_0_1__calculateTransactionType.java b/financer-server/src/main/java/database/common/V48_0_1__calculateTransactionType.java new file mode 100644 index 0000000..fb734c2 --- /dev/null +++ b/financer-server/src/main/java/database/common/V48_0_1__calculateTransactionType.java @@ -0,0 +1,133 @@ +package database.common; + +import de.financer.model.AccountType; +import de.financer.model.TransactionType; +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.SQLException; +import java.util.*; + +import static de.financer.model.AccountType.*; +import static de.financer.model.AccountType.BANK; + +public class V48_0_1__calculateTransactionType extends BaseJavaMigration { + private static class TransactionContainer { + public final Long transactionId; + public final AccountType fromAccountType; + public final AccountType toAccountType; + + public TransactionContainer(Long transactionId, AccountType fromAccountType, AccountType toAccountType) { + this.transactionId = transactionId; + this.fromAccountType = fromAccountType; + this.toAccountType = toAccountType; + } + } + + private static final String GET_CONTAINERS_SQL = "SELECT t.id, fa.type, ta.type FROM \"transaction\" t INNER JOIN account fa ON fa.id = t.from_account_id INNER JOIN account ta ON ta.id = t.to_account_id"; + + private static final String UPDATE_TRANSACTION_SQL = "UPDATE \"transaction\" SET transaction_type = ? WHERE id = ?"; + + private Map transactionTypeDefinitions; + + @Override + public void migrate(Context context) throws Exception { + initTransactionTypes(); + + final List cons = getContainers(context); + TransactionType transactionType; + + for (TransactionContainer con : cons) { + transactionType = getTransactionType(con.fromAccountType, con.toAccountType); + + updateTransaction(context, con.transactionId, transactionType.name()); + } + } + + private void updateTransaction(Context context, Long transactionId, String transactionType) throws SQLException { + try (PreparedStatement statementUpdate = + context + .getConnection() + .prepareStatement(UPDATE_TRANSACTION_SQL)) { + statementUpdate.setString(1, transactionType); + statementUpdate.setLong(2, transactionId); + + statementUpdate.executeUpdate(); + } + } + + 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 TransactionContainer( + rs.getLong(1), // transaction id + AccountType.valueOf(rs.getString(2)), // from account type + AccountType.valueOf(rs.getString(3)) // to account type + )); + } + + return cons; + } + } + + private void initTransactionTypes() { + this.transactionTypeDefinitions = new HashMap<>(); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(CASH, BANK) + .withTo(CASH, BANK), TransactionType.ASSET_SWAP); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(CASH, BANK, LIABILITY) + .withTo(EXPENSE), TransactionType.EXPENSE); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(CASH, BANK) + .withTo(LIABILITY), TransactionType.LIABILITY); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(START, INCOME, LIABILITY) + .withTo(CASH, BANK), TransactionType.INCOME); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(START) + .withTo(LIABILITY), TransactionType.START_LIABILITY); + } + + private TransactionType getTransactionType(AccountType fromAccountType, AccountType toAccountType) { + return this.transactionTypeDefinitions.entrySet().stream() + .filter(ttdE -> ttdE.getKey().matches(fromAccountType, toAccountType)) + .findFirst().map(ttd -> ttd.getValue()).orElse(null); + } + + private static class TransactionTypeDefinition { + private Collection fromAccountTypes = new ArrayList<>(); + private Collection toAccountTypes = new ArrayList<>(); + + public static TransactionTypeDefinition withFrom(AccountType... fromAccountTypes) { + TransactionTypeDefinition def = new TransactionTypeDefinition(); + + def.fromAccountTypes.addAll(Arrays.asList(fromAccountTypes)); + + return def; + } + + public TransactionTypeDefinition withTo(AccountType... toAccountTypes) { + this.toAccountTypes.addAll(Arrays.asList(toAccountTypes)); + + return this; + } + + public boolean matches(AccountType fromAccountType, AccountType toAccountType) { + return this.fromAccountTypes.contains(fromAccountType) && this.toAccountTypes.contains(toAccountType); + } + } +} diff --git a/financer-server/src/main/java/de/financer/fql/FieldMapping.java b/financer-server/src/main/java/de/financer/fql/FieldMapping.java index 995253e..f6fe2a3 100644 --- a/financer-server/src/main/java/de/financer/fql/FieldMapping.java +++ b/financer-server/src/main/java/de/financer/fql/FieldMapping.java @@ -50,7 +50,10 @@ public enum FieldMapping { JoinKey.of(File.class), null, NotNullSyntheticHandler.class), DESCRIPTION("description", Transaction_.DESCRIPTION, Transaction.class, NoopJoinHandler.class, - JoinKey.of(Transaction.class), null, StringHandler.class); + JoinKey.of(Transaction.class), null, StringHandler.class), + + TRANSACTION_TYPE("transactionType", Transaction_.TRANSACTION_TYPE, Transaction.class, NoopJoinHandler.class, + JoinKey.of(Transaction.class), null, TransactionTypeHandler.class); private final String fieldName; diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/AbstractEnumTypeHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/AbstractEnumTypeHandler.java new file mode 100644 index 0000000..42efb2e --- /dev/null +++ b/financer-server/src/main/java/de/financer/fql/field_handler/AbstractEnumTypeHandler.java @@ -0,0 +1,38 @@ +package de.financer.fql.field_handler; + +import de.financer.fql.FQLException; +import de.financer.fql.FieldMapping; +import de.financer.fql.join_handler.JoinKey; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Predicate; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +public abstract class AbstractEnumTypeHandler> implements FieldHandler { + private Class clazz; + + protected AbstractEnumTypeHandler(Class clazz) { + this.clazz = clazz; + } + + @Override + public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, String value) { + return criteriaBuilder + .equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), toEnumType(FieldHandlerUtils.removeQuotes(value))); + } + + private T toEnumType(String value) { + if (value == null) { + throw new FQLException(String.format("NULL cannot be resolved to %s!", this.clazz.getName())); + } + + try { + return (T) this.clazz.getMethod("valueOf", String.class).invoke(null, value.toUpperCase()); + } + catch (IllegalArgumentException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new FQLException(e); + } + } +} diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/AccountTypeHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/AccountTypeHandler.java index 696ef95..0da6dbe 100644 --- a/financer-server/src/main/java/de/financer/fql/field_handler/AccountTypeHandler.java +++ b/financer-server/src/main/java/de/financer/fql/field_handler/AccountTypeHandler.java @@ -1,32 +1,9 @@ package de.financer.fql.field_handler; -import de.financer.fql.FQLException; -import de.financer.fql.FieldMapping; -import de.financer.fql.join_handler.JoinKey; import de.financer.model.AccountType; -import javax.persistence.criteria.CriteriaBuilder; -import javax.persistence.criteria.From; -import javax.persistence.criteria.Predicate; -import java.util.Map; - -public class AccountTypeHandler implements FieldHandler { - @Override - public Predicate apply(FieldMapping fieldMapping, Map> froms, CriteriaBuilder criteriaBuilder, String value) { - return criteriaBuilder - .equal(froms.get(fieldMapping.getJoinKey()).get(fieldMapping.getAttributeName()), toAccountType(FieldHandlerUtils.removeQuotes(value))); - } - - private static AccountType toAccountType(String value) { - if (value == null) { - throw new FQLException("NULL cannot be solved to AccountType!"); - } - - try { - return AccountType.valueOf(value.toUpperCase()); - } - catch (IllegalArgumentException e) { - throw new FQLException(e); - } +public class AccountTypeHandler extends AbstractEnumTypeHandler { + public AccountTypeHandler() { + super(AccountType.class); } } diff --git a/financer-server/src/main/java/de/financer/fql/field_handler/TransactionTypeHandler.java b/financer-server/src/main/java/de/financer/fql/field_handler/TransactionTypeHandler.java new file mode 100644 index 0000000..b24f076 --- /dev/null +++ b/financer-server/src/main/java/de/financer/fql/field_handler/TransactionTypeHandler.java @@ -0,0 +1,9 @@ +package de.financer.fql.field_handler; + +import de.financer.model.TransactionType; + +public class TransactionTypeHandler extends AbstractEnumTypeHandler { + public TransactionTypeHandler() { + super(TransactionType.class); + } +} diff --git a/financer-server/src/main/java/de/financer/service/RuleService.java b/financer-server/src/main/java/de/financer/service/RuleService.java index f326672..1f3dce7 100644 --- a/financer-server/src/main/java/de/financer/service/RuleService.java +++ b/financer-server/src/main/java/de/financer/service/RuleService.java @@ -1,9 +1,7 @@ package de.financer.service; import de.financer.config.FinancerConfig; -import de.financer.model.Account; -import de.financer.model.AccountType; -import de.financer.model.IntervalType; +import de.financer.model.*; import de.jollyday.HolidayManager; import de.jollyday.ManagerParameters; import org.slf4j.Logger; @@ -32,11 +30,13 @@ public class RuleService implements InitializingBean { private Map> bookingRules; private Map intervalPeriods; + private Map transactionTypeDefinitions; @Override public void afterPropertiesSet() { initBookingRules(); initIntervalValues(); + initTransactionTypes(); } private void initIntervalValues() { @@ -63,6 +63,30 @@ public class RuleService implements InitializingBean { this.bookingRules.put(START, Arrays.asList(BANK, CASH, LIABILITY)); } + private void initTransactionTypes() { + this.transactionTypeDefinitions = new HashMap<>(); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(CASH, BANK) + .withTo(CASH, BANK), TransactionType.ASSET_SWAP); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(CASH, BANK, LIABILITY) + .withTo(EXPENSE), TransactionType.EXPENSE); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(CASH, BANK) + .withTo(LIABILITY), TransactionType.LIABILITY); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(START, INCOME, LIABILITY) + .withTo(CASH, BANK), TransactionType.INCOME); + + this.transactionTypeDefinitions + .put(TransactionTypeDefinition.withFrom(START) + .withTo(LIABILITY), TransactionType.START_LIABILITY); + } + /** * This method returns the multiplier for the given from account. *

@@ -185,4 +209,42 @@ public class RuleService implements InitializingBean { public boolean isWeekend(LocalDate now) { return EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY).contains(now.getDayOfWeek()); } + + /** + * @param fromAccountType the from account + * @param toAccountType the to account + * @return the {@link TransactionType} matching the given from and to accounts or null + * if no matching transaction type was found + */ + public TransactionType getTransactionType(AccountType fromAccountType, AccountType toAccountType) { + return this.transactionTypeDefinitions.entrySet().stream() + .filter(ttdE -> ttdE.getKey().matches(fromAccountType, toAccountType)) + .findFirst().map(ttd -> ttd.getValue()).orElse(null); + } + + /** + * Helper class + */ + private static class TransactionTypeDefinition { + private Collection fromAccountTypes = new ArrayList<>(); + private Collection toAccountTypes = new ArrayList<>(); + + public static TransactionTypeDefinition withFrom(AccountType... fromAccountTypes) { + TransactionTypeDefinition def = new TransactionTypeDefinition(); + + def.fromAccountTypes.addAll(Arrays.asList(fromAccountTypes)); + + return def; + } + + public TransactionTypeDefinition withTo(AccountType... toAccountTypes) { + this.toAccountTypes.addAll(Arrays.asList(toAccountTypes)); + + return this; + } + + public boolean matches(AccountType fromAccountType, AccountType toAccountType) { + return this.fromAccountTypes.contains(fromAccountType) && this.toAccountTypes.contains(toAccountType); + } + } } 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 0cae60b..1e8b1f6 100644 --- a/financer-server/src/main/java/de/financer/service/TransactionService.java +++ b/financer-server/src/main/java/de/financer/service/TransactionService.java @@ -105,6 +105,8 @@ public class TransactionService { transaction.setExpenseNeutral(true); } + transaction.setTransactionType(this.ruleService.getTransactionType(fromAccount.getType(), toAccount.getType())); + this.transactionRepository.save(transaction); this.accountStatisticService.calculateStatistics(transaction); diff --git a/financer-server/src/main/resources/database/common/V48_0_0__transactionType.sql b/financer-server/src/main/resources/database/common/V48_0_0__transactionType.sql new file mode 100644 index 0000000..3205f73 --- /dev/null +++ b/financer-server/src/main/resources/database/common/V48_0_0__transactionType.sql @@ -0,0 +1,2 @@ +ALTER TABLE "transaction" --escape keyword "transaction" + ADD COLUMN transaction_type VARCHAR(255); \ No newline at end of file diff --git a/financer-server/src/main/resources/database/common/V48_0_2__transactionTypeNotNull.sql b/financer-server/src/main/resources/database/common/V48_0_2__transactionTypeNotNull.sql new file mode 100644 index 0000000..b669ee3 --- /dev/null +++ b/financer-server/src/main/resources/database/common/V48_0_2__transactionTypeNotNull.sql @@ -0,0 +1,2 @@ +ALTER TABLE "transaction" --escape keyword "transaction" + ALTER COLUMN transaction_type SET NOT NULL; \ No newline at end of file diff --git a/financer-server/src/main/resources/native_queries/period_overview.sql b/financer-server/src/main/resources/native_queries/period_overview.sql index f8bd56d..ca4ca86 100644 --- a/financer-server/src/main/resources/native_queries/period_overview.sql +++ b/financer-server/src/main/resources/native_queries/period_overview.sql @@ -1,41 +1,12 @@ WITH income AS ( - -- Regular income based on INCOME accounts - SELECT p.ID, SUM(asIncome.spending_total_from) AS incomeSum - FROM period p - INNER JOIN account_statistic asIncome ON asIncome.period_id = p.id - INNER JOIN account aIncome ON aIncome.id = asIncome.account_id AND aIncome.type = 'INCOME' + SELECT p.id, SUM(amount) AS incomeSum + 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 1 = 1 + AND t.transaction_type = 'INCOME' GROUP BY p.id, p.type, p.start, p."end" ), -incomeCredit as ( - -- Special case for credits that can be booked from a LIABILITY account to a BANK/CASH account - -- Need to be counted as income as the money is used for expenses and those expenses will be counted - -- as expense, so to make it even we need to count it here as well - SELECT p2.id, SUM(amount) AS incomeCreditSum - FROM "transaction" t - INNER JOIN account a on a.id = t.from_account_id - INNER JOIN account a2 on a2.id = t.to_account_id - INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id - INNER JOIN period p2 on p2.id = ltp.period_id - WHERE 1 = 1 - AND a.type in ('LIABILITY') - AND a2.type in ('BANK', 'CASH') - GROUP BY p2.id, p2.type, p2.start, p2."end" -), -incomeStart as ( - -- Special case for money that was there at the starting time of a financer instance - -- Will be counted as income as this money is used for expanses, so to make it even - -- we need to count it here as well - SELECT p2.id, SUM(amount) AS incomeStartSum - FROM "transaction" t - INNER JOIN account a on a.id = t.from_account_id - INNER JOIN account a2 on a2.id = t.to_account_id - INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id - INNER JOIN period p2 on p2.id = ltp.period_id - WHERE 1 = 1 - AND a.type in ('START') - AND a2.type in ('BANK', 'CASH') - GROUP BY p2.id, p2.type, p2.start, p2."end" -), expense AS ( -- Expense booking - NOT counted is the case LIABILITY -> EXPENSE even though that is a -- valid booking in the app. This is because we would count the expense once here and a second time @@ -43,28 +14,21 @@ expense AS ( SELECT p2.id, SUM(amount) AS expenseSum FROM "transaction" t INNER JOIN account a on a.id = t.from_account_id - INNER JOIN account a2 on a2.id = t.to_account_id INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id INNER JOIN period p2 on p2.id = ltp.period_id WHERE 1 = 1 + AND t.transaction_type = 'EXPENSE' AND a.type in ('BANK', 'CASH') - AND a2.type in ('EXPENSE') GROUP BY p2.id, p2.type, p2.start, p2."end" ), liability AS ( - -- Excluded is the special case for start bookings, START -> LIABILITY - -- as the actual expense for that was some time in the past before the starting - -- of the financer instance - SELECT p2.id, SUM(amount) AS liabilitySum - FROM "transaction" t - INNER JOIN account a on a.id = t.from_account_id - INNER JOIN account a2 on a2.id = t.to_account_id - INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id - INNER JOIN period p2 on p2.id = ltp.period_id - WHERE 1 = 1 - AND a.type in ('BANK', 'CASH') - AND a2.type in ('LIABILITY') - GROUP BY p2.id, p2.type, p2.start, p2."end" + SELECT p3.id, SUM(amount) AS liabilitySum + FROM "transaction" t + INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id + INNER JOIN period p3 on p3.id = ltp.period_id + WHERE 1 = 1 + AND t.transaction_type = 'LIABILITY' + GROUP BY p3.id, p3.type, p3.start, p3."end" ), assets AS ( -- Returns only the assets for closed periods @@ -86,16 +50,9 @@ SELECT p.start PERIOD_START, p."end" PERIOD_END, CASE - -- 2^3 possible cases - WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NULL THEN 0 - WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NULL THEN i.incomeSum - WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NULL THEN ic.incomeCreditSum - WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NULL THEN (i.incomeSum + ic.incomeCreditSum) - WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NOT NULL THEN "is".incomeStartSum - WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NOT NULL THEN (i.incomeSum + "is".incomeStartSum) - WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NOT NULL THEN (ic.incomeCreditSum + "is".incomeStartSum) - WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NOT NULL AND "is".incomeStartSum IS NOT NULL THEN (i.incomeSum + ic.incomeCreditSum + "is".incomeStartSum) - END INCOME_SUM, + WHEN i.incomeSum IS NULL THEN 0 + WHEN i.incomeSum IS NOT NULL THEN i.incomeSum + END INCOME_SUM, CASE WHEN e.expenseSum IS NULL THEN 0 WHEN e.expenseSum IS NOT NULL THEN e.expenseSum @@ -125,8 +82,6 @@ SELECT FROM period p LEFT JOIN income i ON i.ID = p.ID -LEFT JOIN incomeCredit ic ON ic.ID = p.ID -LEFT JOIN incomeStart "is" ON "is".ID = p.ID LEFT JOIN expense e ON e.ID = p.ID LEFT JOIN assets a ON a.ID = p.ID LEFT JOIN liability l ON l.ID = p.ID diff --git a/financer-web-client/src/main/java/de/financer/controller/PeriodController.java b/financer-web-client/src/main/java/de/financer/controller/PeriodController.java index e0b27a3..86f80f4 100644 --- a/financer-web-client/src/main/java/de/financer/controller/PeriodController.java +++ b/financer-web-client/src/main/java/de/financer/controller/PeriodController.java @@ -103,14 +103,15 @@ public class PeriodController { @GetMapping("/showIncomeTransactions") public String showIncomeTransactions(Model model, Long periodId) { - final String fql = String - .format("periodId = '%s' AND (fromAccountType = 'INCOME' OR (fromAccountType = 'LIABILITY' AND (toAccountType = 'BANK' OR toAccountType = 'CASH')) OR (fromAccountType = 'START' AND (toAccountType = 'BANK' OR toAccountType = 'CASH'))) ORDER BY date DESC", periodId); + final String fql = String.format("periodId = '%s' AND transactionType = 'INCOME' ORDER BY date DESC", periodId); return showInternal(model, fql, true); } @GetMapping("/showExpenseTransactions") public String showExpenseTransactions(Model model, Long periodId) { + // We cannot use transactionType = 'EXPENSE' because we do not want to show the Liability -> Expense bookings + // as these are counted via asset type account -> Liability final String fql = String .format("periodId = '%s' AND (fromAccountType = 'BANK' OR fromAccountType = 'CASH') AND toAccountType = 'EXPENSE' ORDER BY date DESC", periodId); @@ -119,8 +120,7 @@ public class PeriodController { @GetMapping("/showLiabilityTransactions") public String showLiabilityTransactions(Model model, Long periodId) { - final String fql = String - .format("periodId = '%s' AND (fromAccountType = 'BANK' OR fromAccountType = 'CASH') AND toAccountType = 'LIABILITY' ORDER BY date DESC", periodId); + final String fql = String.format("periodId = '%s' AND transactionType = 'LIABILITY' ORDER BY date DESC", periodId); return showInternal(model, fql, true); } diff --git a/financer-web-client/src/main/resources/i18n/message.properties b/financer-web-client/src/main/resources/i18n/message.properties index 3e7167a..ec3161e 100644 --- a/financer-web-client/src/main/resources/i18n/message.properties +++ b/financer-web-client/src/main/resources/i18n/message.properties @@ -145,6 +145,7 @@ financer.search-transactions.show-query-options.recurring=recurring\: whether th financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: whether the transaction is relevant for tax declaration financer.search-transactions.show-query-options.hasFile=hasFile\: whether the transaction has a file linked financer.search-transactions.show-query-options.description=description\: the description of the transaction +financer.search-transactions.show-query-options.transactionType=transactionType\: the type of the transaction financer.search-transactions.show-query-options.period=period\: the period the transaction is assigned to financer.search-transactions.show-query-options.period.CURRENT=CURRENT\: denotes the current expense period financer.search-transactions.show-query-options.period.LAST=LAST\: denotes the last expense period diff --git a/financer-web-client/src/main/resources/i18n/message_de_DE.properties b/financer-web-client/src/main/resources/i18n/message_de_DE.properties index 759a376..af9747c 100644 --- a/financer-web-client/src/main/resources/i18n/message_de_DE.properties +++ b/financer-web-client/src/main/resources/i18n/message_de_DE.properties @@ -145,6 +145,7 @@ financer.search-transactions.show-query-options.recurring=recurring\: ob die Buc financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: ob die Buchung als steuerrelevant markiert wurde financer.search-transactions.show-query-options.hasFile=hasFile\: ob eine Datei der Buchung zugeordnet ist financer.search-transactions.show-query-options.description=description\: die Beschreibung der Buchung +financer.search-transactions.show-query-options.transactionType=transactionType\: der Typ der Buchung financer.search-transactions.show-query-options.period=period\: die Periode der Buchung financer.search-transactions.show-query-options.period.CURRENT=CURRENT\: bezeichnet die aktuelle Ausgabenperiode financer.search-transactions.show-query-options.period.LAST=LAST\: bezeichnet die letzte Ausgabenperiode diff --git a/financer-web-client/src/main/resources/static/changelog.txt b/financer-web-client/src/main/resources/static/changelog.txt index 227de70..624cb8e 100644 --- a/financer-web-client/src/main/resources/static/changelog.txt +++ b/financer-web-client/src/main/resources/static/changelog.txt @@ -1,3 +1,10 @@ +v47 -> v48: +- Added new property 'transaction type' to a transaction, denoting the type of the transaction, e.g. asset swap, + expense, liability or income. This can also be queried via FQL + +v46 -> v47: +- Fix a bug that occurred while creating a transaction from a recurring transaction with amount overwrite + v45 -> v46: - #17 Add actions to the period overview - #18 Improve actions in the period overview diff --git a/financer-web-client/src/main/resources/static/readme.txt b/financer-web-client/src/main/resources/static/readme.txt index e22b4e8..7197c6f 100644 --- a/financer-web-client/src/main/resources/static/readme.txt +++ b/financer-web-client/src/main/resources/static/readme.txt @@ -16,7 +16,7 @@ 9. Reporting 10. FQL 11. Setup - 12. Planned features + 12. Links 1. About ======== @@ -165,6 +165,21 @@ 7. Transactions =============== + Transactions have a type that denotes the business case: + - Asset swap: Transferring money from one asset type account (Bank, Cash) to another account of an asset type + E.g. from Bank to Cash, e.g. making a withdrawal at an ATM, B- -> C+ + - Expense: Booking money for an expense from a Bank, Cash or Liability account to an Expense account + E.g. from Cash to Expense, e.g. buying goods, C- -> E+ + - Liability: Paying a credit installment from an asset type account (Bank, Cash) to a Liability account + E.g. from Bank to Liability, e.g. making an installment, B- -> L- + - Income: Increasing the assets from a Start, Income or Liability account to an asset type account (Bank, Cash). + Start -> asset type account bookings need to counted as the money that was there at the starting time of + a financer instance is used for expanses. + Liability -> asset type account bookings need to counted as the money is used for expenses. + E.g. from Liability to Bank, e.g. payout of a credit, L+ -> B+ + - Start liability: Booking the current balance of a liability at the inception of a financer instance + E.g. from Start to Liability, e.g. initial setup of liability amount, S+ -> L+ + 8. Recurring transactions ========================= @@ -195,8 +210,8 @@ \q exit - 12. Planned features - ==================== - This chapter lists planned features. The list is in no particular order: - - Transaction import from online banking (file based) - - Extended reports, e.g. forecasting based on recurring transactions and average spending \ No newline at end of file + 12. Links + ========= + This chapter contains useful links: + - financer web page: https://financer.dev/ + - financer git repository: https://77zzcx7.de/gitea/MK13/financer \ No newline at end of file diff --git a/financer-web-client/src/main/resources/templates/transaction/searchTransactions.html b/financer-web-client/src/main/resources/templates/transaction/searchTransactions.html index 3d5a0e7..fcee601 100644 --- a/financer-web-client/src/main/resources/templates/transaction/searchTransactions.html +++ b/financer-web-client/src/main/resources/templates/transaction/searchTransactions.html @@ -38,6 +38,7 @@

  • +