import transactions from Feidee Mymoney (Elecloud)

This commit is contained in:
MaysWind
2025-03-15 21:00:53 +08:00
parent 00f8b6d950
commit 78c5b1704a
23 changed files with 734 additions and 0 deletions

6
go.mod
View File

@@ -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

13
go.sum
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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{}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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" {

View File

@@ -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',

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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請求ファイル",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "微信支付账单文件",

BIN
testdata/empty_excel_file.xlsx vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
testdata/multiple_sheets_excel_file.xlsx vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
testdata/only_one_row_excel_file.xlsx vendored Normal file

Binary file not shown.

BIN
testdata/simple_excel_file.xlsx vendored Normal file

Binary file not shown.

View File

@@ -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",