#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)
@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;
}
}

View File

@@ -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()));

View File

@@ -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;
}

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
// 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);

View File

@@ -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";
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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\:

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.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\:

View File

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

View File

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

View File

@@ -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}" />

View File

@@ -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"/>

View File

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