import transactions from camt.053 file
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package camt
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
type camtCreditDebitIndicator string
|
||||
|
||||
const (
|
||||
CAMT_INDICATOR_CREDIT camtCreditDebitIndicator = "CRDT"
|
||||
CAMT_INDICATOR_DEBIT camtCreditDebitIndicator = "DBIT"
|
||||
)
|
||||
|
||||
type camt053File struct {
|
||||
XMLName xml.Name `xml:"Document"`
|
||||
BankToCustomerStatement *camtBankToCustomerStatement `xml:"BkToCstmrStmt"`
|
||||
}
|
||||
|
||||
type camtBankToCustomerStatement struct {
|
||||
Statements []*camtStatement `xml:"Stmt"`
|
||||
}
|
||||
|
||||
type camtStatement struct {
|
||||
Account *camtAccount `xml:"Acct"`
|
||||
Entries []*camtEntry `xml:"Ntry"`
|
||||
}
|
||||
|
||||
type camtAccount struct {
|
||||
IBAN string `xml:"Id>IBAN"`
|
||||
OtherIdentification string `xml:"Id>Othr>Id"`
|
||||
Currency string `xml:"Ccy"`
|
||||
}
|
||||
|
||||
type camtEntry struct {
|
||||
Amount *camtAmount `xml:"Amt"`
|
||||
CreditDebitIndicator camtCreditDebitIndicator `xml:"CdtDbtInd"`
|
||||
BookingDate *camtDate `xml:"BookgDt"`
|
||||
EntryDetails *camtEntryDetails `xml:"NtryDtls"`
|
||||
AdditionalEntryInformation string `xml:"AddtlNtryInf"`
|
||||
}
|
||||
|
||||
type camtAmount struct {
|
||||
Value string `xml:",chardata"`
|
||||
Currency string `xml:"Ccy,attr"`
|
||||
}
|
||||
|
||||
type camtDate struct {
|
||||
Date string `xml:"Dt"`
|
||||
DateTime string `xml:"DtTm"`
|
||||
}
|
||||
|
||||
type camtEntryDetails struct {
|
||||
TransactionDetails []*camtTransactionDetails `xml:"TxDtls"`
|
||||
}
|
||||
|
||||
type camtTransactionDetails struct {
|
||||
AmountDetails *camtAmountDetails `xml:"AmtDtls"`
|
||||
RemittanceInformation *camtRemittanceInformation `xml:"RmtInf"`
|
||||
AdditionalTransactionInformation string `xml:"AddtlTxInf"`
|
||||
}
|
||||
|
||||
type camtAmountDetails struct {
|
||||
InstructedAmount *camtAmount `xml:"InstdAmt>Amt"`
|
||||
TransactionAmount *camtAmount `xml:"TxAmt>Amt"`
|
||||
}
|
||||
|
||||
type camtRemittanceInformation struct {
|
||||
Unstructured []string `xml:"Ustrd"`
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package camt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
|
||||
"golang.org/x/net/html/charset"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
)
|
||||
|
||||
// camt053FileReader defines the structure of camt.053 file reader
|
||||
type camt053FileReader struct {
|
||||
xmlDecoder *xml.Decoder
|
||||
}
|
||||
|
||||
// read returns the imported camt.053 data
|
||||
func (r *camt053FileReader) read(ctx core.Context) (*camt053File, error) {
|
||||
file := &camt053File{}
|
||||
|
||||
err := r.xmlDecoder.Decode(&file)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func createNewCamt053FileReader(data []byte) (*camt053FileReader, error) {
|
||||
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 = charset.NewReaderLabel
|
||||
|
||||
return &camt053FileReader{
|
||||
xmlDecoder: xmlDecoder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errs.ErrInvalidXmlFile
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package camt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 camtTransactionSupportedColumns = 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_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_DESCRIPTION: true,
|
||||
}
|
||||
|
||||
// camtStatementTransactionDataTable defines the structure of camt statement transaction data table
|
||||
type camtStatementTransactionDataTable struct {
|
||||
allStatements []*camtStatement
|
||||
}
|
||||
|
||||
// camtStatementTransactionDataRow defines the structure of camt statement transaction data row
|
||||
type camtStatementTransactionDataRow struct {
|
||||
dataTable *camtStatementTransactionDataTable
|
||||
account *camtAccount
|
||||
entry *camtEntry
|
||||
transactionDetails *camtTransactionDetails
|
||||
finalItems map[datatable.TransactionDataTableColumn]string
|
||||
}
|
||||
|
||||
// camtStatementTransactionDataRowIterator defines the structure of camt statement transaction data row iterator
|
||||
type camtStatementTransactionDataRowIterator struct {
|
||||
dataTable *camtStatementTransactionDataTable
|
||||
currentStatementIndex int
|
||||
currentEntryIndex int
|
||||
currentTransactionDetailsIndex int
|
||||
}
|
||||
|
||||
// HasColumn returns whether the transaction data table has specified column
|
||||
func (t *camtStatementTransactionDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
|
||||
_, exists := camtTransactionSupportedColumns[column]
|
||||
return exists
|
||||
}
|
||||
|
||||
// TransactionRowCount returns the total count of transaction data row
|
||||
func (t *camtStatementTransactionDataTable) TransactionRowCount() int {
|
||||
totalDataRowCount := 0
|
||||
|
||||
for i := 0; i < len(t.allStatements); i++ {
|
||||
statement := t.allStatements[i]
|
||||
|
||||
for j := 0; j < len(statement.Entries); j++ {
|
||||
entry := statement.Entries[j]
|
||||
|
||||
if entry.EntryDetails != nil {
|
||||
totalDataRowCount += len(entry.EntryDetails.TransactionDetails)
|
||||
} else {
|
||||
totalDataRowCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return totalDataRowCount
|
||||
}
|
||||
|
||||
// TransactionRowIterator returns the iterator of transaction data row
|
||||
func (t *camtStatementTransactionDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
|
||||
return &camtStatementTransactionDataRowIterator{
|
||||
dataTable: t,
|
||||
currentStatementIndex: 0,
|
||||
currentEntryIndex: 0,
|
||||
currentTransactionDetailsIndex: -1,
|
||||
}
|
||||
}
|
||||
|
||||
// IsValid returns whether this row is valid data for importing
|
||||
func (r *camtStatementTransactionDataRow) IsValid() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetData returns the data in the specified column type
|
||||
func (r *camtStatementTransactionDataRow) GetData(column datatable.TransactionDataTableColumn) string {
|
||||
_, exists := camtTransactionSupportedColumns[column]
|
||||
|
||||
if exists {
|
||||
return r.finalItems[column]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// HasNext returns whether the iterator does not reach the end
|
||||
func (t *camtStatementTransactionDataRowIterator) HasNext() bool {
|
||||
allStatements := t.dataTable.allStatements
|
||||
|
||||
if t.currentStatementIndex >= len(allStatements) {
|
||||
return false
|
||||
}
|
||||
|
||||
currentStatement := allStatements[t.currentStatementIndex]
|
||||
|
||||
if t.currentEntryIndex+1 < len(currentStatement.Entries) {
|
||||
return true
|
||||
} else if t.currentEntryIndex < len(currentStatement.Entries) {
|
||||
currencyEntry := currentStatement.Entries[t.currentEntryIndex]
|
||||
|
||||
if currencyEntry.EntryDetails != nil {
|
||||
if t.currentTransactionDetailsIndex+1 < len(currencyEntry.EntryDetails.TransactionDetails) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if t.currentTransactionDetailsIndex < 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := t.currentStatementIndex + 1; i < len(allStatements); i++ {
|
||||
statement := allStatements[i]
|
||||
|
||||
if len(statement.Entries) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Next returns the next imported data row
|
||||
func (t *camtStatementTransactionDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
|
||||
allStatements := t.dataTable.allStatements
|
||||
|
||||
for i := t.currentStatementIndex; i < len(allStatements); i++ {
|
||||
foundNextRow := false
|
||||
statement := allStatements[i]
|
||||
|
||||
for j := t.currentEntryIndex; j < len(statement.Entries); j++ {
|
||||
if statement.Entries[j].EntryDetails != nil {
|
||||
if t.currentTransactionDetailsIndex+1 < len(statement.Entries[j].EntryDetails.TransactionDetails) {
|
||||
t.currentTransactionDetailsIndex++
|
||||
foundNextRow = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if t.currentTransactionDetailsIndex < 0 {
|
||||
t.currentTransactionDetailsIndex++
|
||||
foundNextRow = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t.currentEntryIndex++
|
||||
t.currentTransactionDetailsIndex = -1
|
||||
}
|
||||
|
||||
if foundNextRow {
|
||||
break
|
||||
}
|
||||
|
||||
t.currentStatementIndex++
|
||||
t.currentEntryIndex = 0
|
||||
t.currentTransactionDetailsIndex = -1
|
||||
}
|
||||
|
||||
if t.currentStatementIndex >= len(allStatements) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
currentStatement := allStatements[t.currentStatementIndex]
|
||||
|
||||
if t.currentEntryIndex >= len(currentStatement.Entries) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
account := currentStatement.Account
|
||||
entry := currentStatement.Entries[t.currentEntryIndex]
|
||||
var transactionDetails *camtTransactionDetails
|
||||
|
||||
if entry.EntryDetails != nil {
|
||||
if t.currentTransactionDetailsIndex >= len(entry.EntryDetails.TransactionDetails) {
|
||||
return nil, nil
|
||||
} else {
|
||||
transactionDetails = entry.EntryDetails.TransactionDetails[t.currentTransactionDetailsIndex]
|
||||
}
|
||||
} else {
|
||||
if t.currentTransactionDetailsIndex >= 1 {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
rowItems, err := t.parseTransaction(ctx, user, account, entry, transactionDetails)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[camt_statement_transaction_data_table.Next] cannot parsing transaction in entry#%d-transaction_detail#%d (statement#%d), because %s", t.currentEntryIndex, t.currentTransactionDetailsIndex, t.currentStatementIndex, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &camtStatementTransactionDataRow{
|
||||
dataTable: t.dataTable,
|
||||
account: account,
|
||||
entry: entry,
|
||||
transactionDetails: transactionDetails,
|
||||
finalItems: rowItems,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *camtStatementTransactionDataRowIterator) parseTransaction(ctx core.Context, user *models.User, account *camtAccount, entry *camtEntry, transactionDetails *camtTransactionDetails) (map[datatable.TransactionDataTableColumn]string, error) {
|
||||
data := make(map[datatable.TransactionDataTableColumn]string, len(camtTransactionSupportedColumns))
|
||||
|
||||
if account == nil {
|
||||
return nil, errs.ErrMissingAccountData
|
||||
}
|
||||
|
||||
if entry.BookingDate != nil && entry.BookingDate.DateTime != "" {
|
||||
if strings.Index(entry.BookingDate.DateTime, "T") <= 0 {
|
||||
return nil, errs.ErrTransactionTimeInvalid
|
||||
}
|
||||
|
||||
dateTime, err := utils.ParseFromLongDateTimeWithTimezone(strings.ReplaceAll(entry.BookingDate.DateTime, "T", " "))
|
||||
|
||||
if err != nil {
|
||||
return nil, 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())
|
||||
} else if entry.BookingDate != nil && entry.BookingDate.Date != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = fmt.Sprintf("%s 00:00:00", entry.BookingDate.Date)
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE
|
||||
} else {
|
||||
return nil, errs.ErrMissingTransactionTime
|
||||
}
|
||||
|
||||
if account.IBAN != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = account.IBAN
|
||||
} else if account.OtherIdentification != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = account.OtherIdentification
|
||||
}
|
||||
|
||||
if transactionDetails != nil && transactionDetails.AmountDetails != nil && transactionDetails.AmountDetails.TransactionAmount != nil && transactionDetails.AmountDetails.TransactionAmount.Currency != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = transactionDetails.AmountDetails.TransactionAmount.Currency
|
||||
} else if entry.Amount != nil && entry.Amount.Currency != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = entry.Amount.Currency
|
||||
} else if account.Currency != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY] = account.Currency
|
||||
} else {
|
||||
return nil, errs.ErrAccountCurrencyInvalid
|
||||
}
|
||||
|
||||
amountValue := ""
|
||||
|
||||
if entry.EntryDetails != nil && len(entry.EntryDetails.TransactionDetails) > 1 && transactionDetails != nil { // when there are multiple transaction details in one entry, only use the amount in the transaction details
|
||||
if transactionDetails.AmountDetails != nil && transactionDetails.AmountDetails.InstructedAmount != nil && transactionDetails.AmountDetails.InstructedAmount.Value != "" {
|
||||
amountValue = transactionDetails.AmountDetails.InstructedAmount.Value
|
||||
} else if transactionDetails.AmountDetails != nil && transactionDetails.AmountDetails.TransactionAmount != nil && transactionDetails.AmountDetails.TransactionAmount.Value != "" {
|
||||
amountValue = transactionDetails.AmountDetails.TransactionAmount.Value
|
||||
} else {
|
||||
return nil, errs.ErrAmountInvalid
|
||||
}
|
||||
} else if entry.Amount != nil && entry.Amount.Value != "" {
|
||||
amountValue = entry.Amount.Value
|
||||
}
|
||||
|
||||
if amountValue == "" {
|
||||
return nil, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
amount, err := utils.ParseAmount(amountValue)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "[camt_statement_transaction_data_table.parseTransaction] cannot parsing transaction amount \"%s\", because %s", amountValue, err.Error())
|
||||
return nil, errs.ErrAmountInvalid
|
||||
}
|
||||
|
||||
data[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
|
||||
|
||||
if entry.CreditDebitIndicator == CAMT_INDICATOR_CREDIT {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_INCOME))
|
||||
} else if entry.CreditDebitIndicator == CAMT_INDICATOR_DEBIT {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = utils.IntToString(int(models.TRANSACTION_TYPE_EXPENSE))
|
||||
} else {
|
||||
return nil, errs.ErrTransactionTypeInvalid
|
||||
}
|
||||
|
||||
if transactionDetails != nil && transactionDetails.AdditionalTransactionInformation != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = transactionDetails.AdditionalTransactionInformation
|
||||
} else if transactionDetails != nil && transactionDetails.RemittanceInformation != nil && len(transactionDetails.RemittanceInformation.Unstructured) > 0 {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = strings.Join(transactionDetails.RemittanceInformation.Unstructured, "\n")
|
||||
} else if entry.AdditionalEntryInformation != "" {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = entry.AdditionalEntryInformation
|
||||
} else {
|
||||
data[datatable.TRANSACTION_DATA_TABLE_DESCRIPTION] = ""
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func createNewCamtStatementTransactionDataTable(file *camt053File) (*camtStatementTransactionDataTable, error) {
|
||||
if file == nil || file.BankToCustomerStatement == nil || len(file.BankToCustomerStatement.Statements) == 0 {
|
||||
return nil, errs.ErrNotFoundTransactionDataInFile
|
||||
}
|
||||
|
||||
return &camtStatementTransactionDataTable{
|
||||
allStatements: file.BankToCustomerStatement.Statements,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package camt
|
||||
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/core"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
var camtTransactionTypeNameMapping = map[models.TransactionType]string{
|
||||
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)),
|
||||
}
|
||||
|
||||
// camt053TransactionDataImporter defines the structure of camt.053 file importer for transaction data
|
||||
type camt053TransactionDataImporter struct {
|
||||
}
|
||||
|
||||
// Initialize a camt.053 transaction data importer singleton instance
|
||||
var (
|
||||
Camt053TransactionDataImporter = &camt053TransactionDataImporter{}
|
||||
)
|
||||
|
||||
// ParseImportedData returns the imported data by parsing the camt.053 file transaction data
|
||||
func (c *camt053TransactionDataImporter) ParseImportedData(ctx core.Context, user *models.User, data []byte, defaultTimezoneOffset int16, accountMap map[string]*models.Account, expenseCategoryMap map[string]map[string]*models.TransactionCategory, incomeCategoryMap map[string]map[string]*models.TransactionCategory, transferCategoryMap map[string]map[string]*models.TransactionCategory, tagMap map[string]*models.TransactionTag) (models.ImportedTransactionSlice, []*models.Account, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionCategory, []*models.TransactionTag, error) {
|
||||
camt053DataReader, err := createNewCamt053FileReader(data)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
camt053Data, err := camt053DataReader.read(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
transactionDataTable, err := createNewCamtStatementTransactionDataTable(camt053Data)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
dataTableImporter := converter.CreateNewSimpleImporterWithTypeNameMapping(camtTransactionTypeNameMapping)
|
||||
|
||||
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
|
||||
}
|
||||
@@ -0,0 +1,765 @@
|
||||
package camt
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_MinimumValidData(t *testing.T) {
|
||||
converter := Camt053TransactionDataImporter
|
||||
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"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T01:23:45+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>DBIT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">0.12</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<Othr>
|
||||
<Id>456</Id>
|
||||
</Othr>
|
||||
</Id>
|
||||
<Ccy>USD</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T23:59:59+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="USD">1.23</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, 3, len(allNewTransactions))
|
||||
assert.Equal(t, 2, len(allNewAccounts))
|
||||
assert.Equal(t, 1, len(allNewSubExpenseCategories))
|
||||
assert.Equal(t, 1, len(allNewSubIncomeCategories))
|
||||
assert.Equal(t, 0, len(allNewSubTransferCategories))
|
||||
assert.Equal(t, 0, len(allNewTags))
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type)
|
||||
assert.Equal(t, int64(1725125025), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "123", allNewTransactions[0].OriginalSourceAccountName)
|
||||
assert.Equal(t, "CNY", allNewTransactions[0].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type)
|
||||
assert.Equal(t, int64(1725165296), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||
assert.Equal(t, int64(12), allNewTransactions[1].Amount)
|
||||
assert.Equal(t, "123", allNewTransactions[1].OriginalSourceAccountName)
|
||||
assert.Equal(t, "CNY", allNewTransactions[1].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid)
|
||||
assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[2].Type)
|
||||
assert.Equal(t, int64(1725206399), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||
assert.Equal(t, int64(123), allNewTransactions[2].Amount)
|
||||
assert.Equal(t, "456", allNewTransactions[2].OriginalSourceAccountName)
|
||||
assert.Equal(t, "USD", allNewTransactions[2].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, "", allNewTransactions[2].OriginalCategoryName)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid)
|
||||
assert.Equal(t, "123", allNewAccounts[0].Name)
|
||||
assert.Equal(t, "CNY", allNewAccounts[0].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid)
|
||||
assert.Equal(t, "456", allNewAccounts[1].Name)
|
||||
assert.Equal(t, "USD", allNewAccounts[1].Currency)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid)
|
||||
assert.Equal(t, "", allNewSubExpenseCategories[0].Name)
|
||||
|
||||
assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid)
|
||||
assert.Equal(t, "", allNewSubIncomeCategories[0].Name)
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_ParseValidTransactionTime(t *testing.T) {
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<Dt>2024-09-01</Dt>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-02T03:04:05Z</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 3, len(allNewTransactions))
|
||||
|
||||
assert.Equal(t, int64(1725148800), utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime))
|
||||
assert.Equal(t, int64(1725165296), utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime))
|
||||
assert.Equal(t, int64(1725246245), utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime))
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_ParseInvalidTransactionTime(t *testing.T) {
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024T1</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01 12:34:56</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<Dt>2024/09/01</Dt>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTimeInvalid.Message)
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_ParseTransactionValidAmountAndCurrency(t *testing.T) {
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="USD">123.45</Amt>
|
||||
<NtryDtls>
|
||||
<TxDtls>
|
||||
<AmtDtls>
|
||||
<TxAmt>
|
||||
<Amt Ccy="USD">100.23</Amt>
|
||||
</TxAmt>
|
||||
</AmtDtls>
|
||||
</TxDtls>
|
||||
<TxDtls>
|
||||
<AmtDtls>
|
||||
<TxAmt>
|
||||
<Amt Ccy="USD">23.22</Amt>
|
||||
</TxAmt>
|
||||
</AmtDtls>
|
||||
</TxDtls>
|
||||
</NtryDtls>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(allNewTransactions))
|
||||
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, int64(2322), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, int64(10023), allNewTransactions[1].Amount)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="USD">123.45</Amt>
|
||||
<NtryDtls>
|
||||
<TxDtls>
|
||||
<AmtDtls>
|
||||
<InstdAmt>
|
||||
<Amt Ccy="USD">99.99</Amt>
|
||||
</InstdAmt>
|
||||
<TxAmt>
|
||||
<Amt Ccy="USD">100.23</Amt>
|
||||
</TxAmt>
|
||||
</AmtDtls>
|
||||
</TxDtls>
|
||||
<TxDtls>
|
||||
<AmtDtls>
|
||||
<InstdAmt>
|
||||
<Amt Ccy="USD">23.46</Amt>
|
||||
</InstdAmt>
|
||||
<TxAmt>
|
||||
<Amt Ccy="USD">23.22</Amt>
|
||||
</TxAmt>
|
||||
</AmtDtls>
|
||||
</TxDtls>
|
||||
</NtryDtls>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(allNewTransactions))
|
||||
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, int64(2346), allNewTransactions[0].Amount)
|
||||
assert.Equal(t, "USD", allNewTransactions[1].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, int64(9999), allNewTransactions[1].Amount)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="USD">123.45</Amt>
|
||||
<NtryDtls>
|
||||
<TxDtls>
|
||||
<AmtDtls>
|
||||
<TxAmt>
|
||||
<Amt Ccy="USD">123.45</Amt>
|
||||
</TxAmt>
|
||||
</AmtDtls>
|
||||
</TxDtls>
|
||||
</NtryDtls>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "USD", allNewTransactions[0].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt>123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "CNY", allNewTransactions[0].OriginalSourceAccountCurrency)
|
||||
assert.Equal(t, int64(12345), allNewTransactions[0].Amount)
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_ParseTransactionInvalidAmountAndCurrency(t *testing.T) {
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt>123 45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="USD">123.45</Amt>
|
||||
<NtryDtls>
|
||||
<TxDtls>
|
||||
<AmtDtls>
|
||||
</AmtDtls>
|
||||
</TxDtls>
|
||||
<TxDtls>
|
||||
</TxDtls>
|
||||
</NtryDtls>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="USD">123.45</Amt>
|
||||
<NtryDtls>
|
||||
<TxDtls>
|
||||
</TxDtls>
|
||||
<TxDtls>
|
||||
<AmtDtls>
|
||||
</AmtDtls>
|
||||
</TxDtls>
|
||||
</NtryDtls>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_ParseDescription(t *testing.T) {
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
<AddtlNtryInf>Test Entry</AddtlNtryInf>
|
||||
<NtryDtls>
|
||||
<TxDtls>
|
||||
<AddtlTxInf>Test Transaction</AddtlTxInf>
|
||||
<RmtInf>
|
||||
<Ustrd>Test Line 1</Ustrd>
|
||||
<Ustrd>Test Line 2</Ustrd>
|
||||
</RmtInf>
|
||||
</TxDtls>
|
||||
</NtryDtls>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "Test Transaction", allNewTransactions[0].Comment)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
<AddtlNtryInf>Test Entry</AddtlNtryInf>
|
||||
<NtryDtls>
|
||||
<TxDtls>
|
||||
<RmtInf>
|
||||
<Ustrd>Test Line 1</Ustrd>
|
||||
<Ustrd>Test Line 2</Ustrd>
|
||||
</RmtInf>
|
||||
</TxDtls>
|
||||
</NtryDtls>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "Test Line 1\nTest Line 2", allNewTransactions[0].Comment)
|
||||
|
||||
allNewTransactions, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
<AddtlNtryInf>Test Entry</AddtlNtryInf>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(allNewTransactions))
|
||||
assert.Equal(t, "Test Entry", allNewTransactions[0].Comment)
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_MissingAccountNode(t *testing.T) {
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingAccountData.Message)
|
||||
}
|
||||
|
||||
func TestCamt053TransactionDataFileParseImportedData_MissingTransactionRequiredNode(t *testing.T) {
|
||||
converter := Camt053TransactionDataImporter
|
||||
context := core.NewNullContext()
|
||||
|
||||
user := &models.User{
|
||||
Uid: 1234567890,
|
||||
DefaultCurrency: "CNY",
|
||||
}
|
||||
|
||||
_, _, _, _, _, _, err := converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrMissingTransactionTime.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<Amt Ccy="CNY">123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrTransactionTypeInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
<Ccy>CNY</Ccy>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAmountInvalid.Message)
|
||||
|
||||
_, _, _, _, _, _, err = converter.ParseImportedData(context, user, []byte(
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
|
||||
<BkToCstmrStmt>
|
||||
<Stmt>
|
||||
<Acct>
|
||||
<Id>
|
||||
<IBAN>123</IBAN>
|
||||
</Id>
|
||||
</Acct>
|
||||
<Ntry>
|
||||
<BookgDt>
|
||||
<DtTm>2024-09-01T12:34:56+08:00</DtTm>
|
||||
</BookgDt>
|
||||
<CdtDbtInd>CRDT</CdtDbtInd>
|
||||
<Amt>123.45</Amt>
|
||||
</Ntry>
|
||||
</Stmt>
|
||||
</BkToCstmrStmt>
|
||||
</Document>`), 0, nil, nil, nil, nil, nil)
|
||||
assert.EqualError(t, err, errs.ErrAccountCurrencyInvalid.Message)
|
||||
}
|
||||
@@ -96,7 +96,8 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
|
||||
|
||||
timezoneOffset := defaultTimezoneOffset
|
||||
|
||||
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) {
|
||||
if dataTable.HasColumn(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) &&
|
||||
dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE) != datatable.TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE {
|
||||
transactionTimezone, err := utils.ParseFromTimezoneOffset(dataRow.GetData(datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE))
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -73,3 +73,6 @@ const (
|
||||
TRANSACTION_DATA_TABLE_TAGS TransactionDataTableColumn = 13
|
||||
TRANSACTION_DATA_TABLE_DESCRIPTION TransactionDataTableColumn = 14
|
||||
)
|
||||
|
||||
// TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE represents the constant for timezone not available
|
||||
const TRANSACTION_DATA_TABLE_TIMEZONE_NOT_AVAILABLE = "TIMEZONE_NOT_AVAILABLE"
|
||||
|
||||
@@ -3,6 +3,7 @@ package converters
|
||||
import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/alipay"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/beancount"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/camt"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/converter"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/converters/default"
|
||||
@@ -47,6 +48,8 @@ func GetTransactionDataImporter(fileType string) (converter.TransactionDataImpor
|
||||
return qif.QifDayMonthYearTransactionDataImporter, nil
|
||||
} else if fileType == "iif" {
|
||||
return iif.IifTransactionDataFileImporter, nil
|
||||
} else if fileType == "camt053" {
|
||||
return camt.Camt053TransactionDataImporter, nil
|
||||
} else if fileType == "gnucash" {
|
||||
return gnucash.GnuCashTransactionDataImporter, nil
|
||||
} else if fileType == "firefly_iii_csv" {
|
||||
|
||||
@@ -28,4 +28,5 @@ var (
|
||||
ErrInvalidBeancountFile = NewNormalError(NormalSubcategoryConverter, 21, http.StatusBadRequest, "invalid beancount file")
|
||||
ErrBeancountFileNotSupportInclude = NewNormalError(NormalSubcategoryConverter, 22, http.StatusBadRequest, "not support include directive for beancount file")
|
||||
ErrInvalidAmountExpression = NewNormalError(NormalSubcategoryConverter, 23, http.StatusBadRequest, "invalid amount expression")
|
||||
ErrInvalidXmlFile = NewNormalError(NormalSubcategoryConverter, 24, http.StatusBadRequest, "invalid xml file")
|
||||
)
|
||||
|
||||
@@ -162,6 +162,11 @@ export const SUPPORTED_IMPORT_FILE_TYPES: ImportFileType[] = [
|
||||
name: 'Intuit Interchange Format (IIF) File',
|
||||
extensions: '.iif'
|
||||
},
|
||||
{
|
||||
type: 'camt053',
|
||||
name: 'Camt.053 Bank to Customer Statement File',
|
||||
extensions: '.xml'
|
||||
},
|
||||
{
|
||||
type: 'gnucash',
|
||||
name: 'GnuCash XML Database File',
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "Invalid Beancount file",
|
||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||
"invalid amount expression": "Amount expression is invalid",
|
||||
"invalid xml file": "Invalid XML file",
|
||||
"user custom exchange rate data not found": "User custom exchange rate data is not found",
|
||||
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
|
||||
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "Monat-Tag-Jahr-Format",
|
||||
"Day-month-year format": "Tag-Monat-Jahr-Format",
|
||||
"Intuit Interchange Format (IIF) File": "Intuit Interchange Format (IIF)-Datei",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
|
||||
"Delimiter-separated Values (DSV) File": "Delimiter-separated Values (DSV) File",
|
||||
"Delimiter-separated Values (DSV) Data": "Delimiter-separated Values (DSV) Data",
|
||||
"GnuCash XML Database File": "GnuCash XML-Datenbankdatei",
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "Invalid Beancount file",
|
||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||
"invalid amount expression": "Amount expression is invalid",
|
||||
"invalid xml file": "Invalid XML file",
|
||||
"user custom exchange rate data not found": "User custom exchange rate data is not found",
|
||||
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
|
||||
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "Month-day-year format",
|
||||
"Day-month-year format": "Day-month-year format",
|
||||
"Intuit Interchange Format (IIF) File": "Intuit Interchange Format (IIF) File",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
|
||||
"Delimiter-separated Values (DSV) File": "Delimiter-separated Values (DSV) File",
|
||||
"Delimiter-separated Values (DSV) Data": "Delimiter-separated Values (DSV) Data",
|
||||
"GnuCash XML Database File": "GnuCash XML Database File",
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "Invalid Beancount file",
|
||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||
"invalid amount expression": "Amount expression is invalid",
|
||||
"invalid xml file": "Invalid XML file",
|
||||
"user custom exchange rate data not found": "User custom exchange rate data is not found",
|
||||
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
|
||||
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "Formato mes-día-año",
|
||||
"Day-month-year format": "Formato día-mes-año",
|
||||
"Intuit Interchange Format (IIF) File": "Archivo de formato de intercambio Intuit (IIF)",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
|
||||
"Delimiter-separated Values (DSV) File": "Delimiter-separated Values (DSV) File",
|
||||
"Delimiter-separated Values (DSV) Data": "Delimiter-separated Values (DSV) Data",
|
||||
"GnuCash XML Database File": "Archivo de base de datos XML GnuCash",
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "File Beancount non valido",
|
||||
"not support include directive for beancount file": "Direttiva \"include\" non supportata per il file Beancount",
|
||||
"invalid amount expression": "Espressione dell'importo non valida",
|
||||
"invalid xml file": "Invalid XML file",
|
||||
"user custom exchange rate data not found": "User custom exchange rate data is not found",
|
||||
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
|
||||
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "Formato mese-giorno-anno",
|
||||
"Day-month-year format": "Formato giorno-mese-anno",
|
||||
"Intuit Interchange Format (IIF) File": "File Intuit Interchange Format (IIF)",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
|
||||
"Delimiter-separated Values (DSV) File": "File valori separati da delimitatore (DSV)",
|
||||
"Delimiter-separated Values (DSV) Data": "Dati valori separati da delimitatore (DSV)",
|
||||
"GnuCash XML Database File": "File database XML GnuCash",
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "Invalid Beancount file",
|
||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||
"invalid amount expression": "Amount expression is invalid",
|
||||
"invalid xml file": "Invalid XML file",
|
||||
"user custom exchange rate data not found": "User custom exchange rate data is not found",
|
||||
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
|
||||
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "月-日-年 形式",
|
||||
"Day-month-year format": "日-月-年 形式",
|
||||
"Intuit Interchange Format (IIF) File": "Intuit Interchange Format (IIF) ファイル",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
|
||||
"Delimiter-separated Values (DSV) File": "Delimiter-separated Values (DSV) ファイル",
|
||||
"Delimiter-separated Values (DSV) Data": "Delimiter-separated Values (DSV) データ",
|
||||
"GnuCash XML Database File": "GnuCash XMLデータベースファイル",
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "Invalid Beancount file",
|
||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||
"invalid amount expression": "Amount expression is invalid",
|
||||
"invalid xml file": "Invalid XML file",
|
||||
"user custom exchange rate data not found": "User custom exchange rate data is not found",
|
||||
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
|
||||
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "Формат месяц-день-год",
|
||||
"Day-month-year format": "Формат день-месяц-год",
|
||||
"Intuit Interchange Format (IIF) File": "Файл Intuit Interchange Format (IIF)",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
|
||||
"Delimiter-separated Values (DSV) File": "Delimiter-separated Values (DSV) File",
|
||||
"Delimiter-separated Values (DSV) Data": "Delimiter-separated Values (DSV) Data",
|
||||
"GnuCash XML Database File": "Файл базы данных GnuCash XML",
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "Недійсний файл Beancount",
|
||||
"not support include directive for beancount file": "Не підтримується директива \"include\" у файлі Beancount",
|
||||
"invalid amount expression": "Недійсний вираз суми",
|
||||
"invalid xml file": "Invalid XML file",
|
||||
"user custom exchange rate data not found": "User custom exchange rate data is not found",
|
||||
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
|
||||
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "Формат місяць-день-рік",
|
||||
"Day-month-year format": "Формат день-місяць-рік",
|
||||
"Intuit Interchange Format (IIF) File": "Файл Intuit Interchange Format (IIF)",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
|
||||
"Delimiter-separated Values (DSV) File": "Файл із розділювачами значень (DSV)",
|
||||
"Delimiter-separated Values (DSV) Data": "Дані з розділювачами значень (DSV)",
|
||||
"GnuCash XML Database File": "Файл бази даних GnuCash XML",
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "Invalid Beancount file",
|
||||
"not support include directive for beancount file": "Not support \"include\" directive for Beancount file",
|
||||
"invalid amount expression": "Amount expression is invalid",
|
||||
"invalid xml file": "Invalid XML file",
|
||||
"user custom exchange rate data not found": "User custom exchange rate data is not found",
|
||||
"cannot update exchange rate data for base currency": "Cannot update exchange rate data for base currency",
|
||||
"cannot delete exchange rate data for base currency": "Cannot delete exchange rate data for base currency",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "Định dạng tháng-ngày-năm",
|
||||
"Day-month-year format": "Định dạng ngày-tháng-năm",
|
||||
"Intuit Interchange Format (IIF) File": "Tệp Intuit Interchange Format (IIF)",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 Bank to Customer Statement File",
|
||||
"Delimiter-separated Values (DSV) File": "Delimiter-separated Values (DSV) File",
|
||||
"Delimiter-separated Values (DSV) Data": "Delimiter-separated Values (DSV) Data",
|
||||
"GnuCash XML Database File": "Tệp cơ sở dữ liệu XML GnuCash",
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "无效的 Beancount 文件",
|
||||
"not support include directive for beancount file": "不支持 Beancount 文件的 \"include\" 指令",
|
||||
"invalid amount expression": "金额表达式无效",
|
||||
"invalid xml file": "无效的 XML 文件",
|
||||
"user custom exchange rate data not found": "用户自定义汇率数据不存在",
|
||||
"cannot update exchange rate data for base currency": "不能更新默认货币的汇率数据",
|
||||
"cannot delete exchange rate data for base currency": "不能删除默认货币的汇率数据",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "月-日-年 格式",
|
||||
"Day-month-year format": "日-月-年 格式",
|
||||
"Intuit Interchange Format (IIF) File": "Intuit Interchange Format (IIF) 文件",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 银行对账单文件",
|
||||
"Delimiter-separated Values (DSV) File": "分隔符分隔值 (DSV) 文件",
|
||||
"Delimiter-separated Values (DSV) Data": "分隔符分隔值 (DSV) 数据",
|
||||
"GnuCash XML Database File": "GnuCash XML 数据库文件",
|
||||
|
||||
@@ -1181,6 +1181,7 @@
|
||||
"invalid beancount file": "無效的 Beancount 檔案",
|
||||
"not support include directive for beancount file": "不支援 Beancount 檔案的 \"include\" 指令",
|
||||
"invalid amount expression": "金額表達式無效",
|
||||
"invalid xml file": "無效的 XML 檔案",
|
||||
"user custom exchange rate data not found": "使用者自訂匯率資料不存在",
|
||||
"cannot update exchange rate data for base currency": "不能更新基準貨幣的匯率資料",
|
||||
"cannot delete exchange rate data for base currency": "不能刪除基準貨幣的匯率資料",
|
||||
@@ -1692,6 +1693,7 @@
|
||||
"Month-day-year format": "月-日-年 格式",
|
||||
"Day-month-year format": "日-月-年 格式",
|
||||
"Intuit Interchange Format (IIF) File": "Intuit Interchange Format (IIF) 檔案",
|
||||
"Camt.053 Bank to Customer Statement File": "Camt.053 銀行對帳單檔案",
|
||||
"Delimiter-separated Values (DSV) File": "分隔符分隔值 (DSV) 檔案",
|
||||
"Delimiter-separated Values (DSV) Data": "分隔符分隔值 (DSV) 資料",
|
||||
"GnuCash XML Database File": "GnuCash XML 資料庫檔案",
|
||||
|
||||
Reference in New Issue
Block a user