Add file uploading to transaction creation

This commit is contained in:
2020-04-10 02:20:34 +02:00
parent 6ebbc47396
commit d8d75f67b4
33 changed files with 417 additions and 39 deletions

View File

@@ -38,7 +38,11 @@ public enum ResponseReason {
LIMIT_NOT_NUMERIC(HttpStatus.BAD_REQUEST), LIMIT_NOT_NUMERIC(HttpStatus.BAD_REQUEST),
INVALID_TAX_RELEVANT_VALUE(HttpStatus.BAD_REQUEST), INVALID_TAX_RELEVANT_VALUE(HttpStatus.BAD_REQUEST),
INVALID_ACCOUNTS_AND_VALUE(HttpStatus.BAD_REQUEST), INVALID_ACCOUNTS_AND_VALUE(HttpStatus.BAD_REQUEST),
FQL_MALFORMED(HttpStatus.BAD_REQUEST); FQL_MALFORMED(HttpStatus.BAD_REQUEST),
INVALID_FILE_CONTENT(HttpStatus.BAD_REQUEST),
INVALID_FILE_NAME(HttpStatus.BAD_REQUEST),
INVALID_HAS_FILE_VALUE(HttpStatus.BAD_REQUEST),
FILE_NOT_FOUND(HttpStatus.BAD_REQUEST);
private final HttpStatus httpStatus; private final HttpStatus httpStatus;

View File

@@ -9,6 +9,8 @@ public class SaveTransactionRequestDto {
private String date; private String date;
private String description; private String description;
private Boolean taxRelevant; private Boolean taxRelevant;
private String fileContent;
private String fileName;
public String getFromAccountKey() { public String getFromAccountKey() {
return fromAccountKey; return fromAccountKey;
@@ -62,4 +64,20 @@ public class SaveTransactionRequestDto {
public String toString() { public String toString() {
return ReflectionToStringBuilder.toString(this); return ReflectionToStringBuilder.toString(this);
} }
public String getFileContent() {
return fileContent;
}
public void setFileContent(String fileContent) {
this.fileContent = fileContent;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
} }

View File

@@ -9,6 +9,7 @@ public class SearchTransactionsRequestDto {
private String limit; private String limit;
private String order; private String order;
private String taxRelevant; private String taxRelevant;
private String hasFile;
public String getFromAccountKey() { public String getFromAccountKey() {
return fromAccountKey; return fromAccountKey;
@@ -62,4 +63,12 @@ public class SearchTransactionsRequestDto {
public String toString() { public String toString() {
return ReflectionToStringBuilder.toString(this); return ReflectionToStringBuilder.toString(this);
} }
public String getHasFile() {
return hasFile;
}
public void setHasFile(String hasFile) {
this.hasFile = hasFile;
}
} }

View File

@@ -13,6 +13,7 @@ public class SearchTransactionsResponseDto {
private long amount; private long amount;
private boolean taxRelevant; private boolean taxRelevant;
private boolean recurring; private boolean recurring;
private Long fileId;
public SearchTransactionsResponseDto() { public SearchTransactionsResponseDto() {
// No-arg constructor for Jackson // No-arg constructor for Jackson
@@ -25,7 +26,8 @@ public class SearchTransactionsResponseDto {
String description, String description,
long amount, long amount,
boolean taxRelevant, boolean taxRelevant,
boolean recurring) { boolean recurring,
Long fileId) {
this.id = id; this.id = id;
this.fromAccount = fromAccount; this.fromAccount = fromAccount;
this.toAccount = toAccount; this.toAccount = toAccount;
@@ -34,6 +36,7 @@ public class SearchTransactionsResponseDto {
this.amount = amount; this.amount = amount;
this.taxRelevant = taxRelevant; this.taxRelevant = taxRelevant;
this.recurring = recurring; this.recurring = recurring;
this.fileId = fileId;
} }
public Long getId() { public Long getId() {
@@ -99,4 +102,12 @@ public class SearchTransactionsResponseDto {
public void setRecurring(boolean recurring) { public void setRecurring(boolean recurring) {
this.recurring = recurring; this.recurring = recurring;
} }
public Long getFileId() {
return fileId;
}
public void setFileId(Long fileId) {
this.fileId = fileId;
}
} }

View File

@@ -0,0 +1,33 @@
package de.financer.model;
import javax.persistence.*;
@Entity
@Table(name = "file")
public class File {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String content;
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}

View File

@@ -28,6 +28,13 @@ public class Transaction {
//@formatter:on //@formatter:on
private Set<Period> periods; private Set<Period> periods;
private boolean taxRelevant; private boolean taxRelevant;
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, fetch = FetchType.EAGER)
//@formatter:off
@JoinTable(name = "link_transaction_file",
joinColumns = @JoinColumn(name = "transaction_id"),
inverseJoinColumns = @JoinColumn(name = "file_id"))
//@formatter:on
private Set<File> files;
public Long getId() { public Long getId() {
return id; return id;
@@ -96,4 +103,12 @@ public class Transaction {
public void setTaxRelevant(boolean taxRelevant) { public void setTaxRelevant(boolean taxRelevant) {
this.taxRelevant = taxRelevant; this.taxRelevant = taxRelevant;
} }
public Set<File> getFiles() {
return files;
}
public void setFiles(Set<File> files) {
this.files = files;
}
} }

View File

@@ -0,0 +1,33 @@
package de.financer.controller;
import de.financer.model.File;
import de.financer.service.FileService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FileController {
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionController.class);
@Autowired
private FileService fileService;
@GetMapping("/file")
public File getFile(Long fileId) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("GET /file got parameter: %s", fileId));
}
final File file = this.fileService.getFile(fileId);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("GET /file returns with %s", file.getName()));
}
return file;
}
}

View File

@@ -46,13 +46,14 @@ public class TransactionController {
String limit, String limit,
String order, String order,
String taxRelevant, String taxRelevant,
String accountsAnd) { String accountsAnd,
String hasFile) {
final String fromDecoded = ControllerUtil.urlDecode(fromAccountKey); final String fromDecoded = ControllerUtil.urlDecode(fromAccountKey);
final String toDecoded = ControllerUtil.urlDecode(toAccountKey); final String toDecoded = ControllerUtil.urlDecode(toAccountKey);
if (LOGGER.isDebugEnabled()) { if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("GET /transactions/ got parameter: %s, %s, %s, %s, %s, %s - with order %s", LOGGER.debug(String.format("GET /transactions/ got parameter: %s, %s, %s, %s, %s, %s, %s - with order %s",
fromDecoded, toDecoded, periodId, limit, taxRelevant, accountsAnd, order)); fromDecoded, toDecoded, periodId, limit, taxRelevant, accountsAnd, hasFile, order));
} }
// Wrap the url parameters into a proper POJO so the service parameter list wont get polluted // Wrap the url parameters into a proper POJO so the service parameter list wont get polluted
@@ -66,6 +67,7 @@ public class TransactionController {
parameter.setOrder(order); parameter.setOrder(order);
parameter.setTaxRelevant(taxRelevant); parameter.setTaxRelevant(taxRelevant);
parameter.setAccountsAnd(accountsAnd); parameter.setAccountsAnd(accountsAnd);
parameter.setHasFile(hasFile);
final Iterable<SearchTransactionsResponseDto> transactionSearchResponse = final Iterable<SearchTransactionsResponseDto> transactionSearchResponse =
this.transactionService.searchTransactions(parameter); this.transactionService.searchTransactions(parameter);
@@ -92,7 +94,9 @@ public class TransactionController {
Long.valueOf(requestDto.getAmount()), Long.valueOf(requestDto.getAmount()),
requestDto.getDate(), requestDto.getDate(),
requestDto.getDescription(), requestDto.getDescription(),
requestDto.getTaxRelevant()); requestDto.getTaxRelevant(),
requestDto.getFileName(),
requestDto.getFileContent());
if (LOGGER.isDebugEnabled()) { if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("POST /transactions returns with %s", responseReason.name())); LOGGER.debug(String.format("POST /transactions returns with %s", responseReason.name()));

View File

@@ -0,0 +1,7 @@
package de.financer.dba;
import de.financer.model.File;
import org.springframework.data.repository.CrudRepository;
public interface FileRepository extends CrudRepository<File, Long> {
}

View File

@@ -12,7 +12,8 @@ public interface TransactionRepositoryCustom {
Integer limit, Integer limit,
Order order, Order order,
Boolean taxRelevant, Boolean taxRelevant,
boolean accountsAnd); boolean accountsAnd,
Boolean hasFile);
Iterable<SearchTransactionsResponseDto> searchTransactions(String fql); Iterable<SearchTransactionsResponseDto> searchTransactions(String fql);
} }

View File

@@ -30,11 +30,13 @@ public class TransactionRepositoryCustomImpl implements TransactionRepositoryCus
Integer limit, Integer limit,
Order order, Order order,
Boolean taxRelevant, Boolean taxRelevant,
boolean accountsAnd) { boolean accountsAnd,
Boolean hasFile) {
final CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder(); final CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
final CriteriaQuery<SearchTransactionsResponseDto> criteriaQuery = criteriaBuilder final CriteriaQuery<SearchTransactionsResponseDto> criteriaQuery = criteriaBuilder
.createQuery(SearchTransactionsResponseDto.class); .createQuery(SearchTransactionsResponseDto.class);
final Root<Transaction> fromTransaction = criteriaQuery.from(Transaction.class); final Root<Transaction> fromTransaction = criteriaQuery.from(Transaction.class);
final SetJoin<Transaction, File> fileJoin = fromTransaction.join(Transaction_.files, JoinType.LEFT);
final List<Predicate> predicates = new ArrayList<>(); final List<Predicate> predicates = new ArrayList<>();
Predicate fromAccountPredicate = null; Predicate fromAccountPredicate = null;
@@ -74,6 +76,15 @@ public class TransactionRepositoryCustomImpl implements TransactionRepositoryCus
} }
// else: both null, nothing to do // else: both null, nothing to do
if (hasFile != null) {
if (hasFile) {
predicates.add(criteriaBuilder.isNotNull(fileJoin.get(File_.id)));
}
else {
predicates.add(criteriaBuilder.isNull(fileJoin.get(File_.id)));
}
}
criteriaQuery.where(predicates.toArray(new Predicate[]{})); criteriaQuery.where(predicates.toArray(new Predicate[]{}));
switch(order) { switch(order) {
@@ -92,7 +103,9 @@ public class TransactionRepositoryCustomImpl implements TransactionRepositoryCus
fromTransaction.get(Transaction_.description), fromTransaction.get(Transaction_.description),
fromTransaction.get(Transaction_.amount), fromTransaction.get(Transaction_.amount),
fromTransaction.get(Transaction_.taxRelevant), fromTransaction.get(Transaction_.taxRelevant),
criteriaBuilder.isNotNull(fromTransaction.get(Transaction_.recurringTransaction)))); criteriaBuilder.isNotNull(fromTransaction.get(Transaction_.recurringTransaction)),
// This is kinda hacky as the relation between transaction and file is one-to-many
fileJoin.get(File_.id)));
final TypedQuery<SearchTransactionsResponseDto> query = this.entityManager.createQuery(criteriaQuery); final TypedQuery<SearchTransactionsResponseDto> query = this.entityManager.createQuery(criteriaQuery);

