Skip to content

Commit

Permalink
fix(wechat): cash withdraw with commission (#27)
Browse files Browse the repository at this point in the history
* fix(wechat): cash withdraw with commission

Signed-off-by: Triple-Z <me@triplez.cn>

* fix(wechat): add commission account in writeHeader

Signed-off-by: Triple-Z <me@triplez.cn>
  • Loading branch information
Triple-Z authored Jul 19, 2021
1 parent e5d5ef1 commit 5120b92
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 71 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,13 @@ alipay:

`alipay` is the provider-specific configuration. Alipay provider has rules matching mechanism.

`alipay` 是蚂蚁账单相关的配置。它提供基于规则的匹配。可以指定 type(收/支)、 method(支付方式)、peer(交易对方)和 item(商品名称)的包含匹配。匹配成功则使用规则中定义的 `targetAccount` 和 `methodAccount` 覆盖默认定义。
`alipay` 蚂蚁账单相关的配置。它提供基于规则的匹配。可以指定:
- peer(交易对方)的包含匹配。
- item(商品说明)的包含匹配。
- type(收/支)的包含匹配。
- method(收/付款方式)的包含匹配。

匹配成功则使用规则中定义的 `targetAccount` 、 `methodAccount` 等账户覆盖默认定义账户。

支付宝提供了“交易方式”字段来标识资金出入账户。这样就可以直接通过“交易方式”,并辅以“收/支”字段确认该账户为增加账户还是减少账户。而复式记账法每笔交易至少需要两个账户,另一个账户则可通过“交易对方”(peer)、“商品”(item)、“收/支”(type)以及“交易方式”(method)的多种包含匹配得出。匹配成功则使用规则中定义的 `targetAccount` 和 `methodAccount` ,并通过确认该笔交易是收入还是支出,决定 `targetAccount` 和 `methodAccount` 的正负关系,来覆盖默认定义的增减账户。

Expand All @@ -187,6 +193,7 @@ alipay:
```yaml
defaultMinusAccount: Assets:FIXME
defaultPlusAccount: Expenses:FIXME
defaultCommissionAccount: Expenses:Commission:FIXME
defaultCurrency: CNY
title: Test WeChat bills config
wechat:
Expand All @@ -196,9 +203,14 @@ wechat:
item: /
targetAccount: Income:Wechat:RedPacket
- type: / # 转入零钱通
txType: 转入零钱
peer: /
item: /
targetAccount: Assets:Digital:WeChat:Cash
- type: / # 零钱提现
txType: 零钱提现
targetAccount: Assets:Digital:WeChat:Cash
commissionAccount: Expenses:Wechat:Commission
- peer: 滴滴
targetAccount: Expenses:Transport:Taxi
Expand All @@ -219,8 +231,19 @@ wechat:

`defaultMinusAccount`, `defaultPlusAccount` 和 `defaultCurrency` 是全局的必填默认值。其中 `defaultMinusAccount` 是默认金额减少的账户,`defaultPlusAccount` 是默认金额增加的账户。 `defaultCurrency` 是默认货币。

> `defaultCommissionAccount` 是默认的服务费账户,若无服务费相关交易,则不需要填写。但笔者还是建议填写一个占位 FIXME 账户,否则遇到带服务费的交易,转换器会报错退出。

`wechat` is the provider-specific configuration. WeChat provider has rules matching mechanism.

`wechat` 是微信相关的配置。它提供基于规则的匹配。可以指定:
- peer(交易对方)的包含匹配。
- item(商品名称)的包含匹配。
- type(收/支)的包含匹配。
- txType(交易类型)的包含匹配。
- method(支付方式)的包含匹配。

匹配成功则使用规则中定义的 `targetAccount` 、 `methodAccount` 等账户覆盖默认定义账户。

微信账单提供了“交易方式”字段来标识资金出入账户。这样就可以直接通过“交易方式”,并辅以“收/支”字段确认该账户为增加账户还是减少账户。而复式记账法每笔交易至少需要两个账户,另一个账户则可通过“交易对方”(peer)、“商品”(item)、“收/支”(type)以及“交易方式”(method)的多种包含匹配得出。如支付宝配置类似,匹配成功则使用规则中定义的 `targetAccount` 和 `methodAccount` ,并通过确认该笔交易是收入还是支出,决定 `targetAccount` 和 `methodAccount` 的正负关系,来覆盖默认定义的增减账户。

`targetAccount` 与 `methodAccount` 的增减账户关系如下表:
Expand Down
8 changes: 7 additions & 1 deletion example/wechat/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defaultMinusAccount: Assets:FIXME
defaultPlusAccount: Expenses:FIXME
defaultCommissionAccount: Expenses:Commission:FIXME
defaultCurrency: CNY
title: 测试
wechat:
Expand All @@ -10,9 +11,14 @@ wechat:
item: /
targetAccount: Income:Wechat:RedPacket
- type: / # 转入零钱通
txType: 转入零钱
peer: /
item: /
targetAccount: Assets:Digital:WeChat:Cash
targetAccount: Assets:Digital:Wechat:Cash
- type: / # 零钱提现
txType: 零钱提现
targetAccount: Assets:Digital:Wechat:Cash
commissionAccount: Expenses:Wechat:Commission

- peer: 云膳过桥米线,餐厅
sep: ','
Expand Down
40 changes: 38 additions & 2 deletions example/wechat/example-wechat-output.beancount
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ option "operating_currency" "CNY"

1970-01-01 open Assets:Bank:CN:BOC:Savings
1970-01-01 open Assets:Bank:CN:ICBC:Savings
1970-01-01 open Assets:Digital:WeChat:Cash
1970-01-01 open Assets:Digital:Wechat:Cash
1970-01-01 open Assets:FIXME
1970-01-01 open Expenses:FIXME
1970-01-01 open Expenses:Food:Meal
1970-01-01 open Expenses:Housing:Rent
1970-01-01 open Expenses:Wechat:Commission
1970-01-01 open Income:Service
1970-01-01 open Income:Wechat:RedPacket

Expand Down Expand Up @@ -43,7 +43,7 @@ option "operating_currency" "CNY"
txType: "收入"

2021-01-17 * "/" "/"
Assets:Digital:WeChat:Cash 2000.00 CNY
Assets:Digital:Wechat:Cash 2000.00 CNY
Assets:Bank:CN:ICBC:Savings -2000.00 CNY
merchantId: "129847129"
method: "工商银行(9876)"
Expand Down Expand Up @@ -72,3 +72,39 @@ option "operating_currency" "CNY"
source: "微信支付"
txType: "支出"

2021-07-11 * "招商银行()" "/"
Assets:Bank:CN:BOC:Savings 1000.10 CNY
Assets:Digital:Wechat:Cash -1000.10 CNY
Expenses:Wechat:Commission 1.00 CNY
Assets:Digital:Wechat:Cash -1.00 CNY
merchantId: "/"
method: "中国银行"
orderId: "207210711100077147832088993175"
payTime: "2021-07-11 14:17:52 +0800 CST"
source: "微信支付"
txType: "/"

2021-07-14 * "招商银行()" "/"
Assets:Bank:CN:ICBC:Savings 10.00 CNY
Assets:Digital:Wechat:Cash -10.00 CNY
Expenses:Wechat:Commission 0.10 CNY
Assets:Digital:Wechat:Cash -0.10 CNY
merchantId: "/"
method: "工商银行"
orderId: "207210714100077147459276708175"
payTime: "2021-07-14 22:18:10 +0800 CST"
source: "微信支付"
txType: "/"

2021-07-15 * "招商银行()" "/"
Assets:Bank:CN:ICBC:Savings 100.00 CNY
Assets:Digital:Wechat:Cash -100.00 CNY
Expenses:Wechat:Commission 0.10 CNY
Assets:Digital:Wechat:Cash -0.10 CNY
merchantId: "/"
method: "工商银行"
orderId: "207210715100077148235523883175"
payTime: "2021-07-15 16:29:37 +0800 CST"
source: "微信支付"
txType: "/"

5 changes: 4 additions & 1 deletion example/wechat/example-wechat-records.csv
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@
2021-01-17 18:03:35,扫二维码付款,某餐厅,收款方备注:二维码收款,支出,¥12.00,零钱通,已转账,3985734,129847129,/
2021-01-22 12:34:56,转账,房东,转账备注:微信转账,支出,¥500.00,零钱通,朋友已收钱,3985734,129847129,/
2020-11-27 19:29:00,二维码收款,用户A,收款方备注:二维码收款,收入,¥23.00,零钱,已收钱,3985734,/ ,/
2021-01-17 10:07:31,转入零钱通-来自工商银行(9876),/,/,/,¥2000.00,工商银行(9876),支付成功,3985734,129847129,/
2021-01-17 10:07:31,转入零钱通-来自工商银行(9876),/,/,/,¥2000.00,工商银行(9876),支付成功,3985734,129847129,/
2021-07-15 16:29:37,零钱提现,招商银行(),"/",/,¥100.10,工商银行,提现已到账,207210715100077148235523883175 ,/ ,"服务费¥0.10"
2021-07-14 22:18:10,零钱提现,招商银行(),"/",/,¥10.10,工商银行,提现已到账,207210714100077147459276708175 ,/ ,"服务费¥0.10"
2021-07-11 14:17:52,零钱提现,招商银行(),"/",/,¥1001.10,中国银行,提现已到账,207210711100077147832088993175 ,/ ,"服务费¥1.00"
32 changes: 29 additions & 3 deletions pkg/analyser/wechat/wechat.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package wechat

import (
"log"

"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"
Expand All @@ -25,6 +27,9 @@ func (w Wechat) GetAllCandidateAccounts(cfg *config.Config) map[string]bool {
if r.TargetAccount != nil {
uniqMap[*r.TargetAccount] = true
}
if r.CommissionAccount != nil {
uniqMap[*r.CommissionAccount] = true
}
}
uniqMap[cfg.DefaultPlusAccount] = true
uniqMap[cfg.DefaultMinusAccount] = true
Expand All @@ -33,8 +38,20 @@ func (w Wechat) GetAllCandidateAccounts(cfg *config.Config) map[string]bool {

// GetAccounts returns minus and plus account.
func (w Wechat) GetAccounts(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string) {
var resCommission string
// check this tx whether has commission
if o.Commission != 0 {
if cfg.DefaultCommissionAccount == "" {
log.Fatalf("Found a tx with commission, but not setting the `defaultCommissionAccount` in config file!")
} else {
resCommission = cfg.DefaultCommissionAccount
}
}

if cfg.Wechat == nil || len(cfg.Wechat.Rules) == 0 {
return cfg.DefaultMinusAccount, cfg.DefaultPlusAccount, nil
return cfg.DefaultMinusAccount, cfg.DefaultPlusAccount, map[ir.Account]string{
ir.CommissionAccount: resCommission,
}
}

resMinus := cfg.DefaultMinusAccount
Expand All @@ -51,7 +68,10 @@ func (w Wechat) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st
match = util.SplitFindContains(*r.Peer, o.Peer, sep, match)
}
if r.Type != nil {
match = util.SplitFindContains(*r.Type, o.TypeOriginal, sep, match)
match = util.SplitFindContains(*r.Type, o.TxTypeOriginal, sep, match)
}
if r.TxType != nil {
match = util.SplitFindContains(*r.TxType, o.TypeOriginal, sep, match)
}
if r.Method != nil {
match = util.SplitFindContains(*r.Method, o.Method, sep, match)
Expand All @@ -78,8 +98,14 @@ func (w Wechat) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st
resMinus = *r.MethodAccount
}
}
if r.CommissionAccount != nil {
resCommission = *r.CommissionAccount
}
}

}
return resMinus, resPlus, nil

return resMinus, resPlus, map[ir.Account]string{
ir.CommissionAccount: resCommission,
}
}
15 changes: 5 additions & 10 deletions pkg/cmd/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,17 @@ func run(args []string) {
c := &config.Config{}
err := viper.Unmarshal(c)
logErrorIfNotNil(err)
// check financial trades by provider
isTrade := false
tradeProviders := map[string]bool{
consts.ProviderHuobi: true,
}
if tradeProviders[providerName] {
isTrade = true
}

if !isTrade {
switch providerName {
case consts.ProviderAlipay:
fallthrough
case consts.ProviderWechat:
if c.DefaultCurrency == "" ||
c.DefaultMinusAccount == "" ||
c.DefaultPlusAccount == "" {
log.Fatalf("Failed to get default options in config")
}
} else {
case consts.ProviderHuobi:
if c.DefaultCurrency == "" ||
c.DefaultCashAccount == "" ||
c.DefaultPositionAccount == "" ||
Expand Down
22 changes: 12 additions & 10 deletions pkg/compiler/beancount/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,16 +161,18 @@ func (b *BeanCount) writeBill(file *os.File, index int) error {
fallthrough
case ir.OrderTypeNormal:
err = normalOrderTemplate.Execute(&buf, &NormalOrderVars{
PayTime: o.PayTime,
Peer: o.Peer,
Item: o.Item,
Note: o.Note,
Money: o.Money,
PlusAccount: o.PlusAccount,
MinusAccount: o.MinusAccount,
PnlAccount: o.ExtraAccounts[ir.PnlAccount],
Metadata: o.Metadata,
Currency: b.Config.DefaultCurrency,
PayTime: o.PayTime,
Peer: o.Peer,
Item: o.Item,
Note: o.Note,
Money: o.Money,
Commission: o.Commission,
PlusAccount: o.PlusAccount,
MinusAccount: o.MinusAccount,
PnlAccount: o.ExtraAccounts[ir.PnlAccount],
CommissionAccount: o.ExtraAccounts[ir.CommissionAccount],
Metadata: o.Metadata,
Currency: b.Config.DefaultCurrency,
})
case ir.OrderTypeHuobiTrade: // Huobi trades
switch o.TxType {
Expand Down
28 changes: 17 additions & 11 deletions pkg/compiler/beancount/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,26 @@ import (
var normalOrder = `{{ .PayTime.Format "2006-01-02" }} * "{{ .Peer }}" "{{ .Item }}"{{ if .Note }} ; {{ .Note }}{{ end }}
{{ .PlusAccount }} {{ .Money | printf "%.2f" }} {{ .Currency }}
{{ .MinusAccount }} -{{ .Money | printf "%.2f" }} {{ .Currency }}
{{ if .PnlAccount }} {{ .PnlAccount }}{{ printf "\n" }}{{ end }}{{ range $key, $value := .Metadata }}{{ if $value }} {{ $key }}: "{{ $value }}"{{ printf "\n" }}{{end}}{{end}}
{{- if .CommissionAccount }}{{ printf "\n" }} {{ .CommissionAccount }} {{ .Commission | printf "%.2f" }} {{ .Currency }}{{ end }}
{{- if .CommissionAccount }}{{ printf "\n" }} {{ .MinusAccount }} -{{ .Commission | printf "%.2f" }} {{ .Currency }}{{ end }}
{{- if .PnlAccount }}{{ printf "\n" }} {{ .PnlAccount }}{{ end }}
{{- range $key, $value := .Metadata }}{{ if $value }}{{ printf "\n" }} {{ $key }}: "{{ $value }}"{{end}}{{end}}
`

type NormalOrderVars struct {
PayTime time.Time
Peer string
Item string
Note string
Money float64
PlusAccount string
MinusAccount string
PnlAccount string
Currency string
Metadata map[string]string // unordered metadata map
PayTime time.Time
Peer string
Item string
Note string
Money float64
Commission float64
PlusAccount string
MinusAccount string
PnlAccount string
CommissionAccount string
Currency string
Metadata map[string]string // unordered metadata map
}

// 火币买入模版(手续费单位为购买单位货币)
Expand Down
2 changes: 1 addition & 1 deletion pkg/ir/ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type Order struct {
Method string
Amount float64
Price float64
Commission float64
Commission float64 // 手续费/服务费
Units map[Unit]string
ExtraAccounts map[Account]string
MinusAccount string
Expand Down
20 changes: 11 additions & 9 deletions pkg/provider/wechat/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ type Config struct {

// Rule is the type for match rules.
type Rule struct {
Peer *string `mapstructure:"peer,omitempty"`
Item *string `mapstructure:"item,omitempty"`
Type *string `mapstructure:"type,omitempty"`
Seperator *string `mapstructure:"sep,omitempty"` // default: ,
Method *string `mapstructure:"method,omitempty"`
StartTime *string `mapstructure:"startTime,omitempty"`
EndTime *string `mapstructure:"endTime,omitempty"`
MethodAccount *string `mapstructure:"methodAccount,omitempty"`
TargetAccount *string `mapstructure:"targetAccount,omitempty"`
Peer *string `mapstructure:"peer,omitempty"`
Item *string `mapstructure:"item,omitempty"`
Type *string `mapstructure:"type,omitempty"`
TxType *string `mapstructure:"txType,omitempty"`
Seperator *string `mapstructure:"sep,omitempty"` // default: ,
Method *string `mapstructure:"method,omitempty"`
StartTime *string `mapstructure:"startTime,omitempty"`
EndTime *string `mapstructure:"endTime,omitempty"`
MethodAccount *string `mapstructure:"methodAccount,omitempty"`
TargetAccount *string `mapstructure:"targetAccount,omitempty"`
CommissionAccount *string `mapstructure:"commissionAccount,omitempty"`
}
18 changes: 10 additions & 8 deletions pkg/provider/wechat/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ func (w *Wechat) convertToIR() *ir.IR {
i := ir.New()
for _, o := range w.Orders {
irO := ir.Order{
Peer: o.Peer,
Item: o.Item,
PayTime: o.PayTime,
Money: o.Money,
OrderID: &o.OrderID,
TxType: conevertType(o.Type),
TypeOriginal: o.TypeOriginal,
Method: o.Method,
Peer: o.Peer,
Item: o.Item,
PayTime: o.PayTime,
Money: o.Money,
OrderID: &o.OrderID,
TxType: conevertType(o.Type),
TxTypeOriginal: o.TypeOriginal,
TypeOriginal: o.TxTypeOriginal,
Method: o.Method,
Commission: o.Commission,
}
irO.Metadata = getMetadata(o)
if o.MechantOrderID != "" {
Expand Down
Loading

0 comments on commit 5120b92

Please sign in to comment.