From 78c5b1704a64ba315038168cce0f02b006c52cab Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sat, 15 Mar 2025 21:00:53 +0800 Subject: [PATCH] import transactions from Feidee Mymoney (Elecloud) --- go.mod | 6 + go.sum | 13 + .../excel_ooxml_file_imported_data_table.go | 211 +++++++++++++++ ...cel_ooxml_file_imported_data_table_test.go | 246 ++++++++++++++++++ ...ey_elecloud_transaction_data_row_parser.go | 71 +++++ ...oud_transaction_data_xlsx_file_importer.go | 46 ++++ ...ransaction_data_xlsx_file_importer_test.go | 117 +++++++++ pkg/converters/transaction_data_converters.go | 2 + src/consts/file.ts | 9 + src/locales/de.json | 1 + src/locales/en.json | 1 + src/locales/es.json | 1 + src/locales/ja.json | 1 + src/locales/ru.json | 1 + src/locales/vi.json | 1 + src/locales/zh_Hans.json | 1 + testdata/empty_excel_file.xlsx | Bin 0 -> 8378 bytes .../feidee_mymoney_elecloud_test_file.xlsx | Bin 0 -> 9804 bytes testdata/multiple_sheets_excel_file.xlsx | Bin 0 -> 12076 bytes ..._with_different_header_row_excel_file.xlsx | Bin 0 -> 9751 bytes testdata/only_one_row_excel_file.xlsx | Bin 0 -> 8853 bytes testdata/simple_excel_file.xlsx | Bin 0 -> 8922 bytes third-party-dependencies.json | 6 + 23 files changed, 734 insertions(+) create mode 100644 pkg/converters/excel/excel_ooxml_file_imported_data_table.go create mode 100644 pkg/converters/excel/excel_ooxml_file_imported_data_table_test.go create mode 100644 pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_row_parser.go create mode 100644 pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go create mode 100644 pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go create mode 100644 testdata/empty_excel_file.xlsx create mode 100644 testdata/feidee_mymoney_elecloud_test_file.xlsx create mode 100644 testdata/multiple_sheets_excel_file.xlsx create mode 100644 testdata/multiple_sheets_with_different_header_row_excel_file.xlsx create mode 100644 testdata/only_one_row_excel_file.xlsx create mode 100644 testdata/simple_excel_file.xlsx diff --git a/go.mod b/go.mod index 97a2d2d1..d10595a6 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v2 v2.27.5 github.com/wk8/go-ordered-map/v2 v2.1.8 + github.com/xuri/excelize/v2 v2.9.0 golang.org/x/crypto v0.33.0 golang.org/x/net v0.34.0 golang.org/x/text v0.22.0 @@ -66,8 +67,11 @@ require ( github.com/minio/md5-simd v1.1.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect github.com/rs/xid v1.6.0 // indirect @@ -77,6 +81,8 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect golang.org/x/arch v0.13.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/sys v0.30.0 // indirect diff --git a/go.sum b/go.sum index dd53a1b3..a8344e45 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -122,6 +124,11 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko= @@ -158,6 +165,12 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA= golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= diff --git a/pkg/converters/excel/excel_ooxml_file_imported_data_table.go b/pkg/converters/excel/excel_ooxml_file_imported_data_table.go new file mode 100644 index 00000000..4dc01a62 --- /dev/null +++ b/pkg/converters/excel/excel_ooxml_file_imported_data_table.go @@ -0,0 +1,211 @@ +package excel + +import ( + "bytes" + "fmt" + + "github.com/xuri/excelize/v2" + + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/errs" +) + +// excelOOXMLSheet defines the structure of excel (Office Open XML) file sheet +type excelOOXMLSheet struct { + sheetName string + allData [][]string +} + +// ExcelOOXMLFileImportedDataTable defines the structure of excel (Office Open XML) file data table +type ExcelOOXMLFileImportedDataTable struct { + sheets []*excelOOXMLSheet + headerLineColumnNames []string +} + +// ExcelOOXMLFileDataRow defines the structure of excel (Office Open XML) file data table row +type ExcelOOXMLFileDataRow struct { + sheet *excelOOXMLSheet + rowData []string + rowIndex int +} + +// ExcelOOXMLFileDataRowIterator defines the structure of excel (Office Open XML) file data table row iterator +type ExcelOOXMLFileDataRowIterator struct { + dataTable *ExcelOOXMLFileImportedDataTable + currentSheetIndex int + currentRowIndexInSheet int +} + +// DataRowCount returns the total count of data row +func (t *ExcelOOXMLFileImportedDataTable) DataRowCount() int { + totalDataRowCount := 0 + + for i := 0; i < len(t.sheets); i++ { + sheet := t.sheets[i] + + if len(sheet.allData) < 1 { + continue + } + + totalDataRowCount += len(sheet.allData) - 1 + } + + return totalDataRowCount +} + +// HeaderColumnNames returns the header column name list +func (t *ExcelOOXMLFileImportedDataTable) HeaderColumnNames() []string { + return t.headerLineColumnNames +} + +// DataRowIterator returns the iterator of data row +func (t *ExcelOOXMLFileImportedDataTable) DataRowIterator() datatable.ImportedDataRowIterator { + return &ExcelOOXMLFileDataRowIterator{ + dataTable: t, + currentSheetIndex: 0, + currentRowIndexInSheet: 0, + } +} + +// ColumnCount returns the total count of column in this data row +func (r *ExcelOOXMLFileDataRow) ColumnCount() int { + return len(r.rowData) +} + +// GetData returns the data in the specified column index +func (r *ExcelOOXMLFileDataRow) GetData(columnIndex int) string { + if columnIndex < 0 || columnIndex >= len(r.rowData) { + return "" + } + + return r.rowData[columnIndex] +} + +// HasNext returns whether the iterator does not reach the end +func (t *ExcelOOXMLFileDataRowIterator) HasNext() bool { + sheets := t.dataTable.sheets + + if t.currentSheetIndex >= len(sheets) { + return false + } + + currentSheet := sheets[t.currentSheetIndex] + + if t.currentRowIndexInSheet+1 < len(currentSheet.allData) { + return true + } + + for i := t.currentSheetIndex + 1; i < len(sheets); i++ { + sheet := sheets[i] + + if len(sheet.allData) <= 1 { + continue + } + + return true + } + + return false +} + +// CurrentRowId returns current index +func (t *ExcelOOXMLFileDataRowIterator) CurrentRowId() string { + return fmt.Sprintf("table#%d-row#%d", t.currentSheetIndex, t.currentRowIndexInSheet) +} + +// Next returns the next imported data row +func (t *ExcelOOXMLFileDataRowIterator) Next() datatable.ImportedDataRow { + sheets := t.dataTable.sheets + currentRowIndexInTable := t.currentRowIndexInSheet + + for i := t.currentSheetIndex; i < len(sheets); i++ { + sheet := sheets[i] + + if currentRowIndexInTable+1 < len(sheet.allData) { + t.currentRowIndexInSheet++ + currentRowIndexInTable = t.currentRowIndexInSheet + break + } + + t.currentSheetIndex++ + t.currentRowIndexInSheet = 0 + currentRowIndexInTable = 0 + } + + if t.currentSheetIndex >= len(sheets) { + return nil + } + + currentSheet := sheets[t.currentSheetIndex] + + if t.currentRowIndexInSheet >= len(currentSheet.allData) { + return nil + } + + return &ExcelOOXMLFileDataRow{ + sheet: currentSheet, + rowData: currentSheet.allData[t.currentRowIndexInSheet], + rowIndex: t.currentRowIndexInSheet, + } +} + +// CreateNewExcelOOXMLFileImportedDataTable returns excel (Office Open XML) data table by file binary data +func CreateNewExcelOOXMLFileImportedDataTable(data []byte) (*ExcelOOXMLFileImportedDataTable, error) { + reader := bytes.NewReader(data) + file, err := excelize.OpenReader(reader) + + defer file.Close() + + if err != nil { + return nil, err + } + + sheetNames := file.GetSheetList() + var headerRowItems []string + var sheets []*excelOOXMLSheet + + for i := 0; i < len(sheetNames); i++ { + sheetName := sheetNames[i] + allData, err := file.GetRows(sheetName) + + if err != nil { + return nil, err + } + + if allData == nil || len(allData) < 1 { + continue + } + + row := allData[0] + + if i == 0 { + for j := 0; j < len(row); j++ { + headerItem := row[j] + + if headerItem == "" { + break + } + + headerRowItems = append(headerRowItems, headerItem) + } + } else { + for j := 0; j < min(len(row), len(headerRowItems)); j++ { + headerItem := row[j] + + if headerItem != headerRowItems[j] { + return nil, errs.ErrFieldsInMultiTableAreDifferent + } + } + } + + sheets = append(sheets, &excelOOXMLSheet{ + sheetName: sheetName, + allData: allData, + }) + } + + return &ExcelOOXMLFileImportedDataTable{ + sheets: sheets, + headerLineColumnNames: headerRowItems, + }, nil +} diff --git a/pkg/converters/excel/excel_ooxml_file_imported_data_table_test.go b/pkg/converters/excel/excel_ooxml_file_imported_data_table_test.go new file mode 100644 index 00000000..d70ae52f --- /dev/null +++ b/pkg/converters/excel/excel_ooxml_file_imported_data_table_test.go @@ -0,0 +1,246 @@ +package excel + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/errs" +) + +func TestExcelOOXMLFileImportedDataTableDataRowCount(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, err) + assert.Equal(t, 2, datatable.DataRowCount()) +} + +func TestExcelOOXMLFileImportedDataTableDataRowCount_MultipleSheets(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, err) + assert.Equal(t, 5, datatable.DataRowCount()) +} + +func TestExcelOOXMLFileImportedDataTableDataRowCount_OnlyHeaderLine(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, err) + assert.Equal(t, 0, datatable.DataRowCount()) +} + +func TestExcelOOXMLFileImportedDataTableDataRowCount_EmptyContent(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, err) + assert.Equal(t, 0, datatable.DataRowCount()) +} + +func TestExcelOOXMLFileImportedDataTableHeaderColumnNames(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.EqualValues(t, []string{"A1", "B1", "C1"}, datatable.HeaderColumnNames()) +} + +func TestExcelOOXMLFileImportedDataTableHeaderColumnNames_EmptyContent(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.Nil(t, datatable.HeaderColumnNames()) +} + +func TestExcelOOXMLFileDataRowIterator(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + assert.True(t, iterator.HasNext()) + + // data row 1 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // data row 2 + assert.NotNil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row 3 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row 4 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) +} + +func TestExcelOOXMLFileDataRowIterator_MultipleSheets(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + assert.True(t, iterator.HasNext()) + + // sheet 1 data row 1 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // sheet 1 data row 2 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // sheet 3 data row 1 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // sheet 5 data row 1 + assert.NotNil(t, iterator.Next()) + assert.True(t, iterator.HasNext()) + + // sheet 5 data row 2 + assert.NotNil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) +} + +func TestExcelOOXMLFileDataRowIterator_OnlyHeaderLine(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/only_one_row_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + assert.False(t, iterator.HasNext()) + + // not existed data row 1 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row 2 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) +} + +func TestExcelOOXMLFileDataRowIterator_EmptyContent(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/empty_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + assert.False(t, iterator.HasNext()) + + // not existed data row 1 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) + + // not existed data row 2 + assert.Nil(t, iterator.Next()) + assert.False(t, iterator.HasNext()) +} + +func TestExcelOOXMLFileDataRowColumnCount(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + + row1 := iterator.Next() + assert.EqualValues(t, 3, row1.ColumnCount()) + + row2 := iterator.Next() + assert.EqualValues(t, 3, row2.ColumnCount()) +} + +func TestExcelOOXMLFileDataRowGetData(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + + row1 := iterator.Next() + assert.Equal(t, "A2", row1.GetData(0)) + assert.Equal(t, "B2", row1.GetData(1)) + assert.Equal(t, "C2", row1.GetData(2)) + + row2 := iterator.Next() + assert.Equal(t, "A3", row2.GetData(0)) + assert.Equal(t, "B3", row2.GetData(1)) + assert.Equal(t, "C3", row2.GetData(2)) +} + +func TestExcelOOXMLFileDataRowGetData_GetNotExistedColumnData(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/simple_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + + row1 := iterator.Next() + assert.Equal(t, "", row1.GetData(3)) +} + +func TestExcelOOXMLFileDataRowGetData_MultipleSheets(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/multiple_sheets_excel_file.xlsx") + assert.Nil(t, err) + + datatable, err := CreateNewExcelOOXMLFileImportedDataTable(testdata) + iterator := datatable.DataRowIterator() + + sheet1Row1 := iterator.Next() + assert.Equal(t, "1-A2", sheet1Row1.GetData(0)) + assert.Equal(t, "1-B2", sheet1Row1.GetData(1)) + assert.Equal(t, "1-C2", sheet1Row1.GetData(2)) + + sheet1Row2 := iterator.Next() + assert.Equal(t, "1-A3", sheet1Row2.GetData(0)) + assert.Equal(t, "1-B3", sheet1Row2.GetData(1)) + assert.Equal(t, "1-C3", sheet1Row2.GetData(2)) + + // skip empty sheet2 + + sheet3Row1 := iterator.Next() + assert.Equal(t, "3-A2", sheet3Row1.GetData(0)) + assert.Equal(t, "3-B2", sheet3Row1.GetData(1)) + assert.Equal(t, "", sheet3Row1.GetData(2)) + + // skip no data row sheet4 + + sheet5Row1 := iterator.Next() + assert.Equal(t, "5-A2", sheet5Row1.GetData(0)) + assert.Equal(t, "5-B2", sheet5Row1.GetData(1)) + assert.Equal(t, "5-C2", sheet5Row1.GetData(2)) + + sheet5Row2 := iterator.Next() + assert.Equal(t, "5-A3", sheet5Row2.GetData(0)) + assert.Equal(t, "5-B3", sheet5Row2.GetData(1)) + assert.Equal(t, "5-C3", sheet5Row2.GetData(2)) +} + +func TestCreateNewExcelOOXMLFileImportedDataTable_MultipleSheetsWithDifferentHeaders(t *testing.T) { + testdata, err := os.ReadFile("../../../testdata/multiple_sheets_with_different_header_row_excel_file.xlsx") + assert.Nil(t, err) + + _, err = CreateNewExcelOOXMLFileImportedDataTable(testdata) + assert.EqualError(t, err, errs.ErrFieldsInMultiTableAreDifferent.Message) +} diff --git a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_row_parser.go b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_row_parser.go new file mode 100644 index 00000000..b48759c7 --- /dev/null +++ b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_row_parser.go @@ -0,0 +1,71 @@ +package feidee + +import ( + "strings" + + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +var FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_MODIFY_BALANCE_NAME = "余额变更" +var FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_INCOME_NAME = "收入" +var FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_EXPENSE_NAME = "支出" + +var feideeMymoneyElecloudTransactionTypeNameMapping = map[string]models.TransactionType{ + FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_MODIFY_BALANCE_NAME: models.TRANSACTION_TYPE_MODIFY_BALANCE, + FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_INCOME_NAME: models.TRANSACTION_TYPE_INCOME, + FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_EXPENSE_NAME: models.TRANSACTION_TYPE_EXPENSE, + "转账": models.TRANSACTION_TYPE_TRANSFER, + "借入": models.TRANSACTION_TYPE_TRANSFER, + "借出": models.TRANSACTION_TYPE_TRANSFER, + "收债": models.TRANSACTION_TYPE_TRANSFER, + "还债": models.TRANSACTION_TYPE_TRANSFER, + "代付": models.TRANSACTION_TYPE_TRANSFER, + "报销": models.TRANSACTION_TYPE_TRANSFER, + "退款": models.TRANSACTION_TYPE_EXPENSE, +} + +// feideeMymoneyElecloudTransactionDataRowParser defines the structure of feidee mymoney (elecloud) transaction data row parser +type feideeMymoneyElecloudTransactionDataRowParser struct { +} + +// GetAddedColumns returns the added columns after converting the data row +func (p *feideeMymoneyElecloudTransactionDataRowParser) GetAddedColumns() []datatable.TransactionDataTableColumn { + return nil +} + +// Parse returns the converted transaction data row +func (p *feideeMymoneyElecloudTransactionDataRowParser) Parse(data map[datatable.TransactionDataTableColumn]string) (rowData map[datatable.TransactionDataTableColumn]string, rowDataValid bool, err error) { + rowData = make(map[datatable.TransactionDataTableColumn]string, len(data)) + + for column, value := range data { + rowData[column] = value + } + + rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = strings.ReplaceAll(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT], ",", "") // remove thousand separator + + if rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] == FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_MODIFY_BALANCE_NAME { + amount, err := utils.ParseAmount(rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT]) + + if err != nil { + return nil, false, errs.ErrAmountInvalid + } + + // balance modification transaction in feidee mymoney (elecloud) is not the opening balance transaction, it can be added many times + if amount >= 0 { + rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_INCOME_NAME + } else { + rowData[datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE] = FEIDEE_MYMONEY_ELECLOUD_TRANSACTION_TYPE_EXPENSE_NAME + rowData[datatable.TRANSACTION_DATA_TABLE_AMOUNT] = utils.FormatAmount(-amount) + } + } + + return rowData, true, nil +} + +// createFeideeMymoneyElecloudTransactionDataRowParser returns feidee mymoney (elecloud) transaction data row parser +func createFeideeMymoneyElecloudTransactionDataRowParser() datatable.TransactionDataRowParser { + return &feideeMymoneyElecloudTransactionDataRowParser{} +} diff --git a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go new file mode 100644 index 00000000..0d9be1ec --- /dev/null +++ b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer.go @@ -0,0 +1,46 @@ +package feidee + +import ( + "github.com/mayswind/ezbookkeeping/pkg/converters/converter" + "github.com/mayswind/ezbookkeeping/pkg/converters/datatable" + "github.com/mayswind/ezbookkeeping/pkg/converters/excel" + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/models" +) + +var feideeMymoneyElecloudDataColumnNameMapping = map[datatable.TransactionDataTableColumn]string{ + datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TIME: "日期", + datatable.TRANSACTION_DATA_TABLE_TRANSACTION_TYPE: "交易类型", + datatable.TRANSACTION_DATA_TABLE_CATEGORY: "分类", + datatable.TRANSACTION_DATA_TABLE_SUB_CATEGORY: "子分类", + datatable.TRANSACTION_DATA_TABLE_ACCOUNT_NAME: "账户1", + datatable.TRANSACTION_DATA_TABLE_ACCOUNT_CURRENCY: "账户币种", + datatable.TRANSACTION_DATA_TABLE_AMOUNT: "金额", + datatable.TRANSACTION_DATA_TABLE_RELATED_ACCOUNT_NAME: "账户2", + datatable.TRANSACTION_DATA_TABLE_DESCRIPTION: "备注", +} + +// feideeMymoneyElecloudTransactionDataXlsxFileImporter defines the structure of feidee mymoney (elecloud) xlsx importer for transaction data +type feideeMymoneyElecloudTransactionDataXlsxFileImporter struct { + converter.DataTableTransactionDataImporter +} + +// Initialize a feidee mymoney (elecloud) transaction data xlsx file importer singleton instance +var ( + FeideeMymoneyElecloudTransactionDataXlsxFileImporter = &feideeMymoneyElecloudTransactionDataXlsxFileImporter{} +) + +// ParseImportedData returns the imported data by parsing the feidee mymoney (elecloud) transaction xlsx data +func (c *feideeMymoneyElecloudTransactionDataXlsxFileImporter) 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) { + dataTable, err := excel.CreateNewExcelOOXMLFileImportedDataTable(data) + + if err != nil { + return nil, nil, nil, nil, nil, nil, err + } + + transactionRowParser := createFeideeMymoneyElecloudTransactionDataRowParser() + transactionDataTable := datatable.CreateNewImportedTransactionDataTableWithRowParser(dataTable, feideeMymoneyElecloudDataColumnNameMapping, transactionRowParser) + dataTableImporter := converter.CreateNewSimpleImporter(feideeMymoneyElecloudTransactionTypeNameMapping) + + return dataTableImporter.ParseImportedData(ctx, user, transactionDataTable, defaultTimezoneOffset, accountMap, expenseCategoryMap, incomeCategoryMap, transferCategoryMap, tagMap) +} diff --git a/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go new file mode 100644 index 00000000..034f432e --- /dev/null +++ b/pkg/converters/feidee/feidee_mymoney_elecloud_transaction_data_xlsx_file_importer_test.go @@ -0,0 +1,117 @@ +package feidee + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +func TestFeideeMymoneyElecloudTransactionDataXlsxImporterParseImportedData_MinimumValidData(t *testing.T) { + converter := FeideeMymoneyElecloudTransactionDataXlsxFileImporter + context := core.NewNullContext() + + user := &models.User{ + Uid: 1234567890, + DefaultCurrency: "USD", + } + + testdata, err := os.ReadFile("../../../testdata/feidee_mymoney_elecloud_test_file.xlsx") + assert.Nil(t, err) + + allNewTransactions, allNewAccounts, allNewSubExpenseCategories, allNewSubIncomeCategories, allNewSubTransferCategories, allNewTags, err := converter.ParseImportedData(context, user, testdata, 0, nil, nil, nil, nil, nil) + assert.Nil(t, err) + + assert.Equal(t, 7, len(allNewTransactions)) + assert.Equal(t, 2, len(allNewAccounts)) + assert.Equal(t, 3, len(allNewSubExpenseCategories)) + assert.Equal(t, 3, len(allNewSubIncomeCategories)) + assert.Equal(t, 1, len(allNewSubTransferCategories)) + assert.Equal(t, 0, len(allNewTags)) + + assert.Equal(t, int64(1234567890), allNewTransactions[0].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[0].Type) + assert.Equal(t, "2024-09-01 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[0].TransactionTime), time.UTC)) + assert.Equal(t, int64(12345), allNewTransactions[0].Amount) + assert.Equal(t, "Test Account", allNewTransactions[0].OriginalSourceAccountName) + assert.Equal(t, "", allNewTransactions[0].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[1].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[1].Type) + assert.Equal(t, "2024-09-01 01:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[1].TransactionTime), time.UTC)) + assert.Equal(t, int64(12), allNewTransactions[1].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[1].OriginalSourceAccountName) + assert.Equal(t, "", allNewTransactions[1].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[2].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[2].Type) + assert.Equal(t, "2024-09-01 01:23:45", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[2].TransactionTime), time.UTC)) + assert.Equal(t, int64(12), allNewTransactions[2].Amount) + assert.Equal(t, "Test Account", allNewTransactions[2].OriginalSourceAccountName) + assert.Equal(t, "Test Category", allNewTransactions[2].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[3].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[3].Type) + assert.Equal(t, "2024-09-01 12:34:56", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[3].TransactionTime), time.UTC)) + assert.Equal(t, int64(100), allNewTransactions[3].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[3].OriginalSourceAccountName) + assert.Equal(t, "Test Category2", allNewTransactions[3].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[4].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_TRANSFER_OUT, allNewTransactions[4].Type) + assert.Equal(t, "2024-09-01 23:59:59", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[4].TransactionTime), time.UTC)) + assert.Equal(t, int64(5), allNewTransactions[4].Amount) + assert.Equal(t, "Test Comment5", allNewTransactions[4].Comment) + assert.Equal(t, "Test Account", allNewTransactions[4].OriginalSourceAccountName) + assert.Equal(t, "Test Account2", allNewTransactions[4].OriginalDestinationAccountName) + assert.Equal(t, "", allNewTransactions[4].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[5].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_INCOME, allNewTransactions[5].Type) + assert.Equal(t, "2024-09-10 00:00:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[5].TransactionTime), time.UTC)) + assert.Equal(t, int64(-654300), allNewTransactions[5].Amount) + assert.Equal(t, "Test Account2", allNewTransactions[5].OriginalSourceAccountName) + assert.Equal(t, "Test Category5", allNewTransactions[5].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewTransactions[6].Uid) + assert.Equal(t, models.TRANSACTION_DB_TYPE_EXPENSE, allNewTransactions[6].Type) + assert.Equal(t, "2024-09-11 05:06:00", utils.FormatUnixTimeToLongDateTime(utils.GetUnixTimeFromTransactionTime(allNewTransactions[6].TransactionTime), time.UTC)) + assert.Equal(t, int64(-112340), allNewTransactions[6].Amount) + assert.Equal(t, "Foo#\\r\\nBar", allNewTransactions[6].Comment) + assert.Equal(t, "Test Account", allNewTransactions[6].OriginalSourceAccountName) + assert.Equal(t, "Test Category4", allNewTransactions[6].OriginalCategoryName) + + assert.Equal(t, int64(1234567890), allNewAccounts[0].Uid) + assert.Equal(t, "Test Account2", allNewAccounts[0].Name) + assert.Equal(t, "CNY", allNewAccounts[0].Currency) + + assert.Equal(t, int64(1234567890), allNewAccounts[1].Uid) + assert.Equal(t, "Test Account", allNewAccounts[1].Name) + assert.Equal(t, "CNY", allNewAccounts[1].Currency) + + assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[0].Uid) + assert.Equal(t, "", allNewSubExpenseCategories[0].Name) + + assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[1].Uid) + assert.Equal(t, "Test Category4", allNewSubExpenseCategories[1].Name) + + assert.Equal(t, int64(1234567890), allNewSubExpenseCategories[2].Uid) + assert.Equal(t, "Test Category2", allNewSubExpenseCategories[2].Name) + + assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[0].Uid) + assert.Equal(t, "", allNewSubIncomeCategories[0].Name) + + assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[1].Uid) + assert.Equal(t, "Test Category5", allNewSubIncomeCategories[1].Name) + + assert.Equal(t, int64(1234567890), allNewSubIncomeCategories[2].Uid) + assert.Equal(t, "Test Category", allNewSubIncomeCategories[2].Name) + + assert.Equal(t, int64(1234567890), allNewSubTransferCategories[0].Uid) + assert.Equal(t, "", allNewSubTransferCategories[0].Name) +} diff --git a/pkg/converters/transaction_data_converters.go b/pkg/converters/transaction_data_converters.go index 1ba60b8c..3c14d6b7 100644 --- a/pkg/converters/transaction_data_converters.go +++ b/pkg/converters/transaction_data_converters.go @@ -54,6 +54,8 @@ func GetTransactionDataImporter(fileType string) (converter.TransactionDataImpor return feidee.FeideeMymoneyAppTransactionDataCsvFileImporter, nil } else if fileType == "feidee_mymoney_xls" { return feidee.FeideeMymoneyWebTransactionDataXlsFileImporter, nil + } else if fileType == "feidee_mymoney_elecloud_xlsx" { + return feidee.FeideeMymoneyElecloudTransactionDataXlsxFileImporter, nil } else if fileType == "alipay_app_csv" { return alipay.AlipayAppTransactionDataCsvFileImporter, nil } else if fileType == "alipay_web_csv" { diff --git a/src/consts/file.ts b/src/consts/file.ts index c21c6266..9755dcee 100644 --- a/src/consts/file.ts +++ b/src/consts/file.ts @@ -189,6 +189,15 @@ export const SUPPORTED_IMPORT_FILE_TYPES: ImportFileType[] = [ anchor: '如何获取随手记web版数据导出文件' } }, + { + type: 'feidee_mymoney_elecloud_xlsx', + name: 'Feidee MyMoney (Elecloud) Data Export File', + extensions: '.xlsx', + document: { + supportMultiLanguages: 'zh-Hans', + anchor: '如何获取随手记神象云账本数据导出文件' + } + }, { type: 'alipay_app_csv', name: 'Alipay (App) Transaction Flow File', diff --git a/src/locales/de.json b/src/locales/de.json index b5b83bc5..95a0ea85 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Firefly III-Datenexportdatei", "Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App)-Datenexportdatei", "Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web)-Datenexportdatei", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Alipay (App)-Transaktionsflussdatei", "Alipay (Web) Transaction Flow File": "Alipay (Web)-Transaktionsflussdatei", "WeChat Pay Billing File": "WeChat Pay-Abrechnungsdatei", diff --git a/src/locales/en.json b/src/locales/en.json index f88956d5..96c2067f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Firefly III Data Export File", "Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) Data Export File", "Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) Data Export File", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Alipay (App) Transaction Flow File", "Alipay (Web) Transaction Flow File": "Alipay (Web) Transaction Flow File", "WeChat Pay Billing File": "WeChat Pay Billing File", diff --git a/src/locales/es.json b/src/locales/es.json index edf54dc3..20a9664b 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1653,6 +1653,7 @@ "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)", "Feidee MyMoney (Web) Data Export File": "Archivo de exportación de datos Feidee MyMoney (Web)", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Archivo de flujo de transacciones de Alipay (aplicación)", "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", diff --git a/src/locales/ja.json b/src/locales/ja.json index 2942d52b..489d0a37 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Firefly III データエクスポートファイル", "Feidee MyMoney (App) Data Export File": "Feidee MyMoney (App) データベースファイル", "Feidee MyMoney (Web) Data Export File": "Feidee MyMoney (Web) データベースファイル", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Alipay (App) トランザクションフローファイル", "Alipay (Web) Transaction Flow File": "Alipay (Web) トランザクションフローファイル", "WeChat Pay Billing File": "WeChat Pay請求ファイル", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4c0cf06e..b028a537 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Файл экспорта данных Firefly III", "Feidee MyMoney (App) Data Export File": "Файл экспорта данных Feidee MyMoney (приложение)", "Feidee MyMoney (Web) Data Export File": "Файл экспорта данных Feidee MyMoney (веб)", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Файл потока транзакций Alipay (приложение)", "Alipay (Web) Transaction Flow File": "Файл потока транзакций Alipay (веб)", "WeChat Pay Billing File": "Файл выставления счетов WeChat Pay", diff --git a/src/locales/vi.json b/src/locales/vi.json index 71b4a4e1..059259df 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1653,6 +1653,7 @@ "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)", "Feidee MyMoney (Web) Data Export File": "Tệp xuất dữ liệu Feidee MyMoney (Web)", + "Feidee MyMoney (Elecloud) Data Export File": "Feidee MyMoney (Elecloud) Data Export File", "Alipay (App) Transaction Flow File": "Tệp luồng giao dịch Alipay (Ứng dụng)", "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", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index f2b07914..4d7d0d3b 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1653,6 +1653,7 @@ "Firefly III Data Export File": "Firefly III 数据导出文件", "Feidee MyMoney (App) Data Export File": "随手记 (App) 数据导出文件", "Feidee MyMoney (Web) Data Export File": "随手记 (Web版) 数据导出文件", + "Feidee MyMoney (Elecloud) Data Export File": "随手记 (神象云账本) 数据导出文件", "Alipay (App) Transaction Flow File": "支付宝 (App) 交易流水文件", "Alipay (Web) Transaction Flow File": "支付宝 (网页版) 交易流水文件", "WeChat Pay Billing File": "微信支付账单文件", diff --git a/testdata/empty_excel_file.xlsx b/testdata/empty_excel_file.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bc5a9bfce3c83ef0a865878b66c6629b864f86e4 GIT binary patch literal 8378 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 NNq7vjqW0u%$EXm}eGMNo0@ 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 U@g%p0AqDeGS)5C-&cA0R48q=t1l#Qy2 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 R000I2lMHqoAG#lZ{SV`v(@FpU literal 0 HcmV?d00001 diff --git a/testdata/multiple_sheets_with_different_header_row_excel_file.xlsx b/testdata/multiple_sheets_with_different_header_row_excel_file.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e7878f6e816cca191ed9344ea30d331b962fe128 GIT binary patch literal 9751 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 C4z%U~ literal 0 HcmV?d00001 diff --git a/testdata/only_one_row_excel_file.xlsx b/testdata/only_one_row_excel_file.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..28b04e77a4c80ec0520770227e478f48d917a1b7 GIT binary patch literal 8853 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 literal 0 HcmV?d00001 diff --git a/testdata/simple_excel_file.xlsx b/testdata/simple_excel_file.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..4a3bbfeaf91a47ead93ecdbae0c68506ca62b008 GIT binary patch literal 8922 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 Q02=HQ2+N{UkAD98AAjXT8UO$Q literal 0 HcmV?d00001 diff --git a/third-party-dependencies.json b/third-party-dependencies.json index dd05a5c7..d4848b84 100644 --- a/third-party-dependencies.json +++ b/third-party-dependencies.json @@ -123,6 +123,12 @@ "url": "https://github.com/boombuler/barcode", "licenseUrl": "https://github.com/boombuler/barcode/blob/v1.0.2/LICENSE" }, + { + "name": "Excelize", + "copyright": "Copyright (c) 2016-2024 The excelize Authors. Copyright (c) 2011-2017 Geoffrey J. Teale", + "url": "https://github.com/qax-os/excelize", + "licenseUrl": "https://github.com/qax-os/excelize/blob/v2.9.0/LICENSE" + }, { "name": "xls", "url": "https://github.com/extrame/xls",