From 470a74f4208c64c2e8348fe44fdec11b44d83644 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Mon, 9 Sep 2024 01:31:43 +0800 Subject: [PATCH] support importing transaction in frontend --- cmd/webserver.go | 5 + conf/ezbookkeeping.ini | 6 + pkg/api/transactions.go | 254 ++++- pkg/cli/user_data.go | 2 +- .../data_table_transaction_data_converter.go | 127 ++- ...g_transaction_data_plain_text_converter.go | 2 +- ..._transaction_data_plain_text_data_table.go | 4 +- pkg/converters/imported_transactions.go | 29 - pkg/converters/imported_transactions_test.go | 53 - pkg/converters/transaction_data_converter.go | 2 +- .../duplicate_checker_type.go | 1 + pkg/errs/global.go | 3 + pkg/errs/system.go | 15 +- pkg/errs/transaction.go | 3 + pkg/middlewares/server_settings_cookie.go | 1 + pkg/models/imported_transaction.go | 145 +++ pkg/models/imported_transaction_test.go | 63 ++ pkg/models/transaction.go | 6 + pkg/settings/setting.go | 8 +- src/components/desktop/ConfirmDialog.vue | 8 +- src/components/desktop/StepsBar.vue | 18 +- src/consts/file.js | 16 +- src/desktop-main.js | 2 + src/lib/i18n.js | 18 + src/lib/server_settings.js | 4 + src/lib/services.js | 12 + src/locales/en.json | 29 +- src/locales/zh_Hans.json | 29 +- src/stores/transaction.js | 135 ++- src/styles/desktop/global.scss | 11 + src/views/desktop/transactions/ListPage.vue | 27 + .../list/dialogs/ImportDialog.vue | 931 ++++++++++++++++++ 32 files changed, 1772 insertions(+), 197 deletions(-) delete mode 100644 pkg/converters/imported_transactions.go delete mode 100644 pkg/converters/imported_transactions_test.go create mode 100644 pkg/models/imported_transaction.go create mode 100644 pkg/models/imported_transaction_test.go create mode 100644 src/views/desktop/transactions/list/dialogs/ImportDialog.vue diff --git a/cmd/webserver.go b/cmd/webserver.go index ce9e4163..6d6c5a9e 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -317,6 +317,11 @@ func startWebServer(c *core.CliContext) error { apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler)) apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler)) + if config.EnableDataImport { + apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler)) + apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler)) + } + // Transaction Pictures if config.EnableTransactionPictures { apiV1Route.POST("/transaction/pictures/upload.json", bindApi(api.TransactionPictures.TransactionPictureUploadHandler)) diff --git a/conf/ezbookkeeping.ini b/conf/ezbookkeeping.ini index 1e05e983..6c04101a 100644 --- a/conf/ezbookkeeping.ini +++ b/conf/ezbookkeeping.ini @@ -221,6 +221,12 @@ max_user_avatar_size = 1048576 # Set to true to allow users to export their data enable_export = true +# Set to true to allow users to import their data +enable_import = true + +# Maximum allowed import file size (1 - 4294967295 bytes) +max_import_file_size = 10485760 + [notification] # Set to true to display custom notification in home page every time users register enable_notification_after_register = false diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index ebfe8702..7ae0a4f8 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -1,11 +1,13 @@ package api import ( + "io" "sort" "strings" orderedmap "github.com/wk8/go-ordered-map/v2" + "github.com/mayswind/ezbookkeeping/pkg/converters" "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" "github.com/mayswind/ezbookkeeping/pkg/errs" @@ -23,12 +25,14 @@ const maximumPicturesCountOfTransaction = 10 type TransactionsApi struct { ApiUsingConfig ApiUsingDuplicateChecker - transactions *services.TransactionService - transactionCategories *services.TransactionCategoryService - transactionTags *services.TransactionTagService - transactionPictures *services.TransactionPictureService - accounts *services.AccountService - users *services.UserService + ezBookKeepingCsvConverter converters.TransactionDataConverter + ezBookKeepingTsvConverter converters.TransactionDataConverter + transactions *services.TransactionService + transactionCategories *services.TransactionCategoryService + transactionTags *services.TransactionTagService + transactionPictures *services.TransactionPictureService + accounts *services.AccountService + users *services.UserService } // Initialize a transaction api singleton instance @@ -40,12 +44,14 @@ var ( ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ container: duplicatechecker.Container, }, - transactions: services.Transactions, - transactionCategories: services.TransactionCategories, - transactionTags: services.TransactionTags, - transactionPictures: services.TransactionPictures, - accounts: services.Accounts, - users: services.Users, + ezBookKeepingCsvConverter: converters.EzBookKeepingTransactionDataCSVFileConverter, + ezBookKeepingTsvConverter: converters.EzBookKeepingTransactionDataTSVFileConverter, + transactions: services.Transactions, + transactionCategories: services.TransactionCategories, + transactionTags: services.TransactionTags, + transactionPictures: services.TransactionPictures, + accounts: services.Accounts, + users: services.Users, } ) @@ -1004,6 +1010,230 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er return true, nil } +// TransactionParseImportFileHandler returns the parsed transaction data by request parameters for current user +func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext) (any, *errs.Error) { + uid := c.GetCurrentUid() + form, err := c.MultipartForm() + + if err != nil { + log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrParameterInvalid + } + + utcOffset, err := c.GetClientTimezoneOffset() + + if err != nil { + log.Warnf(c, "[transactions.TransactionParseImportFileHandler] cannot get client timezone offset, because %s", err.Error()) + return nil, errs.ErrClientTimezoneOffsetInvalid + } + + fileTypes := form.Value["fileType"] + + if len(fileTypes) < 1 || fileTypes[0] == "" { + return nil, errs.ErrImportFileTypeIsEmpty + } + + fileType := fileTypes[0] + var dataImporter converters.TransactionDataImporter + + if fileType == "ezbookkeeping_csv" { + dataImporter = a.ezBookKeepingCsvConverter + } else if fileType == "ezbookkeeping_tsv" { + dataImporter = a.ezBookKeepingTsvConverter + } else { + return nil, errs.ErrImportFileTypeNotSupported + } + + importFiles := form.File["file"] + + if len(importFiles) < 1 { + log.Warnf(c, "[transactions.TransactionParseImportFileHandler] there is no import file in request for user \"uid:%d\"", uid) + return nil, errs.ErrNoFilesUpload + } + + if importFiles[0].Size < 1 { + log.Warnf(c, "[transactions.TransactionParseImportFileHandler] the size of import file in request is zero for user \"uid:%d\"", uid) + return nil, errs.ErrUploadedFileEmpty + } + + if importFiles[0].Size > int64(a.CurrentConfig().MaxImportFileSize) { + log.Warnf(c, "[transactions.TransactionParseImportFileHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid) + return nil, errs.ErrExceedMaxUploadFileSize + } + + importFile, err := importFiles[0].Open() + + if err != nil { + log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + fileData, err := io.ReadAll(importFile) + + if err != nil { + log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + user, err := a.users.GetUserById(c, uid) + + if err != nil { + if !errs.IsCustomError(err) { + log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to get user, because %s", err.Error()) + } + + return nil, errs.ErrUserNotFound + } + + accounts, err := a.accounts.GetAllAccountsByUid(c, user.Uid) + + if err != nil { + log.BootErrorf(c, "[transactions.TransactionParseImportFileHandler] failed to get accounts for user \"uid:%d\", because %s", user.Uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + accountMap := a.accounts.GetAccountNameMapByList(accounts) + + categories, err := a.transactionCategories.GetAllCategoriesByUid(c, user.Uid, 0, -1) + + if err != nil { + log.BootErrorf(c, "[transactions.TransactionParseImportFileHandler] failed to get categories for user \"uid:%d\", because %s", user.Uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + categoryMap := a.transactionCategories.GetCategoryNameMapByList(categories) + + tags, err := a.transactionTags.GetAllTagsByUid(c, user.Uid) + + if err != nil { + log.BootErrorf(c, "[transactions.TransactionParseImportFileHandler] failed to get tags for user \"uid:%d\", because %s", user.Uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + tagMap := a.transactionTags.GetTagNameMapByList(tags) + + parsedTransactions, _, _, _, err := dataImporter.ParseImportedData(c, user, fileData, utcOffset, accountMap, categoryMap, tagMap) + + if err != nil { + log.BootErrorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse imported data for user \"uid:%d\", because %s", user.Uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + parsedTransactionRespsList := parsedTransactions.ToImportTransactionResponseList() + + if len(parsedTransactionRespsList) < 1 { + return nil, errs.ErrNoDataToImport + } + + parsedTransactionResps := &models.ImportTransactionResponsePageWrapper{ + Items: parsedTransactionRespsList, + TotalCount: int64(len(parsedTransactionRespsList)), + } + + return parsedTransactionResps, nil +} + +// TransactionImportHandler imports transactions by request parameters for current user +func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *errs.Error) { + var transactionImportReq models.TransactionImportRequest + err := c.ShouldBindJSON(&transactionImportReq) + + if err != nil { + log.Warnf(c, "[transactions.TransactionImportHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + + if a.CurrentConfig().EnableDuplicateSubmissionsCheck && transactionImportReq.ClientSessionId != "" { + found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId) + + if found { + log.Infof(c, "[transactions.TransactionImportHandler] another \"%s\" transactions has been imported for user \"uid:%d\"", remark, uid) + count, err := utils.StringToInt(remark) + + if err == nil { + return count, nil + } + } + } + + for i := 0; i < len(transactionImportReq.Transactions); i++ { + transactionCreateReq := transactionImportReq.Transactions[i] + tagIds, err := utils.StringArrayToInt64Array(transactionCreateReq.TagIds) + + if err != nil { + log.Warnf(c, "[transactions.TransactionImportHandler] parse tag ids failed of transaction \"index:%d\", because %s", i, err.Error()) + return nil, errs.ErrTransactionTagIdInvalid + } + + if len(tagIds) > maximumTagsCountOfTransaction { + return nil, errs.ErrTransactionHasTooManyTags + } + + if transactionCreateReq.Type < models.TRANSACTION_TYPE_MODIFY_BALANCE || transactionCreateReq.Type > models.TRANSACTION_TYPE_TRANSFER { + log.Warnf(c, "[transactions.TransactionImportHandler] transaction type of transaction \"index:%d\" is invalid", i) + return nil, errs.ErrTransactionTypeInvalid + } + + if transactionCreateReq.Type == models.TRANSACTION_TYPE_MODIFY_BALANCE && transactionCreateReq.CategoryId > 0 { + log.Warnf(c, "[transactions.TransactionImportHandler] balance modification transaction \"index:%d\" cannot set category id", i) + return nil, errs.ErrBalanceModificationTransactionCannotSetCategory + } + + if transactionCreateReq.Type != models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.DestinationAccountId != 0 { + log.Warnf(c, "[transactions.TransactionImportHandler] non-transfer transaction \"index:%d\" destination account cannot be set", i) + return nil, errs.ErrTransactionDestinationAccountCannotBeSet + } else if transactionCreateReq.Type == models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.SourceAccountId == transactionCreateReq.DestinationAccountId { + log.Warnf(c, "[transactions.TransactionImportHandler] transfer transaction \"index:%d\" source account must not be destination account", i) + return nil, errs.ErrTransactionSourceAndDestinationIdCannotBeEqual + } + + if transactionCreateReq.Type != models.TRANSACTION_TYPE_TRANSFER && transactionCreateReq.DestinationAmount != 0 { + log.Warnf(c, "[transactions.TransactionImportHandler] non-transfer transaction \"index:%d\" destination amount cannot be set", i) + return nil, errs.ErrTransactionDestinationAmountCannotBeSet + } + } + + user, err := a.users.GetUserById(c, uid) + + if err != nil { + if !errs.IsCustomError(err) { + log.Errorf(c, "[transactions.TransactionImportHandler] failed to get user, because %s", err.Error()) + } + + return nil, errs.ErrUserNotFound + } + + newTransactions := make([]*models.Transaction, len(transactionImportReq.Transactions)) + + for i := 0; i < len(transactionImportReq.Transactions); i++ { + transactionCreateReq := transactionImportReq.Transactions[i] + transaction := a.createNewTransactionModel(uid, transactionCreateReq, c.ClientIP()) + transactionEditable := user.CanEditTransactionByTransactionTime(transaction.TransactionTime, transactionCreateReq.UtcOffset) + + if !transactionEditable { + return nil, errs.ErrCannotCreateTransactionWithThisTransactionTime + } + + newTransactions[i] = transaction + } + + err = a.transactions.BatchCreateTransactions(c, user.Uid, newTransactions) + count := len(newTransactions) + + if err != nil { + log.Errorf(c, "[transactions.TransactionImportHandler] failed to import %d transactions for user \"uid:%d\", because %s", count, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + log.Infof(c, "[transactions.TransactionImportHandler] user \"uid:%d\" has imported %d transactions successfully", uid, count) + + a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS, uid, transactionImportReq.ClientSessionId, utils.IntToString(count)) + + return count, nil +} + func (a *TransactionsApi) filterTransactions(c *core.WebContext, uid int64, transactions []*models.Transaction, accountMap map[int64]*models.Account) []*models.Transaction { finalTransactions := make([]*models.Transaction, 0, len(transactions)) diff --git a/pkg/cli/user_data.go b/pkg/cli/user_data.go index 51ae148a..2d5b2502 100644 --- a/pkg/cli/user_data.go +++ b/pkg/cli/user_data.go @@ -720,7 +720,7 @@ func (l *UserDataCli) ImportTransaction(c *core.CliContext, username string, fil return errs.ErrOperationFailed } - err = l.transactions.BatchCreateTransactions(c, user.Uid, newTransactions) + err = l.transactions.BatchCreateTransactions(c, user.Uid, newTransactions.ToTransactionsList()) if err != nil { log.BootErrorf(c, "[user_data.ImportTransaction] failed to create transaction, because %s", err.Error()) diff --git a/pkg/converters/data_table_transaction_data_converter.go b/pkg/converters/data_table_transaction_data_converter.go index 6242d3c1..dc162ff1 100644 --- a/pkg/converters/data_table_transaction_data_converter.go +++ b/pkg/converters/data_table_transaction_data_converter.go @@ -82,7 +82,7 @@ func (c *DataTableTransactionDataConverter) buildExportedContent(ctx core.Contex return nil } -func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, user *models.User, dataTable ImportedDataTable, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, user *models.User, dataTable ImportedDataTable, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) { if dataTable.DataRowCount() < 1 { log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] cannot parse import data for user \"uid:%d\", because data table row count is less 1", user.Uid) return nil, nil, nil, nil, errs.ErrOperationFailed @@ -127,7 +127,7 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, tagMap = make(map[string]*models.TransactionTag) } - allNewTransactions := make(ImportedTransactionSlice, 0, dataTable.DataRowCount()) + allNewTransactions := make(models.ImportedTransactionSlice, 0, dataTable.DataRowCount()) allNewAccounts := make([]*models.Account, 0) allNewSubCategories := make([]*models.TransactionCategory, 0) allNewTags := make([]*models.TransactionTag, 0) @@ -177,6 +177,7 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, } categoryId := int64(0) + subCategoryName := "" if transactionDbType != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { transactionCategoryType, err := c.getTransactionCategoryType(transactionDbType) @@ -186,7 +187,7 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, return nil, nil, nil, nil, err } - subCategoryName := dataRow.GetData(subCategoryColumnIdx) + subCategoryName = dataRow.GetData(subCategoryColumnIdx) if subCategoryName == "" { log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] sub category type is empty in data row \"index:%d\" for user \"uid:%d\"", dataRowIndex, user.Uid) @@ -211,30 +212,32 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, return nil, nil, nil, nil, errs.ErrFormatInvalid } + accountCurrency := user.DefaultCurrency + + if accountCurrencyColumnExists { + accountCurrency = dataRow.GetData(accountCurrencyColumnIdx) + + if _, ok := validators.AllCurrencyNames[accountCurrency]; !ok { + log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", accountCurrency, dataRowIndex, user.Uid) + return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid + } + } + account, exists := accountMap[accountName] if !exists { - currency := user.DefaultCurrency - - if accountCurrencyColumnExists { - currency = dataRow.GetData(accountCurrencyColumnIdx) - - if _, ok := validators.AllCurrencyNames[currency]; !ok { - log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", currency, dataRowIndex, user.Uid) - return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid - } - } - - account = c.createNewAccountModel(user.Uid, accountName, currency) + account = c.createNewAccountModel(user.Uid, accountName, accountCurrency) allNewAccounts = append(allNewAccounts, account) accountMap[accountName] = account } if accountCurrencyColumnExists { - if account.Currency != dataRow.GetData(accountCurrencyColumnIdx) { - log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account for user \"uid:%d\"", dataRow.GetData(accountCurrencyColumnIdx), dataRowIndex, account.Currency, user.Uid) + if account.Currency != accountCurrency { + log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account for user \"uid:%d\"", accountCurrency, dataRowIndex, account.Currency, user.Uid) return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid } + } else if exists { + accountCurrency = account.Currency } amount, err := utils.ParseAmount(dataRow.GetData(amountColumnIdx)) @@ -246,39 +249,43 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, relatedAccountId := int64(0) relatedAccountAmount := int64(0) + account2Name := "" + account2Currency := "" if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT { - account2Name := dataRow.GetData(account2ColumnIdx) + account2Name = dataRow.GetData(account2ColumnIdx) if account2Name == "" { log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 name is empty in data row \"index:%d\" for user \"uid:%d\"", dataRowIndex, user.Uid) return nil, nil, nil, nil, errs.ErrFormatInvalid } + account2Currency = user.DefaultCurrency + + if account2CurrencyColumnExists { + account2Currency = dataRow.GetData(account2CurrencyColumnIdx) + + if _, ok := validators.AllCurrencyNames[account2Currency]; !ok { + log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", account2Currency, dataRowIndex, user.Uid) + return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid + } + } + account2, exists := accountMap[account2Name] if !exists { - currency := user.DefaultCurrency - - if accountCurrencyColumnExists { - currency = dataRow.GetData(account2CurrencyColumnIdx) - - if _, ok := validators.AllCurrencyNames[currency]; !ok { - log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] account2 currency \"%s\" is not supported in data row \"index:%d\" for user \"uid:%d\"", currency, dataRowIndex, user.Uid) - return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid - } - } - - account2 = c.createNewAccountModel(user.Uid, account2Name, currency) + account2 = c.createNewAccountModel(user.Uid, account2Name, account2Currency) allNewAccounts = append(allNewAccounts, account2) accountMap[account2Name] = account2 } if account2CurrencyColumnExists { - if account2.Currency != dataRow.GetData(account2CurrencyColumnIdx) { - log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account2 for user \"uid:%d\"", dataRow.GetData(account2CurrencyColumnIdx), dataRowIndex, account2.Currency, user.Uid) + if account2.Currency != account2Currency { + log.Errorf(ctx, "[data_table_transaction_data_converter.parseImportedData] currency \"%s\" in data row \"index:%d\" not equals currency \"%s\" of the account2 for user \"uid:%d\"", account2Currency, dataRowIndex, account2.Currency, user.Uid) return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid } + } else if exists { + account2Currency = account2.Currency } relatedAccountId = account2.AccountId @@ -313,11 +320,14 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, } } - if tagsColumnExists { - tagNames := strings.Split(dataRow.GetData(tagsColumnIdx), c.transactionTagSeparator) + var tagIds []string + var tagNames []string - for i := 0; i < len(tagNames); i++ { - tagName := tagNames[i] + if tagsColumnExists { + tagNameItems := strings.Split(dataRow.GetData(tagsColumnIdx), c.transactionTagSeparator) + + for i := 0; i < len(tagNameItems); i++ { + tagName := tagNameItems[i] if tagName == "" { continue @@ -330,6 +340,12 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, allNewTags = append(allNewTags, tag) tagMap[tagName] = tag } + + if tag != nil { + tagIds = append(tagIds, utils.Int64ToString(tag.TagId)) + } + + tagNames = append(tagNames, tagName) } } @@ -339,21 +355,30 @@ func (c *DataTableTransactionDataConverter) parseImportedData(ctx core.Context, description = dataRow.GetData(descriptionColumnIdx) } - transaction := &models.Transaction{ - Uid: user.Uid, - Type: transactionDbType, - CategoryId: categoryId, - TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()), - TimezoneUtcOffset: timezoneOffset, - AccountId: account.AccountId, - Amount: amount, - HideAmount: false, - RelatedAccountId: relatedAccountId, - RelatedAccountAmount: relatedAccountAmount, - Comment: description, - GeoLongitude: geoLongitude, - GeoLatitude: geoLatitude, - CreatedIp: "127.0.0.1", + transaction := &models.ImportTransaction{ + Transaction: &models.Transaction{ + Uid: user.Uid, + Type: transactionDbType, + CategoryId: categoryId, + TransactionTime: utils.GetMinTransactionTimeFromUnixTime(transactionTime.Unix()), + TimezoneUtcOffset: timezoneOffset, + AccountId: account.AccountId, + Amount: amount, + HideAmount: false, + RelatedAccountId: relatedAccountId, + RelatedAccountAmount: relatedAccountAmount, + Comment: description, + GeoLongitude: geoLongitude, + GeoLatitude: geoLatitude, + CreatedIp: "127.0.0.1", + }, + TagIds: tagIds, + OriginalCategoryName: subCategoryName, + OriginalSourceAccountName: accountName, + OriginalSourceAccountCurrency: accountCurrency, + OriginalDestinationAccountName: account2Name, + OriginalDestinationAccountCurrency: account2Currency, + OriginalTagNames: tagNames, } allNewTransactions = append(allNewTransactions, transaction) diff --git a/pkg/converters/ezbookkeeping_transaction_data_plain_text_converter.go b/pkg/converters/ezbookkeeping_transaction_data_plain_text_converter.go index a9db7c60..63088898 100644 --- a/pkg/converters/ezbookkeeping_transaction_data_plain_text_converter.go +++ b/pkg/converters/ezbookkeeping_transaction_data_plain_text_converter.go @@ -73,7 +73,7 @@ func (c *ezBookKeepingTransactionDataPlainTextConverter) ToExportedContent(ctx c } // ParseImportedData returns the imported data by parsing the transaction plain text data -func (c *ezBookKeepingTransactionDataPlainTextConverter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) { +func (c *ezBookKeepingTransactionDataPlainTextConverter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) { dataTable, err := createNewezbookkeepingTransactionPlainTextDataTable(string(data), c.columnSeparator, c.lineSeparator) if err != nil { diff --git a/pkg/converters/ezbookkeeping_transaction_data_plain_text_data_table.go b/pkg/converters/ezbookkeeping_transaction_data_plain_text_data_table.go index 460ea1fd..4fe334c6 100644 --- a/pkg/converters/ezbookkeeping_transaction_data_plain_text_data_table.go +++ b/pkg/converters/ezbookkeeping_transaction_data_plain_text_data_table.go @@ -147,7 +147,9 @@ func createNewezbookkeepingTransactionPlainTextDataTable(content string, columnS return nil, errs.ErrOperationFailed } - headerLineItems := strings.Split(allLines[0], columnSeparator) + headerLine := allLines[0] + headerLine = strings.ReplaceAll(headerLine, "\r", "") + headerLineItems := strings.Split(headerLine, columnSeparator) return &ezBookKeepingTransactionPlainTextDataTable{ columnSeparator: columnSeparator, diff --git a/pkg/converters/imported_transactions.go b/pkg/converters/imported_transactions.go deleted file mode 100644 index e926db52..00000000 --- a/pkg/converters/imported_transactions.go +++ /dev/null @@ -1,29 +0,0 @@ -package converters - -import "github.com/mayswind/ezbookkeeping/pkg/models" - -// ImportedTransactionSlice represents the slice data structure of import transaction data -type ImportedTransactionSlice []*models.Transaction - -// Len returns the count of items -func (s ImportedTransactionSlice) Len() int { - return len(s) -} - -// Swap swaps two items -func (s ImportedTransactionSlice) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -// Less reports whether the first item is less than the second one -func (s ImportedTransactionSlice) Less(i, j int) bool { - if s[i].Type != s[j].Type && (s[i].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE || s[j].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE) { - if s[i].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { - return true - } else if s[j].Type == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { - return false - } - } - - return s[i].TransactionTime < s[j].TransactionTime -} diff --git a/pkg/converters/imported_transactions_test.go b/pkg/converters/imported_transactions_test.go deleted file mode 100644 index fd17956f..00000000 --- a/pkg/converters/imported_transactions_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package converters - -import ( - "sort" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/mayswind/ezbookkeeping/pkg/models" -) - -func TestImportTransactionSliceLess(t *testing.T) { - var transactionSlice ImportedTransactionSlice - transactionSlice = append(transactionSlice, &models.Transaction{ - TransactionId: 1, - Type: models.TRANSACTION_DB_TYPE_EXPENSE, - TransactionTime: 1, - }) - transactionSlice = append(transactionSlice, &models.Transaction{ - TransactionId: 2, - Type: models.TRANSACTION_DB_TYPE_INCOME, - TransactionTime: 2, - }) - transactionSlice = append(transactionSlice, &models.Transaction{ - TransactionId: 3, - Type: models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, - TransactionTime: 10, - }) - transactionSlice = append(transactionSlice, &models.Transaction{ - TransactionId: 4, - Type: models.TRANSACTION_DB_TYPE_TRANSFER_IN, - TransactionTime: 3, - }) - transactionSlice = append(transactionSlice, &models.Transaction{ - TransactionId: 5, - Type: models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, - TransactionTime: 11, - }) - transactionSlice = append(transactionSlice, &models.Transaction{ - TransactionId: 6, - Type: models.TRANSACTION_DB_TYPE_TRANSFER_OUT, - TransactionTime: 4, - }) - - sort.Sort(transactionSlice) - - assert.Equal(t, int64(3), transactionSlice[0].TransactionId) - assert.Equal(t, int64(5), transactionSlice[1].TransactionId) - assert.Equal(t, int64(1), transactionSlice[2].TransactionId) - assert.Equal(t, int64(2), transactionSlice[3].TransactionId) - assert.Equal(t, int64(4), transactionSlice[4].TransactionId) - assert.Equal(t, int64(6), transactionSlice[5].TransactionId) -} diff --git a/pkg/converters/transaction_data_converter.go b/pkg/converters/transaction_data_converter.go index 3d65f05a..23c41a52 100644 --- a/pkg/converters/transaction_data_converter.go +++ b/pkg/converters/transaction_data_converter.go @@ -14,7 +14,7 @@ type TransactionDataExporter interface { // TransactionDataImporter defines the structure of transaction data importer type TransactionDataImporter interface { // ParseImportedData returns the imported data - ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) ([]*models.Transaction, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) + ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, categoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionTag, error) } // TransactionDataConverter defines the structure of transaction data converter diff --git a/pkg/duplicatechecker/duplicate_checker_type.go b/pkg/duplicatechecker/duplicate_checker_type.go index 06b916bc..032b3f9c 100644 --- a/pkg/duplicatechecker/duplicate_checker_type.go +++ b/pkg/duplicatechecker/duplicate_checker_type.go @@ -11,4 +11,5 @@ const ( DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3 DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4 DUPLICATE_CHECKER_TYPE_NEW_PICTURE DuplicateCheckerType = 5 + DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS DuplicateCheckerType = 6 ) diff --git a/pkg/errs/global.go b/pkg/errs/global.go index a1302c35..22b11e73 100644 --- a/pkg/errs/global.go +++ b/pkg/errs/global.go @@ -22,6 +22,9 @@ var ( ErrParameterInvalid = NewNormalError(NormalSubcategoryGlobal, 12, http.StatusBadRequest, "parameter invalid") ErrFormatInvalid = NewNormalError(NormalSubcategoryGlobal, 13, http.StatusBadRequest, "format invalid") ErrNumberInvalid = NewNormalError(NormalSubcategoryGlobal, 14, http.StatusBadRequest, "number invalid") + ErrNoFilesUpload = NewNormalError(NormalSubcategoryGlobal, 15, http.StatusBadRequest, "no files uploaded") + ErrUploadedFileEmpty = NewNormalError(NormalSubcategoryGlobal, 16, http.StatusBadRequest, "uploaded file is empty") + ErrExceedMaxUploadFileSize = NewNormalError(NormalSubcategoryGlobal, 17, http.StatusBadRequest, "uploaded file size exceeds the maximum allowed size") ) // GetParameterInvalidMessage returns specific error message for invalid parameter error diff --git a/pkg/errs/system.go b/pkg/errs/system.go index 378b782c..04b6f9e8 100644 --- a/pkg/errs/system.go +++ b/pkg/errs/system.go @@ -4,12 +4,11 @@ import "net/http" // Error codes related to transaction categories var ( - ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error") - ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found") - ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed") - ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented") - ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy") - ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported") - ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported") - ErrImportFileTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 7, http.StatusBadRequest, "import file type not supported") + ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error") + ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found") + ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed") + ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented") + ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy") + ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported") + ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported") ) diff --git a/pkg/errs/transaction.go b/pkg/errs/transaction.go index 517e16fb..aa26e899 100644 --- a/pkg/errs/transaction.go +++ b/pkg/errs/transaction.go @@ -29,4 +29,7 @@ var ( ErrCannotUseHiddenTransactionTag = NewNormalError(NormalSubcategoryTransaction, 22, http.StatusBadRequest, "cannot use hidden transaction tag") ErrTransactionHasTooManyTags = NewNormalError(NormalSubcategoryTransaction, 23, http.StatusBadRequest, "transaction has too many tags") ErrTransactionHasTooManyPictures = NewNormalError(NormalSubcategoryTransaction, 24, http.StatusBadRequest, "transaction has too many pictures") + ErrImportFileTypeIsEmpty = NewSystemError(NormalSubcategoryTransaction, 25, http.StatusBadRequest, "import file type is empty") + ErrImportFileTypeNotSupported = NewSystemError(NormalSubcategoryTransaction, 26, http.StatusBadRequest, "import file type not supported") + ErrNoDataToImport = NewSystemError(NormalSubcategoryTransaction, 27, http.StatusBadRequest, "no data to import") ) diff --git a/pkg/middlewares/server_settings_cookie.go b/pkg/middlewares/server_settings_cookie.go index 64793f64..486785ad 100644 --- a/pkg/middlewares/server_settings_cookie.go +++ b/pkg/middlewares/server_settings_cookie.go @@ -22,6 +22,7 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc { buildBooleanSetting("p", config.EnableTransactionPictures), buildBooleanSetting("s", config.EnableScheduledTransaction), buildBooleanSetting("e", config.EnableDataExport), + buildBooleanSetting("i", config.EnableDataImport), buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)), } diff --git a/pkg/models/imported_transaction.go b/pkg/models/imported_transaction.go new file mode 100644 index 00000000..bb977ff0 --- /dev/null +++ b/pkg/models/imported_transaction.go @@ -0,0 +1,145 @@ +package models + +import "github.com/mayswind/ezbookkeeping/pkg/utils" + +// ImportTransaction represents the imported transaction data +type ImportTransaction struct { + *Transaction + TagIds []string + OriginalCategoryName string + OriginalSourceAccountName string + OriginalSourceAccountCurrency string + OriginalDestinationAccountName string + OriginalDestinationAccountCurrency string + OriginalTagNames []string +} + +// ImportTransactionResponse represents a view-object of the imported transaction data +type ImportTransactionResponse struct { + Type TransactionType `json:"type"` + CategoryId int64 `json:"categoryId,string"` + OriginalCategoryName string `json:"originalCategoryName"` + Time int64 `json:"time"` + UtcOffset int16 `json:"utcOffset"` + SourceAccountId int64 `json:"sourceAccountId,string"` + OriginalSourceAccountName string `json:"originalSourceAccountName"` + OriginalSourceAccountCurrency string `json:"originalSourceAccountCurrency"` + DestinationAccountId int64 `json:"destinationAccountId,string,omitempty"` + OriginalDestinationAccountName string `json:"originalDestinationAccountName,omitempty"` + OriginalDestinationAccountCurrency string `json:"originalDestinationAccountCurrency,omitempty"` + SourceAmount int64 `json:"sourceAmount"` + DestinationAmount int64 `json:"destinationAmount,omitempty"` + TagIds []string `json:"tagIds"` + OriginalTagNames []string `json:"originalTagNames"` + Comment string `json:"comment"` + GeoLocation *TransactionGeoLocationResponse `json:"geoLocation,omitempty"` +} + +// ImportTransactionResponsePageWrapper represents a response of imported transaction which contains items and count +type ImportTransactionResponsePageWrapper struct { + Items []*ImportTransactionResponse `json:"items"` + TotalCount int64 `json:"totalCount"` +} + +// ToImportTransactionResponse returns the a view-objects according to imported transaction data +func (t ImportTransaction) ToImportTransactionResponse() *ImportTransactionResponse { + var transactionType TransactionType + + if t.Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE { + transactionType = TRANSACTION_TYPE_MODIFY_BALANCE + } else if t.Type == TRANSACTION_DB_TYPE_EXPENSE { + transactionType = TRANSACTION_TYPE_EXPENSE + } else if t.Type == TRANSACTION_DB_TYPE_INCOME { + transactionType = TRANSACTION_TYPE_INCOME + } else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_OUT { + transactionType = TRANSACTION_TYPE_TRANSFER + } else if t.Type == TRANSACTION_DB_TYPE_TRANSFER_IN { + transactionType = TRANSACTION_TYPE_TRANSFER + } else { + return nil + } + + geoLocation := &TransactionGeoLocationResponse{} + + if t.GeoLongitude != 0 || t.GeoLatitude != 0 { + geoLocation.Longitude = t.GeoLongitude + geoLocation.Latitude = t.GeoLatitude + } else { + geoLocation = nil + } + + return &ImportTransactionResponse{ + Type: transactionType, + CategoryId: t.CategoryId, + OriginalCategoryName: t.OriginalCategoryName, + Time: utils.GetUnixTimeFromTransactionTime(t.TransactionTime), + UtcOffset: t.TimezoneUtcOffset, + SourceAccountId: t.AccountId, + OriginalSourceAccountName: t.OriginalSourceAccountName, + OriginalSourceAccountCurrency: t.OriginalSourceAccountCurrency, + DestinationAccountId: t.RelatedAccountId, + OriginalDestinationAccountName: t.OriginalDestinationAccountName, + OriginalDestinationAccountCurrency: t.OriginalDestinationAccountCurrency, + SourceAmount: t.Amount, + DestinationAmount: t.RelatedAccountAmount, + TagIds: t.TagIds, + OriginalTagNames: t.OriginalTagNames, + Comment: t.Comment, + GeoLocation: geoLocation, + } +} + +// ImportedTransactionSlice represents the slice data structure of import transaction data +type ImportedTransactionSlice []*ImportTransaction + +// Len returns the count of items +func (s ImportedTransactionSlice) Len() int { + return len(s) +} + +// Swap swaps two items +func (s ImportedTransactionSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less reports whether the first item is less than the second one +func (s ImportedTransactionSlice) Less(i, j int) bool { + if s[i].Type != s[j].Type && (s[i].Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE || s[j].Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE) { + if s[i].Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE { + return true + } else if s[j].Type == TRANSACTION_DB_TYPE_MODIFY_BALANCE { + return false + } + } + + return s[i].TransactionTime < s[j].TransactionTime +} + +// ToTransactionsList returns the a list of transactions +func (s ImportedTransactionSlice) ToTransactionsList() []*Transaction { + transactions := make([]*Transaction, s.Len()) + + for i := 0; i < s.Len(); i++ { + transactions[i] = s[i].Transaction + } + + return transactions +} + +// ToImportTransactionResponseList returns the a list of view-objects according to imported transaction data +func (s ImportedTransactionSlice) ToImportTransactionResponseList() []*ImportTransactionResponse { + transactionResps := make([]*ImportTransactionResponse, 0, s.Len()) + + for i := 0; i < s.Len(); i++ { + importedTransaction := s[i] + importedTransactionResp := importedTransaction.ToImportTransactionResponse() + + if importedTransactionResp == nil { + continue + } + + transactionResps = append(transactionResps, importedTransactionResp) + } + + return transactionResps +} diff --git a/pkg/models/imported_transaction_test.go b/pkg/models/imported_transaction_test.go new file mode 100644 index 00000000..aee4b321 --- /dev/null +++ b/pkg/models/imported_transaction_test.go @@ -0,0 +1,63 @@ +package models + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestImportTransactionSliceLess(t *testing.T) { + var transactionSlice ImportedTransactionSlice + transactionSlice = append(transactionSlice, &ImportTransaction{ + Transaction: &Transaction{ + TransactionId: 1, + Type: TRANSACTION_DB_TYPE_EXPENSE, + TransactionTime: 1, + }, + }) + transactionSlice = append(transactionSlice, &ImportTransaction{ + Transaction: &Transaction{ + TransactionId: 2, + Type: TRANSACTION_DB_TYPE_INCOME, + TransactionTime: 2, + }, + }) + transactionSlice = append(transactionSlice, &ImportTransaction{ + Transaction: &Transaction{ + TransactionId: 3, + Type: TRANSACTION_DB_TYPE_MODIFY_BALANCE, + TransactionTime: 10, + }, + }) + transactionSlice = append(transactionSlice, &ImportTransaction{ + Transaction: &Transaction{ + TransactionId: 4, + Type: TRANSACTION_DB_TYPE_TRANSFER_IN, + TransactionTime: 3, + }, + }) + transactionSlice = append(transactionSlice, &ImportTransaction{ + Transaction: &Transaction{ + TransactionId: 5, + Type: TRANSACTION_DB_TYPE_MODIFY_BALANCE, + TransactionTime: 11, + }, + }) + transactionSlice = append(transactionSlice, &ImportTransaction{ + Transaction: &Transaction{ + TransactionId: 6, + Type: TRANSACTION_DB_TYPE_TRANSFER_OUT, + TransactionTime: 4, + }, + }) + + sort.Sort(transactionSlice) + + assert.Equal(t, int64(3), transactionSlice[0].TransactionId) + assert.Equal(t, int64(5), transactionSlice[1].TransactionId) + assert.Equal(t, int64(1), transactionSlice[2].TransactionId) + assert.Equal(t, int64(2), transactionSlice[3].TransactionId) + assert.Equal(t, int64(4), transactionSlice[4].TransactionId) + assert.Equal(t, int64(6), transactionSlice[5].TransactionId) +} diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index 087aed84..dd7a0b84 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -115,6 +115,12 @@ type TransactionModifyRequest struct { GeoLocation *TransactionGeoLocationRequest `json:"geoLocation" binding:"omitempty"` } +// TransactionImportRequest represents all parameters of transaction import request +type TransactionImportRequest struct { + Transactions []*TransactionCreateRequest `json:"transactions"` + ClientSessionId string `json:"clientSessionId"` +} + // TransactionCountRequest represents transaction count request type TransactionCountRequest struct { Type TransactionDbType `form:"type" binding:"min=0,max=4"` diff --git a/pkg/settings/setting.go b/pkg/settings/setting.go index c93bf7ef..3b26e837 100644 --- a/pkg/settings/setting.go +++ b/pkg/settings/setting.go @@ -138,6 +138,8 @@ const ( defaultTransactionPictureFileMaxSize uint32 = 10485760 // 10MB defaultUserAvatarFileMaxSize uint32 = 1048576 // 1MB + defaultImportFileMaxSize uint32 = 10485760 // 10MB + defaultExchangeRatesDataRequestTimeout uint32 = 10000 // 10 seconds ) @@ -282,7 +284,9 @@ type Config struct { MaxAvatarFileSize uint32 // Data - EnableDataExport bool + EnableDataExport bool + EnableDataImport bool + MaxImportFileSize uint32 // Notification AfterRegisterNotification NotificationConfig @@ -768,6 +772,8 @@ func loadUserConfiguration(config *Config, configFile *ini.File, sectionName str func loadDataConfiguration(config *Config, configFile *ini.File, sectionName string) error { config.EnableDataExport = getConfigItemBoolValue(configFile, sectionName, "enable_export", false) + config.EnableDataImport = getConfigItemBoolValue(configFile, sectionName, "enable_import", false) + config.MaxImportFileSize = getConfigItemUint32Value(configFile, sectionName, "max_import_file_size", defaultImportFileMaxSize) return nil } diff --git a/src/components/desktop/ConfirmDialog.vue b/src/components/desktop/ConfirmDialog.vue index b82d2ff1..c6aed3fc 100644 --- a/src/components/desktop/ConfirmDialog.vue +++ b/src/components/desktop/ConfirmDialog.vue @@ -52,12 +52,12 @@ export default { this.showState = true; if (isString(text)) { - this.titleContent = this.$t(title); - this.textContent = this.$t(text); + this.titleContent = this.$t(title, options); + this.textContent = this.$t(text, options); } else { - this.titleContent = this.$t('global.app.title'); - this.textContent = this.$t(title); options = text; + this.titleContent = this.$t('global.app.title'); + this.textContent = this.$t(title, options); } if (options && options.color) { diff --git a/src/components/desktop/StepsBar.vue b/src/components/desktop/StepsBar.vue index cc0249ac..f7bf2bc3 100644 --- a/src/components/desktop/StepsBar.vue +++ b/src/components/desktop/StepsBar.vue @@ -2,8 +2,8 @@