#22 Add transaction upload
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
package de.financer.config;
|
||||
|
||||
import de.financer.dto.TransactionUploadFormat;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.NestedConfigurationProperty;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.util.Currency;
|
||||
import java.util.*;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "financer")
|
||||
@@ -22,6 +24,7 @@ public class FinancerConfig {
|
||||
private Currency currency;
|
||||
private DayOfWeek firstDayOfWeek;
|
||||
private boolean darkMode;
|
||||
private TransactionUpload transactionUpload;
|
||||
|
||||
public String getServerUrl() {
|
||||
return serverUrl;
|
||||
@@ -72,4 +75,45 @@ public class FinancerConfig {
|
||||
public void setDarkMode(boolean darkMode) {
|
||||
this.darkMode = darkMode;
|
||||
}
|
||||
|
||||
public TransactionUpload getTransactionUpload() {
|
||||
return transactionUpload;
|
||||
}
|
||||
|
||||
public void setTransactionUpload(TransactionUpload transactionUpload) {
|
||||
this.transactionUpload = transactionUpload;
|
||||
}
|
||||
|
||||
public static class TransactionUpload {
|
||||
private Map<TransactionUploadFormat, Format> formats;
|
||||
|
||||
public Map<TransactionUploadFormat, Format> getFormats() {
|
||||
return formats;
|
||||
}
|
||||
|
||||
public void setFormats(Map<TransactionUploadFormat, Format> formats) {
|
||||
this.formats = formats;
|
||||
}
|
||||
|
||||
public static class Format {
|
||||
private String dateFormat;
|
||||
private Locale locale;
|
||||
|
||||
public String getDateFormat() {
|
||||
return dateFormat;
|
||||
}
|
||||
|
||||
public void setDateFormat(String dateFormat) {
|
||||
this.dateFormat = dateFormat;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public void setLocale(Locale locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.financer.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@Component
|
||||
@ConfigurationPropertiesBinding
|
||||
public class FormatConverter implements Converter<String, FinancerConfig.TransactionUpload.Format> {
|
||||
@Override
|
||||
public FinancerConfig.TransactionUpload.Format convert(String source) {
|
||||
final String[] data = source.split(",");
|
||||
|
||||
if (data.length != 2) {
|
||||
throw new IllegalArgumentException("Wrong format of Format definition!");
|
||||
}
|
||||
|
||||
FinancerConfig.TransactionUpload.Format format = new FinancerConfig.TransactionUpload.Format();
|
||||
|
||||
format.setDateFormat(data[0]);
|
||||
format.setLocale(new Locale(data[1]));
|
||||
|
||||
return format;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.financer.config;
|
||||
|
||||
import de.financer.dto.TransactionUploadFormat;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationPropertiesBinding
|
||||
public class TransactionUploadFormatConverter implements Converter<String, TransactionUploadFormat> {
|
||||
@Override
|
||||
public TransactionUploadFormat convert(String source) {
|
||||
return TransactionUploadFormat.valueOf(source);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public enum Function {
|
||||
TR_EXPENSES_CURRENT_PERIOD("transactions/getExpensesCurrentPeriod"),
|
||||
TR_EXPENSE_PERIOD_TOTALS("transactions/getExpensePeriodTotals"),
|
||||
TR_EXPENSES_ALL_PERIODS("transactions/getExpensesAllPeriods"),
|
||||
TR_CREATE_UPLOADED_TRANSACTIONS("transactions/upload"),
|
||||
|
||||
RT_GET_ALL("recurringTransactions/getAll"),
|
||||
RT_GET_ALL_ACTIVE("recurringTransactions/getAllActive"),
|
||||
|
||||
@@ -2,19 +2,28 @@ package de.financer.controller;
|
||||
|
||||
import de.financer.ResponseReason;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dto.CreateUploadedTransactionsRequestDto;
|
||||
import de.financer.dto.SaveTransactionRequestDto;
|
||||
import de.financer.dto.SearchTransactionsResponseDto;
|
||||
import de.financer.dto.TransactionUploadFormat;
|
||||
import de.financer.form.CreateUploadedTransactionForm;
|
||||
import de.financer.form.SearchTransactionsForm;
|
||||
import de.financer.form.UploadTransactionsForm;
|
||||
import de.financer.model.AccountType;
|
||||
import de.financer.model.RecurringTransaction;
|
||||
import de.financer.template.*;
|
||||
import de.financer.form.NewTransactionForm;
|
||||
import de.financer.model.Account;
|
||||
import de.financer.template.exception.FinancerRestException;
|
||||
import de.financer.transactionUpload.TransactionUploadWorker;
|
||||
import de.financer.transactionUpload.TransactionUploadWorkerFactory;
|
||||
import de.financer.transactionUpload.UploadedTransaction;
|
||||
import de.financer.util.ControllerUtils;
|
||||
import org.apache.commons.collections4.IterableUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -22,10 +31,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Controller
|
||||
@@ -34,6 +40,9 @@ public class TransactionController {
|
||||
@Autowired
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
@Autowired
|
||||
private TransactionUploadWorkerFactory workerFactory;
|
||||
|
||||
@GetMapping("/searchTransactions")
|
||||
public String searchTransaction(Model model) {
|
||||
model.addAttribute("form", new SearchTransactionsForm());
|
||||
@@ -168,4 +177,141 @@ public class TransactionController {
|
||||
|
||||
return "account/accountDetails";
|
||||
}
|
||||
|
||||
@GetMapping("/uploadTransactions")
|
||||
public String uploadTransaction(Model model) {
|
||||
model.addAttribute("form", new UploadTransactionsForm());
|
||||
model.addAttribute("formats", TransactionUploadFormat.values());
|
||||
|
||||
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||
ControllerUtils.addDarkMode(model, this.financerConfig);
|
||||
|
||||
return "transaction/uploadTransactions";
|
||||
}
|
||||
|
||||
@PostMapping("/uploadTransactions")
|
||||
public String uploadTransactions(UploadTransactionsForm form, Model model) {
|
||||
// TODO input validation
|
||||
// TODO ENCODING??
|
||||
|
||||
try {
|
||||
final TransactionUploadWorker worker = this.workerFactory
|
||||
.getWorker(TransactionUploadFormat.valueOf(form.getFormat()));
|
||||
|
||||
final Collection<UploadedTransaction> uploadedTransactions = worker.process(form.getFile());
|
||||
|
||||
return _uploadTransactions(model, Optional.empty(), CreateUploadedTransactionForm.of(uploadedTransactions));
|
||||
}
|
||||
catch(Exception e) {
|
||||
// TODO
|
||||
model.addAttribute("errorMessage", "KAPUTT");
|
||||
model.addAttribute("formats", TransactionUploadFormat.values());
|
||||
model.addAttribute("form", form);
|
||||
|
||||
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||
ControllerUtils.addDarkMode(model, this.financerConfig);
|
||||
|
||||
return "transaction/createUploadedTransactions";
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/createUploadedTransactions")
|
||||
public String createUploadedTransactions(CreateUploadedTransactionForm form, Model model) {
|
||||
try {
|
||||
final Collection<CreateUploadedTransactionsRequestDto> dtos =
|
||||
form.getEntries()
|
||||
.stream()
|
||||
.filter(CreateUploadedTransactionForm.CreateUploadedTransactionFormEntry::getCreate)
|
||||
.map(e -> {
|
||||
final CreateUploadedTransactionsRequestDto dto = new CreateUploadedTransactionsRequestDto();
|
||||
|
||||
dto.setAmount(e.getAmount().toString());
|
||||
dto.setDate(ControllerUtils.formatDate(this.financerConfig, e.getDate()));
|
||||
dto.setDescription(e.getDescription());
|
||||
dto.setFromAccountKey(e.getFromAccountKey());
|
||||
dto.setToAccountKey(e.getToAccountKey());
|
||||
dto.setRecurringTransactionId(Optional.ofNullable(e.getRecurringTransactionId())
|
||||
.map(id -> id.toString())
|
||||
.orElse(null));
|
||||
dto.setTaxRelevant(e.getTaxRelevant());
|
||||
|
||||
if (e.getFile() != null && StringUtils.isNotEmpty(e.getFile().getOriginalFilename())) {
|
||||
try {
|
||||
dto.setFileContent(Base64.getEncoder().encodeToString(e.getFile().getBytes()));
|
||||
dto.setFileName(e.getFile().getOriginalFilename());
|
||||
} catch (IOException ioe) {
|
||||
// TODO No file for us :(
|
||||
}
|
||||
}
|
||||
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
final ResponseReason responseReason = FinancerRestTemplate
|
||||
.exchangePost(this.financerConfig, Function.TR_CREATE_UPLOADED_TRANSACTIONS, dtos);
|
||||
|
||||
if (!ResponseReason.CREATED.equals(responseReason)) {
|
||||
return _uploadTransactions(model, Optional.of(responseReason), form);
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
// TODO
|
||||
return _uploadTransactions(model, Optional.empty(), form);
|
||||
}
|
||||
|
||||
return "redirect:/accountOverview";
|
||||
}
|
||||
|
||||
private String _uploadTransactions(Model model, Optional<ResponseReason> responseReason, CreateUploadedTransactionForm form) {
|
||||
final Iterable<Account> allAccounts;
|
||||
List<Account> fromAccounts = new ArrayList<>();
|
||||
List<Account> toAccounts = new ArrayList<>();
|
||||
|
||||
try {
|
||||
allAccounts = FinancerRestTemplate.exchangeGet(this.financerConfig,
|
||||
Function.ACC_GET_ALL,
|
||||
new ParameterizedTypeReference<Iterable<Account>>() {
|
||||
});
|
||||
|
||||
fromAccounts = ControllerUtils.filterAndSortAccounts(allAccounts).stream()
|
||||
.filter((a) -> a.getType() != AccountType.EXPENSE)
|
||||
.collect(Collectors.toList());
|
||||
toAccounts = ControllerUtils.filterAndSortAccounts(allAccounts).stream()
|
||||
.filter((a) -> a.getType() != AccountType.INCOME && a
|
||||
.getType() != AccountType.START)
|
||||
.collect(Collectors.toList());
|
||||
} catch (FinancerRestException financerRestException) {
|
||||
// Nothing to do
|
||||
// This is very unlikely to happen and if it happens the account selection stays empty, so the user
|
||||
// cannot create a transaction anyway and is forced to reload the page or navigate back
|
||||
}
|
||||
|
||||
model.addAttribute("fromAccounts", fromAccounts);
|
||||
model.addAttribute("toAccounts", toAccounts);
|
||||
|
||||
final ResponseEntity<Iterable<RecurringTransaction>> response = new GetAllActiveRecurringTransactionsTemplate()
|
||||
.exchange(this.financerConfig);
|
||||
final List<RecurringTransaction> recurringTransactionList = new ArrayList<>();
|
||||
final RecurringTransaction emptyRecurring = new RecurringTransaction();
|
||||
|
||||
emptyRecurring.setDescription("-");
|
||||
|
||||
recurringTransactionList.add(emptyRecurring);
|
||||
recurringTransactionList.addAll(IterableUtils.toList(response.getBody()));
|
||||
|
||||
model.addAttribute("recurringTransactions", recurringTransactionList);
|
||||
|
||||
responseReason.ifPresent(rr -> model.addAttribute("errorMessage", rr.name()));
|
||||
|
||||
model.addAttribute("form", form);
|
||||
|
||||
ControllerUtils.addVersionAttribute(model, this.financerConfig);
|
||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||
ControllerUtils.addDarkMode(model, this.financerConfig);
|
||||
|
||||
return "transaction/createUploadedTransactions";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package de.financer.form;
|
||||
|
||||
import de.financer.transactionUpload.UploadedTransaction;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class CreateUploadedTransactionForm {
|
||||
private List<CreateUploadedTransactionFormEntry> entries = new ArrayList<>();
|
||||
|
||||
public static CreateUploadedTransactionForm of(Collection<UploadedTransaction> uploadedTransactions) {
|
||||
final CreateUploadedTransactionForm form = new CreateUploadedTransactionForm();
|
||||
|
||||
uploadedTransactions.stream().forEach(e -> form.getEntries().add(CreateUploadedTransactionFormEntry.of(e)));
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
public List<CreateUploadedTransactionFormEntry> getEntries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void setEntries(List<CreateUploadedTransactionFormEntry> entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
public static final class CreateUploadedTransactionFormEntry {
|
||||
private Boolean create;
|
||||
private String fromAccountKey;
|
||||
private String toAccountKey;
|
||||
private Long amount;
|
||||
private String description;
|
||||
private String date;
|
||||
private Boolean taxRelevant;
|
||||
private MultipartFile file;
|
||||
private Long recurringTransactionId;
|
||||
|
||||
protected static CreateUploadedTransactionFormEntry of(UploadedTransaction uploadedTransaction) {
|
||||
final CreateUploadedTransactionFormEntry entry = new CreateUploadedTransactionFormEntry();
|
||||
|
||||
entry.setCreate(Boolean.TRUE); // Create by default, user can de-select
|
||||
entry.setAmount(uploadedTransaction.getAmount());
|
||||
entry.setDescription(uploadedTransaction.getDescription());
|
||||
entry.setDate(uploadedTransaction.getDate());
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
public String getFromAccountKey() {
|
||||
return fromAccountKey;
|
||||
}
|
||||
|
||||
public void setFromAccountKey(String fromAccountKey) {
|
||||
this.fromAccountKey = fromAccountKey;
|
||||
}
|
||||
|
||||
public String getToAccountKey() {
|
||||
return toAccountKey;
|
||||
}
|
||||
|
||||
public void setToAccountKey(String toAccountKey) {
|
||||
this.toAccountKey = toAccountKey;
|
||||
}
|
||||
|
||||
public Long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(Long amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(String date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public Boolean getTaxRelevant() {
|
||||
return taxRelevant;
|
||||
}
|
||||
|
||||
public void setTaxRelevant(Boolean taxRelevant) {
|
||||
this.taxRelevant = taxRelevant;
|
||||
}
|
||||
|
||||
public MultipartFile getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void setFile(MultipartFile file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public Long getRecurringTransactionId() {
|
||||
return recurringTransactionId;
|
||||
}
|
||||
|
||||
public void setRecurringTransactionId(Long recurringTransactionId) {
|
||||
this.recurringTransactionId = recurringTransactionId;
|
||||
}
|
||||
|
||||
public Boolean getCreate() {
|
||||
return create;
|
||||
}
|
||||
|
||||
public void setCreate(Boolean create) {
|
||||
this.create = create;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.financer.form;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
public class UploadTransactionsForm {
|
||||
private String format;
|
||||
private MultipartFile file;
|
||||
|
||||
public String getFormat() {
|
||||
return format;
|
||||
}
|
||||
|
||||
public void setFormat(String format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public MultipartFile getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void setFile(MultipartFile file) {
|
||||
this.file = file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.financer.transactionUpload;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dto.TransactionUploadFormat;
|
||||
|
||||
public abstract class AbstractTransactionUploadWorker implements TransactionUploadWorker {
|
||||
private FinancerConfig financerConfig;
|
||||
private TransactionUploadFormat format;
|
||||
|
||||
protected AbstractTransactionUploadWorker(TransactionUploadFormat format, FinancerConfig financerConfig) {
|
||||
this.format = format;
|
||||
this.financerConfig = financerConfig;
|
||||
}
|
||||
|
||||
protected FinancerConfig.TransactionUpload.Format getFormatConfig() {
|
||||
return this.financerConfig.getTransactionUpload().getFormats().get(this.format);
|
||||
}
|
||||
|
||||
protected String removeQuotes(String value) {
|
||||
return CharMatcher.is('\"').trimFrom(value);
|
||||
}
|
||||
|
||||
public FinancerConfig getFinancerConfig() {
|
||||
return financerConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package de.financer.transactionUpload;
|
||||
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dto.TransactionUploadFormat;
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.CSVParser;
|
||||
import org.apache.commons.csv.CSVRecord;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MT940CSVTransactionUploadWorker extends AbstractTransactionUploadWorker {
|
||||
/**
|
||||
* The value date
|
||||
*/
|
||||
private static final int DATE_INDEX = 2;
|
||||
private static final int DESCRIPTION1_INDEX = 3;
|
||||
private static final int DESCRIPTION2_INDEX = 4;
|
||||
private static final int AMOUNT_INDEX = 8;
|
||||
|
||||
protected MT940CSVTransactionUploadWorker(FinancerConfig financerConfig) {
|
||||
super(TransactionUploadFormat.MT940_CSV, financerConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<UploadedTransaction> process(MultipartFile file) throws Exception {
|
||||
final Collection<UploadedTransaction> retVal = new ArrayList<>();
|
||||
|
||||
try (InputStream is = file.getInputStream()) {
|
||||
final CSVParser parser = CSVFormat.DEFAULT.builder()
|
||||
.setDelimiter(";")
|
||||
.setNullString(null)
|
||||
.build()
|
||||
.parse(new InputStreamReader(is));
|
||||
|
||||
retVal.addAll(parser.stream().skip(1)
|
||||
.map(r -> new UploadedTransaction(
|
||||
// Amount
|
||||
Math.abs(formatAmount(removeQuotes(r.get(AMOUNT_INDEX)))),
|
||||
// Description
|
||||
buildDescription(r),
|
||||
// Date
|
||||
formatDate(LocalDate.parse(removeQuotes(r.get(DATE_INDEX)),
|
||||
DateTimeFormatter.ofPattern(this.getFormatConfig()
|
||||
.getDateFormat())))
|
||||
)
|
||||
)
|
||||
.collect(Collectors.toList()));
|
||||
} catch (IOException | RuntimeException e) {
|
||||
if (e instanceof RuntimeException) {
|
||||
if (e.getCause() instanceof ParseException) {
|
||||
// Make it checked again
|
||||
throw (ParseException) e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
private String formatDate(LocalDate date) {
|
||||
// Format to common format
|
||||
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
}
|
||||
|
||||
private String buildDescription(CSVRecord r) {
|
||||
final String description1 = r.get(DESCRIPTION1_INDEX);
|
||||
final String description2 = r.get(DESCRIPTION2_INDEX);
|
||||
|
||||
if (StringUtils.isEmpty(description2)) {
|
||||
return description1;
|
||||
}
|
||||
|
||||
return description2;
|
||||
}
|
||||
|
||||
private Long formatAmount(String amountString) {
|
||||
try {
|
||||
return NumberFormat.getNumberInstance(this.getFormatConfig().getLocale())
|
||||
.parse(amountString)
|
||||
.longValue();
|
||||
} catch (ParseException e) {
|
||||
// Make it unchecked because of the usage in the lambda pipeline
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.financer.transactionUpload;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface TransactionUploadWorker {
|
||||
Collection<UploadedTransaction> process(MultipartFile file) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package de.financer.transactionUpload;
|
||||
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dto.TransactionUploadFormat;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class TransactionUploadWorkerFactory {
|
||||
@Autowired
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
public TransactionUploadWorker getWorker(TransactionUploadFormat format) {
|
||||
AbstractTransactionUploadWorker worker;
|
||||
|
||||
switch (format) {
|
||||
case MT940_CSV:
|
||||
worker = new MT940CSVTransactionUploadWorker(this.financerConfig);
|
||||
|
||||
break;
|
||||
default:
|
||||
worker = null;
|
||||
}
|
||||
|
||||
return worker;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package de.financer.transactionUpload;
|
||||
|
||||
public class UploadedTransaction {
|
||||
private Long amount;
|
||||
private String description;
|
||||
private String date;
|
||||
|
||||
public UploadedTransaction(Long amount, String description, String date) {
|
||||
this.amount = amount;
|
||||
this.description = description;
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public Long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(Long amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(String date) {
|
||||
this.date = date;
|
||||
}
|
||||
}
|
||||
@@ -43,4 +43,23 @@ financer.firstDayOfWeek=MONDAY
|
||||
|
||||
# Whether dark mode should be enabled by default
|
||||
# Possible values: true|default
|
||||
financer.darkMode=true
|
||||
financer.darkMode=true
|
||||
|
||||
# Transaction upload format configs
|
||||
# Spring Boot does not support dynamic properties so we cannot use for example
|
||||
# financer.transactionUpload.MT940_CSV.dateFormat=dd.MM.yy
|
||||
# financer.transactionUpload.MT940_CSV.locale=de_DE
|
||||
# financer.transactionUpload.XXX.dateFormat=yyyy-MM-dd
|
||||
# financer.transactionUpload.XXX.locale=en_US
|
||||
# So we have to fallback to some ugly list-style property. This is handled by some converter that
|
||||
# transforms the list-style property's value into a pojo.
|
||||
#
|
||||
# financer.transactionUpload.formats.YYY=
|
||||
# \ /
|
||||
# v
|
||||
# key -> constant in the TransactionUploadFormat enum
|
||||
#
|
||||
# The list-style property has to be compliant to the format
|
||||
# 1 -> date format
|
||||
# 2 -> locale
|
||||
financer.transactionUpload.formats.MT940_CSV=dd.MM.yy,de_DE
|
||||
@@ -5,6 +5,7 @@ financer.account-overview.available-actions.hide-closed=Hide closed accounts
|
||||
financer.account-overview.available-actions.create-account=Create new account
|
||||
financer.account-overview.available-actions.create-transaction=Create new transaction
|
||||
financer.account-overview.available-actions.search-transactions=Search transactions
|
||||
financer.account-overview.available-actions.upload-transactions=Upload transactions
|
||||
financer.account-overview.available-actions.create-recurring-transaction=Create new recurring transaction
|
||||
financer.account-overview.available-actions.recurring-transaction-all=Show all recurring transactions
|
||||
financer.account-overview.available-actions.create-account-group=Create new account group
|
||||
@@ -163,6 +164,30 @@ financer.search-transactions.show-query-options.like=Fuzzy search with LIKE, % a
|
||||
financer.search-transactions.show-query-options.in=Multiple string or integer values via IN ('value1', 'value2', ...)
|
||||
financer.search-transactions.show-query-options.between=Filtering of values (string, int, date) in a given range, start and end inclusive, via BETWEEN
|
||||
|
||||
financer.upload-transactions.title=financer\: upload transactions
|
||||
financer.upload-transactions.label.format=Format\:
|
||||
financer.upload-transactions.format.MT940_CSV=MT940 CSV
|
||||
financer.upload-transactions.label.file=File\:
|
||||
financer.upload-transactions.submit=Upload transactions
|
||||
|
||||
financer.create-upload-transactions.title=financer\: create uploaded transactions
|
||||
financer.create-upload-transactions.table-header.create=Create
|
||||
financer.create-upload-transactions.table-header.fromAccount=From account
|
||||
financer.create-upload-transactions.table-header.toAccount=To account
|
||||
financer.create-upload-transactions.table-header.date=Date
|
||||
financer.create-upload-transactions.table-header.amount=Amount
|
||||
financer.create-upload-transactions.table-header.description=Description
|
||||
financer.create-upload-transactions.table-header.recurring-transaction=Recurring transaction
|
||||
financer.create-upload-transactions.table-header.taxRelevant=Tax relevant
|
||||
financer.create-upload-transactions.table-header.file=File
|
||||
financer.create-upload-transactions.account-type.BANK={0}|Bank|{1}{2}
|
||||
financer.create-upload-transactions.account-type.CASH={0}|Cash|{1}{2}
|
||||
financer.create-upload-transactions.account-type.INCOME={0}|Income|{1}{2}
|
||||
financer.create-upload-transactions.account-type.LIABILITY={0}|Liability|{1}{2}
|
||||
financer.create-upload-transactions.account-type.EXPENSE={0}|Expense|{1}{2}
|
||||
financer.create-upload-transactions.account-type.START={0}|Start|{1}{2}
|
||||
financer.create-upload-transactions.submit=Create uploaded transactions
|
||||
|
||||
financer.chart-select.title=Select a chart to generate
|
||||
financer.chart-select.submit=Select
|
||||
|
||||
@@ -235,6 +260,8 @@ financer.heading.chart-config-account-expenses-for-period=financer\: configure a
|
||||
financer.heading.search-transactions=financer\: search transactions
|
||||
financer.heading.recurring-transaction-calendar=financer\: recurring transaction calendar
|
||||
financer.heading.period-overview=financer\: period overview
|
||||
financer.heading.upload-transactions=financer\: upload transactions
|
||||
financer.heading.create-upload-transactions=financer\: create uploaded transactions
|
||||
|
||||
financer.cancel-back-to-overview=Cancel and back to overview
|
||||
financer.back-to-overview=Back to overview
|
||||
|
||||
@@ -5,6 +5,7 @@ financer.account-overview.available-actions.hide-closed=Verstecke geschlossene K
|
||||
financer.account-overview.available-actions.create-account=Neues Konto erstellen
|
||||
financer.account-overview.available-actions.create-transaction=Neue Buchung erstellen
|
||||
financer.account-overview.available-actions.search-transactions=Buchungen suchen
|
||||
financer.account-overview.available-actions.upload-transactions=Buchungen hochladen
|
||||
financer.account-overview.available-actions.create-recurring-transaction=Neue wiederkehrende Buchung erstellen
|
||||
financer.account-overview.available-actions.recurring-transaction-all=Zeige alle wiederkehrende Buchungen
|
||||
financer.account-overview.available-actions.create-account-group=Neue Konto-Gruppe erstellen
|
||||
@@ -163,6 +164,30 @@ financer.search-transactions.show-query-options.like=Platzhaltersuche mit LIKE,
|
||||
financer.search-transactions.show-query-options.in=Mehrere Zeichenketten- oder Ganzzahlwerte mit IN ('wert1', 'wert2', ...)
|
||||
financer.search-transactions.show-query-options.between=Filtern von Werten (Zeichenketten, Ganzzahlen, Daten) in einem Bereich, Start und Ende jeweils inklusive, mit BETWEEN
|
||||
|
||||
financer.upload-transactions.title=financer\: Buchungen hochladen
|
||||
financer.upload-transactions.label.format=Format\:
|
||||
financer.upload-transactions.format.MT940_CSV=MT940 CSV
|
||||
financer.upload-transactions.label.file=Datei\:
|
||||
financer.upload-transactions.submit=Buchungen hochladen
|
||||
|
||||
financer.create-upload-transactions.title=financer\: Erstelle hochgeladene Buchungen
|
||||
financer.create-upload-transactions.table-header.create=Erstellen
|
||||
financer.create-upload-transactions.table-header.fromAccount=Von Konto
|
||||
financer.create-upload-transactions.table-header.toAccount=An Konto
|
||||
financer.create-upload-transactions.table-header.date=Datum
|
||||
financer.create-upload-transactions.table-header.amount=Betrag
|
||||
financer.create-upload-transactions.table-header.description=Beschreibung
|
||||
financer.create-upload-transactions.table-header.recurring-transaction=Wiederkehrende Buchung
|
||||
financer.create-upload-transactions.table-header.taxRelevant=Relevant f\u00FCr Steuererkl\u00E4rung
|
||||
financer.create-upload-transactions.table-header.file=Datei
|
||||
financer.create-upload-transactions.account-type.BANK={0}|Bank|{1}{2}
|
||||
financer.create-upload-transactions.account-type.CASH={0}|Bar|{1}{2}
|
||||
financer.create-upload-transactions.account-type.INCOME={0}|Einkommen|{1}{2}
|
||||
financer.create-upload-transactions.account-type.LIABILITY={0}|Verbindlichkeit|{1}{2}
|
||||
financer.create-upload-transactions.account-type.EXPENSE={0}|Aufwand|{1}{2}
|
||||
financer.create-upload-transactions.account-type.START={0}|Anfangsbestand|{1}{2}
|
||||
financer.create-upload-transactions.submit=Erstelle hochgeladene Buchungen
|
||||
|
||||
financer.chart-select.title=Ein Diagramm zum Erzeugen ausw\u00E4hlen
|
||||
financer.chart-select.submit=Ausw\u00E4hlen
|
||||
|
||||
@@ -234,6 +259,8 @@ financer.heading.chart-config-account-group-expenses-for-period=financer\: Konfi
|
||||
financer.heading.search-transactions=financer\: Buchungen suchen
|
||||
financer.heading.recurring-transaction-calendar=financer\: Kalender wiederkehrende Buchung
|
||||
financer.heading.period-overview=financer\: Perioden\u00FCbersicht
|
||||
financer.heading.upload-transactions=financer\: Buchungen hochladen
|
||||
financer.heading.create-upload-transactions=financer\: Erstelle hochgeladene Buchungen
|
||||
|
||||
financer.cancel-back-to-overview=Abbrechen und zur\u00FCck zur \u00DCbersicht
|
||||
financer.back-to-overview=Zur\u00FCck zur \u00DCbersicht
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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
|
||||
- #22 Added new feature to upload a file that contains transactions, e.g. a file export from online banking. Currently
|
||||
supported is the MT940_CSV format
|
||||
|
||||
v46 -> v47:
|
||||
- Fix a bug that occurred while creating a transaction from a recurring transaction with amount overwrite
|
||||
|
||||
@@ -71,7 +71,8 @@ a {
|
||||
#account-overview-table,
|
||||
#transaction-table,
|
||||
#recurring-transaction-list-table,
|
||||
#period-overview-table {
|
||||
#period-overview-table,
|
||||
#create-upload-transactions-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
@@ -86,7 +87,9 @@ a {
|
||||
#recurring-transaction-list-table th,
|
||||
#recurring-transaction-list-table td,
|
||||
#period-overview-table th,
|
||||
#period-overview-table td {
|
||||
#period-overview-table td,
|
||||
#create-upload-transactions-table th,
|
||||
#create-upload-transactions-table td {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 0.3em;
|
||||
vertical-align: top;
|
||||
@@ -95,7 +98,8 @@ a {
|
||||
#account-overview-table th,
|
||||
#transaction-table th,
|
||||
#recurring-transaction-list-table th,
|
||||
#period-overview-table th {
|
||||
#period-overview-table th,
|
||||
#create-upload-transactions-table th{
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
background-color: var(--background-color);
|
||||
@@ -170,7 +174,8 @@ tr:hover {
|
||||
#new-account-group-form *,
|
||||
#chart-config-account-group-expenses-for-period-form *,
|
||||
#chart-config-account-expenses-for-period-form *,
|
||||
#search-transactions-form * {
|
||||
#search-transactions-form *,
|
||||
#upload-transactions-form * {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
width: 20em;
|
||||
@@ -181,6 +186,14 @@ tr:hover {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#create-upload-transactions-table > * > * select {
|
||||
width: 18em;
|
||||
}
|
||||
|
||||
#create-upload-transactions-amount {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
#chart-select-form div {
|
||||
width: 20em;
|
||||
margin-top: 1em;
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<div id="action-container-sub-transactions">
|
||||
<a th:href="@{/newTransaction}" th:text="#{financer.account-overview.available-actions.create-transaction}"/>
|
||||
<a th:href="@{/searchTransactions}" th:text="#{financer.account-overview.available-actions.search-transactions}"/>
|
||||
<a th:href="@{/uploadTransactions}" th:text="#{financer.account-overview.available-actions.upload-transactions}"/>
|
||||
</div>
|
||||
<div id="action-container-sub-recurring-transactions">
|
||||
<a th:href="@{/newRecurringTransaction}"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title th:text="#{financer.create-upload-transactions.title}"/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}"/>
|
||||
<link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}"/>
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="shortcut icon" th:href="@{/favicon.ico}"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1 th:text="#{financer.heading.create-upload-transactions}"/>
|
||||
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
|
||||
<a th:href="@{/accountOverview}" th:text="#{financer.cancel-back-to-overview}"/>
|
||||
<form id="create-upload-transactions-form" action="#" th:action="@{/createUploadedTransactions}" th:object="${form}"
|
||||
method="post" enctype="multipart/form-data">
|
||||
<table id="create-upload-transactions-table">
|
||||
<tr>
|
||||
<th th:text="#{financer.create-upload-transactions.table-header.create}"/>
|
||||
<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.date}"/>
|
||||
<th th:text="#{financer.create-upload-transactions.table-header.amount}"/>
|
||||
<th th:text="#{financer.create-upload-transactions.table-header.description}"/>
|
||||
<th th:text="#{financer.create-upload-transactions.table-header.recurring-transaction}"/>
|
||||
<th th:text="#{financer.create-upload-transactions.table-header.taxRelevant}"/>
|
||||
<th th:text="#{financer.create-upload-transactions.table-header.file}"/>
|
||||
</tr>
|
||||
<tr th:each="entry, i : ${form.entries}">
|
||||
<td>
|
||||
<input type="checkbox" th:field="*{entries[__${i.index}__].create}"/>
|
||||
</td>
|
||||
<td>
|
||||
<select th:field="*{entries[__${i.index}__].fromAccountKey}">
|
||||
<option th:each="acc : ${fromAccounts}" th:value="${acc.key}"
|
||||
th:utext="#{'financer.create-upload-transactions.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')},${currencySymbol})}"/>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select th:field="*{entries[__${i.index}__].toAccountKey}">
|
||||
<option th:each="acc : ${toAccounts}" th:value="${acc.key}"
|
||||
th:utext="#{'financer.create-upload-transactions.account-type.' + ${acc.type}(${acc.key},${#numbers.formatDecimal(acc.currentBalance/100D, 1, 'DEFAULT', 2, 'DEFAULT')},${currencySymbol})}"/>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="date" th:field="*{entries[__${i.index}__].date}"/>
|
||||
</td>
|
||||
<td>
|
||||
<input id="create-upload-transactions-amount" type="text" th:field="*{entries[__${i.index}__].amount}"/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" th:field="*{entries[__${i.index}__].description}"/>
|
||||
</td>
|
||||
<td>
|
||||
<select th:field="*{entries[__${i.index}__].recurringTransactionId}">
|
||||
<option th:each="rt : ${recurringTransactions}" th:value="${rt.id}"
|
||||
th:text="${rt.description}"/>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="checkbox" th:field="*{entries[__${i.index}__].taxRelevant}"/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="file" th:field="*{entries[__${i.index}__].file}"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type="submit" th:value="#{financer.create-upload-transactions.submit}"/>
|
||||
</form>
|
||||
<div th:replace="includes/footer :: footer"/>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title th:text="#{financer.upload-transactions.title}"/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link th:if="${darkMode}" rel="stylesheet" th:href="@{/css/darkModeColors.css}" />
|
||||
<link th:if="${!darkMode}" rel="stylesheet" th:href="@{/css/lightModeColors.css}" />
|
||||
<link rel="stylesheet" th:href="@{/css/main.css}">
|
||||
<link rel="shortcut icon" th:href="@{/favicon.ico}" />
|
||||
</head>
|
||||
<body>
|
||||
<h1 th:text="#{financer.heading.upload-transactions}" />
|
||||
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
|
||||
<a th:href="@{/accountOverview}" th:text="#{financer.cancel-back-to-overview}"/>
|
||||
<form id="upload-transactions-form" action="#" th:action="@{/uploadTransactions}" th:object="${form}"
|
||||
method="post" enctype="multipart/form-data">
|
||||
<label for="selectFormat" th:text="#{financer.upload-transactions.label.format}"/>
|
||||
<select id="selectFormat" th:field="*{format}">
|
||||
<option th:each="format : ${formats}" th:value="${format}"
|
||||
th:utext="#{'financer.upload-transactions.format.' + ${format}}"/>
|
||||
</select>
|
||||
<label for="inputFile" th:text="#{financer.upload-transactions.label.file}" />
|
||||
<input type="file" id="inputFile" th:field="*{file}" />
|
||||
<input type="submit" th:value="#{financer.upload-transactions.submit}" />
|
||||
</form>
|
||||
<div th:replace="includes/footer :: footer" />
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user