Add file uploading to transaction creation
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
financer-common/src/main/java/de/financer/model/File.java
Normal file
33
financer-common/src/main/java/de/financer/model/File.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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())));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -243,3 +246,6 @@ financer.error-message.DUPLICATE_ACCOUNT_KEY=An account with the given key alrea
|
|||||||
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!
|
||||||
@@ -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
|
||||||
@@ -242,3 +245,6 @@ financer.error-message.DUPLICATE_ACCOUNT_KEY=Ein Konto mit diesem Schl\u00FCssel
|
|||||||
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!
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,6 +31,8 @@
|
|||||||
<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}" />
|
||||||
|
<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}" />
|
<input type="submit" th:value="#{financer.transaction-new.submit}" />
|
||||||
</form>
|
</form>
|
||||||
<div th:replace="includes/footer :: footer" />
|
<div th:replace="includes/footer :: footer" />
|
||||||
|
|||||||
@@ -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}" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user