View File

@@ -27,6 +27,9 @@ public class FQLVisitorImpl extends FQLBaseVisitor<Predicate> {
this.froms = new HashMap<>(); this.froms = new HashMap<>();
this.froms.put(JoinKey.of(Transaction.class), this.criteriaRoot); this.froms.put(JoinKey.of(Transaction.class), this.criteriaRoot);
// We always need to join the join for HAS_FILE because it required for the select clause
FieldMapping.HAS_FILE.getJoinHandler().apply(this.froms, FieldMapping.HAS_FILE);
} }
public CriteriaQuery<SearchTransactionsResponseDto> getCriteriaQuery() { public CriteriaQuery<SearchTransactionsResponseDto> getCriteriaQuery() {
@@ -58,7 +61,8 @@ public class FQLVisitorImpl extends FQLBaseVisitor<Predicate> {
this.criteriaRoot.get(Transaction_.description), this.criteriaRoot.get(Transaction_.description),
this.criteriaRoot.get(Transaction_.amount), this.criteriaRoot.get(Transaction_.amount),
this.criteriaRoot.get(Transaction_.taxRelevant), this.criteriaRoot.get(Transaction_.taxRelevant),
criteriaBuilder.isNotNull(this.criteriaRoot.get(Transaction_.recurringTransaction)))); this.criteriaBuilder.isNotNull(this.criteriaRoot.get(Transaction_.recurringTransaction)),
this.froms.get(FieldMapping.HAS_FILE.getJoinKey()).get(FieldMapping.HAS_FILE.getAttributeName())));
} }
@Override @Override

