mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-14 06:57:35 +08:00
import transaction from custom delimiter-separated values file
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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": "Пакетная замена выбранных категорий расходов",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "批量替换选中的支出分类",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user