#24 Transaction import: rules for automatic account matching

This commit is contained in:
2021-09-02 02:30:24 +02:00
parent 70218ad7dc
commit d88e2583b5
22 changed files with 184 additions and 52 deletions

View File

@@ -21,6 +21,7 @@ public class Account {
@OneToMany(fetch = FetchType.EAGER) @OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "account_id") @JoinColumn(name = "account_id")
private Set<AccountStatistic> accountStatistics; private Set<AccountStatistic> accountStatistics;
private String uploadMatchRegexps;
public Long getId() { public Long getId() {
return id; return id;
@@ -73,4 +74,12 @@ public class Account {
public void setAccountStatistics(Set<AccountStatistic> accountStatistics) { public void setAccountStatistics(Set<AccountStatistic> accountStatistics) {
this.accountStatistics = accountStatistics; this.accountStatistics = accountStatistics;
} }
public String getUploadMatchRegexps() {
return uploadMatchRegexps;
}
public void setUploadMatchRegexps(String uploadMatchRegexps) {
this.uploadMatchRegexps = uploadMatchRegexps;
}
} }

View File

@@ -11,6 +11,10 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
@RestController @RestController
@RequestMapping("accounts") @RequestMapping("accounts")
public class AccountController { public class AccountController {
@@ -37,15 +41,22 @@ public class AccountController {
} }
@RequestMapping("createAccount") @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 decoded = ControllerUtil.urlDecode(key);
final String decodedGroup = ControllerUtil.urlDecode(accountGroupName); final String decodedGroup = ControllerUtil.urlDecode(accountGroupName);
String decodedRegexps = null;
if (LOGGER.isDebugEnabled()) { try {
LOGGER.debug(String.format("/accounts/createAccount got parameters: %s, %s, %s", decoded, type, decodedGroup)); 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()) { if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/accounts/createAccount returns with %s", responseReason.name())); LOGGER.debug(String.format("/accounts/createAccount returns with %s", responseReason.name()));
@@ -55,15 +66,22 @@ public class AccountController {
} }
@RequestMapping("editAccount") @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 decoded = ControllerUtil.urlDecode(key);
final String decodedGroup = ControllerUtil.urlDecode(accountGroupName); final String decodedGroup = ControllerUtil.urlDecode(accountGroupName);
String decodedRegexps = null;
if (LOGGER.isDebugEnabled()) { try {
LOGGER.debug(String.format("/accounts/editAccount got parameters: %s, %s, %s", id, decoded, decodedGroup)); 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()) { if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("/accounts/editAccount returns with %s", responseReason.name())); LOGGER.debug(String.format("/accounts/editAccount returns with %s", responseReason.name()));

View File

@@ -73,6 +73,7 @@ public class AccountService {
* @param key the key of the new account * @param key the key of the new account
* @param type the type of the new account. Must be one of {@link AccountType}. * @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 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 * @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 * 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>. * <code>accountGroupName</code> does not identify a valid account group. Never returns <code>null</code>.
*/ */
@Transactional(propagation = Propagation.SUPPORTS) @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)) { if (!AccountType.isValidType(type)) {
return ResponseReason.INVALID_ACCOUNT_TYPE; return ResponseReason.INVALID_ACCOUNT_TYPE;
} }
@@ -101,15 +102,16 @@ public class AccountService {
account.setStatus(AccountStatus.OPEN); account.setStatus(AccountStatus.OPEN);
// and has a current balance of 0 // and has a current balance of 0
account.setCurrentBalance(0L); account.setCurrentBalance(0L);
account.setUploadMatchRegexps(regexps);
try { try {
this.accountRepository.save(account); this.accountRepository.save(account);
} catch (DataIntegrityViolationException dive) { } 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; return ResponseReason.DUPLICATE_ACCOUNT_KEY;
} catch (Exception e) { } 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; return ResponseReason.UNKNOWN_ERROR;
} }
@@ -123,6 +125,7 @@ public class AccountService {
* @param id the id of the account to edit * @param id the id of the account to edit
* @param key the new key of the account * @param key the new key of the account
* @param accountGroupName the new name of the account group to use * @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} * @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 * 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>. * if the given id does not identify a valid account. Never returns <code>null</code>.
*/ */
@Transactional(propagation = Propagation.REQUIRED) @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); final Account account = this.accountRepository.findById(id).orElse(null);
if(account == null) { if(account == null) {
@@ -146,15 +149,16 @@ public class AccountService {
account.setKey(key); account.setKey(key);
account.setAccountGroup(accountGroup); account.setAccountGroup(accountGroup);
account.setUploadMatchRegexps(regexps);
try { try {
this.accountRepository.save(account); this.accountRepository.save(account);
} catch (DataIntegrityViolationException dive) { } 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; return ResponseReason.DUPLICATE_ACCOUNT_KEY;
} catch (Exception e) { } 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; return ResponseReason.UNKNOWN_ERROR;
} }

View File

@@ -0,0 +1,2 @@
ALTER TABLE account
ADD COLUMN upload_match_regexps VARCHAR(2000);

View File

@@ -31,7 +31,7 @@ public class AccountService_createAccountTest {
// Nothing to do // Nothing to do
// Act // Act
ResponseReason response = this.classUnderTest.createAccount(null, null, null); ResponseReason response = this.classUnderTest.createAccount(null, null, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_TYPE, response); Assert.assertEquals(ResponseReason.INVALID_ACCOUNT_TYPE, response);
@@ -45,7 +45,7 @@ public class AccountService_createAccountTest {
.thenReturn(Mockito.mock(AccountGroup.class)); .thenReturn(Mockito.mock(AccountGroup.class));
// Act // Act
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1"); ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1", null);
// Assert // Assert
Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response); Assert.assertEquals(ResponseReason.UNKNOWN_ERROR, response);
@@ -58,7 +58,7 @@ public class AccountService_createAccountTest {
.thenReturn(Mockito.mock(AccountGroup.class)); .thenReturn(Mockito.mock(AccountGroup.class));
// Act // Act
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1"); ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1", null);
// Assert // Assert
Assert.assertEquals(ResponseReason.OK, response); Assert.assertEquals(ResponseReason.OK, response);
@@ -73,7 +73,7 @@ public class AccountService_createAccountTest {
.thenReturn(null); .thenReturn(null);
// Act // Act
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1"); ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1", null);
// Assert // Assert
Assert.assertEquals(ResponseReason.ACCOUNT_GROUP_NOT_FOUND, response); Assert.assertEquals(ResponseReason.ACCOUNT_GROUP_NOT_FOUND, response);
@@ -87,7 +87,7 @@ public class AccountService_createAccountTest {
.thenReturn(Mockito.mock(AccountGroup.class)); .thenReturn(Mockito.mock(AccountGroup.class));
// Act // Act
ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1"); ResponseReason response = this.classUnderTest.createAccount("Test", "BANK", "Group1", null);
// Assert // Assert
Assert.assertEquals(ResponseReason.DUPLICATE_ACCOUNT_KEY, response); Assert.assertEquals(ResponseReason.DUPLICATE_ACCOUNT_KEY, response);

View File

@@ -28,9 +28,12 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -90,12 +93,13 @@ public class AccountController {
} }
@PostMapping("/saveAccount") @PostMapping("/saveAccount")
public String saveAccount(NewAccountForm form, Model model) { public String saveAccount(NewAccountForm form, Model model) throws UnsupportedEncodingException {
final UriComponentsBuilder builder = UriComponentsBuilder final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CREATE_ACCOUNT)) .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_CREATE_ACCOUNT))
.queryParam("key", form.getKey()) .queryParam("key", form.getKey())
.queryParam("accountGroupName", form.getGroup()) .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 ResponseEntity<String> response = new StringTemplate().exchange(builder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(response); final ResponseReason responseReason = ResponseReason.fromResponseEntity(response);
@@ -230,12 +234,12 @@ public class AccountController {
@GetMapping("/editAccount") @GetMapping("/editAccount")
public String editAccount(Model model, String key) { 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"; 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<Account> exchange = new GetAccountByKeyTemplate().exchange(this.financerConfig, originalKey);
final ResponseEntity<Iterable<AccountGroup>> accountGroupResponse = new GetAllAccountGroupsTemplate() final ResponseEntity<Iterable<AccountGroup>> accountGroupResponse = new GetAllAccountGroupsTemplate()
.exchange(this.financerConfig); .exchange(this.financerConfig);
@@ -249,6 +253,7 @@ public class AccountController {
form.setGroup(newGroup.orElse(Optional.ofNullable(account.getAccountGroup()).map(AccountGroup::getName).orElse(null))); form.setGroup(newGroup.orElse(Optional.ofNullable(account.getAccountGroup()).map(AccountGroup::getName).orElse(null)));
form.setId(account.getId().toString()); form.setId(account.getId().toString());
form.setOriginalKey(originalKey); form.setOriginalKey(originalKey);
form.setRegexps(regexps.orElse(account.getUploadMatchRegexps()));
model.addAttribute("form", form); model.addAttribute("form", form);
@@ -258,18 +263,19 @@ public class AccountController {
} }
@PostMapping("/editAccount") @PostMapping("/editAccount")
public String editAccount(Model model, EditAccountForm form) { public String editAccount(Model model, EditAccountForm form) throws UnsupportedEncodingException {
final UriComponentsBuilder editBuilder = UriComponentsBuilder final UriComponentsBuilder editBuilder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_EDIT_ACCOUNT)) .fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.ACC_EDIT_ACCOUNT))
.queryParam("id", form.getId()) .queryParam("id", form.getId())
.queryParam("key", form.getKey()) .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 ResponseEntity<String> closeResponse = new StringTemplate().exchange(editBuilder);
final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse); final ResponseReason responseReason = ResponseReason.fromResponseEntity(closeResponse);
if (!ResponseReason.OK.equals(responseReason)) { 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"; return "account/editAccount";
} }

