mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-20 09:44:26 +08:00
import transaction from GnuCash database
This commit is contained in:
@@ -0,0 +1,87 @@
|
|||||||
|
package gnucash
|
||||||
|
|
||||||
|
import "encoding/xml"
|
||||||
|
|
||||||
|
const gnucashCommodityCurrencySpace = "CURRENCY"
|
||||||
|
const gnucashRootAccountType = "ROOT"
|
||||||
|
const gnucashEquityAccountType = "EQUITY"
|
||||||
|
const gnucashIncomeAccountType = "INCOME"
|
||||||
|
const gnucashExpenseAccountType = "EXPENSE"
|
||||||
|
|
||||||
|
const gnucashSlotEquityType = "equity-type"
|
||||||
|
const gnucashSlotEquityTypeOpeningBalance = "opening-balance"
|
||||||
|
|
||||||
|
var gnucashAssetOrLiabilityAccountTypes = map[string]bool{
|
||||||
|
"ASSET": true,
|
||||||
|
"BANK": true,
|
||||||
|
"CASH": true,
|
||||||
|
"CREDIT": true,
|
||||||
|
"LIABILITY": true,
|
||||||
|
"MUTUAL": true,
|
||||||
|
"PAYABLE": true,
|
||||||
|
"RECEIVABLE": true,
|
||||||
|
"STOCK": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashDatabase represents the struct of gnucash database file
|
||||||
|
type gnucashDatabase struct {
|
||||||
|
XMLName xml.Name `xml:"gnc-v2"`
|
||||||
|
Counts []*gnucashCountData `xml:"count-data"`
|
||||||
|
Books []*gnucashBookData `xml:"book"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashCountData represents the struct of gnucash count data
|
||||||
|
type gnucashCountData struct {
|
||||||
|
Key string `xml:"type,attr"`
|
||||||
|
Value string `xml:",chardata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashBookData represents the struct of gnucash book data
|
||||||
|
type gnucashBookData struct {
|
||||||
|
Id string `xml:"id"`
|
||||||
|
Counts []*gnucashCountData `xml:"count-data"`
|
||||||
|
Accounts []*gnucashAccountData `xml:"account"`
|
||||||
|
Transactions []*gnucashTransactionData `xml:"transaction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashCommodityData represents the struct of gnucash commodity data
|
||||||
|
type gnucashCommodityData struct {
|
||||||
|
Space string `xml:"space"`
|
||||||
|
Id string `xml:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashSlotData represents the struct of gnucash slot data
|
||||||
|
type gnucashSlotData struct {
|
||||||
|
Key string `xml:"key"`
|
||||||
|
Value string `xml:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashAccountData represents the struct of gnucash account data
|
||||||
|
type gnucashAccountData struct {
|
||||||
|
Name string `xml:"name"`
|
||||||
|
Id string `xml:"id"`
|
||||||
|
AccountType string `xml:"type"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
ParentId string `xml:"parent"`
|
||||||
|
Commodity *gnucashCommodityData `xml:"commodity"`
|
||||||
|
Slots []*gnucashSlotData `xml:"slots>slot"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashTransactionData represents the struct of gnucash transaction data
|
||||||
|
type gnucashTransactionData struct {
|
||||||
|
Id string `xml:"id"`
|
||||||
|
Currency *gnucashCommodityData `xml:"currency"`
|
||||||
|
PostedDate string `xml:"date-posted>date"`
|
||||||
|
EnteredDate string `xml:"date-entered>date"`
|
||||||
|
Description string `xml:"description"`
|
||||||
|
Splits []*gnucashTransactionSplitData `xml:"splits>split"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashTransactionSplitData represents the struct of gnucash transaction split data
|
||||||
|
type gnucashTransactionSplitData struct {
|
||||||
|
Id string `xml:"id"`
|
||||||
|
ReconciledState string `xml:"reconciled-state"`
|
||||||
|
Value string `xml:"value"`
|
||||||
|
Quantity string `xml:"quantity"`
|
||||||
|
Account string `xml:"account"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package gnucash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// gnucashDatabaseReader defines the structure of gnucash database reader
|
||||||
|
type gnucashDatabaseReader struct {
|
||||||
|
xmlDecoder *xml.Decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// read returns the imported gnucash data
|
||||||
|
func (r *gnucashDatabaseReader) read(ctx core.Context) (*gnucashDatabase, error) {
|
||||||
|
database := &gnucashDatabase{}
|
||||||
|
|
||||||
|
err := r.xmlDecoder.Decode(&database)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return database, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewGnuCashDatabaseReader(data []byte) (*gnucashDatabaseReader, error) {
|
||||||
|
if len(data) > 2 && data[0] == 0x1F && data[1] == 0x8B { // gzip magic number
|
||||||
|
gzipReader, err := gzip.NewReader(bytes.NewReader(data))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlDecoder := xml.NewDecoder(gzipReader)
|
||||||
|
xmlDecoder.CharsetReader = utils.IdentReader
|
||||||
|
|
||||||
|
return &gnucashDatabaseReader{
|
||||||
|
xmlDecoder: xmlDecoder,
|
||||||
|
}, nil
|
||||||
|
} else if len(data) > 5 && data[0] == 0x3C && data[1] == 0x3F && data[2] == 0x78 && data[3] == 0x6D && data[4] == 0x6C { // <?xml
|
||||||
|
xmlDecoder := xml.NewDecoder(bytes.NewReader(data))
|
||||||
|
xmlDecoder.CharsetReader = utils.IdentReader
|
||||||
|
|
||||||
|
return &gnucashDatabaseReader{
|
||||||
|
xmlDecoder: xmlDecoder,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errs.ErrInvalidGnuCashFile
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package gnucash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var gnucashTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||||
|
models.TRANSACTION_TYPE_MODIFY_BALANCE: utils.IntToString(int(models.TRANSACTION_TYPE_MODIFY_BALANCE)),
|
||||||
|
models.TRANSACTION_TYPE_INCOME: utils.IntToString(int(models.TRANSACTION_TYPE_INCOME)),
|
||||||
|
models.TRANSACTION_TYPE_EXPENSE: utils.IntToString(int(models.TRANSACTION_TYPE_EXPENSE)),
|
||||||
|
models.TRANSACTION_TYPE_TRANSFER: utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashTransactionDataImporter defines the structure of gnucash importer for transaction data
|
||||||
|
type gnucashTransactionDataImporter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize a gnucash transaction data importer singleton instance
|
||||||
|
var (
|
||||||
|
GnuCashTransactionDataImporter = &gnucashTransactionDataImporter{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseImportedData returns the imported data by parsing the gnucash transaction data
|
||||||
|
func (c *gnucashTransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]*models.TransactionCategory, incomeCategoryMap map[string]*models.TransactionCategory, transferCategoryMap map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||||
|
gnucashDataReader, err := createNewGnuCashDatabaseReader(data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gnucashData, err := gnucashDataReader.read(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionDataTable, err := createNewGnuCashTransactionDataTable(gnucashData)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataTableImporter := datatable.CreateNewSimpleImporter(gnucashTransactionTypeNameMapping)
|
||||||
|
|
||||||
|
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||||
|
}
|
||||||
@@ -0,0 +1,921 @@
|
|||||||
|
package gnucash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const gnucashMinimumValidDataCase = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
|
||||||
|
"<gnc-v2\n" +
|
||||||
|
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n" +
|
||||||
|
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n" +
|
||||||
|
" xmlns:book=\"http://www.gnucash.org/XML/book\"\n" +
|
||||||
|
" xmlns:cd=\"http://www.gnucash.org/XML/cd\"\n" +
|
||||||
|
" xmlns:cmdty=\"http://www.gnucash.org/XML/cmdty\"\n" +
|
||||||
|
" xmlns:slot=\"http://www.gnucash.org/XML/slot\"\n" +
|
||||||
|
" xmlns:split=\"http://www.gnucash.org/XML/split\"\n" +
|
||||||
|
" xmlns:trn=\"http://www.gnucash.org/XML/trn\">\n" +
|
||||||
|
"<gnc:book version=\"2.0.0\">\n" +
|
||||||
|
"<gnc:account version=\"2.0.0\">\n" +
|
||||||
|
" <act:name>Root Account</act:name>\n" +
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000001</act:id>\n" +
|
||||||
|
" <act:type>ROOT</act:type>\n" +
|
||||||
|
"</gnc:account>\n" +
|
||||||
|
"<gnc:account version=\"2.0.0\">\n" +
|
||||||
|
" <act:name>Opening Balances</act:name>\n" +
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000010</act:id>\n" +
|
||||||
|
" <act:type>EQUITY</act:type>\n" +
|
||||||
|
" <act:commodity>\n" +
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n" +
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n" +
|
||||||
|
" </act:commodity>\n" +
|
||||||
|
" <act:slots>\n" +
|
||||||
|
" <slot>\n" +
|
||||||
|
" <slot:key>equity-type</slot:key>\n" +
|
||||||
|
" <slot:value type=\"string\">opening-balance</slot:value>\n" +
|
||||||
|
" </slot>\n" +
|
||||||
|
" </act:slots>\n" +
|
||||||
|
"</gnc:account>\n" +
|
||||||
|
"<gnc:account version=\"2.0.0\">\n" +
|
||||||
|
" <act:name>Test Category</act:name>\n" +
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000100</act:id>\n" +
|
||||||
|
" <act:type>INCOME</act:type>\n" +
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n" +
|
||||||
|
"</gnc:account>\n" +
|
||||||
|
"<gnc:account version=\"2.0.0\">\n" +
|
||||||
|
" <act:name>Test Category2</act:name>\n" +
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000200</act:id>\n" +
|
||||||
|
" <act:type>EXPENSE</act:type>\n" +
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n" +
|
||||||
|
"</gnc:account>\n" +
|
||||||
|
"<gnc:account version=\"2.0.0\">\n" +
|
||||||
|
" <act:name>Test Account</act:name>\n" +
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000001000</act:id>\n" +
|
||||||
|
" <act:type>BANK</act:type>\n" +
|
||||||
|
" <act:commodity>\n" +
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n" +
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n" +
|
||||||
|
" </act:commodity>\n" +
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n" +
|
||||||
|
"</gnc:account>\n" +
|
||||||
|
"<gnc:account version=\"2.0.0\">\n" +
|
||||||
|
" <act:name>Test Account2</act:name>\n" +
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000002000</act:id>\n" +
|
||||||
|
" <act:type>CASH</act:type>\n" +
|
||||||
|
" <act:commodity>\n" +
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n" +
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n" +
|
||||||
|
" </act:commodity>\n" +
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n" +
|
||||||
|
"</gnc:account>\n" +
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n" +
|
||||||
|
" <trn:date-posted>\n" +
|
||||||
|
" <ts:date>2024-09-01 00:00:00 +0000</ts:date>\n" +
|
||||||
|
" </trn:date-posted>\n" +
|
||||||
|
" <trn:splits>\n" +
|
||||||
|
" <trn:split>\n" +
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n" +
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n" +
|
||||||
|
" </trn:split>\n" +
|
||||||
|
" <trn:split>\n" +
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n" +
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n" +
|
||||||
|
" </trn:split>\n" +
|
||||||
|
" </trn:splits>\n" +
|
||||||
|
"</gnc:transaction>\n" +
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n" +
|
||||||
|
" <trn:date-posted>\n" +
|
||||||
|
" <ts:date>2024-09-01 01:23:45 +0000</ts:date>\n" +
|
||||||
|
" </trn:date-posted>\n" +
|
||||||
|
" <trn:splits>\n" +
|
||||||
|
" <trn:split>\n" +
|
||||||
|
" <split:quantity>12/100</split:quantity>\n" +
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n" +
|
||||||
|
" </trn:split>\n" +
|
||||||
|
" <trn:split>\n" +
|
||||||
|
" <split:quantity>-12/100</split:quantity>\n" +
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000100</split:account>\n" +
|
||||||
|
" </trn:split>\n" +
|
||||||
|
" </trn:splits>\n" +
|
||||||
|
"</gnc:transaction>\n" +
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n" +
|
||||||
|
" <trn:date-posted>\n" +
|
||||||
|
" <ts:date>2024-09-01 12:34:56 +0000</ts:date>\n" +
|
||||||
|
" </trn:date-posted>\n" +
|
||||||
|
" <trn:splits>\n" +
|
||||||
|
" <trn:split>\n" +
|
||||||
|
" <split:quantity>100/100</split:quantity>\n" +
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000200</split:account>\n" +
|
||||||
|
" </trn:split>\n" +
|
||||||
|
" <trn:split>\n" +
|
||||||
|
" <split:quantity>-100/100</split:quantity>\n" +
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n" +
|
||||||
|
" </trn:split>\n" +
|
||||||
|
" </trn:splits>\n" +
|
||||||
|
"</gnc:transaction>\n" +
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n" +
|
||||||
|
" <trn:date-posted>\n" +
|
||||||
|
" <ts:date>2024-09-01 23:59:59 +0000</ts:date>\n" +
|
||||||
|
" </trn:date-posted>\n" +
|
||||||
|
" <trn:splits>\n" +
|
||||||
|
" <trn:split>\n" +
|
||||||
|
" <split:quantity>5/100</split:quantity>\n" +
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000002000</split:account>\n" +
|
||||||
|
" </trn:split>\n" +
|
||||||
|
" <trn:split>\n" +
|
||||||
|
" <split:quantity>-5/100</split:quantity>\n" +
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n" +
|
||||||
|
" </trn:split>\n" +
|
||||||
|
" </trn:splits>\n" +
|
||||||
|
"</gnc:transaction>\n" +
|
||||||
|
"</gnc:book>\n" +
|
||||||
|
"</gnc-v2>\n"
|
||||||
|
|
||||||
|
const gnucashCommonValidDataCaseHeader = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
|
||||||
|
"<gnc-v2\n" +
|
||||||
|
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n" +
|
||||||
|
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n" +
|
||||||
|
" xmlns:book=\"http://www.gnucash.org/XML/book\"\n" +
|
||||||
|
" xmlns:cd=\"http://www.gnucash.org/XML/cd\"\n" +
|
||||||
|
" xmlns:cmdty=\"http://www.gnucash.org/XML/cmdty\"\n" +
|
||||||
|
" xmlns:slot=\"http://www.gnucash.org/XML/slot\"\n" +
|
||||||
|
" xmlns:split=\"http://www.gnucash.org/XML/split\"\n" +
|
||||||
|
" xmlns:trn=\"http://www.gnucash.org/XML/trn\">\n" +
|
||||||
|
"<gnc:book version=\"2.0.0\">\n" +
|
||||||
|
"<gnc:account version=\"2.0.0\">\n" +
|
||||||
|
" <act:name>Root Account</act:name>\n" +
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000001</act:id>\n" +
|
||||||
|
" <act:type>ROOT</act:type>\n" +
|
||||||
|
"</gnc:account>\n" +
|
||||||
|
"<gnc:account version=\"2.0.0\">\n" +
|
||||||
|
" <act:name>Opening Balances</act:name>\n" +
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000010</act:id>\n" +
|
||||||
|
" <act:type>EQUITY</act:type>\n" +
|
||||||
|
" <act:commodity>\n" +
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n" +
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n" +
|
||||||
|
" </act:commodity>\n" +
|
||||||
|
" <act:slots>\n" +
|
||||||
|
" <slot>\n" +
|
||||||
|
" <slot:key>equity-type</slot:key>\n" +
|
||||||
|
" <slot:value type=\"string\">opening-balance</slot:value>\n" +
|
||||||
|
" </slot>\n" +
|
||||||
|
" </act:slots>\n" +
|
||||||
|
"</gnc:account>\n" +
|
||||||
|
"<gnc:account version=\"2.0.0\">\n" +
|
||||||
|
" <act:name>Test Account</act:name>\n" +
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000001000</act:id>\n" +
|
||||||
|
" <act:type>BANK</act:type>\n" +
|
||||||
|
" <act:commodity>\n" +
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n" +
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n" +
|
||||||
|
" </act:commodity>\n" +
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n" +
|
||||||
|
"</gnc:account>\n"
|
||||||
|
|
||||||
|
const gnucashCommonValidDataCaseFooter = "</gnc:book>\n" +
|
||||||
|
"</gnc-v2>\n"
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidData(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte(gnucashMinimumValidDataCase), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_GzippedMinimumValidData(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
gzipWriter := gzip.NewWriter(&buffer)
|
||||||
|
_, err := gzipWriter.Write([]byte(gnucashMinimumValidDataCase))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
err = gzipWriter.Close()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
gzippedData := buffer.Bytes()
|
||||||
|
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, gzippedData, 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_MinimumValidDataWithReversedSplitOrder(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
|
||||||
|
"<gnc-v2\n"+
|
||||||
|
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
|
||||||
|
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+
|
||||||
|
" xmlns:book=\"http://www.gnucash.org/XML/book\"\n"+
|
||||||
|
" xmlns:cd=\"http://www.gnucash.org/XML/cd\"\n"+
|
||||||
|
" xmlns:cmdty=\"http://www.gnucash.org/XML/cmdty\"\n"+
|
||||||
|
" xmlns:slot=\"http://www.gnucash.org/XML/slot\"\n"+
|
||||||
|
" xmlns:split=\"http://www.gnucash.org/XML/split\"\n"+
|
||||||
|
" xmlns:trn=\"http://www.gnucash.org/XML/trn\">\n"+
|
||||||
|
"<gnc:book version=\"2.0.0\">\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Root Account</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000001</act:id>\n"+
|
||||||
|
" <act:type>ROOT</act:type>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Opening Balances</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000010</act:id>\n"+
|
||||||
|
" <act:type>EQUITY</act:type>\n"+
|
||||||
|
" <act:commodity>\n"+
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n"+
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n"+
|
||||||
|
" </act:commodity>\n"+
|
||||||
|
" <act:slots>\n"+
|
||||||
|
" <slot>\n"+
|
||||||
|
" <slot:key>equity-type</slot:key>\n"+
|
||||||
|
" <slot:value type=\"string\">opening-balance</slot:value>\n"+
|
||||||
|
" </slot>\n"+
|
||||||
|
" </act:slots>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Test Category</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000100</act:id>\n"+
|
||||||
|
" <act:type>INCOME</act:type>\n"+
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Test Category2</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000200</act:id>\n"+
|
||||||
|
" <act:type>EXPENSE</act:type>\n"+
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Test Account</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000001000</act:id>\n"+
|
||||||
|
" <act:type>BANK</act:type>\n"+
|
||||||
|
" <act:commodity>\n"+
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n"+
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n"+
|
||||||
|
" </act:commodity>\n"+
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Test Account2</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000002000</act:id>\n"+
|
||||||
|
" <act:type>CASH</act:type>\n"+
|
||||||
|
" <act:commodity>\n"+
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n"+
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n"+
|
||||||
|
" </act:commodity>\n"+
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 00:00:00 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 01:23:45 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000100</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 12:34:56 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-100/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>100/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000200</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 23:59:59 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-5/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>5/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000002000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
"</gnc:book>\n"+
|
||||||
|
"</gnc-v2>\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
checkParsedMinimumValidData(t, allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseInvalidTime(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 00:00:00</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01T00:00:00+00:00</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidTimezone(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 12:34:56 -1000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, int64(1725230096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 12:34:56 +1245</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, int64(1725148196), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseValidAccountCurrency(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, allNewAccounts, _, _, _, _, err := converter.ParseImportedData(context, user, []byte("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
|
||||||
|
"<gnc-v2\n"+
|
||||||
|
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
|
||||||
|
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+
|
||||||
|
" xmlns:book=\"http://www.gnucash.org/XML/book\"\n"+
|
||||||
|
" xmlns:cd=\"http://www.gnucash.org/XML/cd\"\n"+
|
||||||
|
" xmlns:cmdty=\"http://www.gnucash.org/XML/cmdty\"\n"+
|
||||||
|
" xmlns:slot=\"http://www.gnucash.org/XML/slot\"\n"+
|
||||||
|
" xmlns:split=\"http://www.gnucash.org/XML/split\"\n"+
|
||||||
|
" xmlns:trn=\"http://www.gnucash.org/XML/trn\">\n"+
|
||||||
|
"<gnc:book version=\"2.0.0\">\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Root Account</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000001</act:id>\n"+
|
||||||
|
" <act:type>ROOT</act:type>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Opening Balances</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000010</act:id>\n"+
|
||||||
|
" <act:type>EQUITY</act:type>\n"+
|
||||||
|
" <act:commodity>\n"+
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n"+
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n"+
|
||||||
|
" </act:commodity>\n"+
|
||||||
|
" <act:slots>\n"+
|
||||||
|
" <slot>\n"+
|
||||||
|
" <slot:key>equity-type</slot:key>\n"+
|
||||||
|
" <slot:value type=\"string\">opening-balance</slot:value>\n"+
|
||||||
|
" </slot>\n"+
|
||||||
|
" </act:slots>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Test Account</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000001000</act:id>\n"+
|
||||||
|
" <act:type>BANK</act:type>\n"+
|
||||||
|
" <act:commodity>\n"+
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n"+
|
||||||
|
" <cmdty:id>USD</cmdty:id>\n"+
|
||||||
|
" </act:commodity>\n"+
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Test Account2</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000002000</act:id>\n"+
|
||||||
|
" <act:type>CASH</act:type>\n"+
|
||||||
|
" <act:commodity>\n"+
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n"+
|
||||||
|
" <cmdty:id>EUR</cmdty:id>\n"+
|
||||||
|
" </act:commodity>\n"+
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 01:23:45 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 12:34:56 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>5/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000002000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-5/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
"</gnc:book>\n"+
|
||||||
|
"</gnc-v2>\n"), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, len(allNewTransactions))
|
||||||
|
assert.Equal(t, 2, len(allNewAccounts))
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||||
|
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||||
|
assert.Equal(t, "USD", allNewAccounts[0].Currency)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||||
|
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||||
|
assert.Equal(t, "EUR", allNewAccounts[1].Currency)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseAmount(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 12:34:56 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/1</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/1</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, int64(1234500), allNewTransactions[0].Amount)
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 12:34:56 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/1000</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/1000</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, int64(1234), allNewTransactions[0].Amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_ParseDescription(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 12:34:56 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:description>foo bar\t#test</trn:description>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, 1, len(allNewTransactions))
|
||||||
|
assert.Equal(t, "foo bar\t#test", allNewTransactions[0].Comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_SkipZeroAmountTransaction(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 12:34:56 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>0/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrNotFoundTransactionDataInFile.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_NotSupportedToParseSplitTransaction(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1234567890,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Test Category2</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000200</act:id>\n"+
|
||||||
|
" <act:type>EXPENSE</act:type>\n"+
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Test Account2</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000002000</act:id>\n"+
|
||||||
|
" <act:type>CASH</act:type>\n"+
|
||||||
|
" <act:commodity>\n"+
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n"+
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n"+
|
||||||
|
" </act:commodity>\n"+
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 12:34:56 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>100/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000200</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>200/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000002000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-300/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrNotSupportedSplitTransactions.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_MissingAccountRequiredNode(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing Transaction Time Node
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"+
|
||||||
|
"<gnc-v2\n"+
|
||||||
|
" xmlns:gnc=\"http://www.gnucash.org/XML/gnc\"\n"+
|
||||||
|
" xmlns:act=\"http://www.gnucash.org/XML/act\"\n"+
|
||||||
|
" xmlns:book=\"http://www.gnucash.org/XML/book\"\n"+
|
||||||
|
" xmlns:cd=\"http://www.gnucash.org/XML/cd\"\n"+
|
||||||
|
" xmlns:cmdty=\"http://www.gnucash.org/XML/cmdty\"\n"+
|
||||||
|
" xmlns:slot=\"http://www.gnucash.org/XML/slot\"\n"+
|
||||||
|
" xmlns:split=\"http://www.gnucash.org/XML/split\"\n"+
|
||||||
|
" xmlns:trn=\"http://www.gnucash.org/XML/trn\">\n"+
|
||||||
|
"<gnc:book version=\"2.0.0\">\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Root Account</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000001</act:id>\n"+
|
||||||
|
" <act:type>ROOT</act:type>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Opening Balances</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000000010</act:id>\n"+
|
||||||
|
" <act:type>EQUITY</act:type>\n"+
|
||||||
|
" <act:commodity>\n"+
|
||||||
|
" <cmdty:space>CURRENCY</cmdty:space>\n"+
|
||||||
|
" <cmdty:id>CNY</cmdty:id>\n"+
|
||||||
|
" </act:commodity>\n"+
|
||||||
|
" <act:slots>\n"+
|
||||||
|
" <slot>\n"+
|
||||||
|
" <slot:key>equity-type</slot:key>\n"+
|
||||||
|
" <slot:value type=\"string\">opening-balance</slot:value>\n"+
|
||||||
|
" </slot>\n"+
|
||||||
|
" </act:slots>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:account version=\"2.0.0\">\n"+
|
||||||
|
" <act:name>Test Account</act:name>\n"+
|
||||||
|
" <act:id type=\"guid\">00000000000000000000000000001000</act:id>\n"+
|
||||||
|
" <act:type>BANK</act:type>\n"+
|
||||||
|
" <act:parent type=\"guid\">00000000000000000000000000000001</act:parent>\n"+
|
||||||
|
"</gnc:account>\n"+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 00:00:00 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGnuCashTransactionDatabaseFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
|
||||||
|
converter := GnuCashTransactionDataImporter
|
||||||
|
context := core.NewNullContext()
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Uid: 1,
|
||||||
|
DefaultCurrency: "CNY",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing Transaction Time Node
|
||||||
|
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000000010</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message)
|
||||||
|
|
||||||
|
// Missing Transaction Splits Node
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 00:00:00 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrInvalidGnuCashFile.Message)
|
||||||
|
|
||||||
|
// Missing Transaction Split Account Node
|
||||||
|
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||||
|
gnucashCommonValidDataCaseHeader+
|
||||||
|
"<gnc:transaction version=\"2.0.0\">\n"+
|
||||||
|
" <trn:date-posted>\n"+
|
||||||
|
" <ts:date>2024-09-01 00:00:00 +0000</ts:date>\n"+
|
||||||
|
" </trn:date-posted>\n"+
|
||||||
|
" <trn:splits>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>12345/100</split:quantity>\n"+
|
||||||
|
" <split:account type=\"guid\">00000000000000000000000000001000</split:account>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" <trn:split>\n"+
|
||||||
|
" <split:quantity>-12345/100</split:quantity>\n"+
|
||||||
|
" </trn:split>\n"+
|
||||||
|
" </trn:splits>\n"+
|
||||||
|
"</gnc:transaction>\n"+
|
||||||
|
gnucashCommonValidDataCaseFooter), 0, nil, nil, nil, nil, nil)
|
||||||
|
assert.EqualError(t, err, errs.ErrMissingAccountData.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkParsedMinimumValidData(t *testing.T, allNewTransactions models.ImportedTransactionSlice, allNewAccounts []*models.Account, allNewSubExpenseCategories []*models.TransactionCategory, allNewSubIncomeCategories []*models.TransactionCategory, allNewSubTransferCategories []*models.TransactionCategory, allNewTags []*models.TransactionTag) {
|
||||||
|
assert.Equal(t, 4, len(allNewTransactions))
|
||||||
|
assert.Equal(t, 2, len(allNewAccounts))
|
||||||
|
assert.Equal(t, 1, len(allNewSubExpenseCategories))
|
||||||
|
assert.Equal(t, 1, len(allNewSubIncomeCategories))
|
||||||
|
assert.Equal(t, 1, len(allNewSubTransferCategories))
|
||||||
|
assert.Equal(t, 0, len(allNewTags))
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_MODIFY_BALANCE, allNewTransactions[0].Type)
|
||||||
|
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||||
|
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[1].Type)
|
||||||
|
assert.Equal(t, int64(1725153825), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||||
|
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[1].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Category", allNewTransactions[1].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[2].Type)
|
||||||
|
assert.Equal(t, int64(1725194096), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||||
|
assert.Equal(t, int64(100), allNewTransactions[2].Amount)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Category2", allNewTransactions[2].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid)
|
||||||
|
assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[3].Type)
|
||||||
|
assert.Equal(t, int64(1725235199), utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime))
|
||||||
|
assert.Equal(t, int64(5), allNewTransactions[3].Amount)
|
||||||
|
assert.Equal(t, "Test Account", allNewTransactions[3].OriginalSourceAccountName)
|
||||||
|
assert.Equal(t, "Test Account2", allNewTransactions[3].OriginalDestinationAccountName)
|
||||||
|
assert.Equal(t, "", allNewTransactions[3].OriginalCategoryName)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||||
|
assert.Equal(t, "Test Account", allNewAccounts[0].Name)
|
||||||
|
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||||
|
assert.Equal(t, "Test Account2", allNewAccounts[1].Name)
|
||||||
|
assert.Equal(t, "CNY", allNewAccounts[1].Currency)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
|
||||||
|
assert.Equal(t, "Test Category2", allNewSubExpenseCategories[0].Name)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
|
||||||
|
assert.Equal(t, "Test Category", allNewSubIncomeCategories[0].Name)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid)
|
||||||
|
assert.Equal(t, "", allNewSubTransferCategories[0].Name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
package gnucash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
var gnucashTransactionSupportedColumns = map[datatable.TransactionDataTableColumn]bool{
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_CATEGORY: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_AMOUNT: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT: true,
|
||||||
|
datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashTransactionDataTable defines the structure of gnucash transaction data table
|
||||||
|
type gnucashTransactionDataTable struct {
|
||||||
|
allData []*gnucashTransactionData
|
||||||
|
accountMap map[string]*gnucashAccountData
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashTransactionDataRow defines the structure of gnucash transaction data row
|
||||||
|
type gnucashTransactionDataRow struct {
|
||||||
|
dataTable *gnucashTransactionDataTable
|
||||||
|
data *gnucashTransactionData
|
||||||
|
finalItems map[datatable.TransactionDataTableColumn]string
|
||||||
|
isValid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// gnucashTransactionDataRowIterator defines the structure of gnucash transaction data row iterator
|
||||||
|
type gnucashTransactionDataRowIterator struct {
|
||||||
|
dataTable *gnucashTransactionDataTable
|
||||||
|
currentIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasColumn returns whether the transaction data table has specified column
|
||||||
|
func (t *gnucashTransactionDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
|
||||||
|
_, exists := gnucashTransactionSupportedColumns[column]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionRowCount returns the total count of transaction data row
|
||||||
|
func (t *gnucashTransactionDataTable) TransactionRowCount() int {
|
||||||
|
return len(t.allData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransactionRowIterator returns the iterator of transaction data row
|
||||||
|
func (t *gnucashTransactionDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
|
||||||
|
return &gnucashTransactionDataRowIterator{
|
||||||
|
dataTable: t,
|
||||||
|
currentIndex: -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether this row is valid data for importing
|
||||||
|
func (r *gnucashTransactionDataRow) IsValid() bool {
|
||||||
|
return r.isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetData returns the data in the specified column type
|
||||||
|
func (r *gnucashTransactionDataRow) GetData(column datatable.TransactionDataTableColumn) string {
|
||||||
|
_, exists := gnucashTransactionSupportedColumns[column]
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return r.finalItems[column]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNext returns whether the iterator does not reach the end
|
||||||
|
func (t *gnucashTransactionDataRowIterator) HasNext() bool {
|
||||||
|
return t.currentIndex+1 < len(t.dataTable.allData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next imported data row
|
||||||
|
func (t *gnucashTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
||||||
|
if t.currentIndex+1 >= len(t.dataTable.allData) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t.currentIndex++
|
||||||
|
|
||||||
|
data := t.dataTable.allData[t.currentIndex]
|
||||||
|
rowItems, isValid, err := t.parseTransaction(ctx, user, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &gnucashTransactionDataRow{
|
||||||
|
dataTable: t.dataTable,
|
||||||
|
data: data,
|
||||||
|
finalItems: rowItems,
|
||||||
|
isValid: isValid,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *gnucashTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, gnucashTransaction *gnucashTransactionData) (map[datatable.TransactionDataTableColumn]string, bool, error) {
|
||||||
|
data := make(map[datatable.TransactionDataTableColumn]string, len(gnucashTransactionSupportedColumns))
|
||||||
|
|
||||||
|
if gnucashTransaction.PostedDate == "" {
|
||||||
|
return nil, false, errs.ErrMissingTransactionTime
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTime, err := utils.ParseFromLongDateTimeWithTimezone2(gnucashTransaction.PostedDate)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, errs.ErrTransactionTimeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
|
||||||
|
|
||||||
|
if len(gnucashTransaction.Splits) == 2 {
|
||||||
|
splitData1 := gnucashTransaction.Splits[0]
|
||||||
|
splitData2 := gnucashTransaction.Splits[1]
|
||||||
|
|
||||||
|
account1 := t.dataTable.accountMap[splitData1.Account]
|
||||||
|
account2 := t.dataTable.accountMap[splitData2.Account]
|
||||||
|
|
||||||
|
if account1 == nil || account2 == nil {
|
||||||
|
return nil, false, errs.ErrMissingAccountData
|
||||||
|
}
|
||||||
|
|
||||||
|
amount1, err := t.parseAmount(splitData1.Quantity)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
amount2, err := t.parseAmount(splitData2.Quantity)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((account1.AccountType == gnucashEquityAccountType || account1.AccountType == gnucashIncomeAccountType) && gnucashAssetOrLiabilityAccountTypes[account2.AccountType]) ||
|
||||||
|
((account2.AccountType == gnucashEquityAccountType || account2.AccountType == gnucashIncomeAccountType) && gnucashAssetOrLiabilityAccountTypes[account1.AccountType]) { // income
|
||||||
|
fromAccount := account1
|
||||||
|
toAccount := account2
|
||||||
|
toAmount := amount2
|
||||||
|
|
||||||
|
if (account2.AccountType == gnucashEquityAccountType || account2.AccountType == gnucashIncomeAccountType) && gnucashAssetOrLiabilityAccountTypes[account1.AccountType] {
|
||||||
|
fromAccount = account2
|
||||||
|
toAccount = account1
|
||||||
|
toAmount = amount1
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.hasSpecifiedSlotKeyValue(fromAccount.Slots, gnucashSlotEquityType, gnucashSlotEquityTypeOpeningBalance) {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_MODIFY_BALANCE))
|
||||||
|
} else {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_INCOME))
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = t.getCategoryName(fromAccount)
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = fromAccount.Name
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = toAccount.Name
|
||||||
|
|
||||||
|
if toAccount.Commodity != nil && toAccount.Commodity.Space == gnucashCommodityCurrencySpace {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = toAccount.Commodity.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = toAmount
|
||||||
|
} else if (account1.AccountType == gnucashExpenseAccountType && gnucashAssetOrLiabilityAccountTypes[account2.AccountType]) ||
|
||||||
|
(account2.AccountType == gnucashExpenseAccountType && gnucashAssetOrLiabilityAccountTypes[account1.AccountType]) { // expense
|
||||||
|
fromAccount := account1
|
||||||
|
fromAmount := amount1
|
||||||
|
toAccount := account2
|
||||||
|
|
||||||
|
if account1.AccountType == gnucashExpenseAccountType && gnucashAssetOrLiabilityAccountTypes[account2.AccountType] {
|
||||||
|
fromAccount = account2
|
||||||
|
fromAmount = amount2
|
||||||
|
toAccount = account1
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fromAmount) > 0 && fromAmount[0] == '-' {
|
||||||
|
amount, err := utils.ParseAmount(fromAmount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
fromAmount = utils.FormatAmount(-amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_EXPENSE))
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = t.getCategoryName(toAccount)
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = toAccount.Name
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = fromAccount.Name
|
||||||
|
|
||||||
|
if fromAccount.Commodity != nil && fromAccount.Commodity.Space == gnucashCommodityCurrencySpace {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = fromAccount.Commodity.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = fromAmount
|
||||||
|
} else if gnucashAssetOrLiabilityAccountTypes[account1.AccountType] && gnucashAssetOrLiabilityAccountTypes[account2.AccountType] {
|
||||||
|
var fromAccount, toAccount *gnucashAccountData
|
||||||
|
var fromAmount, toAmount string
|
||||||
|
|
||||||
|
if len(amount1) > 0 && amount1[0] == '-' {
|
||||||
|
fromAccount = account1
|
||||||
|
fromAmount = amount1[1:]
|
||||||
|
toAccount = account2
|
||||||
|
toAmount = amount2
|
||||||
|
} else if len(amount2) > 0 && amount2[0] == '-' {
|
||||||
|
fromAccount = account2
|
||||||
|
fromAmount = amount2[1:]
|
||||||
|
toAccount = account1
|
||||||
|
toAmount = amount1
|
||||||
|
} else {
|
||||||
|
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse transfer transaction \"id:%s\", because unexcepted account amounts \"%s\" and \"%s\"", gnucashTransaction.Id, amount1, amount2)
|
||||||
|
return nil, false, errs.ErrInvalidGnuCashFile
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_TRANSFER))
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_CATEGORY] = ""
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = fromAccount.Name
|
||||||
|
|
||||||
|
if fromAccount.Commodity != nil && fromAccount.Commodity.Space == gnucashCommodityCurrencySpace {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = fromAccount.Commodity.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = fromAmount
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = toAccount.Name
|
||||||
|
|
||||||
|
if toAccount.Commodity != nil && toAccount.Commodity.Space == gnucashCommodityCurrencySpace {
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_CURRENCY] = toAccount.Commodity.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = toAmount
|
||||||
|
} else {
|
||||||
|
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse transaction \"id:%s\", because unexcepted account types \"%s\" and \"%s\"", gnucashTransaction.Id, account1.AccountType, account2.AccountType)
|
||||||
|
return nil, false, errs.ErrThereAreNotSupportedTransactionType
|
||||||
|
}
|
||||||
|
} else if len(gnucashTransaction.Splits) == 1 {
|
||||||
|
splitData := gnucashTransaction.Splits[0]
|
||||||
|
account := t.dataTable.accountMap[splitData.Account]
|
||||||
|
|
||||||
|
if account == nil {
|
||||||
|
return nil, false, errs.ErrMissingAccountData
|
||||||
|
}
|
||||||
|
|
||||||
|
amount, err := t.parseAmount(splitData.Quantity)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
amountNum, err := utils.ParseAmount(amount)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if amountNum == 0 {
|
||||||
|
log.Warnf(ctx, "[gnucash_transaction_table.parseTransaction] skip parsing transaction \"id:%s\" with zero amount", gnucashTransaction.Id)
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse transaction \"id:%s\", because split count is %d", gnucashTransaction.Id, len(gnucashTransaction.Splits))
|
||||||
|
return nil, false, errs.ErrThereAreNotSupportedTransactionType
|
||||||
|
} else if len(gnucashTransaction.Splits) < 1 {
|
||||||
|
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse transaction \"id:%s\", because split count is %d", gnucashTransaction.Id, len(gnucashTransaction.Splits))
|
||||||
|
return nil, false, errs.ErrInvalidGnuCashFile
|
||||||
|
} else {
|
||||||
|
log.Errorf(ctx, "[gnucash_transaction_table.parseTransaction] cannot parse split transaction \"id:%s\", because split count is %d", gnucashTransaction.Id, len(gnucashTransaction.Splits))
|
||||||
|
return nil, false, errs.ErrNotSupportedSplitTransactions
|
||||||
|
}
|
||||||
|
|
||||||
|
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = gnucashTransaction.Description
|
||||||
|
|
||||||
|
return data, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *gnucashTransactionDataRowIterator) parseAmount(quantity string) (string, error) {
|
||||||
|
items := strings.Split(quantity, "/")
|
||||||
|
|
||||||
|
if len(items) != 2 {
|
||||||
|
return "", errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := utils.StringToInt64(items[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if items[1] == "100" {
|
||||||
|
return utils.FormatAmount(value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
factor, err := utils.StringToInt64(items[1])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", errs.ErrAmountInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value * 100 / factor
|
||||||
|
|
||||||
|
return utils.FormatAmount(value), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *gnucashTransactionDataRowIterator) getCategoryName(accountData *gnucashAccountData) string {
|
||||||
|
if accountData == nil || accountData.ParentId == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
parentAccount := t.dataTable.accountMap[accountData.ParentId]
|
||||||
|
|
||||||
|
if parentAccount == nil || parentAccount.AccountType == gnucashRootAccountType {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentAccount.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *gnucashTransactionDataRowIterator) hasSpecifiedSlotKeyValue(slots []*gnucashSlotData, key string, value string) bool {
|
||||||
|
for i := 0; i < len(slots); i++ {
|
||||||
|
if slots[i].Key == key && slots[i].Value == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewGnuCashTransactionDataTable(database *gnucashDatabase) (*gnucashTransactionDataTable, error) {
|
||||||
|
if database == nil || len(database.Books) < 1 {
|
||||||
|
return nil, errs.ErrNotFoundTransactionDataInFile
|
||||||
|
}
|
||||||
|
|
||||||
|
allData := make([]*gnucashTransactionData, 0)
|
||||||
|
accountMap := make(map[string]*gnucashAccountData)
|
||||||
|
|
||||||
|
for i := 0; i < len(database.Books); i++ {
|
||||||
|
book := database.Books[i]
|
||||||
|
allData = append(allData, book.Transactions...)
|
||||||
|
|
||||||
|
for j := 0; j < len(book.Accounts); j++ {
|
||||||
|
account := book.Accounts[j]
|
||||||
|
accountMap[account.Id] = account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &gnucashTransactionDataTable{
|
||||||
|
allData: allData,
|
||||||
|
accountMap: accountMap,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/default"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/default"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/feidee"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/feidee"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/converters/gnucash"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/qif"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/qif"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/converters/wechat"
|
"github.com/mayswind/ezbookkeeping/pkg/converters/wechat"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
@@ -34,6 +35,8 @@ func GetTransactionDataImporter(fileType string) (base.TransactionDataImporter,
|
|||||||
return qif.QifMonthDayYearTransactionDataImporter, nil
|
return qif.QifMonthDayYearTransactionDataImporter, nil
|
||||||
} else if fileType == "qif_dmy" {
|
} else if fileType == "qif_dmy" {
|
||||||
return qif.QifDayMonthYearTransactionDataImporter, nil
|
return qif.QifDayMonthYearTransactionDataImporter, nil
|
||||||
|
} else if fileType == "gnucash" {
|
||||||
|
return gnucash.GnuCashTransactionDataImporter, nil
|
||||||
} else if fileType == "firefly_iii_csv" {
|
} else if fileType == "firefly_iii_csv" {
|
||||||
return fireflyIII.FireflyIIITransactionDataCsvFileImporter, nil
|
return fireflyIII.FireflyIIITransactionDataCsvFileImporter, nil
|
||||||
} else if fileType == "feidee_mymoney_csv" {
|
} else if fileType == "feidee_mymoney_csv" {
|
||||||
|
|||||||
@@ -18,4 +18,8 @@ var (
|
|||||||
ErrFoundRecordNotHasRelatedRecord = NewNormalError(NormalSubcategoryConverter, 11, http.StatusBadRequest, "found some transactions without related records")
|
ErrFoundRecordNotHasRelatedRecord = NewNormalError(NormalSubcategoryConverter, 11, http.StatusBadRequest, "found some transactions without related records")
|
||||||
ErrInvalidQIFFile = NewNormalError(NormalSubcategoryConverter, 12, http.StatusBadRequest, "invalid qif file")
|
ErrInvalidQIFFile = NewNormalError(NormalSubcategoryConverter, 12, http.StatusBadRequest, "invalid qif file")
|
||||||
ErrMissingTransactionTime = NewNormalError(NormalSubcategoryConverter, 13, http.StatusBadRequest, "missing transaction time field")
|
ErrMissingTransactionTime = NewNormalError(NormalSubcategoryConverter, 13, http.StatusBadRequest, "missing transaction time field")
|
||||||
|
ErrInvalidGnuCashFile = NewNormalError(NormalSubcategoryConverter, 14, http.StatusBadRequest, "invalid gnucash file")
|
||||||
|
ErrMissingAccountData = NewNormalError(NormalSubcategoryConverter, 15, http.StatusBadRequest, "missing account data")
|
||||||
|
ErrNotSupportedSplitTransactions = NewNormalError(NormalSubcategoryConverter, 16, http.StatusBadRequest, "not supported to import split transaction")
|
||||||
|
ErrThereAreNotSupportedTransactionType = NewNormalError(NormalSubcategoryConverter, 17, http.StatusBadRequest, "there are not supported transaction type")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
longDateTimeFormat = "2006-01-02 15:04:05"
|
longDateTimeFormat = "2006-01-02 15:04:05"
|
||||||
longDateTimeWithTimezoneFormat = "2006-01-02 15:04:05Z07:00"
|
longDateTimeWithTimezoneFormat = "2006-01-02 15:04:05Z07:00"
|
||||||
|
longDateTimeWithTimezoneFormat2 = "2006-01-02 15:04:05 Z0700"
|
||||||
longDateTimeWithoutSecondFormat = "2006-01-02 15:04"
|
longDateTimeWithoutSecondFormat = "2006-01-02 15:04"
|
||||||
shortDateTimeFormat = "2006-1-2 15:4:5"
|
shortDateTimeFormat = "2006-1-2 15:4:5"
|
||||||
yearMonthDateTimeFormat = "2006-01"
|
yearMonthDateTimeFormat = "2006-01"
|
||||||
@@ -141,6 +142,11 @@ func ParseFromLongDateTimeWithTimezone(t string) (time.Time, error) {
|
|||||||
return time.Parse(longDateTimeWithTimezoneFormat, t)
|
return time.Parse(longDateTimeWithTimezoneFormat, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseFromLongDateTimeWithTimezone2 parses a formatted string in long date time format
|
||||||
|
func ParseFromLongDateTimeWithTimezone2(t string) (time.Time, error) {
|
||||||
|
return time.Parse(longDateTimeWithTimezoneFormat2, t)
|
||||||
|
}
|
||||||
|
|
||||||
// ParseFromLongDateTimeWithoutSecond parses a formatted string in long date time format (no second)
|
// ParseFromLongDateTimeWithoutSecond parses a formatted string in long date time format (no second)
|
||||||
func ParseFromLongDateTimeWithoutSecond(t string, utcOffset int16) (time.Time, error) {
|
func ParseFromLongDateTimeWithoutSecond(t string, utcOffset int16) (time.Time, error) {
|
||||||
timezone := time.FixedZone("Timezone", int(utcOffset)*60)
|
timezone := time.FixedZone("Timezone", int(utcOffset)*60)
|
||||||
|
|||||||
@@ -140,6 +140,15 @@ func TestParseFromLongDateTimeWithTimezone(t *testing.T) {
|
|||||||
assert.Equal(t, expectedValue, actualValue)
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseFromLongDateTimeWithTimezone2(t *testing.T) {
|
||||||
|
expectedValue := int64(1617238883)
|
||||||
|
actualTime, err := ParseFromLongDateTimeWithTimezone2("2021-04-01 06:01:23 +0500")
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
actualValue := actualTime.Unix()
|
||||||
|
assert.Equal(t, expectedValue, actualValue)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseFromLongDateTimeWithoutSecond(t *testing.T) {
|
func TestParseFromLongDateTimeWithoutSecond(t *testing.T) {
|
||||||
expectedValue := int64(1691947440)
|
expectedValue := int64(1691947440)
|
||||||
actualTime, err := ParseFromLongDateTimeWithoutSecond("2023-08-13 17:24", 0)
|
actualTime, err := ParseFromLongDateTimeWithoutSecond("2023-08-13 17:24", 0)
|
||||||
|
|||||||
@@ -38,6 +38,15 @@ const supportedImportFileTypes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'gnucash',
|
||||||
|
name: 'GnuCash XML Database File',
|
||||||
|
extensions: '.gnucash',
|
||||||
|
document: {
|
||||||
|
supportMultiLanguages: true,
|
||||||
|
anchor: 'how-to-get-gnucash-data-export-file'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'firefly_iii_csv',
|
type: 'firefly_iii_csv',
|
||||||
name: 'Firefly III Data Export File',
|
name: 'Firefly III Data Export File',
|
||||||
|
|||||||
@@ -1128,6 +1128,10 @@
|
|||||||
"found some transactions without related records": "There are some transactions which don't have related records",
|
"found some transactions without related records": "There are some transactions which don't have related records",
|
||||||
"invalid qif file": "Invalid QIF file",
|
"invalid qif file": "Invalid QIF file",
|
||||||
"missing transaction time field": "Missing transaction time field",
|
"missing transaction time field": "Missing transaction time field",
|
||||||
|
"invalid gnucash file": "Invalid GnuCash file",
|
||||||
|
"missing account data": "Missing account data",
|
||||||
|
"not supported to import split transaction": "Not supported to import split transaction",
|
||||||
|
"there are not supported transaction type": "There are not supported transaction type in import file",
|
||||||
"query items cannot be blank": "There are no query items",
|
"query items cannot be blank": "There are no query items",
|
||||||
"query items too much": "There are too many query items",
|
"query items too much": "There are too many query items",
|
||||||
"query items have invalid item": "There is invalid item in query items",
|
"query items have invalid item": "There is invalid item in query items",
|
||||||
@@ -1523,6 +1527,7 @@
|
|||||||
"Year-month-day format": "Year-month-day format",
|
"Year-month-day format": "Year-month-day format",
|
||||||
"Month-day-year format": "Month-day-year format",
|
"Month-day-year format": "Month-day-year format",
|
||||||
"Day-month-year format": "Day-month-year format",
|
"Day-month-year format": "Day-month-year format",
|
||||||
|
"GnuCash XML Database File": "GnuCash XML Database File",
|
||||||
"Firefly III Data Export File": "Firefly III Data Export File",
|
"Firefly III Data Export File": "Firefly III Data Export File",
|
||||||
"Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) Data Export File",
|
"Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) Data Export File",
|
||||||
"Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File",
|
"Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File",
|
||||||
|
|||||||
@@ -1128,6 +1128,10 @@
|
|||||||
"found some transactions without related records": "有一些交易没有关联记录",
|
"found some transactions without related records": "有一些交易没有关联记录",
|
||||||
"invalid qif file": "无效的 QIF 文件",
|
"invalid qif file": "无效的 QIF 文件",
|
||||||
"missing transaction time field": "缺少交易时间字段",
|
"missing transaction time field": "缺少交易时间字段",
|
||||||
|
"invalid gnucash file": "无效的 GnuCash 文件",
|
||||||
|
"missing account data": "缺少账户数据",
|
||||||
|
"not supported to import split transaction": "不支持导入拆分的交易",
|
||||||
|
"there are not supported transaction type": "导入文件中有不支持的交易类型",
|
||||||
"query items cannot be blank": "请求项目不能为空",
|
"query items cannot be blank": "请求项目不能为空",
|
||||||
"query items too much": "请求项目过多",
|
"query items too much": "请求项目过多",
|
||||||
"query items have invalid item": "请求项目中有非法项目",
|
"query items have invalid item": "请求项目中有非法项目",
|
||||||
@@ -1523,6 +1527,7 @@
|
|||||||
"Year-month-day format": "年-月-日 格式",
|
"Year-month-day format": "年-月-日 格式",
|
||||||
"Month-day-year format": "月-日-年 格式",
|
"Month-day-year format": "月-日-年 格式",
|
||||||
"Day-month-year format": "日-月-年 格式",
|
"Day-month-year format": "日-月-年 格式",
|
||||||
|
"GnuCash XML Database File": "GnuCash XML 数据库文件",
|
||||||
"Firefly III Data Export File": "Firefly III 数据导出文件",
|
"Firefly III Data Export File": "Firefly III 数据导出文件",
|
||||||
"Feidee MyMoney (App) Data Export File": "金蝶随手记 (App) 数据导出文件",
|
"Feidee MyMoney (App) Data Export File": "金蝶随手记 (App) 数据导出文件",
|
||||||
"Feidee MyMoney (Web) Data Export File": "金蝶随手记 (Web版) 数据导出文件",
|
"Feidee MyMoney (Web) Data Export File": "金蝶随手记 (Web版) 数据导出文件",
|
||||||
|
|||||||
Reference in New Issue
Block a user