Skip to content

Commit

Permalink
[feat] 增加对海外银行 bmo provider 的支持 (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
ramsayleung authored Jun 19, 2023
1 parent 108fca1 commit 75d1e35
Show file tree
Hide file tree
Showing 22 changed files with 624 additions and 1 deletion.
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ clean: ## Clean all the temporary files
@rm -rf ./double-entry-generator
@rm -rf ./wasm-dist

test: test-go test-alipay-beancount test-alipay-ledger test-wechat-beancount test-wechat-ledger test-huobi-beancount test-huobi-ledger test-htsec-beancount test-htsec-ledger test-icbc-beancount test-icbc-ledger test-td-beancount test-td-ledger ## Run all tests
test: test-go test-alipay-beancount test-alipay-ledger test-wechat-beancount test-wechat-ledger test-huobi-beancount test-huobi-ledger test-htsec-beancount test-htsec-ledger test-icbc-beancount test-icbc-ledger test-td-beancount test-td-ledger test-bmo-beancount test-bmo-ledger ## Run all tests

test-go: ## Run Golang tests
@go test ./...
Expand Down Expand Up @@ -127,6 +127,11 @@ test-td-beancount: ## Run tests for TD provider against beancount compiler
test-td-ledger: ## Run tests for TD provider against ledger compiler
@$(SHELL) ./test/td-test-ledger.sh

test-bmo-beancount: ## Run tests for BMO provider against beancount compiler
@$(SHELL) ./test/bmo-test-beancount.sh
test-bmo-ledger: ## Run tests for BMO provider against ledger compiler
@$(SHELL) ./test/bmo-test-ledger.sh

format: ## Format code
@gofmt -s -w pkg
@goimports -w pkg
Expand Down
24 changes: 24 additions & 0 deletions example/bmo/credit/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defaultMinusAccount: Assets:FIXME
defaultPlusAccount: Expenses:FIXME
defaultCashAccount: Liabilities:CreditCard:BMO
defaultCurrency: CAD
title: 测试
bmo:
rules:
- item: "T T"
targetAccount: Expenses:Grocery
tag: tt_tag
- item: "COSTCO"
targetAccount: Expenses:Grocery
- item: "T&T SUPERMARKET"
targetAccount: Expenses:Grocery
- item: "JOLLIBEE"
targetAccount: Expenses:Food:Lunch
- item: "DOLLARAMA"
targetAccount: Expenses:Grocery
tag: grocery_tag1,cheap_tag2
- item: "DEVELOPM MSP"
targetAccount: Income:Salary
- type: 收入
item: "SEND E-TFR"
targetAccount: Income:FIXME
22 changes: 22 additions & 0 deletions example/bmo/credit/example-bmo-output.beancount
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
option "title" "测试"
option "operating_currency" "CAD"

1970-01-01 open Assets:FIXME
1970-01-01 open Expenses:FIXME
1970-01-01 open Expenses:Food:Lunch
1970-01-01 open Expenses:Grocery
1970-01-01 open Income:FIXME
1970-01-01 open Income:Salary

2023-05-11 * "BMO" "COSTCO WHOLESALE W54 SURREY BC"
Expenses:Grocery 3.27 CAD
Liabilities:CreditCard:BMO -3.27 CAD

2023-05-11 * "BMO" "COSTCO WHOLESALE W54 SURREY BC"
Expenses:Grocery 85.06 CAD
Liabilities:CreditCard:BMO -85.06 CAD

2023-05-12 * "BMO" "T&T SUPERMARKET #026 SURREY BC"
Expenses:Grocery 6.00 CAD
Liabilities:CreditCard:BMO -6.00 CAD

20 changes: 20 additions & 0 deletions example/bmo/credit/example-bmo-output.ledger
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
1970/01/01 * Open Balance
Assets:FIXME 0 CAD
Expenses:FIXME 0 CAD
Expenses:Food:Lunch 0 CAD
Expenses:Grocery 0 CAD
Income:FIXME 0 CAD
Income:Salary 0 CAD
Equity:Opening Balances
2023/05/11 * BMO - COSTCO WHOLESALE W54 SURREY BC
Expenses:Grocery 3.27 CAD
Liabilities:CreditCard:BMO - 3.27 CAD

2023/05/11 * BMO - COSTCO WHOLESALE W54 SURREY BC
Expenses:Grocery 85.06 CAD
Liabilities:CreditCard:BMO - 85.06 CAD

2023/05/12 * BMO - T&T SUPERMARKET #026 SURREY BC
Expenses:Grocery 6.00 CAD
Liabilities:CreditCard:BMO - 6.00 CAD

6 changes: 6 additions & 0 deletions example/bmo/credit/example-bmo-records.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Following data is valid as of 202304230942432:

Item #,Card #,Transaction Date,Posting Date,Transaction Amount,Description
1,'5191230567984561',20230511,20230512,3.27,COSTCO WHOLESALE W54 SURREY BC
2,'5191230567984561',20230511,20230512,85.06,COSTCO WHOLESALE W54 SURREY BC
3,'5191230567984561',20230512,20230515,6.0,T&T SUPERMARKET #026 SURREY BC
18 changes: 18 additions & 0 deletions example/bmo/debit/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defaultMinusAccount: Assets:FIXME
defaultPlusAccount: Expenses:FIXME
defaultCashAccount: Assets:DebitCard:BMOChequing
defaultCurrency: CAD
title: 测试
bmo:
rules:
- item: "T T"
targetAccount: Expenses:Grocery
tag: tt_tag
- item: "DOLLARAMA"
targetAccount: Expenses:Grocery
tag: grocery_tag1,cheap_tag2
- item: "DEVELOPM MSP"
targetAccount: Income:Salary
- type: 收入
item: "SEND E-TFR"
targetAccount: Income:FIXME
25 changes: 25 additions & 0 deletions example/bmo/debit/example-bmo-output.beancount
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
option "title" "测试"
option "operating_currency" "CAD"

1970-01-01 open Assets:FIXME
1970-01-01 open Expenses:FIXME
1970-01-01 open Expenses:Grocery
1970-01-01 open Income:FIXME
1970-01-01 open Income:Salary

2023-05-01 * "BMO" "[DN]ACFS MSP/DIV"
Assets:DebitCard:BMOChequing 0.01 CAD
Assets:FIXME -0.01 CAD

2023-05-18 * "BMO" "[CW]TELUS MOB"
Expenses:FIXME 68.32 CAD
Assets:DebitCard:BMOChequing -68.32 CAD

2023-05-23 * "BMO" "[CW]BC HYDRO"
Expenses:FIXME 50.00 CAD
Assets:DebitCard:BMOChequing -50.00 CAD

2023-06-12 * "BMO" "[IB] 6088 #3 ROAD"
Expenses:FIXME 40.00 CAD
Assets:DebitCard:BMOChequing -40.00 CAD

23 changes: 23 additions & 0 deletions example/bmo/debit/example-bmo-output.ledger
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
1970/01/01 * Open Balance
Assets:FIXME 0 CAD
Expenses:FIXME 0 CAD
Expenses:Grocery 0 CAD
Income:FIXME 0 CAD
Income:Salary 0 CAD
Equity:Opening Balances
2023/05/01 * BMO - [DN]ACFS MSP/DIV
Assets:DebitCard:BMOChequing 0.01 CAD
Assets:FIXME - 0.01 CAD

2023/05/18 * BMO - [CW]TELUS MOB
Expenses:FIXME 68.32 CAD
Assets:DebitCard:BMOChequing - 68.32 CAD

2023/05/23 * BMO - [CW]BC HYDRO
Expenses:FIXME 50.00 CAD
Assets:DebitCard:BMOChequing - 50.00 CAD

2023/06/12 * BMO - [IB] 6088 #3 ROAD
Expenses:FIXME 40.00 CAD
Assets:DebitCard:BMOChequing - 40.00 CAD

10 changes: 10 additions & 0 deletions example/bmo/debit/example-bmo-records.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Following data is valid as of 20239810344234 (Year/Month/Day/Hour/Minute/Second)


First Bank Card,Transaction Type,Date Posted, Transaction Amount,Description


'5678653123124124',CREDIT,20230501,0.01,[DN]ACFS MSP/DIV
'5678653123124124',DEBIT,20230518,-68.32,[CW]TELUS MOB
'5678653123124124',DEBIT,20230523,-50.0,[CW]BC HYDRO
'5678653123124124',DEBIT,20230612,-40.0,[IB] 6088 #3 ROAD
98 changes: 98 additions & 0 deletions pkg/analyser/bmo/bmo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package bmo

import (
"strings"

"github.com/deb-sig/double-entry-generator/pkg/config"
"github.com/deb-sig/double-entry-generator/pkg/ir"
"github.com/deb-sig/double-entry-generator/pkg/util"
)

type Bmo struct {
}

// GetAllCandidateAccounts returns all accounts defined in config.
func (bmo Bmo) GetAllCandidateAccounts(cfg *config.Config) map[string]bool {
// uniqMap will be used to create the concepts.
uniqMap := make(map[string]bool)

if cfg.Bmo == nil || len(cfg.Bmo.Rules) == 0 {
return uniqMap
}

for _, rule := range cfg.Bmo.Rules {
if rule.MethodAccount != nil {
uniqMap[*rule.MethodAccount] = true
}
if rule.TargetAccount != nil {
uniqMap[*rule.TargetAccount] = true
}
}
uniqMap[cfg.DefaultPlusAccount] = true
uniqMap[cfg.DefaultMinusAccount] = true
return uniqMap
}

// GetAccountsAndTags GetAccounts returns minus and plus account.
func (bmo Bmo) GetAccountsAndTags(order *ir.Order, cfg *config.Config, target, provider string) (bool, string, string, map[ir.Account]string, []string) {
ignore := false

if cfg.Bmo == nil || len(cfg.Bmo.Rules) == 0 {
return ignore, cfg.DefaultMinusAccount, cfg.DefaultPlusAccount, nil, nil
}

var tags = make([]string, 0)
resMinus := cfg.DefaultMinusAccount
resPlus := cfg.DefaultPlusAccount
cashAccount := cfg.DefaultCashAccount

// method account (bank card account)
if order.Type == ir.TypeRecv {
resPlus = cashAccount
} else {
resMinus = cashAccount
}

for _, rule := range cfg.Bmo.Rules {
match := true
// get separator
sep := ","
if rule.Separator != nil {
sep = *rule.Separator
}

matchFunc := util.SplitFindContains
if rule.FullMatch {
matchFunc = util.SplitFindEquals
}

if rule.Item != nil {
match = matchFunc(*rule.Item, order.Item, sep, match)
}
if rule.Type != nil {
match = matchFunc(*rule.Type, order.TypeOriginal, sep, match)
}

if match {
if rule.Ignore {
ignore = true
break
}
// Support multiple matches, like one rule matches the minus account, the other rule matches the plus account.
if rule.TargetAccount != nil {
if order.Type == ir.TypeRecv {
resMinus = *rule.TargetAccount
} else {
resPlus = *rule.TargetAccount
}
}

if rule.Tag != nil {
tags = strings.Split(*rule.Tag, sep)
}

}

}
return ignore, resMinus, resPlus, nil, tags
}
3 changes: 3 additions & 0 deletions pkg/analyser/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package analyser
import (
"fmt"

"github.com/deb-sig/double-entry-generator/pkg/analyser/bmo"
"github.com/deb-sig/double-entry-generator/pkg/analyser/icbc"
"github.com/deb-sig/double-entry-generator/pkg/analyser/td"

Expand Down Expand Up @@ -36,6 +37,8 @@ func New(providerName string) (Interface, error) {
return icbc.Icbc{}, nil
case consts.ProviderTd:
return td.Td{}, nil
case consts.ProviderBmo:
return bmo.Bmo{}, nil
default:
return nil, fmt.Errorf("Fail to create the analyser for the given name %s", providerName)
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"github.com/deb-sig/double-entry-generator/pkg/provider/alipay"
"github.com/deb-sig/double-entry-generator/pkg/provider/bmo"
"github.com/deb-sig/double-entry-generator/pkg/provider/htsec"
"github.com/deb-sig/double-entry-generator/pkg/provider/huobi"
"github.com/deb-sig/double-entry-generator/pkg/provider/icbc"
Expand All @@ -25,4 +26,5 @@ type Config struct {
Htsec *htsec.Config `yaml:"htsec,omitempty"`
Icbc *icbc.Config `yaml:"icbc,omitempty"`
Td *td.Config `yaml:"td,omitempty"`
Bmo *bmo.Config `yaml:"bmo,omitempty"`
}
2 changes: 2 additions & 0 deletions pkg/consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ const (
ProviderIcbc = "icbc"
//ProviderTd is the name for TD provider.
ProviderTd = "td"
//ProviderBmo is the name for BMO provider.
ProviderBmo = "bmo"
)
84 changes: 84 additions & 0 deletions pkg/provider/bmo/bmo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package bmo

import (
"encoding/csv"
"fmt"
"io"
"log"

"github.com/deb-sig/double-entry-generator/pkg/io/reader"
"github.com/deb-sig/double-entry-generator/pkg/ir"
)

type Bmo struct {
Statistics Statistics `json:"statistics,omitempty"`
LineNum int `json:"line_num,omitempty"`
Orders []Order `json:"orders,omitempty"`
CardName string `json:"card_name,omitempty"`
Mode CardMode `json:"mode,omitempty"`
}

func New() *Bmo {
return &Bmo{
Statistics: Statistics{},
LineNum: 0,
Orders: make([]Order, 0),
CardName: "",
Mode: DebitMode,
}
}

// Translate the bmo bill records to IR.
func (bmo *Bmo) Translate(filename string) (*ir.IR, error) {
log.SetPrefix("[Provider-BMO] ")

billReader, err := reader.GetReader(filename)
if err != nil {
return nil, fmt.Errorf("can't get bill reader, err: %v", err)
}

csvReader := csv.NewReader(billReader)
csvReader.LazyQuotes = true
// If FieldsPerRecord is negative, no check is made and records
// may have a variable number of fields.
csvReader.FieldsPerRecord = -1

for {
line, err := csvReader.Read()

if err == io.EOF {
break
} else if err != nil {
return nil, err
}

bmo.LineNum++

if bmo.LineNum == 2 {
// 借记卡(default) or 信用卡
if line[0] == "Item #" {
bmo.Mode = CreditMode
continue
}
}

if bmo.Mode == DebitMode && bmo.LineNum <= 2 {
// bypass the useless
continue
}

if bmo.Mode == DebitMode {
err = bmo.translateDebitToOrders(line)
if err != nil {
return nil, fmt.Errorf("Failed to translate debit bill: line %d: %v", bmo.LineNum, err)
}
} else {
err = bmo.translateCreditToOrders(line)
if err != nil {
return nil, fmt.Errorf("Failed to translate credit bill: line %d: %v", bmo.LineNum, err)
}
}
}
log.Printf("Finished to parse the file %s", filename)
return bmo.convertToIR(), nil
}
Loading

0 comments on commit 75d1e35

Please sign in to comment.