#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
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
: (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')+ ;
|
||||
|
||||
@@ -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),
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
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<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);
|
||||
}
|
||||
public class AccountTypeHandler extends AbstractEnumTypeHandler<AccountType> {
|
||||
public AccountTypeHandler() {
|
||||
super(AccountType.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
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<AccountType, Collection<AccountType>> bookingRules;
|
||||
private Map<IntervalType, Period> intervalPeriods;
|
||||
private Map<TransactionTypeDefinition, TransactionType> 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.
|
||||
* <p>
|
||||
@@ -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 <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.setTransactionType(this.ruleService.getTransactionType(fromAccount.getType(), toAccount.getType()));
|
||||
|
||||
this.transactionRepository.save(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 (
|
||||
-- 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
|
||||
SELECT p3.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
|
||||
INNER JOIN period p3 on p3.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"
|
||||
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,15 +50,8 @@ 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)
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
12. Links
|
||||
=========
|
||||
This chapter contains useful links:
|
||||
- financer web page: https://financer.dev/
|
||||
- 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.hasFile}" />
|
||||
<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}" />
|
||||
<ul>
|
||||
<li th:text="#{financer.search-transactions.show-query-options.period.CURRENT}" />
|
||||
|
||||
Reference in New Issue
Block a user