import transaction from custom delimiter-separated values file

This commit is contained in:
MaysWind
2025-03-03 23:55:25 +08:00
parent 703ceb44e4
commit 9430f57a0b
24 changed files with 3374 additions and 91 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ Online Demo: [https://ezbookkeeping-demo.mayswind.net](https://ezbookkeeping-dem
7. Multi-language support
8. Two-factor authentication
9. Application lock (PIN code / WebAuthn)
10. Data import & export (OFX, QFX, QIF, IIF, GnuCash, FireFly III, etc.)
10. Data import & export (OFX, QFX, QIF, IIF, CSV, GnuCash, FireFly III, etc.)
## Screenshots
### Desktop Version
+1
View File
@@ -315,6 +315,7 @@ func startWebServer(c *core.CliContext) error {
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
if config.EnableDataImport {
apiV1Route.POST("/transactions/parse_dsv_file.json", bindApi(api.Transactions.TransactionParseImportDsvFileDataHandler))
apiV1Route.POST("/transactions/parse_import.json", bindApi(api.Transactions.TransactionParseImportFileHandler))
apiV1Route.POST("/transactions/import.json", bindApi(api.Transactions.TransactionImportHandler))
}
+159 -1
View File
@@ -1,6 +1,7 @@
package api
import (
"encoding/json"
"io"
"sort"
"strings"
@@ -8,6 +9,8 @@ import (
orderedmap "github.com/wk8/go-ordered-map/v2"
"github.com/mayswind/ezbookkeeping/pkg/converters"
baseconverters "github.com/mayswind/ezbookkeeping/pkg/converters/base"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/core"
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
"github.com/mayswind/ezbookkeeping/pkg/errs"
@@ -1030,6 +1033,83 @@ func (a *TransactionsApi) TransactionDeleteHandler(c *core.WebContext) (any, *er
return true, nil
}
// TransactionParseImportDsvFileDataHandler returns the parsed file data by request parameters for current user
func (a *TransactionsApi) TransactionParseImportDsvFileDataHandler(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid()
form, err := c.MultipartForm()
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrParameterInvalid
}
fileTypes := form.Value["fileType"]
if len(fileTypes) < 1 || fileTypes[0] == "" {
return nil, errs.ErrImportFileTypeIsEmpty
}
fileType := fileTypes[0]
if !converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
}
fileEncodings := form.Value["fileEncoding"]
if len(fileEncodings) < 1 || fileEncodings[0] == "" {
return nil, errs.ErrImportFileEncodingIsEmpty
}
fileEncoding := fileEncodings[0]
dataParser, err := converters.CreateNewDelimiterSeparatedValuesDataParser(fileType, fileEncoding)
if err != nil {
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
}
importFiles := form.File["file"]
if len(importFiles) < 1 {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] there is no import file in request for user \"uid:%d\"", uid)
return nil, errs.ErrNoFilesUpload
}
if importFiles[0].Size < 1 {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the size of import file in request is zero for user \"uid:%d\"", uid)
return nil, errs.ErrUploadedFileEmpty
}
if importFiles[0].Size > int64(a.CurrentConfig().MaxImportFileSize) {
log.Warnf(c, "[transactions.TransactionParseImportDsvFileDataHandler] the upload file size \"%d\" exceeds the maximum size \"%d\" of import file for user \"uid:%d\"", importFiles[0].Size, a.CurrentConfig().MaxImportFileSize, uid)
return nil, errs.ErrExceedMaxUploadFileSize
}
importFile, err := importFiles[0].Open()
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to get import file from request for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
defer importFile.Close()
fileData, err := io.ReadAll(importFile)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to read import file data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
allLines, err := dataParser.ParseDsvFileLines(c, fileData)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportDsvFileDataHandler] failed to parse import file data for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
return allLines, nil
}
// TransactionParseImportFileHandler returns the parsed transaction data by request parameters for current user
func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext) (any, *errs.Error) {
uid := c.GetCurrentUid()
@@ -1054,7 +1134,84 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
}
fileType := fileTypes[0]
dataImporter, err := converters.GetTransactionDataImporter(fileType)
var dataImporter baseconverters.TransactionDataImporter
if converters.IsCustomDelimiterSeparatedValuesFileType(fileType) {
fileEncodings := form.Value["fileEncoding"]
if len(fileEncodings) < 1 || fileEncodings[0] == "" {
return nil, errs.ErrImportFileEncodingIsEmpty
}
fileEncoding := fileEncodings[0]
columnMappings := form.Value["columnMapping"]
if len(columnMappings) < 1 || columnMappings[0] == "" {
return nil, errs.ErrImportFileColumnMappingInvalid
}
var columnIndexMapping = map[datatable.TransactionDataTableColumn]int{}
err = json.Unmarshal([]byte(columnMappings[0]), &columnIndexMapping)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse column mapping for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrImportFileColumnMappingInvalid
}
transactionTypeMappings := form.Value["transactionTypeMapping"]
if len(transactionTypeMappings) < 1 || transactionTypeMappings[0] == "" {
return nil, errs.ErrImportFileTransactionTypeMappingInvalid
}
var transactionTypeNameMapping = map[string]models.TransactionType{}
err = json.Unmarshal([]byte(transactionTypeMappings[0]), &transactionTypeNameMapping)
if err != nil {
log.Errorf(c, "[transactions.TransactionParseImportFileHandler] failed to parse transaction type mapping for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrImportFileTransactionTypeMappingInvalid
}
hasHeaderLines := form.Value["hasHeaderLine"]
hasHeaderLine := false
if len(hasHeaderLines) > 0 {
hasHeaderLine = hasHeaderLines[0] == "true"
}
timeFormats := form.Value["timeFormat"]
if len(timeFormats) < 1 || timeFormats[0] == "" {
return nil, errs.ErrImportFileTransactionTimeFormatInvalid
}
timezoneFormats := form.Value["timezoneFormat"]
timezoneFormat := ""
if len(timezoneFormats) > 0 {
timezoneFormat = timezoneFormats[0]
}
geoLocationSeparators := form.Value["geoSeparator"]
geoLocationSeparator := ""
if len(geoLocationSeparators) > 0 {
geoLocationSeparator = geoLocationSeparators[0]
}
transactionTagSeparators := form.Value["tagSeparator"]
transactionTagSeparator := ""
if len(transactionTagSeparators) > 0 {
transactionTagSeparator = transactionTagSeparators[0]
}
dataImporter, err = converters.CreateNewDelimiterSeparatedValuesDataImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormats[0], timezoneFormat, geoLocationSeparator, transactionTagSeparator)
} else {
dataImporter, err = converters.GetTransactionDataImporter(fileType)
}
if err != nil {
return nil, errs.Or(err, errs.ErrImportFileTypeNotSupported)
@@ -1084,6 +1241,7 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext)
return nil, errs.ErrOperationFailed
}
defer importFile.Close()
fileData, err := io.ReadAll(importFile)
if err != nil {
@@ -418,7 +418,7 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
geoLongitude := float64(0)
geoLatitude := float64(0)
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION) {
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION) && c.geoLocationSeparator != "" {
geoLocationItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_GEOGRAPHIC_LOCATION), c.geoLocationSeparator)
if len(geoLocationItems) == 2 {
@@ -442,7 +442,13 @@ func (c *DataTableTransactionDataImporter) ParseImportedData(ctx core.Context, u
var tagNames []string
if dataTable.HasColumn(TRANSACTION_DATA_TABLE_TAGS) {
tagNameItems := strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS), c.transactionTagSeparator)
var tagNameItems []string
if c.transactionTagSeparator != "" {
tagNameItems = strings.Split(dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS), c.transactionTagSeparator)
} else {
tagNameItems = append(tagNameItems, dataRow.GetData(TRANSACTION_DATA_TABLE_TAGS))
}
for i := 0; i < len(tagNameItems); i++ {
tagName := tagNameItems[i]
@@ -0,0 +1,227 @@
package dsv
import (
"bytes"
"encoding/csv"
"io"
"strings"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/encoding/japanese"
"golang.org/x/text/encoding/korean"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/encoding/traditionalchinese"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
"github.com/mayswind/ezbookkeeping/pkg/converters/base"
csvconverter "github.com/mayswind/ezbookkeeping/pkg/converters/csv"
"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 supportedFileTypeSeparators = map[string]rune{
"custom_csv": ',',
"custom_tsv": '\t',
}
var supportedFileEncodings = map[string]encoding.Encoding{
"utf-8": unicode.UTF8, // UTF-8
"utf-8-bom": unicode.UTF8BOM, // UTF-8 with BOM
"utf-16le": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), // UTF-16 Little Endian
"utf-16be": unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM), // UTF-16 Big Endian
"cp437": charmap.CodePage437, // OEM United States (CP-437)
"cp863": charmap.CodePage863, // OEM Canadian French (CP-863)
"cp037": charmap.CodePage037, // IBM EBCDIC US/Canada (CP-037)
"cp1047": charmap.CodePage1047, // IBM EBCDIC Open Systems (CP-1047)
"cp1140": charmap.CodePage1140, // IBM EBCDIC US/Canada with Euro (CP-1140)
"iso-8859-1": charmap.ISO8859_1, // Western European (ISO-8859-1)
"cp850": charmap.CodePage850, // Western European (CP-850)
"cp858": charmap.CodePage858, // Western European with Euro (CP-858)
"windows-1252": charmap.Windows1252, // Western European (Windows-1252)
"iso-8859-15": charmap.ISO8859_15, // Western European (ISO-8859-15)
"iso-8859-4": charmap.ISO8859_4, // North European (ISO-8859-4)
"iso-8859-10": charmap.ISO8859_10, // North European (ISO-8859-10)
"cp865": charmap.CodePage865, // North European (CP-865)
"iso-8859-2": charmap.ISO8859_2, // Central European (ISO-8859-2)
"cp852": charmap.CodePage852, // Central European (CP-852)
"windows-1250": charmap.Windows1250, // Central European (Windows-1250)
"iso-8859-14": charmap.ISO8859_14, // Celtic (ISO-8859-14)
"iso-8859-3": charmap.ISO8859_3, // South European (ISO-8859-3)
"cp860": charmap.CodePage860, // Portuguese (CP-860)
"iso-8859-7": charmap.ISO8859_7, // Greek (ISO-8859-7)
"windows-1253": charmap.Windows1253, // Greek (Windows-1253)
"iso-8859-9": charmap.ISO8859_9, // Turkish (ISO-8859-9)
"windows-1254": charmap.Windows1254, // Turkish (Windows-1254)
"iso-8859-13": charmap.ISO8859_13, // Baltic (ISO-8859-13)
"windows-1257": charmap.Windows1257, // Baltic (Windows-1257)
"iso-8859-16": charmap.ISO8859_16, // South-Eastern European (ISO-8859-16)
"iso-8859-5": charmap.ISO8859_5, // Cyrillic (ISO-8859-5)
"cp855": charmap.CodePage855, // Cyrillic (CP-855)
"cp866": charmap.CodePage866, // Cyrillic (CP-866)
"windows-1251": charmap.Windows1251, // Cyrillic (Windows-1251)
"koi8r": charmap.KOI8R, // Cyrillic (KOI8-R)
"koi8u": charmap.KOI8U, // Cyrillic (KOI8-U)
"iso-8859-6": charmap.ISO8859_6, // Arabic (ISO-8859-6)
"windows-1256": charmap.Windows1256, // Arabic (Windows-1256)
"iso-8859-8": charmap.ISO8859_8, // Hebrew (ISO-8859-8)
"cp862": charmap.CodePage862, // Hebrew (CP-862)
"windows-1255": charmap.Windows1255, // Hebrew (Windows-1255)
"windows-874": charmap.Windows874, // Thai (Windows-874)
"windows-1258": charmap.Windows1258, // Vietnamese (Windows-1258)
"gb18030": simplifiedchinese.GB18030, // Simplified Chinese (GB18030)
"gbk": simplifiedchinese.GBK, // Simplified Chinese (GBK)
"big5": traditionalchinese.Big5, // Traditional Chinese (Big5)
"euc-kr": korean.EUCKR, // Korean (EUC-KR)
"euc-jp": japanese.EUCJP, // Japanese (EUC-JP)
"iso-2022-jp": japanese.ISO2022JP, // Japanese (ISO-2022-JP)
"shift_jis": japanese.ShiftJIS, // Japanese (Shift JIS)
}
var customTransactionTypeNameMapping = 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)),
}
type CustomTransactionDataDsvFileParser interface {
ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error)
}
// customTransactionDataDsvFileImporter defines the structure of custom dsv importer for transaction data
type customTransactionDataDsvFileImporter struct {
fileEncoding encoding.Encoding
separator rune
columnIndexMapping map[datatable.TransactionDataTableColumn]int
transactionTypeNameMapping map[string]models.TransactionType
hasHeaderLine bool
timeFormat string
timezoneFormat string
geoLocationSeparator string
transactionTagSeparator string
}
// ParseDsvFileLines returns the parsed file lines for specified the dsv file data
func (c *customTransactionDataDsvFileImporter) ParseDsvFileLines(ctx core.Context, data []byte) ([][]string, error) {
reader := transform.NewReader(bytes.NewReader(data), c.fileEncoding.NewDecoder())
csvReader := csv.NewReader(reader)
csvReader.Comma = c.separator
csvReader.FieldsPerRecord = -1
allLines := make([][]string, 0)
for {
items, err := csvReader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Errorf(ctx, "[custom_transaction_data_dsv_file_importer.ParseDsvFileLines] cannot parse dsv data, because %s", err.Error())
return nil, errs.ErrInvalidCSVFile
}
if len(items) == 1 && items[0] == "" {
continue
}
for index := range items {
items[index] = strings.Trim(items[index], " ")
}
allLines = append(allLines, items)
}
return allLines, nil
}
// ParseImportedData returns the imported data by parsing the custom transaction dsv data
func (c *customTransactionDataDsvFileImporter) 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) {
allLines, err := c.ParseDsvFileLines(ctx, data)
if err != nil {
return nil, nil, nil, nil, nil, nil, err
}
if !c.hasHeaderLine {
allLines = append([][]string{{}}, allLines...)
}
dataTable := csvconverter.CreateNewCustomCsvImportedDataTable(allLines)
transactionDataTable := CreateNewCustomPlainTextDataTable(dataTable, c.columnIndexMapping, c.transactionTypeNameMapping, c.timeFormat, c.timezoneFormat)
dataTableImporter := datatable.CreateNewImporter(customTransactionTypeNameMapping, c.geoLocationSeparator, c.transactionTagSeparator)
return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap)
}
// IsDelimiterSeparatedValuesFileType returns whether the file type is the delimiter-separated values file type
func IsDelimiterSeparatedValuesFileType(fileType string) bool {
_, exists := supportedFileTypeSeparators[fileType]
return exists
}
// CreateNewCustomTransactionDataDsvFileParser returns a new custom dsv parser for transaction data
func CreateNewCustomTransactionDataDsvFileParser(fileType string, fileEncoding string) (CustomTransactionDataDsvFileParser, error) {
separator, exists := supportedFileTypeSeparators[fileType]
if !exists {
return nil, errs.ErrImportFileTypeNotSupported
}
enc, exists := supportedFileEncodings[fileEncoding]
if !exists {
return nil, errs.ErrImportFileEncodingNotSupported
}
return &customTransactionDataDsvFileImporter{
fileEncoding: enc,
separator: separator,
}, nil
}
// CreateNewCustomTransactionDataDsvFileImporter returns a new custom dsv importer for transaction data
func CreateNewCustomTransactionDataDsvFileImporter(fileType string, fileEncoding string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, geoLocationSeparator string, transactionTagSeparator string) (base.TransactionDataImporter, error) {
separator, exists := supportedFileTypeSeparators[fileType]
if !exists {
return nil, errs.ErrImportFileTypeNotSupported
}
enc, exists := supportedFileEncodings[fileEncoding]
if !exists {
return nil, errs.ErrImportFileEncodingNotSupported
}
if _, exists = columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME]; !exists {
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
if _, exists = columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]; !exists {
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
if _, exists = columnIndexMapping[datatable.TRANSACTION_DATA_TABLE_AMOUNT]; !exists {
return nil, errs.ErrMissingRequiredFieldInHeaderRow
}
return &customTransactionDataDsvFileImporter{
fileEncoding: enc,
separator: separator,
columnIndexMapping: columnIndexMapping,
transactionTypeNameMapping: transactionTypeNameMapping,
hasHeaderLine: hasHeaderLine,
timeFormat: timeFormat,
timezoneFormat: timezoneFormat,
geoLocationSeparator: geoLocationSeparator,
transactionTagSeparator: transactionTagSeparator,
}, nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,275 @@
package dsv
import (
"strings"
"time"
"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"
)
// customPlainTextDataTable defines the structure of custom plain text transaction data table
type customPlainTextDataTable struct {
innerDataTable datatable.ImportedDataTable
columnIndexMapping map[datatable.TransactionDataTableColumn]int
transactionTypeNameMapping map[string]models.TransactionType
timeFormat string
timezoneFormat string
timeFormatIncludeTimezone bool
}
// customPlainTextDataRow defines the structure of custom plain text transaction data row
type customPlainTextDataRow struct {
transactionDataTable *customPlainTextDataTable
rowData map[datatable.TransactionDataTableColumn]string
isValid bool
}
// customPlainTextDataRowIterator defines the structure of custom plain text transaction data row iterator
type customPlainTextDataRowIterator struct {
transactionDataTable *customPlainTextDataTable
innerIterator datatable.ImportedDataRowIterator
}
// HasColumn returns whether the data table has specified column
func (t *customPlainTextDataTable) HasColumn(column datatable.TransactionDataTableColumn) bool {
// custom dsv file allows no sub category, account name and related account name column mapping, but data table converter needs these columns
if column == datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY ||
column == datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME ||
column == datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME {
return true
}
// timezone column will be added when original time format contains timezone
if t.timeFormatIncludeTimezone && column == datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE {
return true
}
_, exists := t.columnIndexMapping[column]
return exists
}
// TransactionRowCount returns the total count of transaction data row
func (t *customPlainTextDataTable) TransactionRowCount() int {
return t.innerDataTable.DataRowCount()
}
// TransactionRowIterator returns the iterator of transaction data row
func (t *customPlainTextDataTable) TransactionRowIterator() datatable.TransactionDataRowIterator {
return &customPlainTextDataRowIterator{
transactionDataTable: t,
innerIterator: t.innerDataTable.DataRowIterator(),
}
}
// IsValid returns whether this row is valid data for importing
func (r *customPlainTextDataRow) IsValid() bool {
return r.isValid
}
// GetData returns the data in the specified column type
func (r *customPlainTextDataRow) GetData(column datatable.TransactionDataTableColumn) string {
return r.rowData[column]
}
// HasNext returns whether the iterator does not reach the end
func (t *customPlainTextDataRowIterator) HasNext() bool {
return t.innerIterator.HasNext()
}
// Next returns the next transaction data row
func (t *customPlainTextDataRowIterator) Next(ctx core.Context, user *models.User) (daraRow datatable.TransactionDataRow, err error) {
importedRow := t.innerIterator.Next()
if importedRow == nil {
return nil, nil
}
rowData, isValid, err := t.parseTransaction(ctx, user, importedRow)
if err != nil {
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.Next] cannot parsing transaction in row \"%s\", because %s", t.innerIterator.CurrentRowId(), err.Error())
return nil, err
}
return &customPlainTextDataRow{
transactionDataTable: t.transactionDataTable,
rowData: rowData,
isValid: isValid,
}, nil
}
func (t *customPlainTextDataRowIterator) parseTransaction(ctx core.Context, user *models.User, row datatable.ImportedDataRow) (map[datatable.TransactionDataTableColumn]string, bool, error) {
rowData := make(map[datatable.TransactionDataTableColumn]string, len(t.transactionDataTable.columnIndexMapping))
for column, columnIndex := range t.transactionDataTable.columnIndexMapping {
if columnIndex < 0 || columnIndex >= row.ColumnCount() {
continue
}
value := row.GetData(columnIndex)
rowData[column] = value
}
// parse transaction type
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] != "" {
transactionType, exists := t.transactionDataTable.transactionTypeNameMapping[rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE]]
if !exists {
log.Warnf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] skip parsing this transaction, because transaction type \"%s\" mapping not defined", rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE])
return nil, false, nil
}
mappedTransactionType, exists := customTransactionTypeNameMapping[transactionType]
if !exists {
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] cannot parsing transaction type \"%s\", because type \"%d\" is invalid", rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE], transactionType)
return nil, false, errs.ErrTransactionTypeInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = mappedTransactionType
}
// parse date time
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] != "" {
dateTime, err := time.Parse(t.transactionDataTable.timeFormat, rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME])
if err != nil {
return nil, false, errs.ErrTransactionTimeInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME] = utils.FormatUnixTimeToLongDateTime(dateTime.Unix(), dateTime.Location())
if t.transactionDataTable.timeFormatIncludeTimezone {
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = utils.FormatTimezoneOffset(dateTime.Location())
}
}
// parse timezone
if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] != "" {
if t.transactionDataTable.timezoneFormat == "Z" || t.transactionDataTable.timezoneFormat == "" { // -HH:mm
// Do Nothing
} else if t.transactionDataTable.timezoneFormat == "ZZ" { // -HHmm
timezone := rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE]
if len(timezone) != 5 {
return nil, false, errs.ErrTransactionTimeZoneInvalid
}
timezone = timezone[:3] + ":" + timezone[3:]
rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIMEZONE] = timezone
} else {
return nil, false, errs.ErrImportFileTransactionTimezoneFormatInvalid
}
}
// use primary category if sub category is empty
if rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] == "" && rowData[datatable.TRANSACTION_DATA_TABLE_CATEGORY] != "" {
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = rowData[datatable.TRANSACTION_DATA_TABLE_CATEGORY]
}
// trim trailing zero in decimal
if rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] != "" {
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT])
if err != nil {
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] cannot parsing transaction amount \"%s\", because %s", rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT], err.Error())
return nil, false, errs.ErrAmountInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(amount)
}
if rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] != "" {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.TrimTrailingZerosInDecimal(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT])
if err != nil {
log.Errorf(ctx, "[custom_transaction_plain_text_data_table.parseTransaction] cannot parsing transaction related amount \"%s\", because %s", rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT], err.Error())
return nil, false, errs.ErrAmountInvalid
}
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_AMOUNT] = utils.FormatAmount(amount)
}
if _, exists := rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY]; !exists {
rowData[datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY] = ""
}
if _, exists := rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME]; !exists {
rowData[datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME] = ""
}
if _, exists := rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME]; !exists {
rowData[datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME] = ""
}
return rowData, true, nil
}
// CreateNewCustomPlainTextDataTable returns transaction data table from imported data table
func CreateNewCustomPlainTextDataTable(dataTable datatable.ImportedDataTable, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, timeFormat string, timezoneFormat string) *customPlainTextDataTable {
timeFormatIncludeTimezone := strings.Contains(timeFormat, "z") || strings.Contains(timeFormat, "Z")
return &customPlainTextDataTable{
innerDataTable: dataTable,
columnIndexMapping: columnIndexMapping,
transactionTypeNameMapping: transactionTypeNameMapping,
timeFormat: getDateTimeFormat(timeFormat),
timezoneFormat: timezoneFormat,
timeFormatIncludeTimezone: timeFormatIncludeTimezone,
}
}
func getDateTimeFormat(format string) string {
// convert moment.js format to Go format
format = strings.ReplaceAll(format, "YYYY", "2006")
format = strings.ReplaceAll(format, "YY", "06")
format = strings.ReplaceAll(format, "MMMM", "January")
format = strings.ReplaceAll(format, "MMM", "Jan")
format = strings.ReplaceAll(format, "MM", "01")
format = strings.ReplaceAll(format, "M", "1")
format = strings.ReplaceAll(format, "DD", "02")
format = strings.ReplaceAll(format, "D", "2")
format = strings.ReplaceAll(format, "dddd", "Monday")
format = strings.ReplaceAll(format, "ddd", "Mon")
format = strings.ReplaceAll(format, "HH", "15")
format = strings.ReplaceAll(format, "H", "15")
format = strings.ReplaceAll(format, "hh", "03")
format = strings.ReplaceAll(format, "h", "3")
format = strings.ReplaceAll(format, "mm", "04")
format = strings.ReplaceAll(format, "m", "4")
format = strings.ReplaceAll(format, "ss", "05")
format = strings.ReplaceAll(format, "s", "5")
for i := 9; i >= 1; i-- {
format = strings.ReplaceAll(format, "."+strings.Repeat("S", i), "."+strings.Repeat("9", i))
}
format = strings.ReplaceAll(format, "A", "PM")
format = strings.ReplaceAll(format, "a", "pm")
format = strings.ReplaceAll(format, "zz", "MST")
format = strings.ReplaceAll(format, "z", "MST")
if strings.Contains(format, "ZZ") {
format = strings.ReplaceAll(format, "ZZ", "Z0700")
} else if strings.Contains(format, "Z") {
format = strings.ReplaceAll(format, "Z", "Z07:00")
}
return format
}
@@ -3,7 +3,9 @@ package converters
import (
"github.com/mayswind/ezbookkeeping/pkg/converters/alipay"
"github.com/mayswind/ezbookkeeping/pkg/converters/base"
"github.com/mayswind/ezbookkeeping/pkg/converters/datatable"
"github.com/mayswind/ezbookkeeping/pkg/converters/default"
"github.com/mayswind/ezbookkeeping/pkg/converters/dsv"
"github.com/mayswind/ezbookkeeping/pkg/converters/feidee"
"github.com/mayswind/ezbookkeeping/pkg/converters/fireflyIII"
"github.com/mayswind/ezbookkeeping/pkg/converters/gnucash"
@@ -12,6 +14,7 @@ import (
"github.com/mayswind/ezbookkeeping/pkg/converters/qif"
"github.com/mayswind/ezbookkeeping/pkg/converters/wechat"
"github.com/mayswind/ezbookkeeping/pkg/errs"
"github.com/mayswind/ezbookkeeping/pkg/models"
)
// GetTransactionDataExporter returns the transaction data exporter according to the file type
@@ -61,3 +64,18 @@ func GetTransactionDataImporter(fileType string) (base.TransactionDataImporter,
return nil, errs.ErrImportFileTypeNotSupported
}
}
// IsCustomDelimiterSeparatedValuesFileType returns whether the file type is the delimiter-separated values file type
func IsCustomDelimiterSeparatedValuesFileType(fileType string) bool {
return dsv.IsDelimiterSeparatedValuesFileType(fileType)
}
// CreateNewDelimiterSeparatedValuesDataParser returns a new delimiter-separated values data parser according to the file type and encoding
func CreateNewDelimiterSeparatedValuesDataParser(fileType string, fileEncoding string) (dsv.CustomTransactionDataDsvFileParser, error) {
return dsv.CreateNewCustomTransactionDataDsvFileParser(fileType, fileEncoding)
}
// CreateNewDelimiterSeparatedValuesDataImporter returns a new delimiter-separated values data importer according to the file type and encoding
func CreateNewDelimiterSeparatedValuesDataImporter(fileType string, fileEncoding string, columnIndexMapping map[datatable.TransactionDataTableColumn]int, transactionTypeNameMapping map[string]models.TransactionType, hasHeaderLine bool, timeFormat string, timezoneFormat string, geoLocationSeparator string, transactionTagSeparator string) (base.TransactionDataImporter, error) {
return dsv.CreateNewCustomTransactionDataDsvFileImporter(fileType, fileEncoding, columnIndexMapping, transactionTypeNameMapping, hasHeaderLine, timeFormat, timezoneFormat, geoLocationSeparator, transactionTagSeparator)
}
+6
View File
@@ -35,4 +35,10 @@ var (
ErrCannotAddTransactionBeforeBalanceModificationTransaction = NewSystemError(NormalSubcategoryTransaction, 28, http.StatusBadRequest, "cannot add transaction before balance modification transaction")
ErrBalanceModificationTransactionCannotModifyTime = NewSystemError(NormalSubcategoryTransaction, 29, http.StatusBadRequest, "balance modification transaction cannot modify transaction time")
ErrTransferTransactionAmountCannotBeLessThanZero = NewNormalError(NormalSubcategoryTransaction, 30, http.StatusBadRequest, "transfer transaction amount cannot be less than zero")
ErrImportFileEncodingIsEmpty = NewSystemError(NormalSubcategoryTransaction, 31, http.StatusBadRequest, "import file encoding is empty")
ErrImportFileEncodingNotSupported = NewSystemError(NormalSubcategoryTransaction, 32, http.StatusBadRequest, "import file encoding not supported")
ErrImportFileColumnMappingInvalid = NewSystemError(NormalSubcategoryTransaction, 33, http.StatusBadRequest, "column mapping invalid")
ErrImportFileTransactionTypeMappingInvalid = NewSystemError(NormalSubcategoryTransaction, 34, http.StatusBadRequest, "transaction type mapping invalid")
ErrImportFileTransactionTimeFormatInvalid = NewSystemError(NormalSubcategoryTransaction, 35, http.StatusBadRequest, "transaction time format invalid")
ErrImportFileTransactionTimezoneFormatInvalid = NewSystemError(NormalSubcategoryTransaction, 36, http.StatusBadRequest, "transaction time zone format invalid")
)
+89
View File
@@ -8,6 +8,59 @@ export const SUPPORTED_DOCUMENT_LANGUAGES_FOR_IMPORT_FILE: Record<string, boolea
'zh-Hans': true
};
export const SUPPORTED_FILE_ENCODINGS: string[] = [
'utf-8', // UTF-8
'utf-8-bom', // UTF-8 with BOM
'utf-16le', // UTF-16 Little Endian
'utf-16be', // UTF-16 Big Endian
'cp437', // OEM United States (CP-437)
'cp863', // OEM Canadian French (CP-863)
'cp037', // IBM EBCDIC US/Canada (CP-037)
'cp1047', // IBM EBCDIC Open Systems (CP-1047)
'cp1140', // IBM EBCDIC US/Canada with Euro (CP-1140)
"iso-8859-1", // Western European (ISO-8859-1)
'cp850', // Western European (CP-850)
'cp858', // Western European with Euro (CP-858)
'windows-1252', // Western European (Windows-1252)
'iso-8859-15', // Western European (ISO-8859-15)
'iso-8859-4', // North European (ISO-8859-4)
'iso-8859-10', // North European (ISO-8859-10)
'cp865', // North European (CP-865)
'iso-8859-2', // Central European (ISO-8859-2)
'cp852', // Central European (CP-852)
'windows-1250', // Central European (Windows-1250)
'iso-8859-14', // Celtic (ISO-8859-14)
'iso-8859-3', // South European (ISO-8859-3)
'cp860', // Portuguese (CP-860)
'iso-8859-7', // Greek (ISO-8859-7)
'windows-1253', // Greek (Windows-1253)
'iso-8859-9', // Turkish (ISO-8859-9)
'windows-1254', // Turkish (Windows-1254)
'iso-8859-13', // Baltic (ISO-8859-13)
'windows-1257', // Baltic (Windows-1257)
'iso-8859-16', // South-Eastern European (ISO-8859-16)
'iso-8859-5', // Cyrillic (ISO-8859-5)
'cp855', // Cyrillic (CP-855)
'cp866', // Cyrillic (CP-866)
'windows-1251', // Cyrillic (Windows-1251)
'koi8r', // Cyrillic (KOI8-R)
'koi8u', // Cyrillic (KOI8-U)
'iso-8859-6', // Arabic (ISO-8859-6)
'windows-1256', // Arabic (Windows-1256)
'iso-8859-8', // Hebrew (ISO-8859-8)
'cp862', // Hebrew (CP-862)
'windows-1255', // Hebrew (Windows-1255)
'windows-874', // Thai (Windows-874)
'windows-1258', // Vietnamese (Windows-1258)
'gb18030', // Simplified Chinese (GB18030)
'gbk', // Simplified Chinese (GBK)
'big5', // Traditional Chinese (Big5)
'euc-kr', // Korean (EUC-KR)
'euc-jp', // Japanese (EUC-JP)
'iso-2022-jp', // Japanese (ISO-2022-JP)
'shift_jis', // Japanese (Shift_JIS)
];
export const SUPPORTED_IMPORT_FILE_TYPES: ImportFileType[] = [
{
type: 'ezbookkeeping',
@@ -64,6 +117,42 @@ export const SUPPORTED_IMPORT_FILE_TYPES: ImportFileType[] = [
name: 'Intuit Interchange Format (IIF) File',
extensions: '.iif'
},
{
type: 'dsv',
name: 'Delimiter-separated Values (DSV) File',
extensions: '.csv,.tsv',
subTypes: [
{
type: 'custom_csv',
name: 'CSV (Comma-separated values) File',
extensions: '.csv',
},
{
type: 'custom_tsv',
name: 'TSV (Tab-separated values) File',
extensions: '.tsv,.txt',
}
],
supportedEncodings: SUPPORTED_FILE_ENCODINGS
},
{
type: 'dsv_data',
name: 'Delimiter-separated Values (DSV) Data',
extensions: '.csv,.tsv',
subTypes: [
{
type: 'custom_csv',
name: 'CSV (Comma-separated values) File',
extensions: '.csv',
},
{
type: 'custom_tsv',
name: 'TSV (Tab-separated values) File',
extensions: '.tsv,.txt',
}
],
dataFromTextbox: true
},
{
type: 'gnucash',
name: 'GnuCash XML Database File',
+72
View File
@@ -230,6 +230,78 @@ export class MeridiemIndicator {
}
}
export class KnownDateTimeFormat {
private static readonly allInstances: KnownDateTimeFormat[] = [];
public static readonly DefaultDateTime = new KnownDateTimeFormat('YYYY-MM-DD HH:mm:ss', /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1]) ([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/);
public static readonly DefaultDateTimeWithTimezone = new KnownDateTimeFormat('YYYY-MM-DD HH:mm:ssZ', /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1]) ([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](Z|[+-](0[0-9]|1[0-4]):[0-5][0-9])$/);
public static readonly DefaultDateTimeWithoutSecond = new KnownDateTimeFormat('YYYY-MM-DD HH:mm', /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1]) ([0-1][0-9]|2[0-3]):[0-5][0-9]$/);
public static readonly DefaultDate = new KnownDateTimeFormat('YYYY-MM-DD', /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/);
public static readonly RFC3339 = new KnownDateTimeFormat('YYYY-MM-DDTHH:mm:ssZ', /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](Z|[+-](0[0-9]|1[0-4]):[0-5][0-9])$/);
public static readonly YYYYMMDDSlashWithTime = new KnownDateTimeFormat('YYYY/MM/DD HH:mm:ss', /^\d{4}\/(0[1-9]|1[0-2])\/(0[1-9]|[1-2][0-9]|3[0-1]) ([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/);
public static readonly MMDDYYSlashWithTime = new KnownDateTimeFormat('MM/DD/YYYY HH:mm:ss', /^(0[1-9]|1[0-2])\/(0[1-9]|[1-2][0-9]|3[0-1])\/\d{4} ([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/);
public static readonly DDMMYYSlashWithTime = new KnownDateTimeFormat('DD/MM/YYYY HH:mm:ss', /^(0[1-9]|[1-2][0-9]|3[0-1])\/(0[1-9]|1[0-2])\/\d{4} ([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/);
public static readonly YYYYMMDDSlash = new KnownDateTimeFormat('YYYY/MM/DD', /^\d{4}\/(0[1-9]|1[0-2])\/(0[1-9]|[1-2][0-9]|3[0-1])$/);
public static readonly MMDDYYSlash = new KnownDateTimeFormat('MM/DD/YYYY', /^(0[1-9]|1[0-2])\/(0[1-9]|[1-2][0-9]|3[0-1])\/\d{4}$/);
public static readonly DDMMYYSlash = new KnownDateTimeFormat('DD/MM/YYYY', /^(0[1-9]|[1-2][0-9]|3[0-1])\/(0[1-9]|1[0-2])\/\d{4}$/);
public readonly format: string;
private readonly regex: RegExp;
private constructor(format: string, regex: RegExp) {
this.format = format;
this.regex = regex;
KnownDateTimeFormat.allInstances.push(this);
}
public isValid(dateTime: string): boolean {
return this.regex.test(dateTime);
}
public static values(): KnownDateTimeFormat[] {
return KnownDateTimeFormat.allInstances;
}
public static detect(dateTime: string): KnownDateTimeFormat[] | undefined {
const result: KnownDateTimeFormat[] = [];
for (const format of KnownDateTimeFormat.allInstances) {
if (format.isValid(dateTime)) {
result.push(format);
}
}
return result.length > 0 ? result : undefined;
}
public static detectMany(dateTimes: string[]): KnownDateTimeFormat[] | undefined {
const detectedCounts: Record<string, number> = {};
for (const dateTime of dateTimes) {
const detectedFormats = KnownDateTimeFormat.detect(dateTime);
if (detectedFormats) {
for (const format of detectedFormats) {
detectedCounts[format.format] = (detectedCounts[format.format] || 0) + 1;
}
} else {
return undefined;
}
}
const result: KnownDateTimeFormat[] = [];
for (const format of KnownDateTimeFormat.allInstances) {
if (detectedCounts[format.format] === dateTimes.length) {
result.push(format);
}
}
return result.length > 0 ? result : undefined;
}
}
export const LANGUAGE_DEFAULT_DATE_TIME_FORMAT_VALUE: number = 0;
export interface DateFormat {
+9
View File
@@ -8,6 +8,8 @@ export interface ImportFileType extends ImportFileTypeAndExtensions {
readonly name: string;
readonly extensions: string;
readonly subTypes?: ImportFileTypeSubType[];
readonly supportedEncodings?: string[];
readonly dataFromTextbox?: boolean;
readonly document?: {
readonly supportMultiLanguages: boolean | string;
readonly anchor: string;
@@ -25,6 +27,8 @@ export interface LocalizedImportFileType extends ImportFileTypeAndExtensions {
readonly displayName: string;
readonly extensions: string;
readonly subTypes?: LocalizedImportFileTypeSubType[];
readonly supportedEncodings?: LocalizedImportFileTypeSupportedEncodings[];
readonly dataFromTextbox?: boolean;
readonly document?: LocalizedImportFileDocument;
}
@@ -34,6 +38,11 @@ export interface LocalizedImportFileTypeSubType extends ImportFileTypeAndExtensi
readonly extensions?: string;
}
export interface LocalizedImportFileTypeSupportedEncodings {
readonly encoding: string;
readonly displayName: string;
}
export interface LocalizedImportFileDocument {
readonly language: string;
readonly displayLanguageName: string;
+72 -1
View File
@@ -1,4 +1,4 @@
import type { TypeAndName } from './base.ts';
import type { NameValue, TypeAndName } from './base.ts';
export interface TimezoneInfo {
readonly displayName: string;
@@ -13,6 +13,77 @@ export interface LocalizedTimezoneInfo {
readonly displayNameWithUtcOffset: string;
}
export class KnownDateTimezoneFormat implements NameValue {
private static readonly allInstances: KnownDateTimezoneFormat[] = [];
private static readonly allInstancesByValue: Record<string, KnownDateTimezoneFormat> = {};
public static readonly HHColonMM = new KnownDateTimezoneFormat('±HH:mm', 'Z', /^[+-]?([0-1][0-9]|2[0-3]):[0-5][0-9]$/);
public static readonly HHMM = new KnownDateTimezoneFormat('±HHmm', 'ZZ', /^[+-]?([0-1][0-9]|2[0-3])[0-5][0-9]$/);
public readonly name: string;
public readonly value: string;
private readonly regex: RegExp;
private constructor(name: string, value: string, regex: RegExp) {
this.name = name;
this.value = value;
this.regex = regex;
KnownDateTimezoneFormat.allInstances.push(this);
KnownDateTimezoneFormat.allInstancesByValue[value] = this;
}
public isValid(dateTime: string): boolean {
return this.regex.test(dateTime);
}
public static values(): KnownDateTimezoneFormat[] {
return KnownDateTimezoneFormat.allInstances;
}
public static valueOf(value: string): KnownDateTimezoneFormat | undefined {
return KnownDateTimezoneFormat.allInstancesByValue[value];
}
public static detect(dateTime: string): KnownDateTimezoneFormat[] | undefined {
const result: KnownDateTimezoneFormat[] = [];
for (const format of KnownDateTimezoneFormat.allInstances) {
if (format.isValid(dateTime)) {
result.push(format);
}
}
return result.length > 0 ? result : undefined;
}
public static detectMany(dateTimes: string[]): KnownDateTimezoneFormat[] | undefined {
const detectedCounts: Record<string, number> = {};
for (const dateTime of dateTimes) {
const detectedFormats = KnownDateTimezoneFormat.detect(dateTime);
if (detectedFormats) {
for (const format of detectedFormats) {
detectedCounts[format.value] = (detectedCounts[format.value] || 0) + 1;
}
} else {
return undefined;
}
}
const result: KnownDateTimezoneFormat[] = [];
for (const format of KnownDateTimezoneFormat.allInstances) {
if (detectedCounts[format.value] === dateTimes.length) {
result.push(format);
}
}
return result.length > 0 ? result : undefined;
}
}
export class TimezoneTypeForStatistics implements TypeAndName {
public static readonly ApplicationTimezone = new TimezoneTypeForStatistics(0, 'Application Timezone');
public static readonly TransactionTimezone = new TimezoneTypeForStatistics(1, 'Transaction Timezone');
+33
View File
@@ -57,3 +57,36 @@ export class TransactionTagFilterType implements TypeAndName {
return TransactionTagFilterType.allInstances;
}
}
export class ImportTransactionColumnType implements TypeAndName {
private static readonly allInstances: ImportTransactionColumnType[] = [];
public static readonly TransactionTime = new ImportTransactionColumnType(1, 'Transaction Time');
public static readonly TransactionTimezone = new ImportTransactionColumnType(2, 'Transaction Timezone');
public static readonly TransactionType = new ImportTransactionColumnType(3, 'Transaction Type');
public static readonly Category = new ImportTransactionColumnType(4, 'Category');
public static readonly SubCategory = new ImportTransactionColumnType(5, 'Secondary Category');
public static readonly AccountName = new ImportTransactionColumnType(6, 'Account Name');
public static readonly AccountCurrency = new ImportTransactionColumnType(7, 'Currency');
public static readonly Amount = new ImportTransactionColumnType(8, 'Amount');
public static readonly RelatedAccountName = new ImportTransactionColumnType(9, 'Transfer In Account Name');
public static readonly RelatedAccountCurrency = new ImportTransactionColumnType(10, 'Transfer In Currency');
public static readonly RelatedAmount = new ImportTransactionColumnType(11, 'Transfer In Amount');
public static readonly GeographicLocation = new ImportTransactionColumnType(12, 'Geographic Location');
public static readonly Tags = new ImportTransactionColumnType(13, 'Tags');
public static readonly Description = new ImportTransactionColumnType(14, 'Description');
public readonly type: number;
public readonly name: string;
private constructor(type: number, name: string) {
this.type = type;
this.name = name;
ImportTransactionColumnType.allInstances.push(this);
}
public static values(): ImportTransactionColumnType[] {
return ImportTransactionColumnType.allInstances;
}
}
+39 -2
View File
@@ -2,6 +2,10 @@ import axios, { type AxiosRequestConfig, type AxiosRequestHeaders, type AxiosRes
import type { ApiResponse } from '@/core/api.ts';
import {
TransactionType
} from '@/core/transaction.ts';
import {
BASE_API_URL_PATH,
BASE_QRCODE_PATH,
@@ -426,10 +430,43 @@ export default {
deleteTransaction: (req: TransactionDeleteRequest): ApiResponsePromise<boolean> => {
return axios.post<ApiResponse<boolean>>('v1/transactions/delete.json', req);
},
parseImportTransaction: ({ fileType, importFile }: { fileType: string, importFile: File }): ApiResponsePromise<ImportTransactionResponsePageWrapper> => {
parseImportDsvFile: ({ fileType, fileEncoding, importFile }: { fileType: string, fileEncoding?: string, importFile: File }): ApiResponsePromise<string[][]> => {
return axios.postForm<ApiResponse<string[][]>>('v1/transactions/parse_dsv_file.json', {
fileType: fileType,
fileEncoding: fileEncoding,
file: importFile
}, {
timeout: DEFAULT_UPLOAD_API_TIMEOUT
} as ApiRequestConfig);
},
parseImportTransaction: ({ fileType, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, geoSeparator, tagSeparator }: { fileType: string, fileEncoding?: string, importFile: File, columnMapping?: Record<number, number>, transactionTypeMapping?: Record<string, TransactionType>, hasHeaderLine?: boolean, timeFormat?: string, timezoneFormat?: string, geoSeparator?: string, tagSeparator?: string }): ApiResponsePromise<ImportTransactionResponsePageWrapper> => {
let textualColumnMapping: string | undefined = undefined;
let textualTransactionTypeMapping: string | undefined = undefined;
let textualHasHeaderLine: string | undefined = undefined;
if (columnMapping) {
textualColumnMapping = JSON.stringify(columnMapping);
}
if (transactionTypeMapping) {
textualTransactionTypeMapping = JSON.stringify(transactionTypeMapping);
}
if (hasHeaderLine) {
textualHasHeaderLine = 'true';
}
return axios.postForm<ApiResponse<ImportTransactionResponsePageWrapper>>('v1/transactions/parse_import.json', {
fileType: fileType,
file: importFile
fileEncoding: fileEncoding,
file: importFile,
columnMapping: textualColumnMapping,
transactionTypeMapping: textualTransactionTypeMapping,
hasHeaderLine: textualHasHeaderLine,
timeFormat: timeFormat,
timezoneFormat: timezoneFormat,
geoSeparator: geoSeparator,
tagSeparator: tagSeparator
}, {
timeout: DEFAULT_UPLOAD_API_TIMEOUT
} as ApiRequestConfig);
+85
View File
@@ -1097,6 +1097,12 @@
"cannot add transaction before balance modification transaction": "Transaktion kann nicht vor der Saldoänderungstransaktion hinzugefügt werden",
"balance modification transaction cannot modify transaction time": "Transaktionszeit kann für Saldoänderungstransaktion nicht geändert werden",
"transfer transaction amount cannot be less than zero": "Betrag für Überweisungstransaktion darf nicht kleiner als 0 sein",
"import file encoding is empty": "Import file encoding is empty",
"import file encoding not supported": "import file encoding is not supported",
"column mapping invalid": "Column mapping is invalid",
"transaction type mapping invalid": "Transaction type mapping is invalid",
"transaction time format invalid": "Transaction time format is invalid",
"transaction time zone format invalid": "Transaction time zone format is invalid",
"transaction category id is invalid": "Transaktionskategorie-ID ist ungültig",
"transaction category not found": "Transaktionskategorie nicht gefunden",
"transaction category type is invalid": "Transaktionskategorietyp ist ungültig",
@@ -1214,6 +1220,58 @@
"parameter invalid color": "{parameter} hat ein ungültiges Format",
"parameter invalid amount filter": "{parameter} hat ein ungültiges Format"
},
"encoding": {
"utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian",
"cp437": "OEM United States (CP-437)",
"cp863": "OEM Canadian French (CP-863)",
"cp037": "IBM EBCDIC US/Canada (CP-037)",
"cp1047": "IBM EBCDIC Open Systems (CP-1047)",
"cp1140": "IBM EBCDIC US/Canada with Euro (CP-1140)",
"iso-8859-1": "Western European (ISO-8859-1)",
"cp850": "Western European (CP-850)",
"cp858": "Western European with Euro (CP-858)",
"windows-1252": "Western European (Windows-1252)",
"iso-8859-15": "Western European (ISO-8859-15)",
"iso-8859-4": "North European (ISO-8859-4)",
"iso-8859-10": "Nordic (ISO-8859-10)",
"cp865": "Nordic (CP-865)",
"iso-8859-2": "Central European (ISO-8859-2)",
"cp852": "Central European (CP-852)",
"windows-1250": "Central European (Windows-1250)",
"iso-8859-14": "Celtic (ISO-8859-14)",
"iso-8859-3": "South European (ISO-8859-3)",
"cp860": "Portuguese (CP-860)",
"iso-8859-7": "Greek (ISO-8859-7)",
"windows-1253": "Greek (Windows-1253)",
"iso-8859-9": "Turkish (ISO-8859-9)",
"windows-1254": "Turkish (Windows-1254)",
"iso-8859-13": "Baltic (ISO-8859-13)",
"windows-1257": "Baltic (Windows-1257)",
"iso-8859-16": "South-Eastern European (ISO-8859-16)",
"iso-8859-5": "Cyrillic (ISO-8859-5)",
"cp855": "Cyrillic (CP-855)",
"cp866": "Cyrillic (CP-866)",
"windows-1251": "Cyrillic (Windows-1251)",
"koi8r": "Cyrillic (KOI8-R)",
"koi8u": "Cyrillic (KOI8-U)",
"iso-8859-6": "Arabic (ISO-8859-6)",
"windows-1256": "Arabic (Windows-1256)",
"iso-8859-8": "Hebrew (ISO-8859-8)",
"cp862": "Hebrew (CP-862)",
"windows-1255": "Hebrew (Windows-1255)",
"windows-874": "Thai (Windows-874)",
"windows-1258": "Vietnamese (Windows-1258)",
"gb18030": "Simplified Chinese (GB18030)",
"gbk": "Simplified Chinese (GBK)",
"big5": "Traditional Chinese (Big5)",
"euc-kr": "Korean (EUC-KR)",
"euc-jp": "Japanese (EUC-JP)",
"iso-2022-jp": "Japanese (ISO-2022-JP)",
"shift_jis": "Japanese (Shift_JIS)"
},
"document": {
"anchor": {
"export_and_import": {
@@ -1242,6 +1300,7 @@
"Not set": "Nicht festgelegt",
"No results": "Keine Ergebnisse",
"Unknown": "Unbekannt",
"Auto detect": "Auto detect",
"Miscellaneous": "Verschiedenes",
"Default": "Standard",
"Done": "Fertig",
@@ -1268,6 +1327,13 @@
"Color": "Farbe",
"Type": "Typ",
"Format": "Format",
"File Encoding": "File Encoding",
"Space": "Space",
"Comma": "Comma",
"Semicolon": "Semicolon",
"Tab": "Tab",
"Vertical Bar": "Vertical Bar",
"Slash": "Slash",
"All Types": "Alle Typen",
"More": "Mehr",
"All": "Alle",
@@ -1521,6 +1587,8 @@
"Income Amount": "Einnahmenbetrag",
"Transfer Out Amount": "Überweisungsbetrag (Ausgang)",
"Transfer In Amount": "Überweisungsbetrag (Eingang)",
"Transfer In Account Name": "Transfer In Account Name",
"Transfer In Currency": "Transfer In Currency",
"Show Amount": "Betrag anzeigen",
"Hide Amount": "Betrag verbergen",
"Swap Account": "Konto tauschen",
@@ -1530,6 +1598,7 @@
"Duplicate (With Geographic Location)": "Duplicate (With Geographic Location)",
"Duplicate (With Time and Geographic Location)": "Duplicate (With Time and Geographic Location)",
"Category": "Kategorie",
"Secondary Category": "Secondary Category",
"Multiple Categories": "Mehrere Kategorien",
"Account": "Konto",
"Multiple Accounts": "Mehrere Konten",
@@ -1545,6 +1614,7 @@
"Scheduled Transaction Frequency": "Häufigkeit der geplanten Transaktion",
"Transaction Timezone": "Transaktionszeitzone",
"Same time as default timezone": "Gleiche Zeit wie Standardzeitzone",
"Transaction Type": "Transaction Type",
"Geographic Location": "Geografischer Standort",
"No Location": "Kein Standort",
"Getting Location...": "Standort wird ermittelt...",
@@ -1559,6 +1629,8 @@
"Import Transactions": "Transaktionen importieren",
"Upload File": "Datei hochladen",
"Upload Transaction Data File": "Transaktionsdatendatei hochladen",
"Define Column": "Define Column",
"Define and Check Column Mapping": "Define and Check Column Mapping",
"Check & Modify": "Überprüfen & Ändern",
"Check and Modify Your Data": "Überprüfen und Ändern Sie Ihre Daten",
"Data Import Completed": "Datenimport abgeschlossen",
@@ -1573,6 +1645,8 @@
"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",
"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",
"Firefly III Data Export File": "Firefly III-Datenexportdatei",
"Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App)-Datenexportdatei",
@@ -1581,8 +1655,19 @@
"Alipay (Web) Transaction Flow File": "Alipay (Web)-Transaktionsflussdatei",
"WeChat Pay Billing File": "WeChat Pay-Abrechnungsdatei",
"Data File": "Datendatei",
"Data to import": "Data to import",
"Please select a file to import": "Bitte wählen Sie eine Datei zum Importieren aus",
"Include Header Line": "Include Header Line",
"Time Format": "Time Format",
"Transaction Type Mapping": "Transaction Type Mapping",
"Timezone Format": "Timezone Format",
"Geographic Location Separator": "Geographic Location Separator",
"Transaction Tags Separator": "Transaction Tags Separator",
"Lines Per Page": "Lines Per Page",
"No data to import": "Keine Daten zum Importieren",
"Missing transaction time, transaction type, or amount column mapping": "Missing transaction time, transaction type, or amount column mapping",
"Transaction type mapping is not set": "Transaction type mapping is not set",
"Transaction time format is not set": "Transaction time format is not set",
"Cannot import invalid transactions": "Ungültige Transaktionen können nicht importiert werden",
"Unable to parse import file": "Importdatei kann nicht geparst werden",
"Batch Replace Selected Expense Categories": "Ausgewählte Ausgabenkategorien im Batch ersetzen",
+85
View File
@@ -1097,6 +1097,12 @@
"cannot add transaction before balance modification transaction": "You cannot add transaction before the balance modification transaction",
"balance modification transaction cannot modify transaction time": "You cannot modify transaction time for balance modification transaction",
"transfer transaction amount cannot be less than zero": "Amount cannot be less than 0 for transfer transaction",
"import file encoding is empty": "Import file encoding is empty",
"import file encoding not supported": "import file encoding is not supported",
"column mapping invalid": "Column mapping is invalid",
"transaction type mapping invalid": "Transaction type mapping is invalid",
"transaction time format invalid": "Transaction time format is invalid",
"transaction time zone format invalid": "Transaction time zone format is invalid",
"transaction category id is invalid": "Transaction category ID is invalid",
"transaction category not found": "Transaction category is not found",
"transaction category type is invalid": "Transaction category type is invalid",
@@ -1214,6 +1220,58 @@
"parameter invalid color": "{parameter} is invalid format",
"parameter invalid amount filter": "{parameter} is invalid format"
},
"encoding": {
"utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian",
"cp437": "OEM United States (CP-437)",
"cp863": "OEM Canadian French (CP-863)",
"cp037": "IBM EBCDIC US/Canada (CP-037)",
"cp1047": "IBM EBCDIC Open Systems (CP-1047)",
"cp1140": "IBM EBCDIC US/Canada with Euro (CP-1140)",
"iso-8859-1": "Western European (ISO-8859-1)",
"cp850": "Western European (CP-850)",
"cp858": "Western European with Euro (CP-858)",
"windows-1252": "Western European (Windows-1252)",
"iso-8859-15": "Western European (ISO-8859-15)",
"iso-8859-4": "North European (ISO-8859-4)",
"iso-8859-10": "Nordic (ISO-8859-10)",
"cp865": "Nordic (CP-865)",
"iso-8859-2": "Central European (ISO-8859-2)",
"cp852": "Central European (CP-852)",
"windows-1250": "Central European (Windows-1250)",
"iso-8859-14": "Celtic (ISO-8859-14)",
"iso-8859-3": "South European (ISO-8859-3)",
"cp860": "Portuguese (CP-860)",
"iso-8859-7": "Greek (ISO-8859-7)",
"windows-1253": "Greek (Windows-1253)",
"iso-8859-9": "Turkish (ISO-8859-9)",
"windows-1254": "Turkish (Windows-1254)",
"iso-8859-13": "Baltic (ISO-8859-13)",
"windows-1257": "Baltic (Windows-1257)",
"iso-8859-16": "South-Eastern European (ISO-8859-16)",
"iso-8859-5": "Cyrillic (ISO-8859-5)",
"cp855": "Cyrillic (CP-855)",
"cp866": "Cyrillic (CP-866)",
"windows-1251": "Cyrillic (Windows-1251)",
"koi8r": "Cyrillic (KOI8-R)",
"koi8u": "Cyrillic (KOI8-U)",
"iso-8859-6": "Arabic (ISO-8859-6)",
"windows-1256": "Arabic (Windows-1256)",
"iso-8859-8": "Hebrew (ISO-8859-8)",
"cp862": "Hebrew (CP-862)",
"windows-1255": "Hebrew (Windows-1255)",
"windows-874": "Thai (Windows-874)",
"windows-1258": "Vietnamese (Windows-1258)",
"gb18030": "Simplified Chinese (GB18030)",
"gbk": "Simplified Chinese (GBK)",
"big5": "Traditional Chinese (Big5)",
"euc-kr": "Korean (EUC-KR)",
"euc-jp": "Japanese (EUC-JP)",
"iso-2022-jp": "Japanese (ISO-2022-JP)",
"shift_jis": "Japanese (Shift_JIS)"
},
"document": {
"anchor": {
"export_and_import": {
@@ -1242,6 +1300,7 @@
"Not set": "Not set",
"No results": "No results",
"Unknown": "Unknown",
"Auto detect": "Auto detect",
"Miscellaneous": "Miscellaneous",
"Default": "Default",
"Done": "Done",
@@ -1268,6 +1327,13 @@
"Color": "Color",
"Type": "Type",
"Format": "Format",
"File Encoding": "File Encoding",
"Space": "Space",
"Comma": "Comma",
"Semicolon": "Semicolon",
"Tab": "Tab",
"Vertical Bar": "Vertical Bar",
"Slash": "Slash",
"All Types": "All Types",
"More": "More",
"All": "All",
@@ -1521,6 +1587,8 @@
"Income Amount": "Income Amount",
"Transfer Out Amount": "Transfer Out Amount",
"Transfer In Amount": "Transfer In Amount",
"Transfer In Account Name": "Transfer In Account Name",
"Transfer In Currency": "Transfer In Currency",
"Show Amount": "Show Amount",
"Hide Amount": "Hide Amount",
"Swap Account": "Swap Account",
@@ -1530,6 +1598,7 @@
"Duplicate (With Geographic Location)": "Duplicate (With Geographic Location)",
"Duplicate (With Time and Geographic Location)": "Duplicate (With Time and Geographic Location)",
"Category": "Category",
"Secondary Category": "Secondary Category",
"Multiple Categories": "Multiple Categories",
"Account": "Account",
"Multiple Accounts": "Multiple Accounts",
@@ -1545,6 +1614,7 @@
"Scheduled Transaction Frequency": "Scheduled Transaction Frequency",
"Transaction Timezone": "Transaction Timezone",
"Same time as default timezone": "Same time as default timezone",
"Transaction Type": "Transaction Type",
"Geographic Location": "Geographic Location",
"No Location": "No Location",
"Getting Location...": "Getting Location...",
@@ -1559,6 +1629,8 @@
"Import Transactions": "Import Transactions",
"Upload File": "Upload File",
"Upload Transaction Data File": "Upload Transaction Data File",
"Define Column": "Define Column",
"Define and Check Column Mapping": "Define and Check Column Mapping",
"Check & Modify": "Check & Modify",
"Check and Modify Your Data": "Check and Modify Your Data",
"Data Import Completed": "Data Import Completed",
@@ -1573,6 +1645,8 @@
"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",
"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",
"Firefly III Data Export File": "Firefly III Data Export File",
"Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) Data Export File",
@@ -1581,8 +1655,19 @@
"Alipay (Web) Transaction Flow File": "Alipay (Web) Transaction Flow File",
"WeChat Pay Billing File": "WeChat Pay Billing File",
"Data File": "Data File",
"Data to import": "Data to import",
"Please select a file to import": "Please select a file to import",
"Include Header Line": "Include Header Line",
"Time Format": "Time Format",
"Transaction Type Mapping": "Transaction Type Mapping",
"Timezone Format": "Timezone Format",
"Geographic Location Separator": "Geographic Location Separator",
"Transaction Tags Separator": "Transaction Tags Separator",
"Lines Per Page": "Lines Per Page",
"No data to import": "No data to import",
"Missing transaction time, transaction type, or amount column mapping": "Missing transaction time, transaction type, or amount column mapping",
"Transaction type mapping is not set": "Transaction type mapping is not set",
"Transaction time format is not set": "Transaction time format is not set",
"Cannot import invalid transactions": "Cannot import invalid transactions",
"Unable to parse import file": "Unable to parse import file",
"Batch Replace Selected Expense Categories": "Batch Replace Selected Expense Categories",
+85
View File
@@ -1097,6 +1097,12 @@
"cannot add transaction before balance modification transaction": "No puede agregar una transacción antes de la transacción de modificación del saldo",
"balance modification transaction cannot modify transaction time": "No puede modificar el tiempo de transacción para la transacción de modificación de saldo",
"transfer transaction amount cannot be less than zero": "El Importe no puede ser menor que 0 para la transacción de transferencia",
"import file encoding is empty": "Import file encoding is empty",
"import file encoding not supported": "import file encoding is not supported",
"column mapping invalid": "Column mapping is invalid",
"transaction type mapping invalid": "Transaction type mapping is invalid",
"transaction time format invalid": "Transaction time format is invalid",
"transaction time zone format invalid": "Transaction time zone format is invalid",
"transaction category id is invalid": "El ID de categoría de transacción no es válido",
"transaction category not found": "No se encuentra la categoría de transacción",
"transaction category type is invalid": "El tipo de categoría de transacción no es válido",
@@ -1214,6 +1220,58 @@
"parameter invalid color": "{parameter} es un formato no válido",
"parameter invalid amount filter": "{parameter} es un formato no válido"
},
"encoding": {
"utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian",
"cp437": "OEM United States (CP-437)",
"cp863": "OEM Canadian French (CP-863)",
"cp037": "IBM EBCDIC US/Canada (CP-037)",
"cp1047": "IBM EBCDIC Open Systems (CP-1047)",
"cp1140": "IBM EBCDIC US/Canada with Euro (CP-1140)",
"iso-8859-1": "Western European (ISO-8859-1)",
"cp850": "Western European (CP-850)",
"cp858": "Western European with Euro (CP-858)",
"windows-1252": "Western European (Windows-1252)",
"iso-8859-15": "Western European (ISO-8859-15)",
"iso-8859-4": "North European (ISO-8859-4)",
"iso-8859-10": "Nordic (ISO-8859-10)",
"cp865": "Nordic (CP-865)",
"iso-8859-2": "Central European (ISO-8859-2)",
"cp852": "Central European (CP-852)",
"windows-1250": "Central European (Windows-1250)",
"iso-8859-14": "Celtic (ISO-8859-14)",
"iso-8859-3": "South European (ISO-8859-3)",
"cp860": "Portuguese (CP-860)",
"iso-8859-7": "Greek (ISO-8859-7)",
"windows-1253": "Greek (Windows-1253)",
"iso-8859-9": "Turkish (ISO-8859-9)",
"windows-1254": "Turkish (Windows-1254)",
"iso-8859-13": "Baltic (ISO-8859-13)",
"windows-1257": "Baltic (Windows-1257)",
"iso-8859-16": "South-Eastern European (ISO-8859-16)",
"iso-8859-5": "Cyrillic (ISO-8859-5)",
"cp855": "Cyrillic (CP-855)",
"cp866": "Cyrillic (CP-866)",
"windows-1251": "Cyrillic (Windows-1251)",
"koi8r": "Cyrillic (KOI8-R)",
"koi8u": "Cyrillic (KOI8-U)",
"iso-8859-6": "Arabic (ISO-8859-6)",
"windows-1256": "Arabic (Windows-1256)",
"iso-8859-8": "Hebrew (ISO-8859-8)",
"cp862": "Hebrew (CP-862)",
"windows-1255": "Hebrew (Windows-1255)",
"windows-874": "Thai (Windows-874)",
"windows-1258": "Vietnamese (Windows-1258)",
"gb18030": "Simplified Chinese (GB18030)",
"gbk": "Simplified Chinese (GBK)",
"big5": "Traditional Chinese (Big5)",
"euc-kr": "Korean (EUC-KR)",
"euc-jp": "Japanese (EUC-JP)",
"iso-2022-jp": "Japanese (ISO-2022-JP)",
"shift_jis": "Japanese (Shift_JIS)"
},
"document": {
"anchor": {
"export_and_import": {
@@ -1242,6 +1300,7 @@
"Not set": "No establecido",
"No results": "Sin resultados",
"Unknown": "Desconocido",
"Auto detect": "Auto detect",
"Miscellaneous": "Misceláneas",
"Default": "Por defecto",
"Done": "Hecho",
@@ -1268,6 +1327,13 @@
"Color": "Color",
"Type": "Tipo",
"Format": "Formato",
"File Encoding": "File Encoding",
"Space": "Space",
"Comma": "Comma",
"Semicolon": "Semicolon",
"Tab": "Tab",
"Vertical Bar": "Vertical Bar",
"Slash": "Slash",
"All Types": "Todos los tipos",
"More": "Más",
"All": "Todo",
@@ -1521,6 +1587,8 @@
"Income Amount": "Importe de ingresos",
"Transfer Out Amount": "Importe de transferencias enviadas",
"Transfer In Amount": "Importe de transferencias recibidas",
"Transfer In Account Name": "Transfer In Account Name",
"Transfer In Currency": "Transfer In Currency",
"Show Amount": "Mostrar importe",
"Hide Amount": "Ocultar importe",
"Swap Account": "Intercambiar cuenta",
@@ -1530,6 +1598,7 @@
"Duplicate (With Geographic Location)": "Duplicate (With Geographic Location)",
"Duplicate (With Time and Geographic Location)": "Duplicate (With Time and Geographic Location)",
"Category": "Categoría",
"Secondary Category": "Secondary Category",
"Multiple Categories": "Múltiples categorías",
"Account": "Cuenta",
"Multiple Accounts": "Varias cuentas",
@@ -1545,6 +1614,7 @@
"Scheduled Transaction Frequency": "Frecuencia de transacciones programadas",
"Transaction Timezone": "Zona horaria de transacción",
"Same time as default timezone": "Misma hora que la zona horaria predeterminada",
"Transaction Type": "Transaction Type",
"Geographic Location": "Ubicación geográfica",
"No Location": "Sin ubicación",
"Getting Location...": "Obteniendo ubicación...",
@@ -1559,6 +1629,8 @@
"Import Transactions": "Importar transacciones",
"Upload File": "Cargar archivo",
"Upload Transaction Data File": "Cargar archivo de datos de transacción",
"Define Column": "Define Column",
"Define and Check Column Mapping": "Define and Check Column Mapping",
"Check & Modify": "Verificar y modificar",
"Check and Modify Your Data": "Verifique y modifique sus datos",
"Data Import Completed": "Importación de datos completada",
@@ -1573,6 +1645,8 @@
"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)",
"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",
"Firefly III Data Export File": "Archivo de exportación de datos de Firefly III",
"Feidee MyMoney (App) Data Export File": "Archivo de exportación de datos Feidee MyMoney (aplicación)",
@@ -1581,8 +1655,19 @@
"Alipay (Web) Transaction Flow File": "Archivo de flujo de transacciones de Alipay (web)",
"WeChat Pay Billing File": "Archivo de facturación de pago de WeChat",
"Data File": "Archivo de datos",
"Data to import": "Data to import",
"Please select a file to import": "Please select a file to import",
"Include Header Line": "Include Header Line",
"Time Format": "Time Format",
"Transaction Type Mapping": "Transaction Type Mapping",
"Timezone Format": "Timezone Format",
"Geographic Location Separator": "Geographic Location Separator",
"Transaction Tags Separator": "Transaction Tags Separator",
"Lines Per Page": "Lines Per Page",
"No data to import": "No hay datos para importar",
"Missing transaction time, transaction type, or amount column mapping": "Missing transaction time, transaction type, or amount column mapping",
"Transaction type mapping is not set": "Transaction type mapping is not set",
"Transaction time format is not set": "Transaction time format is not set",
"Cannot import invalid transactions": "No se pueden importar transacciones no válidas",
"Unable to parse import file": "No se puede analizar el archivo de importación",
"Batch Replace Selected Expense Categories": "Reemplazar por lotes categorías de gastos seleccionadas",
+20 -1
View File
@@ -66,7 +66,8 @@ import {
import {
TransactionEditScopeType,
TransactionTagFilterType
TransactionTagFilterType,
ImportTransactionColumnType
} from '@/core/transaction.ts';
import {
@@ -85,6 +86,7 @@ import {
import {
type LocalizedImportFileType,
type LocalizedImportFileTypeSubType,
type LocalizedImportFileTypeSupportedEncodings,
type LocalizedImportFileDocument,
} from '@/core/file.ts';
@@ -1138,11 +1140,27 @@ export function useI18n() {
}
}
const supportedEncodings: LocalizedImportFileTypeSupportedEncodings[] = [];
if (fileType.supportedEncodings) {
for (let i = 0; i < fileType.supportedEncodings.length; i++) {
const encoding = fileType.supportedEncodings[i];
const localizedEncoding: LocalizedImportFileTypeSupportedEncodings = {
encoding: encoding,
displayName: t(`encoding.${encoding}`)
};
supportedEncodings.push(localizedEncoding);
}
}
const localizedFileType: LocalizedImportFileType = {
type: fileType.type,
displayName: t(fileType.name),
extensions: fileType.extensions,
subTypes: subTypes.length ? subTypes : undefined,
supportedEncodings: supportedEncodings.length ? supportedEncodings : undefined,
dataFromTextbox: fileType.dataFromTextbox,
document: document
};
allSupportedImportFileTypes.push(localizedFileType);
@@ -1680,6 +1698,7 @@ export function useI18n() {
getAllTransactionEditScopeTypes: () => getLocalizedDisplayNameAndType(TransactionEditScopeType.values()),
getAllTransactionTagFilterTypes: () => getLocalizedDisplayNameAndType(TransactionTagFilterType.values()),
getAllTransactionScheduledFrequencyTypes: () => getLocalizedDisplayNameAndType(ScheduledTemplateFrequencyType.values()),
getAllImportTransactionColumnTypes: () => getLocalizedDisplayNameAndType(ImportTransactionColumnType.values()),
getAllTransactionDefaultCategories,
getAllDisplayExchangeRates,
getAllSupportedImportFileTypes,
+85
View File
@@ -1097,6 +1097,12 @@
"cannot add transaction before balance modification transaction": "Нельзя добавить транзакцию до транзакции изменения баланса",
"balance modification transaction cannot modify transaction time": "Нельзя изменить время транзакции для транзакции изменения баланса",
"transfer transaction amount cannot be less than zero": "Сумма перевода не может быть меньше нуля",
"import file encoding is empty": "Import file encoding is empty",
"import file encoding not supported": "import file encoding is not supported",
"column mapping invalid": "Column mapping is invalid",
"transaction type mapping invalid": "Transaction type mapping is invalid",
"transaction time format invalid": "Transaction time format is invalid",
"transaction time zone format invalid": "Transaction time zone format is invalid",
"transaction category id is invalid": "ID категории транзакции недействителен",
"transaction category not found": "Категория транзакции не найдена",
"transaction category type is invalid": "Тип категории транзакции недействителен",
@@ -1214,6 +1220,58 @@
"parameter invalid color": "{parameter} имеет неверный формат",
"parameter invalid amount filter": "{parameter} имеет неверный формат"
},
"encoding": {
"utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian",
"cp437": "OEM United States (CP-437)",
"cp863": "OEM Canadian French (CP-863)",
"cp037": "IBM EBCDIC US/Canada (CP-037)",
"cp1047": "IBM EBCDIC Open Systems (CP-1047)",
"cp1140": "IBM EBCDIC US/Canada with Euro (CP-1140)",
"iso-8859-1": "Western European (ISO-8859-1)",
"cp850": "Western European (CP-850)",
"cp858": "Western European with Euro (CP-858)",
"windows-1252": "Western European (Windows-1252)",
"iso-8859-15": "Western European (ISO-8859-15)",
"iso-8859-4": "North European (ISO-8859-4)",
"iso-8859-10": "Nordic (ISO-8859-10)",
"cp865": "Nordic (CP-865)",
"iso-8859-2": "Central European (ISO-8859-2)",
"cp852": "Central European (CP-852)",
"windows-1250": "Central European (Windows-1250)",
"iso-8859-14": "Celtic (ISO-8859-14)",
"iso-8859-3": "South European (ISO-8859-3)",
"cp860": "Portuguese (CP-860)",
"iso-8859-7": "Greek (ISO-8859-7)",
"windows-1253": "Greek (Windows-1253)",
"iso-8859-9": "Turkish (ISO-8859-9)",
"windows-1254": "Turkish (Windows-1254)",
"iso-8859-13": "Baltic (ISO-8859-13)",
"windows-1257": "Baltic (Windows-1257)",
"iso-8859-16": "South-Eastern European (ISO-8859-16)",
"iso-8859-5": "Cyrillic (ISO-8859-5)",
"cp855": "Cyrillic (CP-855)",
"cp866": "Cyrillic (CP-866)",
"windows-1251": "Cyrillic (Windows-1251)",
"koi8r": "Cyrillic (KOI8-R)",
"koi8u": "Cyrillic (KOI8-U)",
"iso-8859-6": "Arabic (ISO-8859-6)",
"windows-1256": "Arabic (Windows-1256)",
"iso-8859-8": "Hebrew (ISO-8859-8)",
"cp862": "Hebrew (CP-862)",
"windows-1255": "Hebrew (Windows-1255)",
"windows-874": "Thai (Windows-874)",
"windows-1258": "Vietnamese (Windows-1258)",
"gb18030": "Simplified Chinese (GB18030)",
"gbk": "Simplified Chinese (GBK)",
"big5": "Traditional Chinese (Big5)",
"euc-kr": "Korean (EUC-KR)",
"euc-jp": "Japanese (EUC-JP)",
"iso-2022-jp": "Japanese (ISO-2022-JP)",
"shift_jis": "Japanese (Shift_JIS)"
},
"document": {
"anchor": {
"export_and_import": {
@@ -1242,6 +1300,7 @@
"Not set": "Не установлено",
"No results": "Нет результатов",
"Unknown": "Неизвестно",
"Auto detect": "Auto detect",
"Miscellaneous": "Разное",
"Default": "По умолчанию",
"Done": "Готово",
@@ -1268,6 +1327,13 @@
"Color": "Цвет",
"Type": "Тип",
"Format": "Формат",
"File Encoding": "File Encoding",
"Space": "Space",
"Comma": "Comma",
"Semicolon": "Semicolon",
"Tab": "Tab",
"Vertical Bar": "Vertical Bar",
"Slash": "Slash",
"All Types": "Все типы",
"More": "Еще",
"All": "Все",
@@ -1521,6 +1587,8 @@
"Income Amount": "Сумма дохода",
"Transfer Out Amount": "Сумма перевода (исходящий)",
"Transfer In Amount": "Сумма перевода (входящий)",
"Transfer In Account Name": "Transfer In Account Name",
"Transfer In Currency": "Transfer In Currency",
"Show Amount": "Показать сумму",
"Hide Amount": "Скрыть сумму",
"Swap Account": "Поменять счет",
@@ -1530,6 +1598,7 @@
"Duplicate (With Geographic Location)": "Duplicate (With Geographic Location)",
"Duplicate (With Time and Geographic Location)": "Duplicate (With Time and Geographic Location)",
"Category": "Категория",
"Secondary Category": "Secondary Category",
"Multiple Categories": "Несколько категорий",
"Account": "Счет",
"Multiple Accounts": "Несколько счетов",
@@ -1545,6 +1614,7 @@
"Scheduled Transaction Frequency": "Частота запланированных транзакций",
"Transaction Timezone": "Часовой пояс транзакции",
"Same time as default timezone": "То же время, что и в часовом поясе по умолчанию",
"Transaction Type": "Transaction Type",
"Geographic Location": "Географическое местоположение",
"No Location": "Нет местоположения",
"Getting Location...": "Получение местоположения...",
@@ -1559,6 +1629,8 @@
"Import Transactions": "Импорт транзакций",
"Upload File": "Загрузить файл",
"Upload Transaction Data File": "Загрузить файл данных транзакций",
"Define Column": "Define Column",
"Define and Check Column Mapping": "Define and Check Column Mapping",
"Check & Modify": "Проверить и изменить",
"Check and Modify Your Data": "Проверьте и измените свои данные",
"Data Import Completed": "Импорт данных завершен",
@@ -1573,6 +1645,8 @@
"Month-day-year format": "Формат месяц-день-год",
"Day-month-year format": "Формат день-месяц-год",
"Intuit Interchange Format (IIF) File": "Файл Intuit Interchange Format (IIF)",
"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",
"Firefly III Data Export File": "Файл экспорта данных Firefly III",
"Feidee MyMoney (App) Data Export File": "Файл экспорта данных Feidee MyMoney (приложение)",
@@ -1581,8 +1655,19 @@
"Alipay (Web) Transaction Flow File": "Файл потока транзакций Alipay (веб)",
"WeChat Pay Billing File": "Файл выставления счетов WeChat Pay",
"Data File": "Файл данных",
"Data to import": "Data to import",
"Please select a file to import": "Please select a file to import",
"Include Header Line": "Include Header Line",
"Time Format": "Time Format",
"Transaction Type Mapping": "Transaction Type Mapping",
"Timezone Format": "Timezone Format",
"Geographic Location Separator": "Geographic Location Separator",
"Transaction Tags Separator": "Transaction Tags Separator",
"Lines Per Page": "Lines Per Page",
"No data to import": "Нет данных для импорта",
"Missing transaction time, transaction type, or amount column mapping": "Missing transaction time, transaction type, or amount column mapping",
"Transaction type mapping is not set": "Transaction type mapping is not set",
"Transaction time format is not set": "Transaction time format is not set",
"Cannot import invalid transactions": "Невозможно импортировать недействительные транзакции",
"Unable to parse import file": "Не удалось обработать файл импорта",
"Batch Replace Selected Expense Categories": "Пакетная замена выбранных категорий расходов",
+85
View File
@@ -1097,6 +1097,12 @@
"cannot add transaction before balance modification transaction": "Bạn không thể thêm giao dịch trước giao dịch sửa đổi số dư",
"balance modification transaction cannot modify transaction time": "Bạn không thể sửa đổi thời gian giao dịch cho giao dịch sửa đổi số dư",
"transfer transaction amount cannot be less than zero": "Số tiền không thể nhỏ hơn 0 đối với giao dịch chuyển khoản",
"import file encoding is empty": "Import file encoding is empty",
"import file encoding not supported": "import file encoding is not supported",
"column mapping invalid": "Column mapping is invalid",
"transaction type mapping invalid": "Transaction type mapping is invalid",
"transaction time format invalid": "Transaction time format is invalid",
"transaction time zone format invalid": "Transaction time zone format is invalid",
"transaction category id is invalid": "ID danh mục giao dịch không hợp lệ",
"transaction category not found": "Không tìm thấy danh mục giao dịch",
"transaction category type is invalid": "Loại danh mục giao dịch không hợp lệ",
@@ -1214,6 +1220,58 @@
"parameter invalid color": "{parameter} có định dạng không hợp lệ",
"parameter invalid amount filter": "{parameter} có định dạng không hợp lệ"
},
"encoding": {
"utf-8": "UTF-8",
"utf-8-bom": "UTF-8 with BOM",
"utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian",
"cp437": "OEM United States (CP-437)",
"cp863": "OEM Canadian French (CP-863)",
"cp037": "IBM EBCDIC US/Canada (CP-037)",
"cp1047": "IBM EBCDIC Open Systems (CP-1047)",
"cp1140": "IBM EBCDIC US/Canada with Euro (CP-1140)",
"iso-8859-1": "Western European (ISO-8859-1)",
"cp850": "Western European (CP-850)",
"cp858": "Western European with Euro (CP-858)",
"windows-1252": "Western European (Windows-1252)",
"iso-8859-15": "Western European (ISO-8859-15)",
"iso-8859-4": "North European (ISO-8859-4)",
"iso-8859-10": "Nordic (ISO-8859-10)",
"cp865": "Nordic (CP-865)",
"iso-8859-2": "Central European (ISO-8859-2)",
"cp852": "Central European (CP-852)",
"windows-1250": "Central European (Windows-1250)",
"iso-8859-14": "Celtic (ISO-8859-14)",
"iso-8859-3": "South European (ISO-8859-3)",
"cp860": "Portuguese (CP-860)",
"iso-8859-7": "Greek (ISO-8859-7)",
"windows-1253": "Greek (Windows-1253)",
"iso-8859-9": "Turkish (ISO-8859-9)",
"windows-1254": "Turkish (Windows-1254)",
"iso-8859-13": "Baltic (ISO-8859-13)",
"windows-1257": "Baltic (Windows-1257)",
"iso-8859-16": "South-Eastern European (ISO-8859-16)",
"iso-8859-5": "Cyrillic (ISO-8859-5)",
"cp855": "Cyrillic (CP-855)",
"cp866": "Cyrillic (CP-866)",
"windows-1251": "Cyrillic (Windows-1251)",
"koi8r": "Cyrillic (KOI8-R)",
"koi8u": "Cyrillic (KOI8-U)",
"iso-8859-6": "Arabic (ISO-8859-6)",
"windows-1256": "Arabic (Windows-1256)",
"iso-8859-8": "Hebrew (ISO-8859-8)",
"cp862": "Hebrew (CP-862)",
"windows-1255": "Hebrew (Windows-1255)",
"windows-874": "Thai (Windows-874)",
"windows-1258": "Vietnamese (Windows-1258)",
"gb18030": "Simplified Chinese (GB18030)",
"gbk": "Simplified Chinese (GBK)",
"big5": "Traditional Chinese (Big5)",
"euc-kr": "Korean (EUC-KR)",
"euc-jp": "Japanese (EUC-JP)",
"iso-2022-jp": "Japanese (ISO-2022-JP)",
"shift_jis": "Japanese (Shift_JIS)"
},
"document": {
"anchor": {
"export_and_import": {
@@ -1242,6 +1300,7 @@
"Not set": "Not set",
"No results": "Không có kết quả",
"Unknown": "Không rõ",
"Auto detect": "Auto detect",
"Miscellaneous": "Linh tinh",
"Default": "Mặc định",
"Done": "Hoàn tất",
@@ -1268,6 +1327,13 @@
"Color": "Màu sắc",
"Type": "Loại",
"Format": "Định dạng",
"File Encoding": "File Encoding",
"Space": "Space",
"Comma": "Comma",
"Semicolon": "Semicolon",
"Tab": "Tab",
"Vertical Bar": "Vertical Bar",
"Slash": "Slash",
"All Types": "Tất cả các loại",
"More": "Thêm",
"All": "Tất cả",
@@ -1521,6 +1587,8 @@
"Income Amount": "Số tiền thu nhập",
"Transfer Out Amount": "Số tiền chuyển ra",
"Transfer In Amount": "Số tiền chuyển vào",
"Transfer In Account Name": "Transfer In Account Name",
"Transfer In Currency": "Transfer In Currency",
"Show Amount": "Hiển thị số tiền",
"Hide Amount": "Ẩn số tiền",
"Swap Account": "Hoán đổi tài khoản",
@@ -1530,6 +1598,7 @@
"Duplicate (With Geographic Location)": "Duplicate (With Geographic Location)",
"Duplicate (With Time and Geographic Location)": "Duplicate (With Time and Geographic Location)",
"Category": "Danh mục",
"Secondary Category": "Secondary Category",
"Multiple Categories": "Nhiều danh mục",
"Account": "Tài khoản",
"Multiple Accounts": "Nhiều tài khoản",
@@ -1545,6 +1614,7 @@
"Scheduled Transaction Frequency": "Tần suất giao dịch theo lịch trình",
"Transaction Timezone": "Múi giờ giao dịch",
"Same time as default timezone": "Cùng thời gian với múi giờ mặc định",
"Transaction Type": "Transaction Type",
"Geographic Location": "Vị trí địa lý",
"No Location": "Không có vị trí",
"Getting Location...": "Đang lấy vị trí...",
@@ -1559,6 +1629,8 @@
"Import Transactions": "Nhập giao dịch",
"Upload File": "Tải lên tệp",
"Upload Transaction Data File": "Tải lên tệp dữ liệu giao dịch",
"Define Column": "Define Column",
"Define and Check Column Mapping": "Define and Check Column Mapping",
"Check & Modify": "Kiểm tra & Sửa đổi",
"Check and Modify Your Data": "Kiểm tra và sửa đổi dữ liệu của bạn",
"Data Import Completed": "Nhập dữ liệu hoàn tất",
@@ -1573,6 +1645,8 @@
"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)",
"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",
"Firefly III Data Export File": "Tệp xuất dữ liệu Firefly III",
"Feidee MyMoney (App) Data Export File": "Tệp xuất dữ liệu Feidee MyMoney (Ứng dụng)",
@@ -1581,8 +1655,19 @@
"Alipay (Web) Transaction Flow File": "Tệp luồng giao dịch Alipay (Web)",
"WeChat Pay Billing File": "Tệp thanh toán WeChat Pay",
"Data File": "Tệp dữ liệu",
"Data to import": "Data to import",
"Please select a file to import": "Please select a file to import",
"Include Header Line": "Include Header Line",
"Time Format": "Time Format",
"Transaction Type Mapping": "Transaction Type Mapping",
"Timezone Format": "Timezone Format",
"Geographic Location Separator": "Geographic Location Separator",
"Transaction Tags Separator": "Transaction Tags Separator",
"Lines Per Page": "Lines Per Page",
"No data to import": "Không có dữ liệu để nhập",
"Missing transaction time, transaction type, or amount column mapping": "Missing transaction time, transaction type, or amount column mapping",
"Transaction type mapping is not set": "Transaction type mapping is not set",
"Transaction time format is not set": "Transaction time format is not set",
"Cannot import invalid transactions": "Không thể nhập giao dịch không hợp lệ",
"Unable to parse import file": "Không thể phân tích tệp nhập",
"Batch Replace Selected Expense Categories": "Thay thế hàng loạt các danh mục chi phí đã chọn",
+85
View File
@@ -1097,6 +1097,12 @@
"cannot add transaction before balance modification transaction": "不能添加早于修改余额的交易",
"balance modification transaction cannot modify transaction time": "您无法对修改余额的交易修改交易时间",
"transfer transaction amount cannot be less than zero": "转账交易的金额不能小于0",
"import file encoding is empty": "导入文件编码为空",
"import file encoding not supported": "导入文件编码不支持",
"column mapping invalid": "列映射无效",
"transaction type mapping invalid": "交易类型映射无效",
"transaction time format invalid": "交易时间格式无效",
"transaction time zone format invalid": "交易时区格式无效",
"transaction category id is invalid": "交易分类ID无效",
"transaction category not found": "交易分类不存在",
"transaction category type is invalid": "交易分类类型无效",
@@ -1214,6 +1220,58 @@
"parameter invalid color": "{parameter}格式错误",
"parameter invalid amount filter": "{parameter}格式错误"
},
"encoding": {
"utf-8": "UTF-8",
"utf-8-bom": "UTF-8 带签名",
"utf-16le": "UTF-16 Little Endian",
"utf-16be": "UTF-16 Big Endian",
"cp437": "OEM 美国 (CP-437)",
"cp863": "OEM 加拿大法语 (CP-863)",
"cp037": "IBM EBCDIC 美国/加拿大 (CP-037)",
"cp1047": "IBM EBCDIC 开放系统 (CP-1047)",
"cp1140": "IBM EBCDIC 美国/加拿大 含欧元 (CP-1140)",
"iso-8859-1": "西欧 (ISO-8859-1)",
"cp850": "西欧 (CP-850)",
"cp858": "西欧 含欧元 (CP-858)",
"windows-1252": "西欧 (Windows-1252)",
"iso-8859-15": "西欧 (ISO-8859-15)",
"iso-8859-4": "北欧 (ISO-8859-4)",
"iso-8859-10": "北欧 (ISO-8859-10)",
"cp865": "北欧 (CP-865)",
"iso-8859-2": "中欧 (ISO-8859-2)",
"cp852": "中欧 (CP-852)",
"windows-1250": "中欧 (Windows-1250)",
"iso-8859-14": "凯尔特语族 (ISO-8859-14)",
"iso-8859-3": "南欧 (ISO-8859-3)",
"cp860": "葡萄牙语 (CP-860)",
"iso-8859-7": "希腊语 (ISO-8859-7)",
"windows-1253": "希腊语 (Windows-1253)",
"iso-8859-9": "土耳其语 (ISO-8859-9)",
"windows-1254": "土耳其语 (Windows-1254)",
"iso-8859-13": "波罗的语族 (ISO-8859-13)",
"windows-1257": "波罗的语族 (Windows-1257)",
"iso-8859-16": "东南欧 (ISO-8859-16)",
"iso-8859-5": "西里尔文 (ISO-8859-5)",
"cp855": "西里尔文 (CP-855)",
"cp866": "西里尔文 (CP-866)",
"windows-1251": "西里尔文 (Windows-1251)",
"koi8r": "西里尔文 (KOI8-R)",
"koi8u": "西里尔文 (KOI8-U)",
"iso-8859-6": "阿拉伯语 (ISO-8859-6)",
"windows-1256": "阿拉伯语 (Windows-1256)",
"iso-8859-8": "希伯来语 (ISO-8859-8)",
"cp862": "希伯来语 (CP-862)",
"windows-1255": "希伯来语 (Windows-1255)",
"windows-874": "泰语 (Windows-874)",
"windows-1258": "越南语 (Windows-1258)",
"gb18030": "简体中文 (GB18030)",
"gbk": "简体中文 (GBK)",
"big5": "繁体中文 (Big5)",
"euc-kr": "韩语 (EUC-KR)",
"euc-jp": "日语 (EUC-JP)",
"iso-2022-jp": "日语 (ISO-2022-JP)",
"shift_jis": "日语 (Shift_JIS)"
},
"document": {
"anchor": {
"export_and_import": {
@@ -1242,6 +1300,7 @@
"Not set": "未设置",
"No results": "无结果",
"Unknown": "未知",
"Auto detect": "自动检测",
"Miscellaneous": "杂项",
"Default": "默认",
"Done": "完成",
@@ -1268,6 +1327,13 @@
"Color": "颜色",
"Type": "类型",
"Format": "格式",
"File Encoding": "文件编码",
"Space": "空格",
"Comma": "逗号",
"Semicolon": "分号",
"Tab": "制表符 Tab",
"Vertical Bar": "竖线",
"Slash": "Slash",
"All Types": "全部类型",
"More": "更多",
"All": "全部",
@@ -1521,6 +1587,8 @@
"Income Amount": "收入金额",
"Transfer Out Amount": "转出金额",
"Transfer In Amount": "转入金额",
"Transfer In Account Name": "转入账户名",
"Transfer In Currency": "转入货币",
"Show Amount": "显示金额",
"Hide Amount": "隐藏金额",
"Swap Account": "交换账户",
@@ -1530,6 +1598,7 @@
"Duplicate (With Geographic Location)": "复制 (含地理位置)",
"Duplicate (With Time and Geographic Location)": "复制 (含时间和地理位置)",
"Category": "分类",
"Secondary Category": "二级分类",
"Multiple Categories": "多个分类",
"Account": "账户",
"Multiple Accounts": "多个账户",
@@ -1545,6 +1614,7 @@
"Scheduled Transaction Frequency": "定时交易周期",
"Transaction Timezone": "交易时区",
"Same time as default timezone": "与默认时区时间相同",
"Transaction Type": "交易类型",
"Geographic Location": "地理位置",
"No Location": "没有位置",
"Getting Location...": "正在获取位置...",
@@ -1559,6 +1629,8 @@
"Import Transactions": "导入交易",
"Upload File": "上传文件",
"Upload Transaction Data File": "上传交易数据文件",
"Define Column": "定义列",
"Define and Check Column Mapping": "定义及检查列映射",
"Check & Modify": "检查及修改",
"Check and Modify Your Data": "检查及修改您的数据",
"Data Import Completed": "数据导入完成",
@@ -1573,6 +1645,8 @@
"Month-day-year format": "月-日-年 格式",
"Day-month-year format": "日-月-年 格式",
"Intuit Interchange Format (IIF) File": "Intuit Interchange Format (IIF) 文件",
"Delimiter-separated Values (DSV) File": "分隔符分隔值 (DSV) 文件",
"Delimiter-separated Values (DSV) Data": "分隔符分隔值 (DSV) 数据",
"GnuCash XML Database File": "GnuCash XML 数据库文件",
"Firefly III Data Export File": "Firefly III 数据导出文件",
"Feidee MyMoney (App) Data Export File": "随手记 (App) 数据导出文件",
@@ -1581,8 +1655,19 @@
"Alipay (Web) Transaction Flow File": "支付宝 (网页版) 交易流水文件",
"WeChat Pay Billing File": "微信支付账单文件",
"Data File": "数据文件",
"Data to import": "要导入的数据",
"Please select a file to import": "请选择要导入的文件",
"Include Header Line": "包含标题行",
"Time Format": "时间格式",
"Transaction Type Mapping": "交易类型映射",
"Timezone Format": "时区格式",
"Geographic Location Separator": "地理位置分隔符",
"Transaction Tags Separator": "交易标签分隔符",
"Lines Per Page": "每页行数",
"No data to import": "没有可以导入的数据",
"Missing transaction time, transaction type, or amount column mapping": "缺少交易时间、交易类型或金额列映射",
"Transaction type mapping is not set": "交易类型映射没有设置",
"Transaction time format is not set": "交易时间格式没有设置",
"Cannot import invalid transactions": "不能导入无效的交易",
"Unable to parse import file": "无法解析导入的文件",
"Batch Replace Selected Expense Categories": "批量替换选中的支出分类",
+28 -2
View File
@@ -1056,9 +1056,34 @@ export const useTransactionsStore = defineStore('transactions', () => {
});
}
function parseImportTransaction({ fileType, importFile }: { fileType: string, importFile: File }): Promise<ImportTransactionResponsePageWrapper> {
function parseImportDsvFile({ fileType, fileEncoding, importFile }: { fileType: string, fileEncoding?: string, importFile: File }): Promise<string[][]> {
return new Promise((resolve, reject) => {
services.parseImportTransaction({ fileType, importFile }).then(response => {
services.parseImportDsvFile({ fileType, fileEncoding, importFile }).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to parse import file' });
return;
}
resolve(data.result);
}).catch(error => {
logger.error('Unable to parse import file', error);
if (error.response && error.response.data && error.response.data.errorMessage) {
reject({ error: error.response.data });
} else if (!error.processed) {
reject({ message: 'Unable to parse import file' });
} else {
reject(error);
}
});
});
}
function parseImportTransaction({ fileType, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, geoSeparator, tagSeparator }: { fileType: string, fileEncoding?: string, importFile: File, columnMapping?: Record<number, number>, transactionTypeMapping?: Record<string, TransactionType>, hasHeaderLine?: boolean, timeFormat?: string, timezoneFormat?: string, geoSeparator?: string, tagSeparator?: string }): Promise<ImportTransactionResponsePageWrapper> {
return new Promise((resolve, reject) => {
services.parseImportTransaction({ fileType, fileEncoding, importFile, columnMapping, transactionTypeMapping, hasHeaderLine, timeFormat, timezoneFormat, geoSeparator, tagSeparator }).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
@@ -1215,6 +1240,7 @@ export const useTransactionsStore = defineStore('transactions', () => {
getTransaction,
saveTransaction,
deleteTransaction,
parseImportDsvFile,
parseImportTransaction,
importTransactions,
uploadTransactionPicture,
@@ -179,7 +179,19 @@
/>
</v-col>
<v-col cols="12" md="12">
<v-col cols="12" md="12" v-if="!isImportDataFromTextbox && allSupportedEncodings">
<v-select
item-title="displayName"
item-value="encoding"
:disabled="submitting"
:label="tt('File Encoding')"
:placeholder="tt('File Encoding')"
:items="allSupportedEncodings"
v-model="fileEncoding"
/>
</v-col>
<v-col cols="12" md="12" v-if="!isImportDataFromTextbox">
<v-text-field
readonly
persistent-placeholder
@@ -193,6 +205,17 @@
/>
</v-col>
<v-col cols="12" md="12" v-if="isImportDataFromTextbox">
<v-textarea
type="text"
persistent-placeholder
rows="5"
:disabled="submitting"
:placeholder="tt('Data to import')"
v-model="importData"
/>
</v-col>
<v-col cols="12" md="12" class="mb-0 pb-0" v-if="exportFileGuideDocumentUrl">
<a :href="exportFileGuideDocumentUrl" :class="{ 'disabled': submitting }" target="_blank">
<v-icon :icon="mdiHelpCircleOutline" size="16" />
@@ -202,6 +225,184 @@
</v-col>
</v-row>
</v-window-item>
<v-window-item value="defineColumn">
<v-data-table
fixed-header
fixed-footer
density="compact"
item-value="index"
:class="{ 'import-transaction-table': true, 'disabled': loading || submitting }"
:height="parsedFileLinesTableHeight"
:disable-sort="true"
:headers="parsedFileLinesHeaders"
:items="parsedFileLines"
:no-data-text="tt('No data to import')"
v-model:items-per-page="countPerPage"
v-model:page="currentPage"
>
<template #headers="{ columns }">
<tr>
<th class="text-no-wrap" :key="column.key ?? undefined" v-for="column in columns">
<span v-if="!column.key || column.key === 'index'">{{ column.title }}</span>
<div class="py-1" v-if="column.key && column.key !== 'index'">
<span>{{ getParseDataMappedColumnDisplayName(parseInt(column.key)) }}</span>
<br/>
<span>({{ column.title }})</span>
<v-menu activator="parent" location="bottom" max-height="500">
<v-list>
<v-list-item :key="columnType.type"
:append-icon="parsedFileDataColumnMapping[columnType.type] === parseInt(column.key) ? mdiCheck : undefined"
v-for="columnType in allImportTransactionColumnTypes"
@click="updateParseDataMappedColumn(parseInt(column.key), columnType.type)">
<v-list-item-title class="cursor-pointer">
{{ columnType.displayName }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</div>
</th>
</tr>
</template>
<template #bottom>
<div class="title-and-toolbar d-flex align-center text-no-wrap mt-2" v-if="parsedFileData">
<v-btn color="secondary" density="compact" variant="outlined"
:append-icon="parsedFileIncludeHeader ? mdiCheck : mdiClose"
@click="parsedFileIncludeHeader = !parsedFileIncludeHeader">{{ tt('Include Header Line') }}</v-btn>
<v-btn class="ml-2" color="secondary" density="compact" variant="outlined"
:disabled="!parsedFileDataColumnMapping || !isNumber(parsedFileDataColumnMapping[ImportTransactionColumnType.TransactionType.type]) || !parsedFileAllTransactionTypes">
<span>{{ tt('Transaction Type Mapping') }}</span>
<span class="ml-1" v-if="parsedFileDataColumnMapping && isNumber(parsedFileDataColumnMapping[ImportTransactionColumnType.TransactionType.type]) && parsedFileAllTransactionTypes">({{ getObjectOwnFieldCount(parsedFileValidMappedTransactionTypes) || tt('None') }})</span>
<v-menu eager activator="parent" location="bottom" max-height="500"
:close-on-content-click="false">
<v-list class="pa-0">
<v-list-item class="pa-0">
<v-table class="transaction-types-popup-menu">
<tbody>
<tr :key="typeName"
v-for="typeName in parsedFileAllTransactionTypes">
<td>{{ typeName }}</td>
<td>
<v-btn-toggle class="transaction-types-toggle" density="compact" variant="outlined"
mandatory="force" divided
v-model="parsedFileTransactionTypeMapping[typeName]">
<v-btn :value="undefined">{{ tt('None') }}</v-btn>
<v-btn :value="TransactionType.ModifyBalance">{{ tt('Modify Balance') }}</v-btn>
<v-btn :value="TransactionType.Income">{{ tt('Income') }}</v-btn>
<v-btn :value="TransactionType.Expense">{{ tt('Expense') }}</v-btn>
<v-btn :value="TransactionType.Transfer">{{ tt('Transfer') }}</v-btn>
</v-btn-toggle>
</td>
</tr>
</tbody>
</v-table>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
<v-btn class="ml-2" color="secondary" density="compact" variant="outlined"
:disabled="!parsedFileDataColumnMapping || !isNumber(parsedFileDataColumnMapping[ImportTransactionColumnType.TransactionTime.type])">
<span>{{ tt('Time Format') }}</span>
<span class="ml-1" v-if="parsedFileDataColumnMapping && isNumber(parsedFileDataColumnMapping[ImportTransactionColumnType.TransactionTime.type])">({{ parsedFileTimeFormat || parsedFileAutoDetectedTimeFormat || tt('Unknown') }})</span>
<v-menu eager activator="parent" location="bottom" max-height="500">
<v-list>
<v-list-item key="auto"
:append-icon="parsedFileTimeFormat === '' ? mdiCheck : undefined"
@click="parsedFileTimeFormat = ''">
<v-list-item-title class="cursor-pointer">
<span>{{ tt('Auto detect') }}</span>
<span class="ml-1" v-if="parsedFileAutoDetectedTimeFormat">({{ parsedFileAutoDetectedTimeFormat }})</span>
<span class="ml-1" v-if="!parsedFileAutoDetectedTimeFormat">({{ tt('Unknown') }})</span>
</v-list-item-title>
</v-list-item>
<v-list-item :key="dateTimeFormat.format"
:append-icon="parsedFileTimeFormat === dateTimeFormat.format ? mdiCheck : undefined"
v-for="dateTimeFormat in KnownDateTimeFormat.values()"
@click="parsedFileTimeFormat = dateTimeFormat.format">
<v-list-item-title class="cursor-pointer">
{{ dateTimeFormat.format }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
<v-btn class="ml-2" color="secondary" density="compact" variant="outlined"
v-if="parsedFileDataColumnMapping && isNumber(parsedFileDataColumnMapping[ImportTransactionColumnType.TransactionTimezone.type])">
<span>{{ tt('Timezone Format') }}</span>
<span class="ml-1" v-if="parsedFileDataColumnMapping && isNumber(parsedFileDataColumnMapping[ImportTransactionColumnType.TransactionTimezone.type])">({{ KnownDateTimezoneFormat.valueOf(parsedFileTimezoneFormat || parsedFileAutoDetectedTimezoneFormat || '')?.name || tt('Unknown') }})</span>
<v-menu eager activator="parent" location="bottom" max-height="500">
<v-list>
<v-list-item key="auto"
:append-icon="parsedFileTimezoneFormat === '' ? mdiCheck : undefined"
@click="parsedFileTimezoneFormat = ''">
<v-list-item-title class="cursor-pointer">
<span>{{ tt('Auto detect') }}</span>
<span class="ml-1" v-if="parsedFileAutoDetectedTimezoneFormat && KnownDateTimezoneFormat.valueOf(parsedFileAutoDetectedTimezoneFormat || '')">({{ KnownDateTimezoneFormat.valueOf(parsedFileAutoDetectedTimezoneFormat || '')?.name }})</span>
<span class="ml-1" v-if="!parsedFileAutoDetectedTimezoneFormat || !KnownDateTimezoneFormat.valueOf(parsedFileAutoDetectedTimezoneFormat || '')">({{ tt('Unknown') }})</span>
</v-list-item-title>
</v-list-item>
<v-list-item :key="timezoneFormat.value"
:append-icon="parsedFileTimezoneFormat === timezoneFormat.value ? mdiCheck : undefined"
v-for="timezoneFormat in KnownDateTimezoneFormat.values()"
@click="parsedFileTimezoneFormat = timezoneFormat.value">
<v-list-item-title class="cursor-pointer">
{{ timezoneFormat.name }}
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
<v-btn class="ml-2" color="secondary" density="compact" variant="outlined"
v-if="parsedFileDataColumnMapping && isNumber(parsedFileDataColumnMapping[ImportTransactionColumnType.GeographicLocation.type])">
<span>{{ tt('Geographic Location Separator') }}</span>
<span class="ml-1" v-if="parsedFileGeoLocationSeparator">({{ parsedFileGeoLocationSeparator }})</span>
<v-menu eager activator="parent" location="bottom" max-height="500">
<v-list>
<v-list-item :key="separator.value"
:append-icon="parsedFileGeoLocationSeparator === separator.value ? mdiCheck : undefined"
v-for="separator in allSeparators"
@click="parsedFileGeoLocationSeparator = separator.value">
<v-list-item-title class="cursor-pointer">
{{ separator.name }} ({{separator.value}})
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
<v-btn class="ml-2" color="secondary" density="compact" variant="outlined"
v-if="parsedFileDataColumnMapping && isNumber(parsedFileDataColumnMapping[ImportTransactionColumnType.Tags.type])">
<span>{{ tt('Transaction Tags Separator') }}</span>
<span class="ml-1" v-if="parsedFileTagSeparator">({{ parsedFileTagSeparator }})</span>
<v-menu eager activator="parent" location="bottom" max-height="500">
<v-list>
<v-list-item :key="separator.value"
:append-icon="parsedFileTagSeparator === separator.value ? mdiCheck : undefined"
v-for="separator in allSeparators"
@click="parsedFileTagSeparator = separator.value">
<v-list-item-title class="cursor-pointer">
{{ separator.name }} ({{separator.value}})
</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-btn>
<v-spacer/>
<span>{{ tt('Lines Per Page') }}</span>
<v-select class="ml-2" density="compact" max-width="100"
item-title="title"
item-value="value"
:disabled="loading || submitting"
:items="parsedFileLinesTablePageOptions"
v-model="countPerPage"
/>
<pagination-buttons density="compact"
:disabled="loading || submitting"
:totalPageCount="Math.ceil((parsedFileLines ? parsedFileLines.length : 0) / countPerPage)"
v-model="currentPage"></pagination-buttons>
</div>
</template>
</v-data-table>
</v-window-item>
<v-window-item value="checkData">
<v-data-table
fixed-header
@@ -508,9 +709,9 @@
<v-btn color="secondary" variant="tonal" :disabled="loading || submitting"
:prepend-icon="mdiClose" @click="close(false)"
v-if="currentStep !== 'finalResult'">{{ tt('Cancel') }}</v-btn>
<v-btn color="primary" :disabled="loading || submitting || !importFile"
<v-btn color="primary" :disabled="loading || submitting || (!isImportDataFromTextbox && !importFile) || (isImportDataFromTextbox && !importData)"
:append-icon="!submitting ? mdiArrowRight : undefined" @click="parseData"
v-if="currentStep === 'uploadFile'">
v-if="currentStep === 'defineColumn' || currentStep === 'uploadFile'">
{{ tt('Next') }}
<v-progress-circular indeterminate size="22" class="ml-2" v-if="submitting"></v-progress-circular>
</v-btn>
@@ -585,10 +786,12 @@ import { useTransactionsStore } from '@/stores/transaction.ts';
import { useOverviewStore } from '@/stores/overview.ts';
import { useStatisticsStore } from '@/stores/statistics.ts';
import type { NameValue } from '@/core/base.ts';
import type { NameValue, TypeAndDisplayName } from '@/core/base.ts';
import { KnownDateTimeFormat } from '@/core/datetime.ts';
import { KnownDateTimezoneFormat } from '@/core/timezone.ts';
import { CategoryType } from '@/core/category.ts';
import { TransactionType } from '@/core/transaction.ts';
import type { LocalizedImportFileType, LocalizedImportFileTypeSubType } from '@/core/file.ts';
import { TransactionType, ImportTransactionColumnType } from '@/core/transaction.ts';
import type { LocalizedImportFileType, LocalizedImportFileTypeSubType, LocalizedImportFileTypeSupportedEncodings } from '@/core/file.ts';
import { Account, type CategorizedAccountWithDisplayBalance } from '@/models/account.ts';
import type { TransactionCategory } from '@/models/transaction_category.ts';
import type { TransactionTag } from '@/models/transaction_tag.ts';
@@ -597,6 +800,9 @@ import { ImportTransaction } from '@/models/imported_transaction.ts';
import {
isString,
isNumber,
isObjectEmpty,
getObjectOwnFieldCount,
findDisplayNameByType,
objectFieldToArrayItem
} from '@/lib/common.ts';
import {
@@ -636,6 +842,8 @@ type ConfirmDialogType = InstanceType<typeof ConfirmDialog>;
type SnackBarType = InstanceType<typeof SnackBar>;
type BatchReplaceDialogType = InstanceType<typeof BatchReplaceDialog>;
type ImportTransactionDialogStep = 'uploadFile' | 'defineColumn' | 'checkData' | 'finalResult';
interface ImportTransactionDialogFilter {
minDatetime: number | null; // minDatetime or maxDatetime is null for 'All Date Range', all are not null for 'Custom Date Range'
maxDatetime: number | null;
@@ -657,6 +865,7 @@ defineProps<{
const {
tt,
getAllImportTransactionColumnTypes,
getAllSupportedImportFileTypes,
formatUnixTimeToLongDateTime,
formatAmountWithCurrency,
@@ -679,10 +888,20 @@ const fileInput = useTemplateRef<HTMLInputElement>('fileInput');
const showState = ref<boolean>(false);
const clientSessionId = ref<string>('');
const currentStep = ref<string>('uploadFile');
const currentStep = ref<ImportTransactionDialogStep>('uploadFile');
const fileType = ref<string>('ezbookkeeping');
const fileSubType = ref<string>('ezbookkeeping_csv');
const fileEncoding = ref<string>('utf-8');
const importFile = ref<File | null>(null);
const importData = ref<string>('');
const parsedFileData = ref<string[][] | undefined>(undefined);
const parsedFileIncludeHeader = ref<boolean>(true);
const parsedFileDataColumnMapping = ref<Record<number, number>>({});
const parsedFileTransactionTypeMapping = ref<Record<string, TransactionType>>({});
const parsedFileTimeFormat = ref<string>('');
const parsedFileTimezoneFormat = ref<string>('');
const parsedFileGeoLocationSeparator = ref<string>(' ');
const parsedFileTagSeparator = ref<string>(';');
const importTransactions = ref<ImportTransaction[] | undefined>(undefined);
const editingTransaction = ref<ImportTransaction | null>(null);
const editingTags = ref<string[]>([]);
@@ -713,26 +932,79 @@ const currentTimezoneOffsetMinutes = computed<number>(() => getTimezoneOffsetMin
const defaultCurrency = computed<string>(() => userStore.currentUserDefaultCurrency);
const allSteps = computed<StepBarItem[]>(() => [
{
name: 'uploadFile',
title: tt('Upload File'),
subTitle: tt('Upload Transaction Data File')
},
{
name: 'checkData',
title: tt('Check & Modify'),
subTitle: tt('Check and Modify Your Data')
},
{
name: 'finalResult',
title: tt('Complete'),
subTitle: tt('Data Import Completed')
}
]);
const allSteps = computed<StepBarItem[]>(() => {
const steps: StepBarItem[] = [
{
name: 'uploadFile',
title: tt('Upload File'),
subTitle: tt('Upload Transaction Data File')
}
];
if (fileType.value === 'dsv' || fileType.value === 'dsv_data') {
steps.push({
name: 'defineColumn',
title: tt('Define Column'),
subTitle: tt('Define and Check Column Mapping')
});
}
steps.push(...[
{
name: 'checkData',
title: tt('Check & Modify'),
subTitle: tt('Check and Modify Your Data')
},
{
name: 'finalResult',
title: tt('Complete'),
subTitle: tt('Data Import Completed')
}
]);
return steps;
});
const allImportTransactionColumnTypes = computed<TypeAndDisplayName[]>(() => getAllImportTransactionColumnTypes());
const allSupportedImportFileTypes = computed<LocalizedImportFileType[]>(() => getAllSupportedImportFileTypes());
const allSeparators = computed<NameValue[]>(() => {
const separators: NameValue[] = [
{
name: tt('Space'),
value: ' '
},
{
name: tt('Comma'),
value: ','
},
{
name: tt('Semicolon'),
value: ';'
},
{
name: tt('Tab'),
value: '\t'
},
{
name: tt('Vertical Bar'),
value: '|'
}
];
return separators;
});
const isImportDataFromTextbox = computed<boolean>(() => {
for (const importFileType of allSupportedImportFileTypes.value) {
if (importFileType.type === fileType.value) {
return !!importFileType.dataFromTextbox;
}
}
return false;
});
const allFileSubTypes = computed<LocalizedImportFileTypeSubType[] | undefined>(() => {
for (const importFileType of allSupportedImportFileTypes.value) {
if (importFileType.type === fileType.value) {
@@ -743,6 +1015,16 @@ const allFileSubTypes = computed<LocalizedImportFileTypeSubType[] | undefined>((
return undefined;
});
const allSupportedEncodings = computed<LocalizedImportFileTypeSupportedEncodings[] | undefined>(() => {
for (const importFileType of allSupportedImportFileTypes.value) {
if (importFileType.type === fileType.value) {
return importFileType.supportedEncodings;
}
}
return undefined;
});
const allAccounts = computed<Account[]>(() => accountsStore.allPlainAccounts);
const allVisibleAccounts = computed<Account[]>(() => accountsStore.allVisiblePlainAccounts);
const allVisibleCategorizedAccounts = computed<CategorizedAccountWithDisplayBalance[]>(() => getCategorizedAccountsWithDisplayBalance(allVisibleAccounts.value, showAccountBalance.value));
@@ -799,6 +1081,182 @@ const exportFileGuideDocumentLanguageName = computed<string | undefined>(() => {
const fileName = computed<string>(() => importFile.value?.name || '');
const parsedFileLines = computed<Record<string, string>[] | undefined>(() => {
if (!parsedFileData.value) {
return undefined;
}
const allLines: Record<string, string>[] = [];
const startIndex = parsedFileIncludeHeader.value ? 1 : 0;
for (let i = startIndex, index = 1; i < parsedFileData.value.length; i++, index++) {
const line: Record<string, string> = {};
const columns = parsedFileData.value[i];
for (let j = 0; j < columns.length; j++) {
line['index'] = index.toString();
line[`column${j + 1}`] = columns[j];
}
allLines.push(line);
}
return allLines;
});
const parsedFileLinesTableHeight = computed<number | undefined>(() => {
if (countPerPage.value <= 10 || !parsedFileLines.value || parsedFileLines.value.length <= 10) {
return undefined;
} else {
return 400;
}
});
const parsedFileLinesHeaders = computed<object[]>(() => {
let maxColumnCount = 0;
if (parsedFileData.value) {
for (let i = 0; i < parsedFileData.value.length; i++) {
if (parsedFileData.value[i].length > maxColumnCount) {
maxColumnCount = parsedFileData.value[i].length;
}
}
}
const headers: object[] = [];
headers.push({ key: 'index', value: 'index', title: '#', sortable: true, nowrap: true });
for (let i = 0; i < maxColumnCount; i++) {
let title = `#${i + 1}`;
if (parsedFileIncludeHeader.value && parsedFileData.value && parsedFileData.value[0][i]) {
title = parsedFileData.value[0][i];
}
headers.push({ key: i.toString(), value: `column${i + 1}`, title: title, sortable: true, nowrap: true });
}
return headers;
});
const parsedFileLinesTablePageOptions = computed<ImportTransactionsDialogTablePageOption[]>(() => getTablePageOptions(parsedFileLines.value?.length));
const parsedFileAllTransactionTypes = computed<string[]>(() => {
if (!parsedFileData.value || !parsedFileData.value.length || !isNumber(parsedFileDataColumnMapping.value[ImportTransactionColumnType.TransactionType.type])) {
return [];
}
const allTypeMap: Record<string, boolean> = {};
const allTypes: string[] = [];
const typeColumnIndex = parsedFileDataColumnMapping.value[ImportTransactionColumnType.TransactionType.type];
const startIndex = parsedFileIncludeHeader.value ? 1 : 0;
for (let i = startIndex; i < parsedFileData.value.length; i++) {
if (parsedFileData.value[i].length <= typeColumnIndex) {
continue;
}
const type = parsedFileData.value[i][typeColumnIndex];
if (type && !allTypeMap[type]) {
allTypes.push(type);
allTypeMap[type] = true;
}
}
return allTypes;
});
const parsedFileValidMappedTransactionTypes = computed<Record<string, TransactionType>>(() => {
if (!parsedFileData.value || !parsedFileData.value.length || !isNumber(parsedFileDataColumnMapping.value[ImportTransactionColumnType.TransactionType.type])) {
return {};
}
const result: Record<string, TransactionType> = {};
if (!parsedFileTransactionTypeMapping.value) {
return result;
}
for (const name in parsedFileTransactionTypeMapping.value) {
if (!Object.prototype.hasOwnProperty.call(parsedFileTransactionTypeMapping.value, name)) {
continue;
}
const type = parsedFileTransactionTypeMapping.value[name];
if (TransactionType.ModifyBalance <= type && type <= TransactionType.Transfer) {
result[name] = type;
}
}
return result;
});
const parsedFileAutoDetectedTimeFormat = computed<string | undefined>(() => {
if (!parsedFileData.value || !parsedFileData.value.length || !isNumber(parsedFileDataColumnMapping.value[ImportTransactionColumnType.TransactionTime.type])) {
return undefined;
}
const allDateTimes: string[] = [];
const dateTimeColumnIndex = parsedFileDataColumnMapping.value[ImportTransactionColumnType.TransactionTime.type];
const startIndex = parsedFileIncludeHeader.value ? 1 : 0;
for (let i = startIndex; i < parsedFileData.value.length; i++) {
if (parsedFileData.value[i].length <= dateTimeColumnIndex) {
continue;
}
const dateTime = parsedFileData.value[i][dateTimeColumnIndex];
if (dateTime) {
allDateTimes.push(dateTime);
}
}
const detectedFormats = KnownDateTimeFormat.detectMany(allDateTimes);
if (!detectedFormats || !detectedFormats.length || detectedFormats.length > 1) {
return undefined;
}
return detectedFormats[0].format;
});
const parsedFileAutoDetectedTimezoneFormat = computed<string | undefined>(() => {
if (!parsedFileData.value || !parsedFileData.value.length || !isNumber(parsedFileDataColumnMapping.value[ImportTransactionColumnType.TransactionTimezone.type])) {
return undefined;
}
const allTimezones: string[] = [];
const timezoneColumnIndex = parsedFileDataColumnMapping.value[ImportTransactionColumnType.TransactionTimezone.type];
const startIndex = parsedFileIncludeHeader.value ? 1 : 0;
for (let i = startIndex; i < parsedFileData.value.length; i++) {
if (parsedFileData.value[i].length <= timezoneColumnIndex) {
continue;
}
const timezone = parsedFileData.value[i][timezoneColumnIndex];
if (timezone) {
allTimezones.push(timezone);
}
}
const detectedFormats = KnownDateTimezoneFormat.detectMany(allTimezones);
if (!detectedFormats || !detectedFormats.length || detectedFormats.length > 1) {
return undefined;
}
return detectedFormats[0].value;
});
const importTransactionsTableHeight = computed<number | undefined>(() => {
if (countPerPage.value <= 10 || !importTransactions.value || importTransactions.value.length <= 10) {
return undefined;
@@ -821,30 +1279,7 @@ const importTransactionHeaders = computed<object[]>(() => {
];
});
const importTransactionsTablePageOptions = computed<ImportTransactionsDialogTablePageOption[]>(() => {
const pageOptions: ImportTransactionsDialogTablePageOption[] = [];
if (!importTransactions.value || importTransactions.value.length < 1) {
pageOptions.push({ value: -1, title: tt('All') });
return pageOptions;
}
const availableCountPerPage = [ 5, 10, 15, 20, 25, 30, 50 ];
for (let i = 0; i < availableCountPerPage.length; i++) {
const count = availableCountPerPage[i];
if (importTransactions.value.length < count) {
break;
}
pageOptions.push({ value: count, title: count.toString() });
}
pageOptions.push({ value: -1, title: tt('All') });
return pageOptions;
});
const importTransactionsTablePageOptions = computed<ImportTransactionsDialogTablePageOption[]>(() => getTablePageOptions(importTransactions.value?.length));
const totalPageCount = computed<number>(() => {
if (!importTransactions.value || importTransactions.value.length < 1) {
@@ -1052,6 +1487,55 @@ const displayFilterCustomDateRange = computed<string>(() => {
return `${minDisplayTime} - ${maxDisplayTime}`
});
function getTablePageOptions(linesCount?: number): ImportTransactionsDialogTablePageOption[] {
const pageOptions: ImportTransactionsDialogTablePageOption[] = [];
if (!linesCount || linesCount < 1) {
pageOptions.push({ value: -1, title: tt('All') });
return pageOptions;
}
const availableCountPerPage = [ 5, 10, 15, 20, 25, 30, 50 ];
for (let i = 0; i < availableCountPerPage.length; i++) {
const count = availableCountPerPage[i];
if (linesCount < count) {
break;
}
pageOptions.push({ value: count, title: count.toString() });
}
pageOptions.push({ value: -1, title: tt('All') });
return pageOptions;
}
function getParseDataMappedColumnDisplayName(columnIndex: number): string {
for (const columnType in parsedFileDataColumnMapping.value) {
if (parsedFileDataColumnMapping.value[columnType] === columnIndex) {
return findDisplayNameByType(allImportTransactionColumnTypes.value, parseInt(columnType)) || tt('Unspecified');
}
}
return tt('Unspecified');
}
function updateParseDataMappedColumn(columnIndex: number, columnType: number): void {
if (parsedFileDataColumnMapping.value[columnType] === columnIndex) {
delete parsedFileDataColumnMapping.value[columnType];
} else {
parsedFileDataColumnMapping.value[columnType] = columnIndex;
}
for (const otherColumnType in parsedFileDataColumnMapping.value) {
if (otherColumnType !== columnType.toString() && parsedFileDataColumnMapping.value[otherColumnType] === columnIndex) {
delete parsedFileDataColumnMapping.value[otherColumnType];
}
}
}
function isTransactionDisplayed(transaction: ImportTransaction): boolean {
if (isNumber(filters.value.minDatetime) && isNumber(filters.value.maxDatetime) && (transaction.time < filters.value.minDatetime || transaction.time > filters.value.maxDatetime)) {
return false;
@@ -1328,8 +1812,18 @@ function getCurrentInvalidTagNames(): NameValue[] {
function open(): Promise<void> {
fileType.value = 'ezbookkeeping';
fileSubType.value = 'ezbookkeeping_csv';
fileEncoding.value = 'utf-8';
currentStep.value = 'uploadFile';
importFile.value = null;
importData.value = '';
parsedFileData.value = undefined;
parsedFileIncludeHeader.value = true;
parsedFileDataColumnMapping.value = {};
parsedFileTransactionTypeMapping.value = {};
parsedFileTimeFormat.value = '';
parsedFileTimezoneFormat.value = '';
parsedFileGeoLocationSeparator.value = ' ';
parsedFileTagSeparator.value = ';';
importTransactions.value = undefined;
editingTransaction.value = null;
editingTags.value = [];
@@ -1396,52 +1890,165 @@ function setImportFile(event: Event): void {
}
function parseData(): void {
if (!importFile.value) {
snackbar.value?.showError('Please select a file to import');
return;
}
submitting.value = true;
let uploadFile: File;
let type: string = fileType.value;
let encoding: string | undefined = undefined;
if (allFileSubTypes.value) {
type = fileSubType.value;
}
transactionsStore.parseImportTransaction({
fileType: type,
importFile: importFile.value
}).then(response => {
const parsedTransactions: ImportTransaction[] = [];
if (allSupportedEncodings.value) {
encoding = fileEncoding.value;
}
if (response.items) {
for (let i = 0; i < response.items.length; i++) {
const parsedTransaction = ImportTransaction.of(response.items[i], i);
parsedTransactions.push(parsedTransaction);
if (!isImportDataFromTextbox.value) {
if (!importFile.value) {
snackbar.value?.showError('Please select a file to import');
return;
}
uploadFile = importFile.value;
} else if (isImportDataFromTextbox.value) {
if (!importData.value) {
snackbar.value?.showError('No data to import');
return;
}
if (type === 'custom_csv') {
uploadFile = new File([importData.value], 'import.csv', { type: 'text/csv' });
} else if (type === 'custom_tsv') {
uploadFile = new File([importData.value], 'import.tsv', { type: 'text/tab-separated-values' });
} else {
snackbar.value?.showError('Parameter Invalid');
return;
}
encoding = 'utf-8';
} else { // should not happen, but ts would check whether uploadFile has been assigned a value
snackbar.value?.showMessage('An error occurred');
return;
}
const isDsvFileType: boolean = fileType.value === 'dsv' || fileType.value === 'dsv_data';
if (isDsvFileType && currentStep.value === 'uploadFile') {
submitting.value = true;
transactionsStore.parseImportDsvFile({
fileType: type,
fileEncoding: encoding,
importFile: uploadFile
}).then(response => {
if (response && response.length) {
parsedFileData.value = response;
currentPage.value = 1;
countPerPage.value = 10;
currentStep.value = 'defineColumn';
} else {
parsedFileData.value = undefined;
snackbar.value?.showError('No data to import');
}
submitting.value = false;
}).catch(error => {
submitting.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
} else {
let columnMapping: Record<number, number> | undefined = undefined;
let transactionTypeMapping: Record<string, TransactionType> | undefined = undefined;
let hasHeaderLine: boolean | undefined = undefined;
let timeFormat: string | undefined = undefined;
let timezoneFormat: string | undefined = undefined;
let geoLocationSeparator: string | undefined = undefined;
let tagSeparator: string | undefined = undefined;
if (isDsvFileType) {
columnMapping = parsedFileDataColumnMapping.value;
transactionTypeMapping = parsedFileValidMappedTransactionTypes.value;
hasHeaderLine = parsedFileIncludeHeader.value;
timeFormat = parsedFileTimeFormat.value;
timezoneFormat = parsedFileTimezoneFormat.value;
geoLocationSeparator = parsedFileGeoLocationSeparator.value;
tagSeparator = parsedFileTagSeparator.value;
if (!columnMapping
|| !isNumber(columnMapping[ImportTransactionColumnType.TransactionTime.type])
|| !isNumber(columnMapping[ImportTransactionColumnType.TransactionType.type])
|| !isNumber(columnMapping[ImportTransactionColumnType.Amount.type])) {
snackbar.value?.showError('Missing transaction time, transaction type, or amount column mapping');
return;
}
if (!transactionTypeMapping || isObjectEmpty(transactionTypeMapping)) {
snackbar.value?.showError('Transaction type mapping is not set');
return;
}
if (!parsedFileTimeFormat.value) {
timeFormat = parsedFileAutoDetectedTimeFormat.value;
}
if (!parsedFileTimezoneFormat.value) {
timezoneFormat = parsedFileAutoDetectedTimezoneFormat.value;
}
if (!timeFormat) {
snackbar.value?.showError('Transaction time format is not set');
return;
}
}
importTransactions.value = parsedTransactions;
editingTransaction.value = null;
editingTags.value = [];
currentPage.value = 1;
submitting.value = true;
if (importTransactions.value && importTransactions.value.length >= 0 && importTransactions.value.length < 10) {
countPerPage.value = -1;
} else {
transactionsStore.parseImportTransaction({
fileType: type,
fileEncoding: encoding,
importFile: uploadFile,
columnMapping: columnMapping,
transactionTypeMapping: transactionTypeMapping,
hasHeaderLine: hasHeaderLine,
timeFormat: timeFormat,
timezoneFormat: timezoneFormat,
geoSeparator: geoLocationSeparator,
tagSeparator: tagSeparator
}).then(response => {
const parsedTransactions: ImportTransaction[] = [];
if (response.items) {
for (let i = 0; i < response.items.length; i++) {
const parsedTransaction = ImportTransaction.of(response.items[i], i);
parsedTransactions.push(parsedTransaction);
}
}
importTransactions.value = parsedTransactions;
editingTransaction.value = null;
editingTags.value = [];
currentPage.value = 1;
if (importTransactions.value && importTransactions.value.length >= 0 && importTransactions.value.length < 10) {
countPerPage.value = -1;
} else {
countPerPage.value = 10;
}
currentPage.value = 1;
countPerPage.value = 10;
}
currentStep.value = 'checkData';
submitting.value = false;
}).catch(error => {
submitting.value = false;
currentStep.value = 'checkData';
submitting.value = false;
}).catch(error => {
submitting.value = false;
if (!error.processed) {
snackbar.value?.showError(error);
}
});
if (!error.processed) {
snackbar.value?.showError(error);
}
});
}
}
function submit(): void {
@@ -1820,6 +2427,40 @@ defineExpose({
</script>
<style>
.transaction-types-popup-menu .transaction-types-toggle {
overflow-x: auto;
white-space: nowrap;
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle {
height: auto !important;
padding: 0;
border: none;
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn {
border-color: rgba(var(--v-border-color), var(--v-border-opacity));
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle > .v-btn {
border: thin solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.transaction-types-popup-menu .transaction-types-toggle.v-btn-toggle button.v-btn {
width: auto !important;
}
.import-transaction-table .v-autocomplete.v-input.v-input--density-compact:not(.v-textarea) .v-field__input,
.import-transaction-table .v-select.v-input.v-input--density-compact:not(.v-textarea) .v-field__input {
min-height: inherit;