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('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();