#20 Add transaction type
This commit is contained in:
@@ -39,6 +39,8 @@ public class Transaction {
|
|||||||
// the inline expense history chart and the expense/income/liability chart
|
// 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
|
// No UI to set the flag as its use is only for very special cases
|
||||||
private boolean expenseNeutral;
|
private boolean expenseNeutral;
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private TransactionType transactionType;
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
@@ -123,4 +125,12 @@ public class Transaction {
|
|||||||
public void setExpenseNeutral(boolean expenseNeutral) {
|
public void setExpenseNeutral(boolean expenseNeutral) {
|
||||||
this.expenseNeutral = expenseNeutral;
|
this.expenseNeutral = expenseNeutral;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TransactionType getTransactionType() {
|
||||||
|
return transactionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTransactionType(TransactionType transactionType) {
|
||||||
|
this.transactionType = transactionType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <code>financer-server#RuleService</code>
|
||||||
|
*/
|
||||||
|
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<String> valueList() {
|
||||||
|
return Arrays.stream(TransactionType.values()).map(TransactionType::name).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ orderByExpression
|
|||||||
regularExpression
|
regularExpression
|
||||||
: (stringExpression | intExpression | booleanExpression | dateExpression) ;
|
: (stringExpression | intExpression | booleanExpression | dateExpression) ;
|
||||||
stringExpression
|
stringExpression
|
||||||
: field=IDENTIFIER operator=STRING_OPERATOR value=(STRING_VALUE | ACCOUNT_TYPE_VALUE) ;
|
: field=IDENTIFIER operator=STRING_OPERATOR value=STRING_VALUE ;
|
||||||
intExpression
|
intExpression
|
||||||
: field=IDENTIFIER operator=(INT_OPERATOR | STRING_OPERATOR) value=INT_VALUE ;
|
: field=IDENTIFIER operator=(INT_OPERATOR | STRING_OPERATOR) value=INT_VALUE ;
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ L_PAREN : '(' ;
|
|||||||
R_PAREN : ')' ;
|
R_PAREN : ')' ;
|
||||||
IDENTIFIER : [a-zA-Z]+ ;
|
IDENTIFIER : [a-zA-Z]+ ;
|
||||||
INT_VALUE : [0-9]+ ;
|
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
|
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')+ ;
|
NEWLINE : ('\r'? '\n' | '\r')+ ;
|
||||||
|
|||||||
@@ -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<TransactionTypeDefinition, TransactionType> transactionTypeDefinitions;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void migrate(Context context) throws Exception {
|
||||||
|
initTransactionTypes();
|
||||||
|
|
||||||
|
final List<TransactionContainer> 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<TransactionContainer> getContainers(Context context) throws SQLException {
|
||||||
|
try (PreparedStatement statement =
|
||||||
|
context
|
||||||
|
.getConnection()
|
||||||
|
.prepareStatement(GET_CONTAINERS_SQL)) {
|
||||||
|
final ResultSet rs = statement.executeQuery();
|
||||||
|
final List<TransactionContainer> 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<AccountType> fromAccountTypes = new ArrayList<>();
|
||||||
|
private Collection<AccountType> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,10 @@ public enum FieldMapping {
|
|||||||
JoinKey.of(File.class), null, NotNullSyntheticHandler.class),
|
JoinKey.of(File.class), null, NotNullSyntheticHandler.class),
|
||||||
|
|
||||||
DESCRIPTION("description", Transaction_.DESCRIPTION, Transaction.class, NoopJoinHandler.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;
|
private final String fieldName;
|
||||||
|
|
||||||
|
|||||||
@@ -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<T extends Enum<?>> implements FieldHandler<String> {
|
||||||
|
private Class<T> clazz;
|
||||||
|
|
||||||
|
protected AbstractEnumTypeHandler(Class<T> clazz) {
|
||||||
|
this.clazz = clazz;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,9 @@
|
|||||||
package de.financer.fql.field_handler;
|
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 de.financer.model.AccountType;
|
||||||
|
|
||||||
import javax.persistence.criteria.CriteriaBuilder;
|
public class AccountTypeHandler extends AbstractEnumTypeHandler<AccountType> {
|
||||||
import javax.persistence.criteria.From;
|
public AccountTypeHandler() {
|
||||||
import javax.persistence.criteria.Predicate;
|
super(AccountType.class);
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class AccountTypeHandler implements FieldHandler<String> {
|
|
||||||
@Override
|
|
||||||
public Predicate apply(FieldMapping fieldMapping, Map<JoinKey, From<?, ?>> 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.financer.fql.field_handler;
|
||||||
|
|
||||||
|
import de.financer.model.TransactionType;
|
||||||
|
|
||||||
|
public class TransactionTypeHandler extends AbstractEnumTypeHandler<TransactionType> {
|
||||||
|
public TransactionTypeHandler() {
|
||||||
|
super(TransactionType.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package de.financer.service;
|
package de.financer.service;
|
||||||
|
|
||||||
import de.financer.config.FinancerConfig;
|
import de.financer.config.FinancerConfig;
|
||||||
import de.financer.model.Account;
|
import de.financer.model.*;
|
||||||
import de.financer.model.AccountType;
|
|
||||||
import de.financer.model.IntervalType;
|
|
||||||
import de.jollyday.HolidayManager;
|
import de.jollyday.HolidayManager;
|
||||||
import de.jollyday.ManagerParameters;
|
import de.jollyday.ManagerParameters;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -32,11 +30,13 @@ public class RuleService implements InitializingBean {
|
|||||||
|
|
||||||
private Map<AccountType, Collection<AccountType>> bookingRules;
|
private Map<AccountType, Collection<AccountType>> bookingRules;
|
||||||
private Map<IntervalType, Period> intervalPeriods;
|
private Map<IntervalType, Period> intervalPeriods;
|
||||||
|
private Map<TransactionTypeDefinition, TransactionType> transactionTypeDefinitions;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() {
|
public void afterPropertiesSet() {
|
||||||
initBookingRules();
|
initBookingRules();
|
||||||
initIntervalValues();
|
initIntervalValues();
|
||||||
|
initTransactionTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initIntervalValues() {
|
private void initIntervalValues() {
|
||||||
@@ -63,6 +63,30 @@ public class RuleService implements InitializingBean {
|
|||||||
this.bookingRules.put(START, Arrays.asList(BANK, CASH, LIABILITY));
|
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.
|
* This method returns the multiplier for the given from account.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -185,4 +209,42 @@ public class RuleService implements InitializingBean {
|
|||||||
public boolean isWeekend(LocalDate now) {
|
public boolean isWeekend(LocalDate now) {
|
||||||
return EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY).contains(now.getDayOfWeek());
|
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 <code>null</code>
|
||||||
|
* 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<AccountType> fromAccountTypes = new ArrayList<>();
|
||||||
|
private Collection<AccountType> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ public class TransactionService {
|
|||||||
transaction.setExpenseNeutral(true);
|
transaction.setExpenseNeutral(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transaction.setTransactionType(this.ruleService.getTransactionType(fromAccount.getType(), toAccount.getType()));
|
||||||
|
|
||||||
this.transactionRepository.save(transaction);
|
this.transactionRepository.save(transaction);
|
||||||
|
|
||||||
this.accountStatisticService.calculateStatistics(transaction);
|
this.accountStatisticService.calculateStatistics(transaction);
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "transaction" --escape keyword "transaction"
|
||||||
|
ADD COLUMN transaction_type VARCHAR(255);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "transaction" --escape keyword "transaction"
|
||||||
|
ALTER COLUMN transaction_type SET NOT NULL;
|
||||||
@@ -1,41 +1,12 @@
|
|||||||
WITH income AS (
|
WITH income AS (
|
||||||
-- Regular income based on INCOME accounts
|
SELECT p.id, SUM(amount) AS incomeSum
|
||||||
SELECT p.ID, SUM(asIncome.spending_total_from) AS incomeSum
|
FROM "transaction" t
|
||||||
FROM period p
|
INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id
|
||||||
INNER JOIN account_statistic asIncome ON asIncome.period_id = p.id
|
INNER JOIN period p on p.id = ltp.period_id
|
||||||
INNER JOIN account aIncome ON aIncome.id = asIncome.account_id AND aIncome.type = 'INCOME'
|
WHERE 1 = 1
|
||||||
|
AND t.transaction_type = 'INCOME'
|
||||||
GROUP BY p.id, p.type, p.start, p."end"
|
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 AS (
|
||||||
-- Expense booking - NOT counted is the case LIABILITY -> EXPENSE even though that is a
|
-- 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
|
-- 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
|
SELECT p2.id, SUM(amount) AS expenseSum
|
||||||
FROM "transaction" t
|
FROM "transaction" t
|
||||||
INNER JOIN account a on a.id = t.from_account_id
|
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 link_transaction_period ltp on ltp.transaction_id = t.id
|
||||||
INNER JOIN period p2 on p2.id = ltp.period_id
|
INNER JOIN period p2 on p2.id = ltp.period_id
|
||||||
WHERE 1 = 1
|
WHERE 1 = 1
|
||||||
|
AND t.transaction_type = 'EXPENSE'
|
||||||
AND a.type in ('BANK', 'CASH')
|
AND a.type in ('BANK', 'CASH')
|
||||||
AND a2.type in ('EXPENSE')
|
|
||||||
GROUP BY p2.id, p2.type, p2.start, p2."end"
|
GROUP BY p2.id, p2.type, p2.start, p2."end"
|
||||||
),
|
),
|
||||||
liability AS (
|
liability AS (
|
||||||
-- Excluded is the special case for start bookings, START -> LIABILITY
|
SELECT p3.id, SUM(amount) AS liabilitySum
|
||||||
-- as the actual expense for that was some time in the past before the starting
|
FROM "transaction" t
|
||||||
-- of the financer instance
|
INNER JOIN link_transaction_period ltp on ltp.transaction_id = t.id
|
||||||
SELECT p2.id, SUM(amount) AS liabilitySum
|
INNER JOIN period p3 on p3.id = ltp.period_id
|
||||||
FROM "transaction" t
|
WHERE 1 = 1
|
||||||
INNER JOIN account a on a.id = t.from_account_id
|
AND t.transaction_type = 'LIABILITY'
|
||||||
INNER JOIN account a2 on a2.id = t.to_account_id
|
GROUP BY p3.id, p3.type, p3.start, p3."end"
|
||||||
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"
|
|
||||||
),
|
),
|
||||||
assets AS (
|
assets AS (
|
||||||
-- Returns only the assets for closed periods
|
-- Returns only the assets for closed periods
|
||||||
@@ -86,16 +50,9 @@ SELECT
|
|||||||
p.start PERIOD_START,
|
p.start PERIOD_START,
|
||||||
p."end" PERIOD_END,
|
p."end" PERIOD_END,
|
||||||
CASE
|
CASE
|
||||||
-- 2^3 possible cases
|
WHEN i.incomeSum IS NULL THEN 0
|
||||||
WHEN i.incomeSum IS NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NULL THEN 0
|
WHEN i.incomeSum IS NOT NULL THEN i.incomeSum
|
||||||
WHEN i.incomeSum IS NOT NULL AND ic.incomeCreditSum IS NULL AND "is".incomeStartSum IS NULL THEN i.incomeSum
|
END INCOME_SUM,
|
||||||
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,
|
|
||||||
CASE
|
CASE
|
||||||
WHEN e.expenseSum IS NULL THEN 0
|
WHEN e.expenseSum IS NULL THEN 0
|
||||||
WHEN e.expenseSum IS NOT NULL THEN e.expenseSum
|
WHEN e.expenseSum IS NOT NULL THEN e.expenseSum
|
||||||
@@ -125,8 +82,6 @@ SELECT
|
|||||||
FROM
|
FROM
|
||||||
period p
|
period p
|
||||||
LEFT JOIN income i ON i.ID = p.ID
|
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 expense e ON e.ID = p.ID
|
||||||
LEFT JOIN assets a ON a.ID = p.ID
|
LEFT JOIN assets a ON a.ID = p.ID
|
||||||
LEFT JOIN liability l ON l.ID = p.ID
|
LEFT JOIN liability l ON l.ID = p.ID
|
||||||
|
|||||||
@@ -103,14 +103,15 @@ public class PeriodController {
|
|||||||
|
|
||||||
@GetMapping("/showIncomeTransactions")
|
@GetMapping("/showIncomeTransactions")
|
||||||
public String showIncomeTransactions(Model model, Long periodId) {
|
public String showIncomeTransactions(Model model, Long periodId) {
|
||||||
final String fql = String
|
final String fql = String.format("periodId = '%s' AND transactionType = 'INCOME' ORDER BY date DESC", periodId);
|
||||||
.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);
|
|
||||||
|
|
||||||
return showInternal(model, fql, true);
|
return showInternal(model, fql, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/showExpenseTransactions")
|
@GetMapping("/showExpenseTransactions")
|
||||||
public String showExpenseTransactions(Model model, Long periodId) {
|
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
|
final String fql = String
|
||||||
.format("periodId = '%s' AND (fromAccountType = 'BANK' OR fromAccountType = 'CASH') AND toAccountType = 'EXPENSE' ORDER BY date DESC", periodId);
|
.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")
|
@GetMapping("/showLiabilityTransactions")
|
||||||
public String showLiabilityTransactions(Model model, Long periodId) {
|
public String showLiabilityTransactions(Model model, Long periodId) {
|
||||||
final String fql = String
|
final String fql = String.format("periodId = '%s' AND transactionType = 'LIABILITY' ORDER BY date DESC", periodId);
|
||||||
.format("periodId = '%s' AND (fromAccountType = 'BANK' OR fromAccountType = 'CASH') AND toAccountType = 'LIABILITY' ORDER BY date DESC", periodId);
|
|
||||||
|
|
||||||
return showInternal(model, fql, true);
|
return showInternal(model, fql, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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.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.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=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.CURRENT=CURRENT\: denotes the current expense period
|
||||||
financer.search-transactions.show-query-options.period.LAST=LAST\: denotes the last expense period
|
financer.search-transactions.show-query-options.period.LAST=LAST\: denotes the last expense period
|
||||||
|
|||||||
@@ -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.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.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.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=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.CURRENT=CURRENT\: bezeichnet die aktuelle Ausgabenperiode
|
||||||
financer.search-transactions.show-query-options.period.LAST=LAST\: bezeichnet die letzte Ausgabenperiode
|
financer.search-transactions.show-query-options.period.LAST=LAST\: bezeichnet die letzte Ausgabenperiode
|
||||||
|
|||||||
@@ -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:
|
v45 -> v46:
|
||||||
- #17 Add actions to the period overview
|
- #17 Add actions to the period overview
|
||||||
- #18 Improve actions in the period overview
|
- #18 Improve actions in the period overview
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
9. Reporting
|
9. Reporting
|
||||||
10. FQL
|
10. FQL
|
||||||
11. Setup
|
11. Setup
|
||||||
12. Planned features
|
12. Links
|
||||||
|
|
||||||
1. About
|
1. About
|
||||||
========
|
========
|
||||||
@@ -165,6 +165,21 @@
|
|||||||
7. Transactions
|
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
|
8. Recurring transactions
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
@@ -195,8 +210,8 @@
|
|||||||
\q
|
\q
|
||||||
exit
|
exit
|
||||||
|
|
||||||
12. Planned features
|
12. Links
|
||||||
====================
|
=========
|
||||||
This chapter lists planned features. The list is in no particular order:
|
This chapter contains useful links:
|
||||||
- Transaction import from online banking (file based)
|
- financer web page: https://financer.dev/
|
||||||
- Extended reports, e.g. forecasting based on recurring transactions and average spending
|
- financer git repository: https://77zzcx7.de/gitea/MK13/financer
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
<li th:text="#{financer.search-transactions.show-query-options.taxRelevant}" />
|
<li th:text="#{financer.search-transactions.show-query-options.taxRelevant}" />
|
||||||
<li th:text="#{financer.search-transactions.show-query-options.hasFile}" />
|
<li th:text="#{financer.search-transactions.show-query-options.hasFile}" />
|
||||||
<li th:text="#{financer.search-transactions.show-query-options.description}" />
|
<li th:text="#{financer.search-transactions.show-query-options.description}" />
|
||||||
|
<li th:text="#{financer.search-transactions.show-query-options.transactionType}" />
|
||||||
<li><span th:text="#{financer.search-transactions.show-query-options.period}" />
|
<li><span th:text="#{financer.search-transactions.show-query-options.period}" />
|
||||||
<ul>
|
<ul>
|
||||||
<li th:text="#{financer.search-transactions.show-query-options.period.CURRENT}" />
|
<li th:text="#{financer.search-transactions.show-query-options.period.CURRENT}" />
|
||||||
|
|||||||
Reference in New Issue
Block a user