mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-20 01:34:24 +08:00
support importing transaction by csv/tsv file via command line
This commit is contained in:
@@ -2,15 +2,18 @@ package converters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||
)
|
||||
|
||||
// EzBookKeepingPlainFileExporter defines the structure of plain file exporter
|
||||
type EzBookKeepingPlainFileExporter struct {
|
||||
// EzBookKeepingPlainFileConverter defines the structure of plain file converter
|
||||
type EzBookKeepingPlainFileConverter struct {
|
||||
}
|
||||
|
||||
const lineSeparator = "\n"
|
||||
@@ -20,7 +23,7 @@ const headerLine = "Time,Timezone,Type,Category,Sub Category,Account,Account Cur
|
||||
const dataLineFormat = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" + lineSeparator
|
||||
|
||||
// toExportedContent returns the exported plain data
|
||||
func (e *EzBookKeepingPlainFileExporter) toExportedContent(uid int64, separator string, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
|
||||
func (e *EzBookKeepingPlainFileConverter) toExportedContent(uid int64, separator string, transactions []*models.Transaction, accountMap map[int64]*models.Account, categoryMap map[int64]*models.TransactionCategory, tagMap map[int64]*models.TransactionTag, allTagIndexes map[int64][]int64) ([]byte, error) {
|
||||
var ret strings.Builder
|
||||
|
||||
ret.Grow(len(transactions) * 100)
|
||||
@@ -75,7 +78,270 @@ func (e *EzBookKeepingPlainFileExporter) toExportedContent(uid int64, separator
|
||||
return []byte(ret.String()), nil
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileExporter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
|
||||
func (e *EzBookKeepingPlainFileConverter) parseImportedData(user *models.User, separator string, 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) {
|
||||
lines := strings.Split(string(data), lineSeparator)
|
||||
|
||||
if len(lines) < 2 {
|
||||
return nil, nil, nil, nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
headerLineItems := strings.Split(lines[0], separator)
|
||||
headerItemMap := make(map[string]int)
|
||||
|
||||
for i := 0; i < len(headerLineItems); i++ {
|
||||
headerItemMap[headerLineItems[i]] = i
|
||||
}
|
||||
|
||||
timeColumnIdx, timeColumnExists := headerItemMap["Time"]
|
||||
timezoneColumnIdx, timezoneColumnExists := headerItemMap["Timezone"]
|
||||
typeColumnIdx, typeColumnExists := headerItemMap["Type"]
|
||||
subCategoryColumnIdx, subCategoryColumnExists := headerItemMap["Sub Category"]
|
||||
accountColumnIdx, accountColumnExists := headerItemMap["Account"]
|
||||
accountCurrencyColumnIdx, accountCurrencyColumnExists := headerItemMap["Account Currency"]
|
||||
amountColumnIdx, amountColumnExists := headerItemMap["Amount"]
|
||||
account2ColumnIdx, account2ColumnExists := headerItemMap["Account2"]
|
||||
account2CurrencyColumnIdx, account2CurrencyColumnExists := headerItemMap["Account2 Currency"]
|
||||
amount2ColumnIdx, amount2ColumnExists := headerItemMap["Account2 Amount"]
|
||||
geoLocationIdx, geoLocationExists := headerItemMap["Geographic Location"]
|
||||
tagsColumnIdx, tagsColumnExists := headerItemMap["Tags"]
|
||||
descriptionColumnIdx, descriptionColumnExists := headerItemMap["Description"]
|
||||
|
||||
if !timeColumnExists || !typeColumnExists || !subCategoryColumnExists ||
|
||||
!accountColumnExists || !amountColumnExists || !account2ColumnExists || !amount2ColumnExists {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
if accountMap == nil {
|
||||
accountMap = make(map[string]*models.Account)
|
||||
}
|
||||
|
||||
if categoryMap == nil {
|
||||
categoryMap = make(map[string]*models.TransactionCategory)
|
||||
}
|
||||
|
||||
if tagMap == nil {
|
||||
tagMap = make(map[string]*models.TransactionTag)
|
||||
}
|
||||
|
||||
allNewTransactions := make(ImportTransactionSlice, 0, len(lines))
|
||||
allNewAccounts := make([]*models.Account, 0)
|
||||
allNewSubCategories := make([]*models.TransactionCategory, 0)
|
||||
allNewTags := make([]*models.TransactionTag, 0)
|
||||
|
||||
for i := 1; i < len(lines); i++ {
|
||||
line := lines[i]
|
||||
|
||||
if len(line) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
lineItems := strings.Split(line, separator)
|
||||
|
||||
if len(lineItems) < len(headerLineItems) {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
timezoneOffset := defaultTimezoneOffset
|
||||
|
||||
if timezoneColumnExists {
|
||||
transactionTimezone, err := utils.ParseFromTimezoneOffset(lineItems[timezoneColumnIdx])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
timezoneOffset = utils.GetTimezoneOffsetMinutes(transactionTimezone)
|
||||
}
|
||||
|
||||
transactionTime, err := utils.ParseFromLongDateTime(lineItems[timeColumnIdx], timezoneOffset)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
transactionDbType, err := e.getTransactionDbType(lineItems[typeColumnIdx])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
categoryId := int64(0)
|
||||
|
||||
if transactionDbType != models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
transactionCategoryType, err := e.getTransactionCategoryType(transactionDbType)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
subCategoryName := lineItems[subCategoryColumnIdx]
|
||||
|
||||
if subCategoryName == "" {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
subCategory, exists := categoryMap[subCategoryName]
|
||||
|
||||
if !exists {
|
||||
subCategory = e.createNewTransactionCategoryModel(user.Uid, subCategoryName, transactionCategoryType)
|
||||
allNewSubCategories = append(allNewSubCategories, subCategory)
|
||||
categoryMap[subCategoryName] = subCategory
|
||||
}
|
||||
|
||||
categoryId = subCategory.CategoryId
|
||||
}
|
||||
|
||||
accountName := lineItems[accountColumnIdx]
|
||||
|
||||
if accountName == "" {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
account, exists := accountMap[accountName]
|
||||
|
||||
if !exists {
|
||||
currency := user.DefaultCurrency
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
currency = lineItems[accountCurrencyColumnIdx]
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[currency]; !ok {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account = e.createNewAccountModel(user.Uid, accountName, currency)
|
||||
allNewAccounts = append(allNewAccounts, account)
|
||||
accountMap[accountName] = account
|
||||
}
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
if account.Currency != lineItems[accountCurrencyColumnIdx] {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
amount, err := utils.ParseAmount(lineItems[amountColumnIdx])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
relatedAccountId := int64(0)
|
||||
relatedAccountAmount := int64(0)
|
||||
|
||||
if transactionDbType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
account2Name := lineItems[account2ColumnIdx]
|
||||
|
||||
if account2Name == "" {
|
||||
return nil, nil, nil, nil, errs.ErrFormatInvalid
|
||||
}
|
||||
|
||||
account2, exists := accountMap[account2Name]
|
||||
|
||||
if !exists {
|
||||
currency := user.DefaultCurrency
|
||||
|
||||
if accountCurrencyColumnExists {
|
||||
currency = lineItems[account2CurrencyColumnIdx]
|
||||
|
||||
if _, ok := validators.AllCurrencyNames[currency]; !ok {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
account2 = e.createNewAccountModel(user.Uid, account2Name, currency)
|
||||
allNewAccounts = append(allNewAccounts, account2)
|
||||
accountMap[account2Name] = account2
|
||||
}
|
||||
|
||||
if account2CurrencyColumnExists {
|
||||
if account2.Currency != lineItems[account2CurrencyColumnIdx] {
|
||||
return nil, nil, nil, nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
}
|
||||
|
||||
relatedAccountId = account2.AccountId
|
||||
relatedAccountAmount, err = utils.ParseAmount(lineItems[amount2ColumnIdx])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
geoLongitude := float64(0)
|
||||
geoLatitude := float64(0)
|
||||
|
||||
if geoLocationExists {
|
||||
geoLocationItems := strings.Split(lineItems[geoLocationIdx], geoLocationSeparator)
|
||||
|
||||
if len(geoLocationItems) == 2 {
|
||||
geoLongitude, err = utils.StringToFloat64(geoLocationItems[0])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
geoLatitude, err = utils.StringToFloat64(geoLocationItems[1])
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tagsColumnExists {
|
||||
tagNames := strings.Split(lineItems[tagsColumnIdx], transactionTagSeparator)
|
||||
|
||||
for i := 0; i < len(tagNames); i++ {
|
||||
tagName := tagNames[i]
|
||||
|
||||
if tagName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
tag, exists := tagMap[tagName]
|
||||
|
||||
if !exists {
|
||||
tag = e.createNewTransactionTagModel(user.Uid, tagName)
|
||||
allNewTags = append(allNewTags, tag)
|
||||
tagMap[tagName] = tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
description := ""
|
||||
|
||||
if descriptionColumnExists {
|
||||
description = lineItems[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",
|
||||
}
|
||||
|
||||
allNewTransactions = append(allNewTransactions, transaction)
|
||||
}
|
||||
|
||||
sort.Sort(allNewTransactions)
|
||||
|
||||
return allNewTransactions, allNewAccounts, allNewSubCategories, allNewTags, nil
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionTypeName(transactionDbType models.TransactionDbType) string {
|
||||
if transactionDbType == models.TRANSACTION_DB_TYPE_MODIFY_BALANCE {
|
||||
return "Balance Modification"
|
||||
} else if transactionDbType == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
@@ -89,7 +355,33 @@ func (e *EzBookKeepingPlainFileExporter) getTransactionTypeName(transactionDbTyp
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileExporter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionDbType(transactionTypeName string) (models.TransactionDbType, error) {
|
||||
if transactionTypeName == "Balance Modification" {
|
||||
return models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, nil
|
||||
} else if transactionTypeName == "Income" {
|
||||
return models.TRANSACTION_DB_TYPE_INCOME, nil
|
||||
} else if transactionTypeName == "Expense" {
|
||||
return models.TRANSACTION_DB_TYPE_EXPENSE, nil
|
||||
} else if transactionTypeName == "Transfer" {
|
||||
return models.TRANSACTION_DB_TYPE_TRANSFER_OUT, nil
|
||||
} else {
|
||||
return 0, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionCategoryType(transactionType models.TransactionDbType) (models.TransactionCategoryType, error) {
|
||||
if transactionType == models.TRANSACTION_DB_TYPE_INCOME {
|
||||
return models.CATEGORY_TYPE_INCOME, nil
|
||||
} else if transactionType == models.TRANSACTION_DB_TYPE_EXPENSE {
|
||||
return models.CATEGORY_TYPE_EXPENSE, nil
|
||||
} else if transactionType == models.TRANSACTION_DB_TYPE_TRANSFER_OUT {
|
||||
return models.CATEGORY_TYPE_TRANSFER, nil
|
||||
} else {
|
||||
return 0, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if !exists {
|
||||
@@ -109,7 +401,7 @@ func (e *EzBookKeepingPlainFileExporter) getTransactionCategoryName(categoryId i
|
||||
return parentCategory.Name
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileExporter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
func (e *EzBookKeepingPlainFileConverter) getTransactionSubCategoryName(categoryId int64, categoryMap map[int64]*models.TransactionCategory) string {
|
||||
category, exists := categoryMap[categoryId]
|
||||
|
||||
if exists {
|
||||
@@ -119,7 +411,7 @@ func (e *EzBookKeepingPlainFileExporter) getTransactionSubCategoryName(categoryI
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileExporter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
func (e *EzBookKeepingPlainFileConverter) getAccountName(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
@@ -129,7 +421,7 @@ func (e *EzBookKeepingPlainFileExporter) getAccountName(accountId int64, account
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileExporter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
func (e *EzBookKeepingPlainFileConverter) getAccountCurrency(accountId int64, accountMap map[int64]*models.Account) string {
|
||||
account, exists := accountMap[accountId]
|
||||
|
||||
if exists {
|
||||
@@ -139,7 +431,7 @@ func (e *EzBookKeepingPlainFileExporter) getAccountCurrency(accountId int64, acc
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileExporter) getTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
|
||||
func (e *EzBookKeepingPlainFileConverter) getTags(transactionId int64, allTagIndexes map[int64][]int64, tagMap map[int64]*models.TransactionTag) string {
|
||||
tagIndexes, exists := allTagIndexes[transactionId]
|
||||
|
||||
if !exists {
|
||||
@@ -166,10 +458,33 @@ func (e *EzBookKeepingPlainFileExporter) getTags(transactionId int64, allTagInde
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileExporter) replaceDelimiters(text string, separator string) string {
|
||||
func (e *EzBookKeepingPlainFileConverter) replaceDelimiters(text string, separator string) string {
|
||||
text = strings.Replace(text, separator, " ", -1)
|
||||
text = strings.Replace(text, "\r\n", " ", -1)
|
||||
text = strings.Replace(text, "\n", " ", -1)
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) createNewAccountModel(uid int64, accountName string, currency string) *models.Account {
|
||||
return &models.Account{
|
||||
Uid: uid,
|
||||
Name: accountName,
|
||||
Currency: currency,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) createNewTransactionCategoryModel(uid int64, categoryName string, transactionCategoryType models.TransactionCategoryType) *models.TransactionCategory {
|
||||
return &models.TransactionCategory{
|
||||
Uid: uid,
|
||||
Name: categoryName,
|
||||
Type: transactionCategoryType,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EzBookKeepingPlainFileConverter) createNewTransactionTagModel(uid int64, tagName string) *models.TransactionTag {
|
||||
return &models.TransactionTag{
|
||||
Uid: uid,
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user