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 @@
+