View File

@@ -211,8 +211,12 @@ public class TransactionController {
// TODO ENCODING?? // TODO ENCODING??
try { try {
final Iterable<Account> allAccounts =
FinancerRestTemplate.exchangeGet(this.financerConfig,
Function.ACC_GET_ALL,
new ParameterizedTypeReference<Iterable<Account>>() {});
final TransactionUploadWorker worker = this.workerFactory final TransactionUploadWorker worker = this.workerFactory
.getWorker(TransactionUploadFormat.valueOf(form.getFormat())); .getWorker(TransactionUploadFormat.valueOf(form.getFormat()), allAccounts);
final Collection<UploadedTransaction> uploadedTransactions = worker.process(form.getFile()); final Collection<UploadedTransaction> uploadedTransactions = worker.process(form.getFile());
@@ -229,6 +233,7 @@ public class TransactionController {
} }
catch(Exception e) { catch(Exception e) {
// TODO // TODO
e.printStackTrace();
final ResponseEntity<Iterable<RecurringTransaction>> response = new GetAllRecurringTransactionsTemplate() final ResponseEntity<Iterable<RecurringTransaction>> response = new GetAllRecurringTransactionsTemplate()
.exchange(this.financerConfig); .exchange(this.financerConfig);
final List<RecurringTransaction> recurringTransactionList = new ArrayList<>(); final List<RecurringTransaction> recurringTransactionList = new ArrayList<>();
@@ -248,7 +253,7 @@ public class TransactionController {
ControllerUtils.addCurrencySymbol(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig);
ControllerUtils.addDarkMode(model, this.financerConfig); ControllerUtils.addDarkMode(model, this.financerConfig);
return "transaction/createUploadedTransactions"; return "transaction/uploadTransactions";
} }
} }
@@ -303,6 +308,8 @@ public class TransactionController {
} }
catch(Exception e) { catch(Exception e) {
// TODO // TODO
e.printStackTrace();
return _uploadTransactions(model, Optional.empty(), form); return _uploadTransactions(model, Optional.empty(), form);
} }

