Skip to content

Commit

Permalink
import transaction from GnuCash database
Browse files Browse the repository at this point in the history
  • Loading branch information
mayswind committed Oct 20, 2024
1 parent 6ce6fd3 commit bb4eca1
Show file tree
Hide file tree
Showing 12 changed files with 1,517 additions and 0 deletions.
87 changes: 87 additions & 0 deletions pkg/converters/gnucash/gnucash_data.go
Original file line number Diff line number Diff line change
@@ -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"`
}
55 changes: 55 additions & 0 deletions pkg/converters/gnucash/gnucash_data_reader.go
Original file line number Diff line number Diff line change
@@ -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
}
49 changes: 49 additions & 0 deletions pkg/converters/gnucash/gnucash_transaction_data_file_importer.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit bb4eca1

Please sign in to comment.