View File

@@ -30,7 +30,9 @@ public enum FieldMapping {
JoinKey.of(Transaction.class), null, NotNullSyntheticHandler.class), JoinKey.of(Transaction.class), null, NotNullSyntheticHandler.class),
TAX_RELEVANT("taxRelevant", Transaction_.TAX_RELEVANT, Transaction.class, NoopJoinHandler.class, TAX_RELEVANT("taxRelevant", Transaction_.TAX_RELEVANT, Transaction.class, NoopJoinHandler.class,
JoinKey.of(Transaction.class), null, BooleanHandler.class); JoinKey.of(Transaction.class), null, BooleanHandler.class),
HAS_FILE("hasFile", File_.ID, File.class, FileJoinHandler.class, JoinKey.of(File.class), null, NotNullSyntheticHandler.class);
/** /**
* The name of the field as used in FQL * The name of the field as used in FQL

View File

@@ -0,0 +1,28 @@
package de.financer.fql.join_handler;
import de.financer.fql.FieldMapping;
import de.financer.model.File;
import de.financer.model.Transaction;
import de.financer.model.Transaction_;
import javax.persistence.criteria.From;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.SetJoin;
import java.util.Map;
public class FileJoinHandler implements JoinHandler {
@Override
public Void apply(Map<JoinKey, From<?, ?>> joinKeyFromMap, FieldMapping fieldMapping) {
if (joinKeyFromMap.get(fieldMapping.getJoinKey()) == null) {
final SetJoin<Transaction, File> periodJoin = ((Root<Transaction>) joinKeyFromMap
.get(JoinKey.of(Transaction.class)))
.join(Transaction_.files, JoinType.LEFT);
joinKeyFromMap.put(fieldMapping.getJoinKey(), periodJoin);
}
return null;
}
}

View File

@@ -0,0 +1,28 @@
package de.financer.service;
import de.financer.ResponseReason;
import de.financer.dba.FileRepository;
import de.financer.model.File;
import de.financer.service.exception.FinancerServiceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class FileService {
private static final Logger LOGGER = LoggerFactory.getLogger(TransactionService.class);
@Autowired
private FileRepository fileRepository;
public File getFile(Long fileId) {
if (fileId == null || !this.fileRepository.existsById(fileId)) {
LOGGER.error(String.format("File with ID %s not found!", fileId));
throw new FinancerServiceException(ResponseReason.FILE_NOT_FOUND);
}
return this.fileRepository.findById(fileId).get();
}
}

View File

@@ -476,7 +476,9 @@ public class RecurringTransactionService {
LocalDate.now().format(DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())), LocalDate.now().format(DateTimeFormatter.ofPattern(this.financerConfig.getDateFormat())),
recurringTransaction.getDescription(), recurringTransaction.getDescription(),
recurringTransaction, recurringTransaction,
recurringTransaction.isTaxRelevant()); recurringTransaction.isTaxRelevant(),
null,
null);
} }
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)

View File

@@ -55,19 +55,20 @@ public class TransactionService {
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
String description, Boolean taxRelevant String description, Boolean taxRelevant, String fileName, String fileContent
) { ) {
return this.createTransaction(fromAccountKey, toAccountKey, amount, date, description, null, taxRelevant); return this.createTransaction(fromAccountKey, toAccountKey, amount, date, description, null,
taxRelevant, fileName, fileContent);
} }
@Transactional(propagation = Propagation.REQUIRED) @Transactional(propagation = Propagation.REQUIRED)
public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date, public ResponseReason createTransaction(String fromAccountKey, String toAccountKey, Long amount, String date,
String description, RecurringTransaction recurringTransaction, String description, RecurringTransaction recurringTransaction,
Boolean taxRelevant Boolean taxRelevant, String fileName, String fileContent
) { ) {
final Account fromAccount = this.accountService.getAccountByKey(fromAccountKey); final Account fromAccount = this.accountService.getAccountByKey(fromAccountKey);
final Account toAccount = this.accountService.getAccountByKey(toAccountKey); final Account toAccount = this.accountService.getAccountByKey(toAccountKey);
ResponseReason response = validateParameters(fromAccount, toAccount, amount, date); ResponseReason response = validateParameters(fromAccount, toAccount, amount, date, fileName, fileContent);
// If we detected an issue with the given parameters return the first error found to the caller // If we detected an issue with the given parameters return the first error found to the caller
if (response != null) { if (response != null) {
@@ -79,6 +80,7 @@ public class TransactionService {
buildTransaction(fromAccount, toAccount, amount, description, date, recurringTransaction, taxRelevant); buildTransaction(fromAccount, toAccount, amount, description, date, recurringTransaction, taxRelevant);
transaction.setPeriods(getRelevantPeriods(transaction)); transaction.setPeriods(getRelevantPeriods(transaction));
transaction.setFiles(Collections.singleton(buildFile(fileName, fileContent)));
fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService fromAccount.setCurrentBalance(fromAccount.getCurrentBalance() + (this.ruleService
.getMultiplierFromAccount(fromAccount) * amount)); .getMultiplierFromAccount(fromAccount) * amount));
@@ -110,6 +112,15 @@ public class TransactionService {
return response; return response;
} }
private File buildFile(String fileName, String fileContent) {
final File file = new File();
file.setName(fileName);
file.setContent(fileContent);
return file;
}
private Set<Period> getRelevantPeriods(Transaction transaction) { private Set<Period> getRelevantPeriods(Transaction transaction) {
final Set<Period> periods = new HashSet<>(); final Set<Period> periods = new HashSet<>();
final Period expensePeriod = this.periodService.getExpensePeriodForTransaction(transaction); final Period expensePeriod = this.periodService.getExpensePeriodForTransaction(transaction);
@@ -158,10 +169,13 @@ public class TransactionService {
* @param toAccount the to account * @param toAccount the to account
* @param amount the transaction amount * @param amount the transaction amount
* @param date the transaction date * @param date the transaction date
* @param fileName the name of the file
* @param fileContent the BASE64 encoded content of the file
* *
* @return the first error found or <code>null</code> if all parameters are valid * @return the first error found or <code>null</code> if all parameters are valid
*/ */
private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date) { private ResponseReason validateParameters(Account fromAccount, Account toAccount, Long amount, String date,
String fileName, String fileContent) {
ResponseReason response = null; ResponseReason response = null;
if (fromAccount == null && toAccount == null) { if (fromAccount == null && toAccount == null) {
@@ -184,6 +198,11 @@ public class TransactionService {
} catch (DateTimeParseException e) { } catch (DateTimeParseException e) {
response = ResponseReason.INVALID_DATE_FORMAT; response = ResponseReason.INVALID_DATE_FORMAT;
} }
// A file is only considered valid if both name and content are not empty
} else if (StringUtils.isNotEmpty(fileName) && StringUtils.isEmpty(fileContent)) {
response = ResponseReason.INVALID_FILE_CONTENT;
} else if (StringUtils.isEmpty(fileName) && StringUtils.isNotEmpty(fileContent)) {
response = ResponseReason.INVALID_FILE_NAME;
} }
return response; return response;
@@ -298,6 +317,7 @@ public class TransactionService {
Account toAccount = null; Account toAccount = null;
Boolean taxRelevant = null; Boolean taxRelevant = null;
Boolean accountsAnd = Boolean.FALSE; // account values are OR conjunct by default Boolean accountsAnd = Boolean.FALSE; // account values are OR conjunct by default
Boolean hasFile = null;
if (StringUtils.isNotEmpty(parameter.getPeriodId())) { if (StringUtils.isNotEmpty(parameter.getPeriodId())) {
if (!NumberUtils.isCreatable(parameter.getPeriodId())) { if (!NumberUtils.isCreatable(parameter.getPeriodId())) {
@@ -351,8 +371,16 @@ public class TransactionService {
} }
} }
if(StringUtils.isNotEmpty(parameter.getHasFile())) {
hasFile = BooleanUtils.toBooleanObject(parameter.getAccountsAnd());
if (hasFile == null) {
throw new FinancerServiceException(ResponseReason.INVALID_HAS_FILE_VALUE);
}
}
return this.transactionRepository.searchTransactions(period, fromAccount, toAccount, limit, order, taxRelevant, return this.transactionRepository.searchTransactions(period, fromAccount, toAccount, limit, order, taxRelevant,
accountsAnd); accountsAnd, hasFile);
} }
public Iterable<SearchTransactionsResponseDto> searchTransactions(String fql) { public Iterable<SearchTransactionsResponseDto> searchTransactions(String fql) {

View File

@@ -8,6 +8,7 @@ public class SearchTransactionsParameter {
private String order; private String order;
private String taxRelevant; private String taxRelevant;
private String accountsAnd; private String accountsAnd;
private String hasFile;
public String getFromAccountKey() { public String getFromAccountKey() {
return fromAccountKey; return fromAccountKey;
@@ -64,4 +65,12 @@ public class SearchTransactionsParameter {
public void setAccountsAnd(String accountsAnd) { public void setAccountsAnd(String accountsAnd) {
this.accountsAnd = accountsAnd; this.accountsAnd = accountsAnd;
} }
public String getHasFile() {
return hasFile;
}
public void setHasFile(String hasFile) {
this.hasFile = hasFile;
}
} }

View File

@@ -0,0 +1,14 @@
CREATE TABLE file (
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
name NVARCHAR(1000) NOT NULL,
content NVARCHAR(4000) NOT NULL
);
CREATE TABLE link_transaction_file (
id BIGINT NOT NULL PRIMARY KEY IDENTITY,
transaction_id BIGINT NOT NULL,
file_id BIGINT NOT NULL,
CONSTRAINT fk_link_transaction_file_transaction FOREIGN KEY (transaction_id) REFERENCES "transaction" (id),
CONSTRAINT fk_link_transaction_file_file FOREIGN KEY (file_id) REFERENCES file (id)
);

View File

@@ -0,0 +1,14 @@
CREATE TABLE file (
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
name VARCHAR(1000) NOT NULL,
content VARCHAR NOT NULL
);
CREATE TABLE link_transaction_file (
id BIGINT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
transaction_id BIGINT NOT NULL,
file_id BIGINT NOT NULL,
CONSTRAINT fk_link_transaction_file_transaction FOREIGN KEY (transaction_id) REFERENCES "transaction" (id),
CONSTRAINT fk_link_transaction_file_file FOREIGN KEY (file_id) REFERENCES file (id)
);

View File

@@ -48,7 +48,7 @@ public class TransactionService_createTransactionTest {
// will not be found. // will not be found.
// Act // Act
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", 150L, "24.02.2019", "XXX", false); final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.invalid", 150L, "24.02.2019", "XXX", false, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response); Assert.assertEquals(ResponseReason.FROM_AND_TO_ACCOUNT_NOT_FOUND, response);
@@ -60,7 +60,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null); Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(createAccount(), null);
// Act // Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.invalid", 150L, "24.02.2019", "XXX", false); final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.invalid", 150L, "24.02.2019", "XXX", false, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response); Assert.assertEquals(ResponseReason.TO_ACCOUNT_NOT_FOUND, response);
@@ -72,7 +72,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount()); Mockito.when(this.accountService.getAccountByKey(Mockito.anyString())).thenReturn(null, createAccount());
// Act // Act
final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.to", 150L, "24.02.2019", "XXX", false); final ResponseReason response = this.classUnderTest.createTransaction("account.invalid", "account.to", 150L, "24.02.2019", "XXX", false, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response); Assert.assertEquals(ResponseReason.FROM_ACCOUNT_NOT_FOUND, response);
@@ -85,7 +85,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.FALSE); Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.FALSE);
// Act // Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 150L, "24.02.2019", "XXX", false); final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 150L, "24.02.2019", "XXX", false, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response); Assert.assertEquals(ResponseReason.INVALID_BOOKING_ACCOUNTS, response);
@@ -98,7 +98,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
// Act // Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", null, "24.02.2019", "XXX", false); final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", null, "24.02.2019", "XXX", false, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.MISSING_AMOUNT, response); Assert.assertEquals(ResponseReason.MISSING_AMOUNT, response);
@@ -111,7 +111,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
// Act // Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 0L, "24.02.2019", "XXX", false); final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 0L, "24.02.2019", "XXX", false, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response); Assert.assertEquals(ResponseReason.AMOUNT_ZERO, response);
@@ -124,7 +124,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
// Act // Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, null, "XXX", false); final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, null, "XXX", false, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.MISSING_DATE, response); Assert.assertEquals(ResponseReason.MISSING_DATE, response);
@@ -137,7 +137,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE); Mockito.when(this.ruleService.isValidBooking(Mockito.any(Account.class), Mockito.any(Account.class))).thenReturn(Boolean.TRUE);
// Act // Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, "2019-01-01", "XXX", false); final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, "2019-01-01", "XXX", false, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.INVALID_DATE_FORMAT, response); Assert.assertEquals(ResponseReason.INVALID_DATE_FORMAT, response);
@@ -156,7 +156,7 @@ public class TransactionService_createTransactionTest {
Mockito.when(toAccount.getCurrentBalance()).thenReturn(0L); Mockito.when(toAccount.getCurrentBalance()).thenReturn(0L);
// Act // Act
final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, "24.02.2019", "XXX", false); final ResponseReason response = this.classUnderTest.createTransaction("account.from", "account.to", 125L, "24.02.2019", "XXX", false, null, null);
// Assert // Assert
Assert.assertEquals(ResponseReason.CREATED, response); Assert.assertEquals(ResponseReason.CREATED, response);

