Transaction upload improvements

This commit is contained in:
2024-05-10 18:16:26 +02:00
parent 3d61fc38d7
commit b241e97306
11 changed files with 96 additions and 33 deletions

View File

@@ -12,6 +12,7 @@ import de.financer.notification.Notification;
import de.financer.notification.PushServiceProxy; import de.financer.notification.PushServiceProxy;
import de.financer.notification.Urgency; import de.financer.notification.Urgency;
import de.financer.template.*; import de.financer.template.*;
import de.financer.template.StringTemplate;
import de.financer.template.exception.FinancerRestException; import de.financer.template.exception.FinancerRestException;
import de.financer.util.ControllerUtils; import de.financer.util.ControllerUtils;
import de.financer.util.TransactionUtils; import de.financer.util.TransactionUtils;

View File

@@ -6,6 +6,7 @@ import de.financer.model.*;
import de.financer.template.*; import de.financer.template.*;
import de.financer.form.NewRecurringTransactionForm; import de.financer.form.NewRecurringTransactionForm;
import de.financer.form.RecurringToTransactionWithOverrideForm; import de.financer.form.RecurringToTransactionWithOverrideForm;
import de.financer.template.StringTemplate;
import de.financer.util.ControllerUtils; import de.financer.util.ControllerUtils;
import org.apache.commons.collections4.IterableUtils; import org.apache.commons.collections4.IterableUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -33,8 +33,10 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import org.thymeleaf.expression.Numbers;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDate;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -349,9 +351,17 @@ public class TransactionController {
final RecurringTransaction emptyRecurring = new RecurringTransaction(); final RecurringTransaction emptyRecurring = new RecurringTransaction();
emptyRecurring.setDescription("-"); emptyRecurring.setDescription("-");
emptyRecurring.setAmount(0L);
emptyRecurring.setFirstOccurrence(LocalDate.EPOCH);
emptyRecurring.setLastOccurrence(LocalDate.of(9999, 12, 31));
recurringTransactionList.add(emptyRecurring); recurringTransactionList.add(emptyRecurring);
recurringTransactionList.addAll(IterableUtils.toList(response.getBody())); recurringTransactionList.addAll(IterableUtils.toList(response.getBody()).stream().peek(rt -> {
if(rt.getLastOccurrence() == null) {
rt.setLastOccurrence(LocalDate.of(9999, 12, 31));
}
}).collect(Collectors.toList()));
model.addAttribute("recurringTransactions", recurringTransactionList); model.addAttribute("recurringTransactions", recurringTransactionList);

View File

@@ -14,7 +14,7 @@ public class CreateUploadedTransactionForm {
public static CreateUploadedTransactionForm of(Collection<UploadedTransaction> uploadedTransactions, Long newPeriodOnRecurringTransaction) { public static CreateUploadedTransactionForm of(Collection<UploadedTransaction> uploadedTransactions, Long newPeriodOnRecurringTransaction) {
final CreateUploadedTransactionForm form = new CreateUploadedTransactionForm(); final CreateUploadedTransactionForm form = new CreateUploadedTransactionForm();
uploadedTransactions.stream().forEach(e -> form.getEntries().add(CreateUploadedTransactionFormEntry.of(e))); uploadedTransactions.forEach(e -> form.getEntries().add(CreateUploadedTransactionFormEntry.of(e)));
form.setNewPeriodOnRecurringTransaction(newPeriodOnRecurringTransaction); form.setNewPeriodOnRecurringTransaction(newPeriodOnRecurringTransaction);
return form; return form;
@@ -46,6 +46,7 @@ public class CreateUploadedTransactionForm {
private Boolean taxRelevant; private Boolean taxRelevant;
private MultipartFile file; private MultipartFile file;
private Long recurringTransactionId; private Long recurringTransactionId;
private boolean matched;
protected static CreateUploadedTransactionFormEntry of(UploadedTransaction uploadedTransaction) { protected static CreateUploadedTransactionFormEntry of(UploadedTransaction uploadedTransaction) {
final CreateUploadedTransactionFormEntry entry = new CreateUploadedTransactionFormEntry(); final CreateUploadedTransactionFormEntry entry = new CreateUploadedTransactionFormEntry();
@@ -55,6 +56,8 @@ public class CreateUploadedTransactionForm {
entry.setDescription(uploadedTransaction.getDescription()); entry.setDescription(uploadedTransaction.getDescription());
entry.setDate(uploadedTransaction.getDate()); entry.setDate(uploadedTransaction.getDate());
entry.setToAccountKey(uploadedTransaction.getToAccountKey()); entry.setToAccountKey(uploadedTransaction.getToAccountKey());
entry.setFromAccountKey(uploadedTransaction.getFromAccountKey());
entry.setMatched(uploadedTransaction.isMatched());
return entry; return entry;
} }
@@ -130,5 +133,13 @@ public class CreateUploadedTransactionForm {
public void setCreate(Boolean create) { public void setCreate(Boolean create) {
this.create = create; this.create = create;
} }
public boolean isMatched() {
return matched;
}
public void setMatched(boolean matched) {
this.matched = matched;
}
} }
} }

View File

@@ -40,7 +40,7 @@ public abstract class AbstractTransactionUploadWorker implements TransactionUplo
for (Account a : this.accounts) { for (Account a : this.accounts) {
final String[] regexps = Optional.ofNullable(a.getUploadMatchRegexps()).orElse("").split("\r\n"); final String[] regexps = Optional.ofNullable(a.getUploadMatchRegexps()).orElse("").split("\r\n");
if (regexps.length > 0) { if (regexps.length > 0 && StringUtils.isNotEmpty(regexps[0])) {
for(String regex : regexps) { for(String regex : regexps) {
Matcher matcher = Pattern.compile(regex).matcher(rawDescription); Matcher matcher = Pattern.compile(regex).matcher(rawDescription);

View File

@@ -3,6 +3,7 @@ package de.financer.transactionUpload;
import de.financer.config.FinancerConfig; import de.financer.config.FinancerConfig;
import de.financer.dto.TransactionUploadFormat; import de.financer.dto.TransactionUploadFormat;
import de.financer.model.Account; import de.financer.model.Account;
import de.financer.model.AccountType;
import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord; import org.apache.commons.csv.CSVRecord;
@@ -41,36 +42,41 @@ public class MT940CSVTransactionUploadWorker extends AbstractTransactionUploadWo
try (InputStream is = file.getInputStream()) { try (InputStream is = file.getInputStream()) {
final CSVParser parser = CSVFormat.DEFAULT.builder() final CSVParser parser = CSVFormat.DEFAULT.builder()
.setDelimiter(";") .setDelimiter(";")
.setNullString(null) .setNullString(null)
.setIgnoreEmptyLines(this.getFormatConfig().isRemoveEmptyLines()) .setIgnoreEmptyLines(this.getFormatConfig().isRemoveEmptyLines())
.build() .build()
.parse(new InputStreamReader(is)); .parse(new InputStreamReader(is));
final List<CSVRecord> records = parser.getRecords(); final List<CSVRecord> records = parser.getRecords();
final long limit = records.size() - (this.getFormatConfig().getSkipLinesHead() + final long limit = records.size() - (this.getFormatConfig().getSkipLinesHead() +
this.getFormatConfig().getSkipLinesTail()); this.getFormatConfig().getSkipLinesTail());
retVal.addAll(records.stream() retVal.addAll(records.stream()
.skip(this.getFormatConfig().getSkipLinesHead()) .skip(this.getFormatConfig().getSkipLinesHead())
.limit(limit) .limit(limit)
.map(r -> { .map(r -> {
final Pair<Account, String> accountAndDescription = final Pair<Account, String> accountAndDescription =
this.getAccountAndDescription(buildDescription(r)); this.getAccountAndDescription(buildDescription(r));
return new UploadedTransaction( boolean isIncome = Optional.ofNullable(accountAndDescription.getLeft()).map(a -> a.getType() == AccountType.INCOME).orElse(false);
// Amount
formatAmount(r.get(AMOUNT_INDEX)), return new UploadedTransaction(
// Description // Amount
accountAndDescription.getRight(), formatAmount(r.get(AMOUNT_INDEX)),
// Date // Description
formatDate(r.get(DATE_INDEX)), accountAndDescription.getRight(),
// To account key // Date
Optional.ofNullable(accountAndDescription.getLeft()) formatDate(r.get(DATE_INDEX)),
.map(Account::getKey).orElse(null)); // To account key
} isIncome ? null : Optional.ofNullable(accountAndDescription.getLeft())
) .map(Account::getKey).orElse(null),
.collect(Collectors.toList())); // From account key
isIncome ? Optional.ofNullable(accountAndDescription.getLeft())
.map(Account::getKey).orElse(null) : null);
}
)
.collect(Collectors.toList()));
} catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) {
if (e instanceof RuntimeException) { if (e instanceof RuntimeException) {
if (e.getCause() instanceof ParseException) { if (e.getCause() instanceof ParseException) {

View File

@@ -1,16 +1,20 @@
package de.financer.transactionUpload; package de.financer.transactionUpload;
public class UploadedTransaction { public class UploadedTransaction {
private boolean matched;
private Long amount; private Long amount;
private String description; private String description;
private String date; private String date;
private String toAccountKey; private String toAccountKey;
private String fromAccountKey;
public UploadedTransaction(Long amount, String description, String date, String toAccountKey) { public UploadedTransaction(Long amount, String description, String date, String toAccountKey, String fromAccountKey) {
this.amount = amount; this.amount = amount;
this.description = description; this.description = description;
this.date = date; this.date = date;
this.toAccountKey = toAccountKey; this.toAccountKey = toAccountKey;
this.fromAccountKey = fromAccountKey;
this.matched = toAccountKey != null || fromAccountKey != null;
} }
public Long getAmount() { public Long getAmount() {
@@ -44,4 +48,16 @@ public class UploadedTransaction {
public void setToAccountKey(String toAccountKey) { public void setToAccountKey(String toAccountKey) {
this.toAccountKey = toAccountKey; this.toAccountKey = toAccountKey;
} }
public String getFromAccountKey() {
return fromAccountKey;
}
public void setFromAccountKey(String fromAccountKey) {
this.fromAccountKey = fromAccountKey;
}
public boolean isMatched() {
return matched;
}
} }

View File

@@ -3,6 +3,7 @@ package de.financer.transactionUpload;
import de.financer.config.FinancerConfig; import de.financer.config.FinancerConfig;
import de.financer.dto.TransactionUploadFormat; import de.financer.dto.TransactionUploadFormat;
import de.financer.model.Account; import de.financer.model.Account;
import de.financer.model.AccountType;
import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord; import org.apache.commons.csv.CSVRecord;
@@ -51,6 +52,8 @@ public class VBCSVTransactionUploadWorker extends AbstractTransactionUploadWorke
final Pair<Account, String> accountAndDescription = final Pair<Account, String> accountAndDescription =
this.getAccountAndDescription(buildDescription(r)); this.getAccountAndDescription(buildDescription(r));
boolean isIncome = Optional.ofNullable(accountAndDescription.getLeft()).map(a -> a.getType() == AccountType.INCOME).orElse(false);
return new UploadedTransaction( return new UploadedTransaction(
// Amount // Amount
formatAmount(r.get(AMOUNT_INDEX)), formatAmount(r.get(AMOUNT_INDEX)),
@@ -59,8 +62,11 @@ public class VBCSVTransactionUploadWorker extends AbstractTransactionUploadWorke
// Date // Date
formatDate(r.get(DATE_INDEX)), formatDate(r.get(DATE_INDEX)),
// To account key // To account key
Optional.ofNullable(accountAndDescription.getLeft()) isIncome ? null : Optional.ofNullable(accountAndDescription.getLeft())
.map(Account::getKey).orElse(null)); .map(Account::getKey).orElse(null),
// From account key
isIncome ? Optional.ofNullable(accountAndDescription.getLeft())
.map(Account::getKey).orElse(null) : null);
} }
) )
.collect(Collectors.toList())); .collect(Collectors.toList()));

View File

@@ -1,5 +1,8 @@
v55 -> v56: v55 -> v56:
- - Change description field in transaction upload from input to textarea and increase its width
- Recurring transaction selection in transaction upload now shows additional information like amount and dates
- From account regex support for transaction upload
- Add matching indicator to transaction upload
v54 -> v55: v54 -> v55:
- Add new deployment profile - Add new deployment profile

View File

@@ -97,6 +97,10 @@ a {
vertical-align: top; vertical-align: top;
} }
#create-upload-transactions-table textarea {
width: 80% !important;
}
#account-overview-table th, #account-overview-table th,
#transaction-table th, #transaction-table th,
#recurring-transaction-list-table th, #recurring-transaction-list-table th,

View File

@@ -19,6 +19,7 @@
<tr> <tr>
<th /> <th />
<th th:text="#{financer.create-upload-transactions.table-header.create}"/> <th th:text="#{financer.create-upload-transactions.table-header.create}"/>
<th />
<th th:text="#{financer.create-upload-transactions.table-header.fromAccount}"/> <th th:text="#{financer.create-upload-transactions.table-header.fromAccount}"/>
<th th:text="#{financer.create-upload-transactions.table-header.toAccount}"/> <th th:text="#{financer.create-upload-transactions.table-header.toAccount}"/>
<th th:text="#{financer.create-upload-transactions.table-header.date}"/> <th th:text="#{financer.create-upload-transactions.table-header.date}"/>
@@ -33,6 +34,10 @@
<td> <td>
<input type="checkbox" th:field="*{entries[__${i.index}__].create}"/> <input type="checkbox" th:field="*{entries[__${i.index}__].create}"/>
</td> </td>
<td>
<span th:if="${entry.matched}" class="icon color-good">&#xe876;</span>
<span th:if="${!entry.matched}" class="icon color-bad">&#xe5cd;</span>
</td>
<td> <td>
<select th:field="*{entries[__${i.index}__].fromAccountKey}"> <select th:field="*{entries[__${i.index}__].fromAccountKey}">
<option th:each="acc : ${fromAccounts}" th:value="${acc.key}" <option th:each="acc : ${fromAccounts}" th:value="${acc.key}"
@@ -52,12 +57,12 @@
<input id="create-upload-transactions-amount" type="text" th:field="*{entries[__${i.index}__].amount}"/> <input id="create-upload-transactions-amount" type="text" th:field="*{entries[__${i.index}__].amount}"/>
</td> </td>
<td> <td>
<input type="text" th:field="*{entries[__${i.index}__].description}"/> <textarea type="text" th:field="*{entries[__${i.index}__].description}"/>
</td> </td>
<td> <td>
<select th:field="*{entries[__${i.index}__].recurringTransactionId}"> <select th:field="*{entries[__${i.index}__].recurringTransactionId}">
<option th:each="rt : ${recurringTransactions}" th:value="${rt.id}" <option th:each="rt : ${recurringTransactions}" th:value="${rt.id}"
th:text="${rt.description}"/> th:utext="${rt.description} + '|' + ${#temporals.format(rt.firstOccurrence)} + '-' + ${#temporals.format(rt.lastOccurrence)} + '|' + ${#numbers.formatDecimal(rt.amount/100D, 1, 'DEFAULT', 2, 'DEFAULT') + currencySymbol}"/>
</select> </select>
</td> </td>
<td> <td>