#24 Transaction import: rules for automatic account matching
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// 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);
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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\:
|
||||||
|
|||||||
@@ -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\:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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}" />
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user