View File

@@ -126,7 +126,7 @@ public class AccountController {
model.addAttribute("account", account); model.addAttribute("account", account);
model.addAttribute("transactions", transactions); model.addAttribute("transactions", transactions);
model.addAttribute("showActions", true); model.addAttribute("showActionDelete", true);
model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus())); model.addAttribute("isClosed", AccountStatus.CLOSED.equals(account.getStatus()));
} }
catch(FinancerRestException e) { catch(FinancerRestException e) {

View File

@@ -0,0 +1,43 @@
package de.financer.controller;
import de.financer.config.FinancerConfig;
import de.financer.model.File;
import de.financer.template.FinancerRestTemplate;
import de.financer.template.exception.FinancerRestException;
import de.financer.util.ControllerUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Base64;
@Controller
public class FileController {
@Autowired
private FinancerConfig financerConfig;
@GetMapping("/downloadFile")
public ResponseEntity<Resource> downloadFile(Long fileId) {
final UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(ControllerUtils.buildUrl(this.financerConfig, Function.FILE_GET))
.queryParam("fileId", fileId);
final File file;
try {
file = FinancerRestTemplate.exchangeGet(builder, File.class);
} catch (FinancerRestException e) {
// TODO
return null;
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"")
.body(new ByteArrayResource(Base64.getDecoder().decode(file.getContent())));
}
}

View File

@@ -32,7 +32,9 @@ public enum Function {
RT_CREATE_TRANSACTION("recurringTransactions/createTransaction"), RT_CREATE_TRANSACTION("recurringTransactions/createTransaction"),
P_GET_CURRENT_EXPENSE_PERIOD("periods/getCurrentExpensePeriod"), P_GET_CURRENT_EXPENSE_PERIOD("periods/getCurrentExpensePeriod"),
P_CLOSE_CURRENT_EXPENSE_PERIOD("periods/closeCurrentExpensePeriod"); P_CLOSE_CURRENT_EXPENSE_PERIOD("periods/closeCurrentExpensePeriod"),
FILE_GET("file");
private final String path; private final String path;

View File

@@ -19,6 +19,8 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
import java.util.Base64;
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;
@@ -32,7 +34,7 @@ public class TransactionController {
@GetMapping("/searchTransactions") @GetMapping("/searchTransactions")
public String searchTransaction(Model model) { public String searchTransaction(Model model) {
model.addAttribute("form", new SearchTransactionsForm()); model.addAttribute("form", new SearchTransactionsForm());
model.addAttribute("showActions", false); model.addAttribute("showActionDelete", false);
ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig);
@@ -60,7 +62,7 @@ public class TransactionController {
} }
model.addAttribute("form", form); model.addAttribute("form", form);
model.addAttribute("showActions", false); model.addAttribute("showActionDelete", false);
ControllerUtils.addVersionAttribute(model, this.financerConfig); ControllerUtils.addVersionAttribute(model, this.financerConfig);
ControllerUtils.addCurrencySymbol(model, this.financerConfig); ControllerUtils.addCurrencySymbol(model, this.financerConfig);
@@ -117,6 +119,15 @@ public class TransactionController {
requestDto.setDescription(form.getDescription()); requestDto.setDescription(form.getDescription());
requestDto.setTaxRelevant(form.getTaxRelevant()); requestDto.setTaxRelevant(form.getTaxRelevant());
if (form.getFile() != null) {
try {
requestDto.setFileContent(Base64.getEncoder().encodeToString(form.getFile().getBytes()));
requestDto.setFileName(form.getFile().getOriginalFilename());
} catch (IOException ioe) {
// TODO No file for us :(
}
}
final ResponseReason responseReason = FinancerRestTemplate final ResponseReason responseReason = FinancerRestTemplate
.exchangePost(this.financerConfig, Function.TR_CREATE_TRANSACTION, requestDto); .exchangePost(this.financerConfig, Function.TR_CREATE_TRANSACTION, requestDto);

View File

@@ -1,5 +1,7 @@
package de.financer.form; package de.financer.form;
import org.springframework.web.multipart.MultipartFile;
public class NewTransactionForm { public class NewTransactionForm {
private String fromAccountKey; private String fromAccountKey;
private String toAccountKey; private String toAccountKey;
@@ -7,6 +9,7 @@ public class NewTransactionForm {
private String date; private String date;
private String description; private String description;
private Boolean taxRelevant; private Boolean taxRelevant;
private MultipartFile file;
public String getFromAccountKey() { public String getFromAccountKey() {
return fromAccountKey; return fromAccountKey;
@@ -55,4 +58,12 @@ public class NewTransactionForm {
public void setTaxRelevant(Boolean taxRelevant) { public void setTaxRelevant(Boolean taxRelevant) {
this.taxRelevant = taxRelevant; this.taxRelevant = taxRelevant;
} }
public MultipartFile getFile() {
return file;
}
public void setFile(MultipartFile file) {
this.file = file;
}
} }

View File

@@ -76,6 +76,19 @@ public class FinancerRestTemplate<T> {
return ResponseReason.fromResponseEntity(response); return ResponseReason.fromResponseEntity(response);
} }
public static <R> R exchangeGet(UriComponentsBuilder builder, Class<R> type) throws FinancerRestException {
final RestTemplate restTemplate = new RestTemplate();
try {
return restTemplate.exchange(builder.toUriString(), HttpMethod.GET, null, type).getBody();
} catch (HttpStatusCodeException e) {
final ResponseEntity<String> exceptionResponse = new ResponseEntity<>(e.getResponseBodyAsString(), e
.getStatusCode());
throw new FinancerRestException(ResponseReason.fromResponseEntity(exceptionResponse));
}
}
public static <R> R exchangeGet(FinancerConfig financerConfig, public static <R> R exchangeGet(FinancerConfig financerConfig,
Function endpoint, Function endpoint,
Class<R> type) throws FinancerRestException { Class<R> type) throws FinancerRestException {

View File

@@ -44,6 +44,7 @@ financer.transaction-new.label.amount=Amount\:
financer.transaction-new.label.date=Date\: financer.transaction-new.label.date=Date\:
financer.transaction-new.label.description=Description\: financer.transaction-new.label.description=Description\:
financer.transaction-new.label.taxRelevant=Tax relevant\: financer.transaction-new.label.taxRelevant=Tax relevant\:
financer.transaction-new.label.file=File\:
financer.transaction-new.submit=Create transaction financer.transaction-new.submit=Create transaction
financer.transaction-new.account-type.BANK={0}|Bank|{1}{2} financer.transaction-new.account-type.BANK={0}|Bank|{1}{2}
financer.transaction-new.account-type.CASH={0}|Cash|{1}{2} financer.transaction-new.account-type.CASH={0}|Cash|{1}{2}
@@ -112,6 +113,7 @@ financer.account-details.details.balance=Current balance\:
financer.account-details.details.group=Group\: financer.account-details.details.group=Group\:
financer.transaction-list.table-header.actions=Actions financer.transaction-list.table-header.actions=Actions
financer.transaction-list.table.actions.deleteTransaction=Delete financer.transaction-list.table.actions.deleteTransaction=Delete
financer.transaction-list.table.actions.downloadFile=Download file
financer.transaction-list.table.recurring.yes=Yes financer.transaction-list.table.recurring.yes=Yes
financer.transaction-list.table.recurring.no=No financer.transaction-list.table.recurring.no=No
financer.transaction-list.table.taxRelevant.true=Yes financer.transaction-list.table.taxRelevant.true=Yes
@@ -135,6 +137,7 @@ financer.search-transactions.show-query-options.toAccountGroup=toAccountGroup\:
financer.search-transactions.show-query-options.date=date\: the date of the transaction in the format yyyy-mm-dd financer.search-transactions.show-query-options.date=date\: the date of the transaction in the format yyyy-mm-dd
financer.search-transactions.show-query-options.recurring=recurring\: whether the transaction has been created from a recurring transaction financer.search-transactions.show-query-options.recurring=recurring\: whether the transaction has been created from a recurring transaction
financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: whether the transaction is relevant for tax declaration financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: whether the transaction is relevant for tax declaration
financer.search-transactions.show-query-options.hasFile=hasFile\: whether the transaction has a file linked
financer.search-transactions.show-query-options.period=period\: the period the transaction is assigned to financer.search-transactions.show-query-options.period=period\: the period the transaction is assigned to
financer.search-transactions.show-query-options.period.CURRENT=CURRENT\: denotes the current expense period financer.search-transactions.show-query-options.period.CURRENT=CURRENT\: denotes the current expense period
financer.search-transactions.show-query-options.period.LAST=LAST\: denotes the last expense period financer.search-transactions.show-query-options.period.LAST=LAST\: denotes the last expense period
@@ -242,4 +245,7 @@ financer.error-message.ACCOUNT_NOT_FOUND=The account could not be found!
financer.error-message.DUPLICATE_ACCOUNT_KEY=An account with the given key already exists! financer.error-message.DUPLICATE_ACCOUNT_KEY=An account with the given key already exists!
financer.error-message.DUPLICATE_ACCOUNT_GROUP_NAME=An account group with the given key already exists! financer.error-message.DUPLICATE_ACCOUNT_GROUP_NAME=An account group with the given key already exists!
financer.error-message.ACCOUNT_GROUP_NOT_FOUND=The account group could not be found! financer.error-message.ACCOUNT_GROUP_NOT_FOUND=The account group could not be found!
financer.error-message.UNKNOWN_CHART_TYPE=The selected chart type is not known! financer.error-message.UNKNOWN_CHART_TYPE=The selected chart type is not known!
financer.error-message.INVALID_HAS_FILE_VALUE=The value for parameter hasFile cannot be parsed!
financer.error-message.INVALID_FILE_CONTENT=File content is missing!
financer.error-message.INVALID_FILE_NAME=File name is missing!

View File

@@ -44,6 +44,7 @@ financer.transaction-new.label.amount=Betrag\:
financer.transaction-new.label.date=Datum\: financer.transaction-new.label.date=Datum\:
financer.transaction-new.label.description=Beschreibung\: financer.transaction-new.label.description=Beschreibung\:
financer.transaction-new.label.taxRelevant=Relevant f\u00FCr Steuererkl\u00E4rung\: financer.transaction-new.label.taxRelevant=Relevant f\u00FCr Steuererkl\u00E4rung\:
financer.transaction-new.label.file=Datei\:
financer.transaction-new.submit=Buchung erstellen financer.transaction-new.submit=Buchung erstellen
financer.transaction-new.account-type.BANK={0}|Bank|{1}{2} financer.transaction-new.account-type.BANK={0}|Bank|{1}{2}
financer.transaction-new.account-type.CASH={0}|Bar|{1}{2} financer.transaction-new.account-type.CASH={0}|Bar|{1}{2}
@@ -112,6 +113,7 @@ financer.account-details.details.balance=Kontostand\:
financer.account-details.details.group=Gruppe\: financer.account-details.details.group=Gruppe\:
financer.transaction-list.table-header.actions=Aktionen financer.transaction-list.table-header.actions=Aktionen
financer.transaction-list.table.actions.deleteTransaction=L\u00F6schen financer.transaction-list.table.actions.deleteTransaction=L\u00F6schen
financer.transaction-list.table.actions.downloadFile=Datei herunterladen
financer.transaction-list.table.recurring.yes=Ja financer.transaction-list.table.recurring.yes=Ja
financer.transaction-list.table.recurring.no=Nein financer.transaction-list.table.recurring.no=Nein
financer.transaction-list.table.taxRelevant.true=Ja financer.transaction-list.table.taxRelevant.true=Ja
@@ -135,6 +137,7 @@ financer.search-transactions.show-query-options.toAccountGroup=toAccountGroup\:
financer.search-transactions.show-query-options.date=date\: das Datum der Buchung im Format jjjj-mm-tt financer.search-transactions.show-query-options.date=date\: das Datum der Buchung im Format jjjj-mm-tt
financer.search-transactions.show-query-options.recurring=recurring\: ob die Buchung durch eine wiederkehrende Buchung erzeugt wurde financer.search-transactions.show-query-options.recurring=recurring\: ob die Buchung durch eine wiederkehrende Buchung erzeugt wurde
financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: ob die Buchung als steuerrelevant markiert wurde financer.search-transactions.show-query-options.taxRelevant=taxRelevant\: ob die Buchung als steuerrelevant markiert wurde
financer.search-transactions.show-query-options.hasFile=hasFile\: ob eine Datei der Buchung zugeordnet ist
financer.search-transactions.show-query-options.period=period\: die Periode der Buchung financer.search-transactions.show-query-options.period=period\: die Periode der Buchung
financer.search-transactions.show-query-options.period.CURRENT=CURRENT\: bezeichnet die aktuelle Ausgabenperiode financer.search-transactions.show-query-options.period.CURRENT=CURRENT\: bezeichnet die aktuelle Ausgabenperiode
financer.search-transactions.show-query-options.period.LAST=LAST\: bezeichnet die letzte Ausgabenperiode financer.search-transactions.show-query-options.period.LAST=LAST\: bezeichnet die letzte Ausgabenperiode
@@ -241,4 +244,7 @@ financer.error-message.ACCOUNT_NOT_FOUND=Das ausgew\u00E4hlte Konto wurde nicht
financer.error-message.DUPLICATE_ACCOUNT_KEY=Ein Konto mit diesem Schl\u00FCssel existiert bereits! financer.error-message.DUPLICATE_ACCOUNT_KEY=Ein Konto mit diesem Schl\u00FCssel existiert bereits!
financer.error-message.DUPLICATE_ACCOUNT_GROUP_NAME=Eine Konto-Gruppe mit diesem Namen existiert bereits! financer.error-message.DUPLICATE_ACCOUNT_GROUP_NAME=Eine Konto-Gruppe mit diesem Namen existiert bereits!
financer.error-message.ACCOUNT_GROUP_NOT_FOUND=Die ausgew\u00E4hlte Konto-Gruppe wurde nicht gefunden! financer.error-message.ACCOUNT_GROUP_NOT_FOUND=Die ausgew\u00E4hlte Konto-Gruppe wurde nicht gefunden!
financer.error-message.UNKNOWN_CHART_TYPE=Das ausgew\u00E4hlte Diagramm wurde nicht gefunden! financer.error-message.UNKNOWN_CHART_TYPE=Das ausgew\u00E4hlte Diagramm wurde nicht gefunden!
financer.error-message.INVALID_HAS_FILE_VALUE=Der Wert des Parameters hasFile kann nicht verarbeitet werden!
financer.error-message.INVALID_FILE_CONTENT=Der Inhalt der Datei fehlt!
financer.error-message.INVALID_FILE_NAME=Der Dateiname fehlt!

View File

@@ -1,5 +1,7 @@
v27 -> v28: v27 -> v28:
- Add account end balance to account statistics for closed expense periods. This is currently not visible in the UI - Add account end balance to account statistics for closed expense periods. This is currently not visible in the UI
- Add file upload to transaction creation for e.g. invoices. They can be downloaded in the transaction overview of an
account
v26 -> v27: v26 -> v27:
- Changed sort order of accounts in overview page. The accounts are now sorted by the account type first (BCILES), then - Changed sort order of accounts in overview page. The accounts are now sorted by the account type first (BCILES), then
@@ -7,7 +9,7 @@ v26 -> v27:
- Add tax relevance flag to transaction and recurring transaction creation. This flag denotes whether a transaction or - Add tax relevance flag to transaction and recurring transaction creation. This flag denotes whether a transaction or
the instances of a recurring transaction are relevant for a tax declaration. This is preparation for extended reports the instances of a recurring transaction are relevant for a tax declaration. This is preparation for extended reports
- Add searching of transactions via FQL (Financer Query Language) - Add searching of transactions via FQL (Financer Query Language)
- Rework /transaction end point to better adhere to REST API requirements (proper HTTP return codes and HTTP method - Rework /transactions end point to better adhere to REST API requirements (proper HTTP return codes and HTTP method
usage) usage)
v25 -> v26: v25 -> v26:

View File

@@ -12,7 +12,7 @@
<span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/> <span class="errorMessage" th:if="${errorMessage != null}" th:text="#{'financer.error-message.' + ${errorMessage}}"/>
<a th:href="@{/accountOverview}" th:text="#{financer.cancel-back-to-overview}"/> <a th:href="@{/accountOverview}" th:text="#{financer.cancel-back-to-overview}"/>
<form id="new-transaction-form" action="#" th:action="@{/saveTransaction}" th:object="${form}" <form id="new-transaction-form" action="#" th:action="@{/saveTransaction}" th:object="${form}"
method="post"> method="post" enctype="multipart/form-data">
<label for="selectFromAccount" th:text="#{financer.transaction-new.label.from-account}"/> <label for="selectFromAccount" th:text="#{financer.transaction-new.label.from-account}"/>
<select id="selectFromAccount" th:field="*{fromAccountKey}"> <select id="selectFromAccount" th:field="*{fromAccountKey}">
<option th:each="acc : ${fromAccounts}" th:value="${acc.key}" <option th:each="acc : ${fromAccounts}" th:value="${acc.key}"
@@ -31,8 +31,10 @@
<input type="text" id="inputDescription" th:field="*{description}"/> <input type="text" id="inputDescription" th:field="*{description}"/>
<label for="inputTaxRelevant" th:text="#{financer.transaction-new.label.taxRelevant}" /> <label for="inputTaxRelevant" th:text="#{financer.transaction-new.label.taxRelevant}" />
<input type="checkbox" id="inputTaxRelevant" th:field="*{taxRelevant}" /> <input type="checkbox" id="inputTaxRelevant" th:field="*{taxRelevant}" />
<input type="submit" th:value="#{financer.transaction-new.submit}"/> <label for="inputFile" th:text="#{financer.transaction-new.label.file}" />
<input type="file" id="inputFile" th:field="*{file}" />
<input type="submit" th:value="#{financer.transaction-new.submit}" />
</form> </form>
<div th:replace="includes/footer :: footer"/> <div th:replace="includes/footer :: footer" />
</body> </body>
</html> </html>

View File

@@ -31,6 +31,7 @@
<li th:text="#{financer.search-transactions.show-query-options.date}" /> <li th:text="#{financer.search-transactions.show-query-options.date}" />
<li th:text="#{financer.search-transactions.show-query-options.recurring}" /> <li th:text="#{financer.search-transactions.show-query-options.recurring}" />
<li th:text="#{financer.search-transactions.show-query-options.taxRelevant}" /> <li th:text="#{financer.search-transactions.show-query-options.taxRelevant}" />
<li th:text="#{financer.search-transactions.show-query-options.hasFile}" />
<li><span th:text="#{financer.search-transactions.show-query-options.period}" /> <li><span th:text="#{financer.search-transactions.show-query-options.period}" />
<ul> <ul>
<li th:text="#{financer.search-transactions.show-query-options.period.CURRENT}" /> <li th:text="#{financer.search-transactions.show-query-options.period.CURRENT}" />

View File

@@ -21,10 +21,14 @@
<td th:if="${transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.yes}" /> <td th:if="${transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.yes}" />
<td th:if="${!transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.no}" /> <td th:if="${!transaction.recurring}" th:text="#{financer.transaction-list.table.recurring.no}" />
<td th:text="#{'financer.transaction-list.table.taxRelevant.' + ${transaction.taxRelevant}}" /> <td th:text="#{'financer.transaction-list.table.taxRelevant.' + ${transaction.taxRelevant}}" />
<td th:if="${showActions}"> <td nowrap>
<div id="account-transaction-table-actions-container"> <div id="account-transaction-table-actions-container">
<a th:href="@{/deleteTransaction(transactionId=${transaction.id}, accountKey=${account.key})}" <a th:href="@{/deleteTransaction(transactionId=${transaction.id}, accountKey=${account.key})}"
th:text="#{financer.transaction-list.table.actions.deleteTransaction}"/> th:text="#{financer.transaction-list.table.actions.deleteTransaction}"
th:if="${showActionDelete}" />
<a th:href="@{/downloadFile(fileId=${transaction.fileId})}"
th:text="#{financer.transaction-list.table.actions.downloadFile}"
th:if="${transaction.fileId != null}" />
</div> </div>
</td> </td>
</tr> </tr>