diff --git a/cmd/user_data.go b/cmd/user_data.go index fb8b30fb..168b4815 100644 --- a/cmd/user_data.go +++ b/cmd/user_data.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/models" "os" @@ -231,7 +232,7 @@ var UserData = &cli.Command{ }, { Name: "transaction-export", - Usage: "Export user all transactions to csv file", + Usage: "Export user all transactions to file", Action: exportUserTransaction, Flags: []cli.Flag{ &cli.StringFlag{ @@ -246,6 +247,12 @@ var UserData = &cli.Command{ Required: true, Usage: "Specific exported file path (e.g. transaction.csv)", }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Required: false, + Usage: "Export file type, support csv or tsv, default is csv", + }, }, }, }, @@ -555,6 +562,12 @@ func exportUserTransaction(c *cli.Context) error { username := c.String("username") filePath := c.String("file") + fileType := c.String("type") + + if fileType != "" && fileType != "csv" && fileType != "tsv" { + log.BootErrorf("[user_data.exportUserTransaction] export file type is not supported") + return errs.ErrNotSupported + } if filePath == "" { log.BootErrorf("[user_data.exportUserTransaction] export file path is not specified") @@ -570,7 +583,7 @@ func exportUserTransaction(c *cli.Context) error { log.BootInfof("[user_data.exportUserTransaction] starting exporting user \"%s\" data", username) - content, err := clis.UserData.ExportTransaction(c, username) + content, err := clis.UserData.ExportTransaction(c, username, fileType) if err != nil { log.BootErrorf("[user_data.exportUserTransaction] error occurs when exporting user data") diff --git a/cmd/webserver.go b/cmd/webserver.go index a314944f..ae68174f 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -256,7 +256,8 @@ func startWebServer(c *cli.Context) error { apiV1Route.POST("/data/clear.json", bindApi(api.DataManagements.ClearDataHandler)) if config.EnableDataExport { - apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataHandler)) + apiV1Route.GET("/data/export.csv", bindCsv(api.DataManagements.ExportDataToEzbookkeepingCSVHandler)) + apiV1Route.GET("/data/export.tsv", bindTsv(api.DataManagements.ExportDataToEzbookkeepingTSVHandler)) } // Accounts @@ -376,6 +377,19 @@ func bindCsv(fn core.DataHandlerFunc) gin.HandlerFunc { } } +func bindTsv(fn core.DataHandlerFunc) gin.HandlerFunc { + return func(ginCtx *gin.Context) { + c := core.WrapContext(ginCtx) + result, fileName, err := fn(c) + + if err != nil { + utils.PrintDataErrorResult(c, "text/text", err) + } else { + utils.PrintDataSuccessResult(c, "text/tab-separated-values", fileName, result) + } + } +} + func bindCachedPngImage(fn core.DataHandlerFunc, store persistence.CacheStore) gin.HandlerFunc { return cache.CachePage(store, time.Minute, func(ginCtx *gin.Context) { c := core.WrapContext(ginCtx) diff --git a/pkg/api/data_managements.go b/pkg/api/data_managements.go index a69948ae..7acca331 100644 --- a/pkg/api/data_managements.go +++ b/pkg/api/data_managements.go @@ -19,103 +19,38 @@ const pageCountForDataExport = 1000 // DataManagementsApi represents data management api type DataManagementsApi struct { - exporter *converters.EzBookKeepingCSVFileExporter - tokens *services.TokenService - users *services.UserService - accounts *services.AccountService - transactions *services.TransactionService - categories *services.TransactionCategoryService - tags *services.TransactionTagService + ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter + ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter + tokens *services.TokenService + users *services.UserService + accounts *services.AccountService + transactions *services.TransactionService + categories *services.TransactionCategoryService + tags *services.TransactionTagService } // Initialize a data management api singleton instance var ( DataManagements = &DataManagementsApi{ - exporter: &converters.EzBookKeepingCSVFileExporter{}, - tokens: services.Tokens, - users: services.Users, - accounts: services.Accounts, - transactions: services.Transactions, - categories: services.TransactionCategories, - tags: services.TransactionTags, + ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{}, + ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{}, + tokens: services.Tokens, + users: services.Users, + accounts: services.Accounts, + transactions: services.Transactions, + categories: services.TransactionCategories, + tags: services.TransactionTags, } ) -// ExportDataHandler returns exported data in csv format -func (a *DataManagementsApi) ExportDataHandler(c *core.Context) ([]byte, string, *errs.Error) { - if !settings.Container.Current.EnableDataExport { - return nil, "", errs.ErrDataExportNotAllowed - } - - timezone := time.Local - utcOffset, err := c.GetClientTimezoneOffset() - - if err != nil { - log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error()) - } else { - timezone = time.FixedZone("Client Timezone", int(utcOffset)*60) - } - - uid := c.GetCurrentUid() - user, err := a.users.GetUserById(c, uid) - - if err != nil { - if !errs.IsCustomError(err) { - log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error()) - } - - return nil, "", errs.ErrUserNotFound - } - - accounts, err := a.accounts.GetAllAccountsByUid(c, uid) - - if err != nil { - log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error()) - return nil, "", errs.ErrOperationFailed - } - - categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1) - - if err != nil { - log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error()) - return nil, "", errs.ErrOperationFailed - } - - tags, err := a.tags.GetAllTagsByUid(c, uid) - - if err != nil { - log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error()) - return nil, "", errs.ErrOperationFailed - } - - tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(c, uid) - - if err != nil { - log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error()) - return nil, "", errs.ErrOperationFailed - } - - accountMap := a.accounts.GetAccountMapByList(accounts) - categoryMap := a.categories.GetCategoryMapByList(categories) - tagMap := a.tags.GetTagMapByList(tags) - - allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true) - - if err != nil { - log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error()) - return nil, "", errs.ErrOperationFailed - } - - result, err := a.exporter.ToExportedContent(uid, timezone, allTransactions, accountMap, categoryMap, tagMap, tagIndexs) - - if err != nil { - log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error()) - return nil, "", errs.Or(err, errs.ErrOperationFailed) - } - - fileName := a.getFileName(user, timezone) +// ExportDataToEzbookkeepingCSVHandler returns exported data in csv format +func (a *DataManagementsApi) ExportDataToEzbookkeepingCSVHandler(c *core.Context) ([]byte, string, *errs.Error) { + return a.getExportedFileContent(c, "csv") +} - return result, fileName, nil +// ExportDataToEzbookkeepingTSVHandler returns exported data in csv format +func (a *DataManagementsApi) ExportDataToEzbookkeepingTSVHandler(c *core.Context) ([]byte, string, *errs.Error) { + return a.getExportedFileContent(c, "tsv") } // DataStatisticsHandler returns user data statistics @@ -209,11 +144,95 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.Context) (any, *errs.Error return true, nil } -func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location) string { +func (a *DataManagementsApi) getExportedFileContent(c *core.Context, fileType string) ([]byte, string, *errs.Error) { + if !settings.Container.Current.EnableDataExport { + return nil, "", errs.ErrDataExportNotAllowed + } + + timezone := time.Local + utcOffset, err := c.GetClientTimezoneOffset() + + if err != nil { + log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] cannot get client timezone offset, because %s", err.Error()) + } else { + timezone = time.FixedZone("Client Timezone", int(utcOffset)*60) + } + + uid := c.GetCurrentUid() + user, err := a.users.GetUserById(c, uid) + + if err != nil { + if !errs.IsCustomError(err) { + log.WarnfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error()) + } + + return nil, "", errs.ErrUserNotFound + } + + accounts, err := a.accounts.GetAllAccountsByUid(c, uid) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get all accounts for user \"uid:%d\", because %s", uid, err.Error()) + return nil, "", errs.ErrOperationFailed + } + + categories, err := a.categories.GetAllCategoriesByUid(c, uid, 0, -1) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get categories for user \"uid:%d\", because %s", uid, err.Error()) + return nil, "", errs.ErrOperationFailed + } + + tags, err := a.tags.GetAllTagsByUid(c, uid) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tags for user \"uid:%d\", because %s", uid, err.Error()) + return nil, "", errs.ErrOperationFailed + } + + tagIndexs, err := a.tags.GetAllTagIdsOfAllTransactions(c, uid) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get tag index for user \"uid:%d\", because %s", uid, err.Error()) + return nil, "", errs.ErrOperationFailed + } + + accountMap := a.accounts.GetAccountMapByList(accounts) + categoryMap := a.categories.GetCategoryMapByList(categories) + tagMap := a.tags.GetTagMapByList(tags) + + allTransactions, err := a.transactions.GetAllTransactions(c, uid, pageCountForDataExport, true) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to all transactions user \"uid:%d\", because %s", uid, err.Error()) + return nil, "", errs.ErrOperationFailed + } + + var dataExporter converters.DataConverter + + if fileType == "tsv" { + dataExporter = a.ezBookKeepingTsvExporter + } else { + dataExporter = a.ezBookKeepingCsvExporter + } + + result, err := dataExporter.ToExportedContent(uid, timezone, allTransactions, accountMap, categoryMap, tagMap, tagIndexs) + + if err != nil { + log.ErrorfWithRequestId(c, "[data_managements.ExportDataHandler] failed to get csv format exported data for \"uid:%d\", because %s", uid, err.Error()) + return nil, "", errs.Or(err, errs.ErrOperationFailed) + } + + fileName := a.getFileName(user, timezone, fileType) + + return result, fileName, nil +} + +func (a *DataManagementsApi) getFileName(user *models.User, timezone *time.Location, fileExtension string) string { currentTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(time.Now().Unix(), timezone) currentTime = strings.Replace(currentTime, "-", "_", -1) currentTime = strings.Replace(currentTime, " ", "_", -1) currentTime = strings.Replace(currentTime, ":", "_", -1) - return fmt.Sprintf("%s_%s.csv", user.Username, currentTime) + return fmt.Sprintf("%s_%s.%s", user.Username, currentTime, fileExtension) } diff --git a/pkg/cli/user_data.go b/pkg/cli/user_data.go index fd32ddf9..598d6d89 100644 --- a/pkg/cli/user_data.go +++ b/pkg/cli/user_data.go @@ -20,6 +20,7 @@ const pageCountForDataExport = 1000 // UserDataCli represents user data cli type UserDataCli struct { ezBookKeepingCsvExporter *converters.EzBookKeepingCSVFileExporter + ezBookKeepingTsvExporter *converters.EzBookKeepingTSVFileExporter accounts *services.AccountService transactions *services.TransactionService categories *services.TransactionCategoryService @@ -34,6 +35,7 @@ type UserDataCli struct { var ( UserData = &UserDataCli{ ezBookKeepingCsvExporter: &converters.EzBookKeepingCSVFileExporter{}, + ezBookKeepingTsvExporter: &converters.EzBookKeepingTSVFileExporter{}, accounts: services.Accounts, transactions: services.Transactions, categories: services.TransactionCategories, @@ -537,7 +539,7 @@ func (l *UserDataCli) CheckTransactionAndAccount(c *cli.Context, username string } // ExportTransaction returns csv file content according user all transactions -func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte, error) { +func (l *UserDataCli) ExportTransaction(c *cli.Context, username string, fileType string) ([]byte, error) { if username == "" { log.BootErrorf("[user_data.ExportTransaction] user name is empty") return nil, errs.ErrUsernameIsEmpty @@ -564,7 +566,15 @@ func (l *UserDataCli) ExportTransaction(c *cli.Context, username string) ([]byte return nil, err } - result, err := l.ezBookKeepingCsvExporter.ToExportedContent(uid, time.Local, allTransactions, accountMap, categoryMap, tagMap, tagIndexs) + var dataExporter converters.DataConverter + + if fileType == "tsv" { + dataExporter = l.ezBookKeepingTsvExporter + } else { + dataExporter = l.ezBookKeepingCsvExporter + } + + result, err := dataExporter.ToExportedContent(uid, time.Local, allTransactions, accountMap, categoryMap, tagMap, tagIndexs) if err != nil { log.BootErrorf("[user_data.ExportTransaction] failed to get csv format exported data for \"%s\", because %s", username, err.Error()) diff --git a/pkg/converters/ezbookkeeping_csv_file.go b/pkg/converters/ezbookkeeping_csv_file.go index 8abc252a..f1decd05 100644 --- a/pkg/converters/ezbookkeeping_csv_file.go +++ b/pkg/converters/ezbookkeeping_csv_file.go @@ -1,179 +1,17 @@ package converters import ( - "fmt" - "strings" "time" "github.com/mayswind/ezbookkeeping/pkg/models" - "github.com/mayswind/ezbookkeeping/pkg/utils" ) -// EzBookKeepingCSVFileExporter defines the structure of csv file exporter +// EzBookKeepingCSVFileExporter defines the structure of CSV file exporter type EzBookKeepingCSVFileExporter struct { - DataConverter + EzBookKeepingPlainFileExporter } -const csvHeaderLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Tags,Comment\n" -const csvDataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n" - -// ToExportedContent returns the exported csv data +// ToExportedContent returns the exported CSV data func (e *EzBookKeepingCSVFileExporter) ToExportedContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error) { - var ret strings.Builder - - ret.Grow(len(transactions) * 100) - ret.WriteString(csvHeaderLine) - - for i := 0; i < len(transactions); i++ { - transaction := transactions[i] - - if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN { - continue - } - - transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60) - transactionTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone) - transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone) - transactionType := e.getTransactionTypeName(transaction.Type) - category := e.getTransactionCategoryName(transaction.CategoryId, categoryMap) - subCategory := e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap) - account := e.getAccountName(transaction.AccountId, accountMap) - accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap) - amount := e.getDisplayAmount(transaction.Amount) - account2 := "" - account2Currency := "" - account2Amount := "" - - if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT { - account2 = e.getAccountName(transaction.RelatedAccountId, accountMap) - account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap) - account2Amount = e.getDisplayAmount(transaction.RelatedAccountAmount) - } - - tags := e.getTags(transaction.TransactionId, allTagIndexs, tagMap) - comment := e.getComment(transaction.Comment) - - ret.WriteString(fmt.Sprintf(csvDataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, tags, comment)) - } - - return []byte(ret.String()), nil -} - -func (e *EzBookKeepingCSVFileExporter) getTransactionTypeName(transactionDbType models.TransactionDbType) string { - if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { - return "Balance Modification" - } else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME { - return "Income" - } else if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE { - return "Expense" - } else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_IN { - return "Transfer" - } else { - return "" - } -} - -func (e *EzBookKeepingCSVFileExporter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string { - category, exists := categoryMap[categoryId] - - if !exists { - return "" - } - - if category.ParentCategoryId == 0 { - return category.Name - } - - parentCategory, exists := categoryMap[category.ParentCategoryId] - - if !exists { - return "" - } - - return parentCategory.Name -} - -func (e *EzBookKeepingCSVFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string { - category, exists := categoryMap[categoryId] - - if exists { - return category.Name - } else { - return "" - } -} - -func (e *EzBookKeepingCSVFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string { - account, exists := accountMap[accountId] - - if exists { - return account.Name - } else { - return "" - } -} - -func (e *EzBookKeepingCSVFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string { - account, exists := accountMap[accountId] - - if exists { - return account.Currency - } else { - return "" - } -} - -func (e *EzBookKeepingCSVFileExporter) getDisplayAmount(amount int64) string { - displayAmount := utils.Int64ToString(amount) - integer := utils.SubString(displayAmount, 0, len(displayAmount)-2) - decimals := utils.SubString(displayAmount, -2, 2) - - if integer == "" { - integer = "0" - } else if integer == "-" { - integer = "-0" - } - - if len(decimals) == 0 { - decimals = "00" - } else if len(decimals) == 1 { - decimals = "0" + decimals - } - - return integer + "." + decimals -} - -func (e *EzBookKeepingCSVFileExporter) getTags(transactionId int64, allTagIndexs map[int64][]int64, tagMap map[int64]*models.TransactionTag) string { - tagIndexs, exists := allTagIndexs[transactionId] - - if !exists { - return "" - } - - var ret strings.Builder - - for i := 0; i < len(tagIndexs); i++ { - if i > 0 { - ret.WriteString(";") - } - - tagIndex := tagIndexs[i] - tag, exists := tagMap[tagIndex] - - if !exists { - continue - } - - ret.WriteString(tag.Name) - } - - return ret.String() -} - -func (e *EzBookKeepingCSVFileExporter) getComment(comment string) string { - comment = strings.Replace(comment, ",", " ", -1) - comment = strings.Replace(comment, "\r\n", " ", -1) - comment = strings.Replace(comment, "\n", " ", -1) - - return comment + return e.toExportedContent(uid, ",", timezone, transactions, accountMap, categoryMap, tagMap, allTagIndexs) } diff --git a/pkg/converters/ezbookkeeping_plain_file.go b/pkg/converters/ezbookkeeping_plain_file.go new file mode 100644 index 00000000..ea52aa40 --- /dev/null +++ b/pkg/converters/ezbookkeeping_plain_file.go @@ -0,0 +1,187 @@ +package converters + +import ( + "fmt" + "strings" + "time" + + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +// EzBookKeepingPlainFileExporter defines the structure of plain file exporter +type EzBookKeepingPlainFileExporter struct { +} + +const headerLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Currency,Amount,Account2,Account2 Currency,Account2 Amount,Tags,Comment\n" +const dataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n" + +// toExportedContent returns the exported plain data +func (e *EzBookKeepingPlainFileExporter) toExportedContent(uid int64, separator string, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error) { + var ret strings.Builder + + ret.Grow(len(transactions) * 100) + + if separator == "," { + ret.WriteString(headerLine) + } else { + ret.WriteString(strings.Replace(headerLine, ",", separator, -1)) + } + + for i := 0; i < len(transactions); i++ { + transaction := transactions[i] + + if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_IN { + continue + } + + transactionTimeZone := time.FixedZone("Transaction Timezone", int(transaction.TimezoneUtcOffset)*60) + transactionTime := utils.FormatUnixTimeToLongDateTimeWithoutSecond(utils.GetUnixTimeFromTransactionTime(transaction.TransactionTime), transactionTimeZone) + transactionTimezone := utils.FormatTimezoneOffset(transactionTimeZone) + transactionType := e.getTransactionTypeName(transaction.Type) + category := e.getTransactionCategoryName(transaction.CategoryId, categoryMap) + subCategory := e.getTransactionSubCategoryName(transaction.CategoryId, categoryMap) + account := e.getAccountName(transaction.AccountId, accountMap) + accountCurrency := e.getAccountCurrency(transaction.AccountId, accountMap) + amount := e.getDisplayAmount(transaction.Amount) + account2 := "" + account2Currency := "" + account2Amount := "" + + if transaction.Type == models.TRANSACTION_DB_TYPE_TRANSFER_OUT { + account2 = e.getAccountName(transaction.RelatedAccountId, accountMap) + account2Currency = e.getAccountCurrency(transaction.RelatedAccountId, accountMap) + account2Amount = e.getDisplayAmount(transaction.RelatedAccountAmount) + } + + tags := e.getTags(transaction.TransactionId, allTagIndexs, tagMap) + comment := e.getComment(transaction.Comment, separator) + + if separator == "," { + ret.WriteString(fmt.Sprintf(dataLineFormat, transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, tags, comment)) + } else { + ret.WriteString(fmt.Sprintf(strings.Replace(dataLineFormat, ",", separator, -1), transactionTime, transactionTimezone, transactionType, category, subCategory, account, accountCurrency, amount, account2, account2Currency, account2Amount, tags, comment)) + } + } + + return []byte(ret.String()), nil +} + +func (e *EzBookKeepingPlainFileExporter) getTransactionTypeName(transactionDbType models.TransactionDbType) string { + if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE { + return "Balance Modification" + } else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME { + return "Income" + } else if transactionDbType == models.TRANSACTION_DB_TYPE_EXPENSE { + return "Expense" + } else if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT || transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_IN { + return "Transfer" + } else { + return "" + } +} + +func (e *EzBookKeepingPlainFileExporter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string { + category, exists := categoryMap[categoryId] + + if !exists { + return "" + } + + if category.ParentCategoryId == 0 { + return category.Name + } + + parentCategory, exists := categoryMap[category.ParentCategoryId] + + if !exists { + return "" + } + + return parentCategory.Name +} + +func (e *EzBookKeepingPlainFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string { + category, exists := categoryMap[categoryId] + + if exists { + return category.Name + } else { + return "" + } +} + +func (e *EzBookKeepingPlainFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string { + account, exists := accountMap[accountId] + + if exists { + return account.Name + } else { + return "" + } +} + +func (e *EzBookKeepingPlainFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string { + account, exists := accountMap[accountId] + + if exists { + return account.Currency + } else { + return "" + } +} + +func (e *EzBookKeepingPlainFileExporter) getDisplayAmount(amount int64) string { + displayAmount := utils.Int64ToString(amount) + integer := utils.SubString(displayAmount, 0, len(displayAmount)-2) + decimals := utils.SubString(displayAmount, -2, 2) + + if integer == "" { + integer = "0" + } else if integer == "-" { + integer = "-0" + } + + if len(decimals) == 0 { + decimals = "00" + } else if len(decimals) == 1 { + decimals = "0" + decimals + } + + return integer + "." + decimals +} + +func (e *EzBookKeepingPlainFileExporter) getTags(transactionId int64, allTagIndexs map[int64][]int64, tagMap map[int64]*models.TransactionTag) string { + tagIndexs, exists := allTagIndexs[transactionId] + + if !exists { + return "" + } + + var ret strings.Builder + + for i := 0; i < len(tagIndexs); i++ { + if i > 0 { + ret.WriteString(";") + } + + tagIndex := tagIndexs[i] + tag, exists := tagMap[tagIndex] + + if !exists { + continue + } + + ret.WriteString(tag.Name) + } + + return ret.String() +} + +func (e *EzBookKeepingPlainFileExporter) getComment(comment string, separator string) string { + comment = strings.Replace(comment, separator, " ", -1) + comment = strings.Replace(comment, "\r\n", " ", -1) + comment = strings.Replace(comment, "\n", " ", -1) + + return comment +} diff --git a/pkg/converters/ezbookkeeping_tsv_file.go b/pkg/converters/ezbookkeeping_tsv_file.go new file mode 100644 index 00000000..811a6302 --- /dev/null +++ b/pkg/converters/ezbookkeeping_tsv_file.go @@ -0,0 +1,17 @@ +package converters + +import ( + "time" + + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +// EzBookKeepingTSVFileExporter defines the structure of TSV file exporter +type EzBookKeepingTSVFileExporter struct { + EzBookKeepingPlainFileExporter +} + +// ToExportedContent returns the exported TSV data +func (e *EzBookKeepingTSVFileExporter) ToExportedContent(uid int64, timezone *time.Location, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexs map[int64][]int64) ([]byte, error) { + return e.toExportedContent(uid, "\t", timezone, transactions, accountMap, categoryMap, tagMap, allTagIndexs) +} diff --git a/pkg/errs/system.go b/pkg/errs/system.go index 0ad33fda..1fa3bc6d 100644 --- a/pkg/errs/system.go +++ b/pkg/errs/system.go @@ -9,4 +9,5 @@ var ( ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed") ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented") ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusNotImplemented, "system is busy") + ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported") ) diff --git a/src/lib/services.js b/src/lib/services.js index bfd61fdf..c5dca4a3 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -214,8 +214,14 @@ export default { getUserDataStatistics: () => { return axios.get('v1/data/statistics.json'); }, - getExportedUserData: () => { - return axios.get('v1/data/export.csv'); + getExportedUserData: (fileType) => { + if (fileType === 'csv') { + return axios.get('v1/data/export.csv'); + } else if (fileType === 'tsv') { + return axios.get('v1/data/export.tsv'); + } else { + return Promise.reject('Parameter Invalid'); + } }, clearData: ({ password }) => { return axios.post('v1/data/clear.json', { diff --git a/src/locales/en.js b/src/locales/en.js index fde63d13..6febb299 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -560,6 +560,7 @@ export default { 'api not found': 'Failed to request api', 'not implemented': 'Not implemented', 'system is busy': 'System is busy', + 'not supported': 'Not supported', 'database operation failed': 'Database operation failed', 'SMTP server is not enabled': 'SMTP server is not enabled', 'incomplete or incorrect submission': 'Incomplete or incorrect submission', @@ -1087,9 +1088,11 @@ export default { 'Data Management': 'Data Management', 'Unable to get user statistics data': 'Unable to get user statistics data', 'Export Data': 'Export Data', + 'Export Data To CSV File': 'Export Data To CSV File', + 'Export Data To TSV File': 'Export Data To TSV File', 'Clear User Data': 'Clear User Data', - 'Export all data to csv file.': 'Export all data to csv file.', - 'Are you sure you want to export all data to csv file?': 'Are you sure you want to export all data to csv file?', + 'Export all data to file.': 'Export all data to file.', + 'Are you sure you want to export all data to file?': 'Are you sure you want to export all data to file?', 'It may take a long time, please wait for a few minutes.': 'It may take a long time, please wait for a few minutes.', 'Unable to get exported user data': 'Unable to get exported user data', 'Save Data': 'Save Data', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 4b1af0c1..06f590c9 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -560,6 +560,7 @@ export default { 'api not found': '接口调用失败', 'not implemented': '未实现', 'system is busy': '系统繁忙', + 'not supported': '不支持', 'database operation failed': '数据库操作失败', 'SMTP server is not enabled': 'SMTP 服务器没有启用', 'incomplete or incorrect submission': '提交不完整或不正确', @@ -1087,9 +1088,11 @@ export default { 'Data Management': '数据管理', 'Unable to get user statistics data': '无法获取用户统计数据', 'Export Data': '导出数据', + 'Export Data To CSV File': '导出数据到 CSV 文件', + 'Export Data To TSV File': '导出数据到 TSV 文件', 'Clear User Data': '清除用户数据', - 'Export all data to csv file.': '导出所有数据到 csv 文件。', - 'Are you sure you want to export all data to csv file?': '您确定要导出所有数据到 csv 文件?', + 'Export all data to file.': '导出所有数据到文件。', + 'Are you sure you want to export all data to file?': '您确定要导出所有数据到文件?', 'It may take a long time, please wait for a few minutes.': '这可能花费一些时间,请稍等几分钟。', 'Unable to get exported user data': '无法获取导出的用户数据', 'Save Data': '保存数据', diff --git a/src/stores/user.js b/src/stores/user.js index 326e723b..6ae0c4d0 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -131,12 +131,17 @@ export const useUserStore = defineStore('user', { }); }); }, - getExportedUserData() { + getExportedUserData(fileType) { return new Promise((resolve, reject) => { - services.getExportedUserData().then(response => { - if (response && response.headers && response.headers['content-type'] !== 'text/csv') { - reject({ message: 'Unable to get exported user data' }); - return; + services.getExportedUserData(fileType).then(response => { + if (response && response.headers) { + if (fileType === 'csv' && response.headers['content-type'] !== 'text/csv') { + reject({ message: 'Unable to get exported user data' }); + return; + } else if (fileType === 'tsv' && response.headers['content-type'] !== 'text/tab-separated-values') { + reject({ message: 'Unable to get exported user data' }); + return; + } } const blob = new Blob([response.data], { type: response.headers['content-type'] }); diff --git a/src/views/desktop/user/settings/tabs/UserDataManagementSettingTab.vue b/src/views/desktop/user/settings/tabs/UserDataManagementSettingTab.vue index 51d95e8a..c1c5e995 100644 --- a/src/views/desktop/user/settings/tabs/UserDataManagementSettingTab.vue +++ b/src/views/desktop/user/settings/tabs/UserDataManagementSettingTab.vue @@ -85,14 +85,27 @@ - {{ $t('Export all data to csv file.') }} {{ $t('It may take a long time, please wait for a few minutes.') }} + {{ $t('Export all data to file.') }} {{ $t('It may take a long time, please wait for a few minutes.') }} - - {{ $t('Export Data') }} - - + + + {{ $t('Export Data') }} + + + + + {{ $t('Export Data To CSV File') }} + + + {{ $t('Export Data To TSV File') }} + + + + + @@ -209,17 +222,6 @@ export default { }, isDataExportingEnabled() { return isDataExportingEnabled(); - }, - exportFileName() { - const nickname = this.userStore.currentUserNickname; - - if (nickname) { - return this.$t('dataExport.exportFilename', { - nickname: nickname - }) + '.csv'; - } - - return this.$t('dataExport.defaultExportFilename') + '.csv'; } }, created() { @@ -250,7 +252,7 @@ export default { } }); }, - exportData() { + exportData(fileType) { const self = this; if (self.exportingData) { @@ -259,8 +261,8 @@ export default { self.exportingData = true; - self.userStore.getExportedUserData().then(data => { - startDownloadFile(self.exportFileName, data); + self.userStore.getExportedUserData(fileType).then(data => { + startDownloadFile(self.getExportFileName(fileType), data); self.exportingData = false; }).catch(error => { self.exportingData = false; @@ -301,6 +303,17 @@ export default { } }); }); + }, + getExportFileName(fileExtension) { + const nickname = this.userStore.currentUserNickname; + + if (nickname) { + return this.$t('dataExport.exportFilename', { + nickname: nickname + }) + '.' + fileExtension; + } + + return this.$t('dataExport.defaultExportFilename') + '.' + fileExtension; } } } diff --git a/src/views/mobile/users/DataManagementPage.vue b/src/views/mobile/users/DataManagementPage.vue index bde48bda..65f8074e 100644 --- a/src/views/mobile/users/DataManagementPage.vue +++ b/src/views/mobile/users/DataManagementPage.vue @@ -30,7 +30,19 @@
-
{{ $t('Are you sure you want to export all data to csv file?') }}
+
{{ $t('Are you sure you want to export all data to file?') }}
+
+
+ + + + + +

{{ $t('It may take a long time, please wait for a few minutes.') }}

@@ -73,6 +85,7 @@ export default { loading: true, loadingError: null, dataStatistics: null, + exportFileType: 'csv', exportingData: false, exportedData: null, currentPasswordForClearData: '', @@ -109,10 +122,10 @@ export default { if (nickname) { return this.$t('dataExport.exportFilename', { nickname: nickname - }) + '.csv'; + }) + '.' + this.exportFileType; } - return this.$t('dataExport.defaultExportFilename') + '.csv'; + return this.$t('dataExport.defaultExportFilename') + '.' + this.exportFileType; }, }, created() { @@ -142,7 +155,7 @@ export default { self.$showLoading(); self.exportingData = true; - self.userStore.getExportedUserData().then(data => { + self.userStore.getExportedUserData(self.exportFileType).then(data => { self.exportedData = URL.createObjectURL(data); self.exportingData = false; self.$hideLoading();