View File

@@ -54,6 +54,7 @@ public class CreateUploadedTransactionForm {
entry.setAmount(uploadedTransaction.getAmount()); entry.setAmount(uploadedTransaction.getAmount());
entry.setDescription(uploadedTransaction.getDescription()); entry.setDescription(uploadedTransaction.getDescription());
entry.setDate(uploadedTransaction.getDate()); entry.setDate(uploadedTransaction.getDate());
entry.setToAccountKey(uploadedTransaction.getToAccountKey());
return entry; return entry;
} }

View File

@@ -5,6 +5,7 @@ public class EditAccountForm {
private String group; private String group;
private String id; private String id;
private String originalKey; private String originalKey;
private String regexps;
public String getKey() { public String getKey() {
return key; return key;
@@ -37,4 +38,12 @@ public class EditAccountForm {
public void setOriginalKey(String originalKey) { public void setOriginalKey(String originalKey) {
this.originalKey = originalKey; this.originalKey = originalKey;
} }
public String getRegexps() {
return regexps;
}
public void setRegexps(String regexps) {
this.regexps = regexps;
}
} }

View File

@@ -4,6 +4,7 @@ public class NewAccountForm {
private String key; private String key;
private String type; private String type;
private String group; private String group;
private String regexps;
public String getKey() { public String getKey() {
return key; return key;
@@ -28,4 +29,12 @@ public class NewAccountForm {
public void setGroup(String group) { public void setGroup(String group) {
this.group = group; this.group = group;
} }
public String getRegexps() {
return regexps;
}
public void setRegexps(String regexps) {
this.regexps = regexps;
}
} }

View File

@@ -3,14 +3,24 @@ package de.financer.transactionUpload;
import com.google.common.base.CharMatcher; import com.google.common.base.CharMatcher;
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 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 { public abstract class AbstractTransactionUploadWorker implements TransactionUploadWorker {
private FinancerConfig financerConfig; private final FinancerConfig financerConfig;
private TransactionUploadFormat format; 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.format = format;
this.financerConfig = financerConfig; this.financerConfig = financerConfig;
this.accounts = IterableUtils.toList(accounts);
} }
protected FinancerConfig.TransactionUpload.Format getFormatConfig() { protected FinancerConfig.TransactionUpload.Format getFormatConfig() {
@@ -24,4 +34,22 @@ public abstract class AbstractTransactionUploadWorker implements TransactionUplo
public FinancerConfig getFinancerConfig() { public FinancerConfig getFinancerConfig() {
return financerConfig; 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);
}
} }

View File

@@ -2,10 +2,12 @@ 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 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;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
@@ -17,6 +19,7 @@ import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class MT940CSVTransactionUploadWorker extends AbstractTransactionUploadWorker { 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 DESCRIPTION2_INDEX = 4;
private static final int AMOUNT_INDEX = 8; private static final int AMOUNT_INDEX = 8;
protected MT940CSVTransactionUploadWorker(FinancerConfig financerConfig) { protected MT940CSVTransactionUploadWorker(FinancerConfig financerConfig, Iterable<Account> accounts) {
super(TransactionUploadFormat.MT940_CSV, financerConfig); super(TransactionUploadFormat.MT940_CSV, financerConfig, accounts);
} }
@Override @Override
@@ -44,16 +47,23 @@ public class MT940CSVTransactionUploadWorker extends AbstractTransactionUploadWo
.parse(new InputStreamReader(is)); .parse(new InputStreamReader(is));
retVal.addAll(parser.stream().skip(1) 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 // Amount
Math.abs(formatAmount(removeQuotes(r.get(AMOUNT_INDEX)))), Math.abs(formatAmount(removeQuotes(r.get(AMOUNT_INDEX)))),
// Description // Description
buildDescription(r), accountAndDescription.getRight(),
// Date // Date
formatDate(LocalDate.parse(removeQuotes(r.get(DATE_INDEX)), formatDate(LocalDate.parse(removeQuotes(r.get(DATE_INDEX)),
DateTimeFormatter.ofPattern(this.getFormatConfig() DateTimeFormatter.ofPattern(this.getFormatConfig()
.getDateFormat()))) .getDateFormat()))),
) // To account key
Optional.ofNullable(accountAndDescription.getLeft())
.map(Account::getKey).orElse(null));
}
) )
.collect(Collectors.toList())); .collect(Collectors.toList()));
} catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) {

View File

@@ -1,5 +1,6 @@
package de.financer.transactionUpload; package de.financer.transactionUpload;
import de.financer.model.Account;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.util.Collection; import java.util.Collection;

View File

@@ -2,6 +2,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 org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -10,12 +11,12 @@ public class TransactionUploadWorkerFactory {
@Autowired @Autowired
private FinancerConfig financerConfig; private FinancerConfig financerConfig;
public TransactionUploadWorker getWorker(TransactionUploadFormat format) { public TransactionUploadWorker getWorker(TransactionUploadFormat format, Iterable<Account> accounts) {
AbstractTransactionUploadWorker worker; AbstractTransactionUploadWorker worker;
switch (format) { switch (format) {
case MT940_CSV: case MT940_CSV:
worker = new MT940CSVTransactionUploadWorker(this.financerConfig); worker = new MT940CSVTransactionUploadWorker(this.financerConfig, accounts);
break; break;
default: default:

View File

@@ -4,11 +4,13 @@ public class UploadedTransaction {
private Long amount; private Long amount;
private String description; private String description;
private String date; 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.amount = amount;
this.description = description; this.description = description;
this.date = date; this.date = date;
this.toAccountKey = toAccountKey;
} }
public Long getAmount() { public Long getAmount() {
@@ -34,4 +36,12 @@ public class UploadedTransaction {
public void setDate(String date) { public void setDate(String date) {
this.date = date; this.date = date;
} }
public String getToAccountKey() {
return toAccountKey;
}
public void setToAccountKey(String toAccountKey) {
this.toAccountKey = toAccountKey;
}
} }

View File

@@ -34,12 +34,14 @@ financer.account-new.title=financer\: create new account
financer.account-new.label.key=Key\: financer.account-new.label.key=Key\:
financer.account-new.label.type=Type\: financer.account-new.label.type=Type\:
financer.account-new.label.group=Group\: financer.account-new.label.group=Group\:
financer.account-new.label.upload-match-regexps=Import match regexps\:
financer.account-new.submit=Create account financer.account-new.submit=Create account
financer.account-edit.title=financer\: edit account financer.account-edit.title=financer\: edit account
financer.account-edit.label.key=Key\: financer.account-edit.label.key=Key\:
financer.account-edit.label.group=Group\: financer.account-edit.label.group=Group\:
financer.account-edit.submit=Edit account 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.title=financer\: create new account group
financer.account-group-new.label.name=Name\: financer.account-group-new.label.name=Name\:

View File

@@ -34,6 +34,7 @@ financer.account-new.title=financer\: Neues Konto erstellen
financer.account-new.label.key=Schl\u00FCssel\: financer.account-new.label.key=Schl\u00FCssel\:
financer.account-new.label.type=Typ\: financer.account-new.label.type=Typ\:
financer.account-new.label.group=Gruppe\: financer.account-new.label.group=Gruppe\:
financer.account-new.label.upload-match-regexps=Zuordnungsregexps\:
financer.account-new.submit=Konto erstellen financer.account-new.submit=Konto erstellen
financer.account-group-new.title=financer\: Neue Konto-Gruppe 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.key=Schl\u00FCssel\:
financer.account-edit.label.group=Gruppe\: financer.account-edit.label.group=Gruppe\:
financer.account-edit.submit=Konto bearbeiten 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.title=financer\: Neue Buchung erstellen
financer.transaction-new.label.from-account=Von Konto\: financer.transaction-new.label.from-account=Von Konto\:

View File

@@ -5,6 +5,9 @@ v48 -> v49:
- #25 Added the possibility to edit accounts - #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 - #23 Added an option to specify a recurring transaction during transaction upload that will close and open expense
periods 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: v47 -> v48:
- #20 Added new property 'transaction type' to a transaction, denoting the type of the transaction, e.g. asset swap, - #20 Added new property 'transaction type' to a transaction, denoting the type of the transaction, e.g. asset swap,

View File

@@ -13,17 +13,17 @@
6. Account groups 6. Account groups
7. Transactions 7. Transactions
8. Recurring transactions 8. Recurring transactions
9. Reporting 9. Transaction upload
10. FQL 10. Reporting
11. Setup 11. FQL
12. Links 12. Setup
13. Links
1. About 1. About
======== ========
This is the manual for the financer application - a simple app to support you in managing your personal finances. 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 The main goal of the financer application is to keep things simple.
automation. Instead it is merely a tool that provides basic key values to support you.
2. Overview 2. Overview
=========== ===========
@@ -183,18 +183,22 @@
8. Recurring transactions 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 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. 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: First install PostgreSQL. Then create a user for financer:
sudo -iu postgres sudo -iu postgres
@@ -210,7 +214,7 @@
\q \q
exit exit
12. Links 13. Links
========= =========
This chapter contains useful links: This chapter contains useful links:
- financer web page: https://financer.dev/ - financer web page: https://financer.dev/

View File

@@ -20,6 +20,8 @@
<select id="selectGroup" th:field="*{group}"> <select id="selectGroup" th:field="*{group}">
<option th:each="group : ${accountGroups}" th:value="${group.name}" th:text="${group.name}"/> <option th:each="group : ${accountGroups}" th:value="${group.name}" th:text="${group.name}"/>
</select> </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="inputId" th:field="*{id}"/>
<input type="hidden" id="originalKey" th:field="*{originalKey}"/> <input type="hidden" id="originalKey" th:field="*{originalKey}"/>
<input type="submit" th:value="#{financer.account-edit.submit}" /> <input type="submit" th:value="#{financer.account-edit.submit}" />

View File

@@ -24,6 +24,8 @@
<select id="selectGroup" th:field="*{group}"> <select id="selectGroup" th:field="*{group}">
<option th:each="group : ${accountGroups}" th:value="${group.name}" th:text="${group.name}"/> <option th:each="group : ${accountGroups}" th:value="${group.name}" th:text="${group.name}"/>
</select> </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}" /> <input type="submit" th:value="#{financer.account-new.submit}" />
</form> </form>
<div th:replace="includes/footer :: footer"/> <div th:replace="includes/footer :: footer"/>

View File

@@ -17,6 +17,7 @@
method="post" enctype="multipart/form-data"> method="post" enctype="multipart/form-data">
<table id="create-upload-transactions-table"> <table id="create-upload-transactions-table">
<tr> <tr>
<th />
<th th:text="#{financer.create-upload-transactions.table-header.create}"/> <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.fromAccount}"/>
<th th:text="#{financer.create-upload-transactions.table-header.toAccount}"/> <th th:text="#{financer.create-upload-transactions.table-header.toAccount}"/>
@@ -28,6 +29,7 @@
<th th:text="#{financer.create-upload-transactions.table-header.file}"/> <th th:text="#{financer.create-upload-transactions.table-header.file}"/>
</tr> </tr>
<tr th:each="entry, i : ${form.entries}"> <tr th:each="entry, i : ${form.entries}">
<td th:text="${i.count}"/>
<td> <td>
<input type="checkbox" th:field="*{entries[__${i.index}__].create}"/> <input type="checkbox" th:field="*{entries[__${i.index}__].create}"/>
</td> </td>