#24 Transaction import: rules for automatic account matching
This commit is contained in:
@@ -21,6 +21,7 @@ public class Account {
|
||||
@OneToMany(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "account_id")
|
||||
private Set<AccountStatistic> accountStatistics;
|
||||
private String uploadMatchRegexps;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
@@ -73,4 +74,12 @@ public class Account {
|
||||
public void setAccountStatistics(Set<AccountStatistic> accountStatistics) {
|
||||
this.accountStatistics = accountStatistics;
|
||||
}
|
||||
|
||||
public String getUploadMatchRegexps() {
|
||||
return uploadMatchRegexps;
|
||||
}
|
||||
|
||||
public void setUploadMatchRegexps(String uploadMatchRegexps) {
|
||||
this.uploadMatchRegexps = uploadMatchRegexps;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("accounts")
|
||||
public class AccountController {
|
||||
@@ -37,15 +41,22 @@ public class AccountController {
|
||||
}
|
||||
|
||||
@RequestMapping("createAccount")
|
||||
public ResponseEntity createAccount(String key, String type, String accountGroupName) {
|
||||
public ResponseEntity createAccount(String key, String type, String accountGroupName, String regexps) {
|
||||
final String decoded = ControllerUtil.urlDecode(key);
|
||||
final String decodedGroup = ControllerUtil.urlDecode(accountGroupName);
|
||||
String decodedRegexps = null;
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/createAccount got parameters: %s, %s, %s", decoded, type, decodedGroup));
|
||||
try {
|
||||
decodedRegexps = URLDecoder.decode(ControllerUtil.urlDecode(regexps), StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Cannot happen
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.accountService.createAccount(decoded, type, decodedGroup);
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/createAccount got parameters: %s, %s, %s, %s", decoded, type, decodedGroup, decodedRegexps));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.accountService.createAccount(decoded, type, decodedGroup, decodedRegexps);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/createAccount returns with %s", responseReason.name()));
|
||||
@@ -55,15 +66,22 @@ public class AccountController {
|
||||
}
|
||||
|
||||
@RequestMapping("editAccount")
|
||||
public ResponseEntity editAccount(Long id, String key, String accountGroupName) {
|
||||
public ResponseEntity editAccount(Long id, String key, String accountGroupName, String regexps) {
|
||||
final String decoded = ControllerUtil.urlDecode(key);
|
||||
final String decodedGroup = ControllerUtil.urlDecode(accountGroupName);
|
||||
String decodedRegexps = null;
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/editAccount got parameters: %s, %s, %s", id, decoded, decodedGroup));
|
||||
try {
|
||||
decodedRegexps = URLDecoder.decode(ControllerUtil.urlDecode(regexps), StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// Cannot happen
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.accountService.editAccount(id, decoded, decodedGroup);
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/editAccount got parameters: %s, %s, %s, %s", id, decoded, decodedGroup, decodedRegexps));
|
||||
}
|
||||
|
||||
final ResponseReason responseReason = this.accountService.editAccount(id, decoded, decodedGroup, decodedRegexps);
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("/accounts/editAccount returns with %s", responseReason.name()));
|
||||
|
||||
@@ -73,6 +73,7 @@ public class AccountService {
|
||||
* @param key the key of the new account
|
||||
* @param type the type of the new account. Must be one of {@link AccountType}.
|
||||
* @param accountGroupName the name of the account group to use
|
||||
* @param regexps the regular expressions to assign to the new account
|
||||
*
|
||||
* @return {@link ResponseReason#INVALID_ACCOUNT_TYPE} if the given type is not a valid {@link AccountType}, {@link
|
||||
* ResponseReason#UNKNOWN_ERROR} if an unexpected error occurs, {@link ResponseReason#OK} if the operation completed
|
||||
@@ -81,7 +82,7 @@ public class AccountService {
|
||||
* <code>accountGroupName</code> does not identify a valid account group. Never returns <code>null</code>.
|
||||
*/
|
||||
@Transactional(propagation = Propagation.SUPPORTS)
|
||||
public ResponseReason createAccount(String key, String type, String accountGroupName) {
|
||||
public ResponseReason createAccount(String key, String type, String accountGroupName, String regexps) {
|
||||
if (!AccountType.isValidType(type)) {
|
||||
return ResponseReason.INVALID_ACCOUNT_TYPE;
|
||||
}
|
||||
@@ -101,15 +102,16 @@ public class AccountService {
|
||||
account.setStatus(AccountStatus.OPEN);
|
||||
// and has a current balance of 0
|
||||
account.setCurrentBalance(0L);
|
||||
account.setUploadMatchRegexps(regexps);
|
||||
|
||||
try {
|
||||
this.accountRepository.save(account);
|
||||
} catch (DataIntegrityViolationException dive) {
|
||||
LOGGER.error(String.format("Duplicate key! %s|%s|%s", key, type, accountGroupName), dive);
|
||||
LOGGER.error(String.format("Duplicate key! %s|%s|%s|%s", key, type, accountGroupName, regexps), dive);
|
||||
|
||||
return ResponseReason.DUPLICATE_ACCOUNT_KEY;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(String.format("Could not save account %s|%s|%s", key, type, accountGroupName), e);
|
||||
LOGGER.error(String.format("Could not save account %s|%s|%s|%s", key, type, accountGroupName, regexps), e);
|
||||
|
||||
return ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
@@ -123,6 +125,7 @@ public class AccountService {
|
||||
* @param id the id of the account to edit
|
||||
* @param key the new key of the account
|
||||
* @param accountGroupName the new name of the account group to use
|
||||
* @param regexps the regular expressions of the accounts
|
||||
*
|
||||
* @return {@link ResponseReason#OK} if the operation completed successfully, {@link ResponseReason#UNKNOWN_ERROR}
|
||||
* if an unexpected error occurs, {@link ResponseReason#DUPLICATE_ACCOUNT_KEY} if an account with the given key
|
||||
@@ -131,7 +134,7 @@ public class AccountService {
|
||||
* if the given id does not identify a valid account. Never returns <code>null</code>.
|
||||
*/
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public ResponseReason editAccount(Long id, String key, String accountGroupName) {
|
||||
public ResponseReason editAccount(Long id, String key, String accountGroupName, String regexps) {
|
||||
final Account account = this.accountRepository.findById(id).orElse(null);
|
||||
|
||||
if(account == null) {
|
||||
@@ -146,15 +149,16 @@ public class AccountService {
|
||||
|
||||
account.setKey(key);
|
||||
account.setAccountGroup(accountGroup);
|
||||
account.setUploadMatchRegexps(regexps);
|
||||
|
||||
try {
|
||||
this.accountRepository.save(account);
|
||||
} catch (DataIntegrityViolationException dive) {
|
||||
LOGGER.error(String.format("Duplicate key! %s|%s", key, accountGroupName), dive);
|
||||
LOGGER.error(String.format("Duplicate key! %s|%s|%s", key, accountGroupName, regexps), dive);
|
||||
|
||||
return ResponseReason.DUPLICATE_ACCOUNT_KEY;
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(String.format("Could not save account %s|%s", key, accountGroupName), e);
|
||||
LOGGER.error(String.format("Could not save account %s|%s|%s", key, accountGroupName, regexps), e);
|
||||
|
||||
return ResponseReason.UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE account
|
||||
ADD COLUMN upload_match_regexps VARCHAR(2000);
|
||||
@@ -31,7 +31,7 @@ public class AccountService_createAccountTest {
|
||||
// Nothing to do
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount(null, null, null);
|
||||
ResponseReason response = this.classUnderTest.createAccount(null, null, null, null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_TYPE, response);
|
||||
@@ -45,7 +45,7 @@ public class AccountService_createAccountTest {
|
||||
.thenReturn(Mockito.mock(AccountGroup.class));
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1", null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
|
||||
@@ -58,7 +58,7 @@ public class AccountService_createAccountTest {
|
||||
.thenReturn(Mockito.mock(AccountGroup.class));
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1", null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.OK, response);
|
||||
@@ -73,7 +73,7 @@ public class AccountService_createAccountTest {
|
||||
.thenReturn(null);
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1", null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.ACCOUNT_GROUP_NOT_FOUND, response);
|
||||
@@ -87,7 +87,7 @@ public class AccountService_createAccountTest {
|
||||
.thenReturn(Mockito.mock(AccountGroup.class));
|
||||
|
||||
// Act
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1");
|
||||
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1", null);
|
||||
|
||||
// Assert
|
||||
Assert.assertEquals(ResponseReason.DUPLICATE_ACCOUNT_KEY, response);
|
||||
|
||||
@@ -28,9 +28,12 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -90,12 +93,13 @@ public class AccountController {
|
||||
}
|
||||
|
||||
@PostMapping("/saveAccount")
|
||||
public String saveAccount(NewAccountForm form, Model model) {
|
||||
public String saveAccount(NewAccountForm form, Model model) throws UnsupportedEncodingException {
|
||||
final UriComponentsBuilder builder = UriComponentsBuilder
|
||||
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CREATE_ACCOUNT))
|
||||
.queryParam("key", form.getKey())
|
||||
.queryParam("accountGroupName", form.getGroup())
|
||||
.queryParam("type", form.getType());
|
||||
.queryParam("type", form.getType())
|
||||
.queryParam("regexps", URLEncoder.encode(form.getRegexps(), StandardCharsets.UTF_8.name()));
|
||||
|
||||
final ResponseEntity<String> response = new StringTemplate().exchange(builder);
|
||||
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
|
||||
@@ -230,12 +234,12 @@ public class AccountController {
|
||||
|
||||
@GetMapping("/editAccount")
|
||||
public String editAccount(Model model, String key) {
|
||||
_editAccount(model, key, Optional.empty(), Optional.empty());
|
||||
_editAccount(model, key, Optional.empty(), Optional.empty(), Optional.empty());
|
||||
|
||||
return "account/editAccount";
|
||||
}
|
||||
|
||||
private void _editAccount(Model model, String originalKey, Optional<String> newKey, Optional<String> newGroup) {
|
||||
private void _editAccount(Model model, String originalKey, Optional<String> newKey, Optional<String> newGroup, Optional<String> regexps) {
|
||||
final ResponseEntity<Account> exchange = new GetAccountByKeyTemplate().exchange(this.financerConfig, originalKey);
|
||||
final ResponseEntity<Iterable<AccountGroup>> accountGroupResponse = new GetAllAccountGroupsTemplate()
|
||||
.exchange(this.financerConfig);
|
||||
@@ -249,6 +253,7 @@ public class AccountController {
|
||||
form.setGroup(newGroup.orElse(Optional.ofNullable(account.getAccountGroup()).map(AccountGroup::getName).orElse(null)));
|
||||
form.setId(account.getId().toString());
|
||||
form.setOriginalKey(originalKey);
|
||||
form.setRegexps(regexps.orElse(account.getUploadMatchRegexps()));
|
||||
|
||||
model.addAttribute("form", form);
|
||||
|
||||
@@ -258,18 +263,19 @@ public class AccountController {
|
||||
}
|
||||
|
||||
@PostMapping("/editAccount")
|
||||
public String editAccount(Model model, EditAccountForm form) {
|
||||
public String editAccount(Model model, EditAccountForm form) throws UnsupportedEncodingException {
|
||||
final UriComponentsBuilder editBuilder = UriComponentsBuilder
|
||||
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_EDIT_ACCOUNT))
|
||||
.queryParam("id", form.getId())
|
||||
.queryParam("key", form.getKey())
|
||||
.queryParam("accountGroupName", form.getGroup());
|
||||
.queryParam("accountGroupName", form.getGroup())
|
||||
.queryParam("regexps", URLEncoder.encode(form.getRegexps(), StandardCharsets.UTF_8.name()));
|
||||
|
||||
final ResponseEntity<String> closeResponse = new StringTemplate().exchange(editBuilder);
|
||||
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
|
||||
|
||||
if (!ResponseReason.OK.equals(responseReason)) {
|
||||
_editAccount(model, form.getOriginalKey(), Optional.of(form.getKey()), Optional.of(form.getGroup()));
|
||||
_editAccount(model, form.getOriginalKey(), Optional.of(form.getKey()), Optional.of(form.getGroup()), Optional.of(form.getRegexps()));
|
||||
|
||||
return "account/editAccount";
|
||||
}
|
||||
|
||||
@@ -211,8 +211,12 @@ public class TransactionController {
|
||||
// TODO ENCODING??
|
||||
|
||||
try {
|
||||
final Iterable<Account> allAccounts =
|
||||
FinancerRestTemplate.exchangeGet(this.financerConfig,
|
||||
Function.ACC_GET_ALL,
|
||||
new ParameterizedTypeReference<Iterable<Account>>() {});
|
||||
final TransactionUploadWorker worker = this.workerFactory
|
||||
.getWorker(TransactionUploadFormat.valueOf(form.getFormat()));
|
||||
.getWorker(TransactionUploadFormat.valueOf(form.getFormat()), allAccounts);
|
||||
|
||||
final Collection<UploadedTransaction> uploadedTransactions = worker.process(form.getFile());
|
||||
|
||||
@@ -229,6 +233,7 @@ public class TransactionController {
|
||||
}
|
||||
catch(Exception e) {
|
||||
// TODO
|
||||
e.printStackTrace();
|
||||
final ResponseEntity<Iterable<RecurringTransaction>> response = new GetAllRecurringTransactionsTemplate()
|
||||
.exchange(this.financerConfig);
|
||||
final List<RecurringTransaction> recurringTransactionList = new ArrayList<>();
|
||||
@@ -248,7 +253,7 @@ public class TransactionController {
|
||||
ControllerUtils.addCurrencySymbol(model, this.financerConfig);
|
||||
ControllerUtils.addDarkMode(model, this.financerConfig);
|
||||
|
||||
return "transaction/createUploadedTransactions";
|
||||
return "transaction/uploadTransactions";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,6 +308,8 @@ public class TransactionController {
|
||||
}
|
||||
catch(Exception e) {
|
||||
// TODO
|
||||
e.printStackTrace();
|
||||
|
||||
return _uploadTransactions(model, Optional.empty(), form);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ public class CreateUploadedTransactionForm {
|
||||
entry.setAmount(uploadedTransaction.getAmount());
|
||||
entry.setDescription(uploadedTransaction.getDescription());
|
||||
entry.setDate(uploadedTransaction.getDate());
|
||||
entry.setToAccountKey(uploadedTransaction.getToAccountKey());
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ public class EditAccountForm {
|
||||
private String group;
|
||||
private String id;
|
||||
private String originalKey;
|
||||
private String regexps;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
@@ -37,4 +38,12 @@ public class EditAccountForm {
|
||||
public void setOriginalKey(String originalKey) {
|
||||
this.originalKey = originalKey;
|
||||
}
|
||||
|
||||
public String getRegexps() {
|
||||
return regexps;
|
||||
}
|
||||
|
||||
public void setRegexps(String regexps) {
|
||||
this.regexps = regexps;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ public class NewAccountForm {
|
||||
private String key;
|
||||
private String type;
|
||||
private String group;
|
||||
private String regexps;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
@@ -28,4 +29,12 @@ public class NewAccountForm {
|
||||
public void setGroup(String group) {
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
public String getRegexps() {
|
||||
return regexps;
|
||||
}
|
||||
|
||||
public void setRegexps(String regexps) {
|
||||
this.regexps = regexps;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,24 @@ package de.financer.transactionUpload;
|
||||
import com.google.common.base.CharMatcher;
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dto.TransactionUploadFormat;
|
||||
import de.financer.model.Account;
|
||||
import org.apache.commons.collections4.IterableUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public abstract class AbstractTransactionUploadWorker implements TransactionUploadWorker {
|
||||
private FinancerConfig financerConfig;
|
||||
private TransactionUploadFormat format;
|
||||
private final FinancerConfig financerConfig;
|
||||
private final TransactionUploadFormat format;
|
||||
private final List<Account> accounts;
|
||||
|
||||
protected AbstractTransactionUploadWorker(TransactionUploadFormat format, FinancerConfig financerConfig) {
|
||||
protected AbstractTransactionUploadWorker(TransactionUploadFormat format, FinancerConfig financerConfig, Iterable<Account> accounts) {
|
||||
this.format = format;
|
||||
this.financerConfig = financerConfig;
|
||||
this.accounts = IterableUtils.toList(accounts);
|
||||
}
|
||||
|
||||
protected FinancerConfig.TransactionUpload.Format getFormatConfig() {
|
||||
@@ -24,4 +34,22 @@ public abstract class AbstractTransactionUploadWorker implements TransactionUplo
|
||||
public FinancerConfig getFinancerConfig() {
|
||||
return financerConfig;
|
||||
}
|
||||
|
||||
protected Pair<Account, String> getAccountAndDescription(String rawDescription) {
|
||||
for (Account a : this.accounts) {
|
||||
final String[] regexps = Optional.ofNullable(a.getUploadMatchRegexps()).orElse("").split("\r\n");
|
||||
|
||||
if (regexps.length > 0) {
|
||||
for(String regex : regexps) {
|
||||
Matcher matcher = Pattern.compile(regex).matcher(rawDescription);
|
||||
|
||||
if(matcher.matches()) {
|
||||
return Pair.of(a, matcher.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Pair.of(null, rawDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package de.financer.transactionUpload;
|
||||
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dto.TransactionUploadFormat;
|
||||
import de.financer.model.Account;
|
||||
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.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -17,6 +19,7 @@ import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MT940CSVTransactionUploadWorker extends AbstractTransactionUploadWorker {
|
||||
@@ -28,8 +31,8 @@ public class MT940CSVTransactionUploadWorker extends AbstractTransactionUploadWo
|
||||
private static final int DESCRIPTION2_INDEX = 4;
|
||||
private static final int AMOUNT_INDEX = 8;
|
||||
|
||||
protected MT940CSVTransactionUploadWorker(FinancerConfig financerConfig) {
|
||||
super(TransactionUploadFormat.MT940_CSV, financerConfig);
|
||||
protected MT940CSVTransactionUploadWorker(FinancerConfig financerConfig, Iterable<Account> accounts) {
|
||||
super(TransactionUploadFormat.MT940_CSV, financerConfig, accounts);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,16 +47,23 @@ public class MT940CSVTransactionUploadWorker extends AbstractTransactionUploadWo
|
||||
.parse(new InputStreamReader(is));
|
||||
|
||||
retVal.addAll(parser.stream().skip(1)
|
||||
.map(r -> new UploadedTransaction(
|
||||
.map(r -> {
|
||||
final Pair<Account, String> accountAndDescription =
|
||||
this.getAccountAndDescription(buildDescription(r));
|
||||
|
||||
return new UploadedTransaction(
|
||||
// Amount
|
||||
Math.abs(formatAmount(removeQuotes(r.get(AMOUNT_INDEX)))),
|
||||
// Description
|
||||
buildDescription(r),
|
||||
accountAndDescription.getRight(),
|
||||
// Date
|
||||
formatDate(LocalDate.parse(removeQuotes(r.get(DATE_INDEX)),
|
||||
DateTimeFormatter.ofPattern(this.getFormatConfig()
|
||||
.getDateFormat())))
|
||||
)
|
||||
.getDateFormat()))),
|
||||
// To account key
|
||||
Optional.ofNullable(accountAndDescription.getLeft())
|
||||
.map(Account::getKey).orElse(null));
|
||||
}
|
||||
)
|
||||
.collect(Collectors.toList()));
|
||||
} catch (IOException | RuntimeException e) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.financer.transactionUpload;
|
||||
|
||||
import de.financer.model.Account;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.financer.transactionUpload;
|
||||
|
||||
import de.financer.config.FinancerConfig;
|
||||
import de.financer.dto.TransactionUploadFormat;
|
||||
import de.financer.model.Account;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -10,12 +11,12 @@ public class TransactionUploadWorkerFactory {
|
||||
@Autowired
|
||||
private FinancerConfig financerConfig;
|
||||
|
||||
public TransactionUploadWorker getWorker(TransactionUploadFormat format) {
|
||||
public TransactionUploadWorker getWorker(TransactionUploadFormat format, Iterable<Account> accounts) {
|
||||
AbstractTransactionUploadWorker worker;
|
||||
|
||||
switch (format) {
|
||||
case MT940_CSV:
|
||||
worker = new MT940CSVTransactionUploadWorker(this.financerConfig);
|
||||
worker = new MT940CSVTransactionUploadWorker(this.financerConfig, accounts);
|
||||
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -4,11 +4,13 @@ public class UploadedTransaction {
|
||||
private Long amount;
|
||||
private String description;
|
||||
private String date;
|
||||
private String toAccountKey;
|
||||
|
||||
public UploadedTransaction(Long amount, String description, String date) {
|
||||
public UploadedTransaction(Long amount, String description, String date, String toAccountKey) {
|
||||
this.amount = amount;
|
||||
this.description = description;
|
||||
this.date = date;
|
||||
this.toAccountKey = toAccountKey;
|
||||
}
|
||||
|
||||
public Long getAmount() {
|
||||
@@ -34,4 +36,12 @@ public class UploadedTransaction {
|
||||
public void setDate(String date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getToAccountKey() {
|
||||
return toAccountKey;
|
||||
}
|
||||
|
||||
public void setToAccountKey(String toAccountKey) {
|
||||
this.toAccountKey = toAccountKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,14 @@ financer.account-new.title=financer\: create new account
|
||||
financer.account-new.label.key=Key\:
|
||||
financer.account-new.label.type=Type\:
|
||||
financer.account-new.label.group=Group\:
|
||||
financer.account-new.label.upload-match-regexps=Import match regexps\:
|
||||
financer.account-new.submit=Create account
|
||||
|
||||
financer.account-edit.title=financer\: edit account
|
||||
financer.account-edit.label.key=Key\:
|
||||
financer.account-edit.label.group=Group\:
|
||||
financer.account-edit.submit=Edit account
|
||||
financer.account-edit.label.upload-match-regexps=Import match regexps\:
|
||||
|
||||
financer.account-group-new.title=financer\: create new account group
|
||||
financer.account-group-new.label.name=Name\:
|
||||
|
||||
@@ -34,6 +34,7 @@ financer.account-new.title=financer\: Neues Konto erstellen
|
||||
financer.account-new.label.key=Schl\u00FCssel\:
|
||||
financer.account-new.label.type=Typ\:
|
||||
financer.account-new.label.group=Gruppe\:
|
||||
financer.account-new.label.upload-match-regexps=Zuordnungsregexps\:
|
||||
financer.account-new.submit=Konto erstellen
|
||||
|
||||
financer.account-group-new.title=financer\: Neue Konto-Gruppe erstellen
|
||||
@@ -44,6 +45,7 @@ financer.account-edit.title=financer\: Bearbeite Konto
|
||||
financer.account-edit.label.key=Schl\u00FCssel\:
|
||||
financer.account-edit.label.group=Gruppe\:
|
||||
financer.account-edit.submit=Konto bearbeiten
|
||||
financer.account-edit.label.upload-match-regexps=Zuordnungsregexps\:
|
||||
|
||||
financer.transaction-new.title=financer\: Neue Buchung erstellen
|
||||
financer.transaction-new.label.from-account=Von Konto\:
|
||||
|
||||
@@ -5,6 +5,9 @@ v48 -> v49:
|
||||
- #25 Added the possibility to edit accounts
|
||||
- #23 Added an option to specify a recurring transaction during transaction upload that will close and open expense
|
||||
periods
|
||||
- Added a new column to the transaction upload creation form that is a consecutive number
|
||||
- #24 Added the possibility to add regular expressions to accounts so that transactions uploaded can be automatically
|
||||
matched to accounts
|
||||
|
||||
v47 -> v48:
|
||||
- #20 Added new property 'transaction type' to a transaction, denoting the type of the transaction, e.g. asset swap,
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
6. Account groups
|
||||
7. Transactions
|
||||
8. Recurring transactions
|
||||
9. Reporting
|
||||
10. FQL
|
||||
11. Setup
|
||||
12. Links
|
||||
9. Transaction upload
|
||||
10. Reporting
|
||||
11. FQL
|
||||
12. Setup
|
||||
13. Links
|
||||
|
||||
1. About
|
||||
========
|
||||
This is the manual for the financer application - a simple app to support you in managing your personal finances.
|
||||
|
||||
The main goal of the financer application is to keep things simple by not attempting to provide sophisticated
|
||||
automation. Instead it is merely a tool that provides basic key values to support you.
|
||||
The main goal of the financer application is to keep things simple.
|
||||
|
||||
2. Overview
|
||||
===========
|
||||
@@ -183,18 +183,22 @@
|
||||
8. Recurring transactions
|
||||
=========================
|
||||
|
||||
9. Reporting
|
||||
============
|
||||
9. Transaction upload
|
||||
=====================
|
||||
File types + regex
|
||||
|
||||
10. FQL
|
||||
10. Reporting
|
||||
=============
|
||||
|
||||
11. FQL
|
||||
=======
|
||||
|
||||
11. Setup
|
||||
12. Setup
|
||||
=========
|
||||
This chapter explains how to setup a financer instance. It requires PostgreSQL as a database backend and a Java
|
||||
Servlet Container (e.g. Apache Tomcat) as a runtime environment.
|
||||
|
||||
11.1 Database setup
|
||||
12.1 Database setup
|
||||
-------------------
|
||||
First install PostgreSQL. Then create a user for financer:
|
||||
sudo -iu postgres
|
||||
@@ -210,7 +214,7 @@
|
||||
\q
|
||||
exit
|
||||
|
||||
12. Links
|
||||
13. Links
|
||||
=========
|
||||
This chapter contains useful links:
|
||||
- financer web page: https://financer.dev/
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
<select id="selectGroup" th:field="*{group}">
|
||||
<option th:each="group : ${accountGroups}" th:value="${group.name}" th:text="${group.name}"/>
|
||||
</select>
|
||||
<label for="inputRegexps" th:text="#{financer.account-edit.label.upload-match-regexps}"/>
|
||||
<textarea type="text" id="inputRegexps" th:field="*{regexps}" rows="8" cols="70"/>
|
||||
<input type="hidden" id="inputId" th:field="*{id}"/>
|
||||
<input type="hidden" id="originalKey" th:field="*{originalKey}"/>
|
||||
<input type="submit" th:value="#{financer.account-edit.submit}" />
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
<select id="selectGroup" th:field="*{group}">
|
||||
<option th:each="group : ${accountGroups}" th:value="${group.name}" th:text="${group.name}"/>
|
||||
</select>
|
||||
<label for="inputRegexps" th:text="#{financer.account-new.label.upload-match-regexps}"/>
|
||||
<textarea type="text" id="inputRegexps" th:field="*{regexps}" rows="8" cols="70"/>
|
||||
<input type="submit" th:value="#{financer.account-new.submit}" />
|
||||
</form>
|
||||
<div th:replace="includes/footer :: footer"/>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
method="post" enctype="multipart/form-data">
|
||||
<table id="create-upload-transactions-table">
|
||||
<tr>
|
||||
<th />
|
||||
<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}"/>
|
||||
@@ -28,6 +29,7 @@
|
||||
<th th:text="#{financer.create-upload-transactions.table-header.file}"/>
|
||||
</tr>
|
||||
<tr th:each="entry, i : ${form.entries}">
|
||||
<td th:text="${i.count}"/>
|
||||
<td>
|
||||
<input type="checkbox" th:field="*{entries[__${i.index}__].create}"/>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user