Skip to content

Commit

Permalink
feat: add beancount hash tag support (#67)
Browse files Browse the repository at this point in the history
* feat: add beancount hash tag support

* lint

* add doc

* fix the test cases

* fixed alipay test case

* fixed wechat test case

* update template

* update test cases

* refact(wechat): change config field `tags` to `tag`

- test(wechat): add test for multiple tags

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

Signed-off-by: Triple-Z <me@triplez.cn>
Co-authored-by: Triple-Z <me@triplez.cn>
  • Loading branch information
p3psi-boo and Triple-Z authored Nov 1, 2022
1 parent f77e68b commit 59aa3fc
Show file tree
Hide file tree
Showing 17 changed files with 137 additions and 23 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,8 @@ wechat:

在单条规则中可以使用 `fullMatch` 来设置字符匹配规则,`true` 表示使用完全匹配(full match),`false` 表示使用包含匹配(partial match),不设置该项则默认使用包含匹配。

在单条规则中可以使用 `tag` 来设置流水的 [Tag](https://beancount.github.io/docs/beancount_language_syntax.html#tags),使用 `sep` 作为分隔符。

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

规则匹配的顺序是:从 `rules` 配置中的第一条开始匹配,如果匹配成功仍继续匹配。也就是后面的规则优先级要**高于**前面的规则。
Expand Down
1 change: 1 addition & 0 deletions example/wechat/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ wechat:
- peer: 用户
type: 收入
targetAccount: Income:Service
tag: income,service

- peer: 理财通
type: /
Expand Down
2 changes: 1 addition & 1 deletion example/wechat/example-wechat-output.beancount
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ option "operating_currency" "CNY"
Assets:Bank:CN:ICBC:Savings 5505.00 CNY
Assets:Digital:Wechat:Cash -5505.00 CNY

2020-11-27 * "用户A" "收款方备注:二维码收款"
2020-11-27 * "用户A" "收款方备注:二维码收款" #income #service
merchantId: "/"
method: "零钱"
orderId: "3985734"
Expand Down
43 changes: 43 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
description = "Rule-based double-entry bookkeeping importer (from Alipay/WeChat/Huobi to Beancount)";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};

outputs = {
self,
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem
(
system: let
pkgs = import nixpkgs {
inherit system;
};
buildDeps = with pkgs; [git go_1_19 gnumake];
devDeps = with pkgs;
buildDeps
++ [
golangci-lint
];
in rec {
# `nix develop`
devShell = pkgs.mkShell {buildInputs = devDeps;};
checks = {
format =
pkgs.runCommand "check-format"
{
buildInputs = with pkgs; [
nixpkgs-fmt
golangci-lint
];
} ''
${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt ${./.}
${pkgs.golangci-lint}/bin/golangci-lint run --fix --timeout 10m ${./.}
'';
};
}
);
}
13 changes: 9 additions & 4 deletions pkg/analyser/alipay/alipay.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ func (a Alipay) GetAllCandidateAccounts(cfg *config.Config) map[string]bool {
}

// GetAccounts returns minus and plus account.
func (a Alipay) GetAccounts(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string) {
func (a Alipay) GetAccountsAndTags(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string, []string) {

if cfg.Alipay == nil || len(cfg.Alipay.Rules) == 0 {
return cfg.DefaultMinusAccount, cfg.DefaultPlusAccount, nil
return cfg.DefaultMinusAccount, cfg.DefaultPlusAccount, nil, nil
}
resMinus := cfg.DefaultMinusAccount
resPlus := cfg.DefaultPlusAccount
var extraAccounts map[ir.Account]string
var tags = make([]string, 0)

var err error
for _, r := range cfg.Alipay.Rules {
Expand Down Expand Up @@ -112,11 +113,15 @@ func (a Alipay) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st
}
}

if r.Tags != nil {
tags = strings.Split(*r.Tags, sep)
}

}
}

if strings.HasPrefix(o.Item, "退款-") {
return resPlus, resMinus, extraAccounts
return resPlus, resMinus, extraAccounts, tags
}
return resMinus, resPlus, extraAccounts
return resMinus, resPlus, extraAccounts, tags
}
7 changes: 4 additions & 3 deletions pkg/analyser/htsec/htsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ func (h Htsec) GetAllCandidateAccounts(cfg *config.Config) map[string]bool {
return uniqMap
}

func (h Htsec) GetAccounts(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string) {
func (h Htsec) GetAccountsAndTags(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string, []string) {
if cfg.Htsec == nil || len(cfg.Htsec.Rules) == 0 {
return "", "", map[ir.Account]string{
ir.CashAccount: cfg.DefaultCashAccount,
ir.PositionAccount: cfg.DefaultPositionAccount,
ir.CommissionAccount: cfg.DefaultCommissionAccount,
ir.PnlAccount: cfg.DefaultPnlAccount,
}
}, nil
}

cashAccount := cfg.DefaultCashAccount
Expand Down Expand Up @@ -101,6 +101,7 @@ func (h Htsec) GetAccounts(o *ir.Order, cfg *config.Config, target, provider str
if r.PnlAccount != nil {
pnlAccount = *r.PnlAccount
}

}
}

Expand All @@ -109,5 +110,5 @@ func (h Htsec) GetAccounts(o *ir.Order, cfg *config.Config, target, provider str
ir.PositionAccount: positionAccount,
ir.CommissionAccount: commissionAccount,
ir.PnlAccount: pnlAccount,
}
}, nil
}
6 changes: 3 additions & 3 deletions pkg/analyser/huobi/huobi.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ func (h Huobi) GetAllCandidateAccounts(cfg *config.Config) map[string]bool {
return uniqMap
}

func (h Huobi) GetAccounts(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string) {
func (h Huobi) GetAccountsAndTags(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string, []string) {
if cfg.Huobi == nil || len(cfg.Huobi.Rules) == 0 {
return "", "", map[ir.Account]string{
ir.CashAccount: cfg.DefaultCashAccount,
ir.PositionAccount: cfg.DefaultPositionAccount,
ir.CommissionAccount: cfg.DefaultCommissionAccount,
ir.PnlAccount: cfg.DefaultPnlAccount,
}
}, nil
}

cashAccount := cfg.DefaultCashAccount
Expand Down Expand Up @@ -111,5 +111,5 @@ func (h Huobi) GetAccounts(o *ir.Order, cfg *config.Config, target, provider str
ir.PositionAccount: positionAccount,
ir.CommissionAccount: commissionAccount,
ir.PnlAccount: pnlAccount,
}
}, nil
}
3 changes: 2 additions & 1 deletion pkg/analyser/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package analyser

import (
"fmt"

"github.com/deb-sig/double-entry-generator/pkg/analyser/alipay"
"github.com/deb-sig/double-entry-generator/pkg/analyser/htsec"
"github.com/deb-sig/double-entry-generator/pkg/analyser/huobi"
Expand All @@ -14,7 +15,7 @@ import (
// Interface is the interface of analyser.
type Interface interface {
GetAllCandidateAccounts(cfg *config.Config) map[string]bool
GetAccounts(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string)
GetAccountsAndTags(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string, []string)
}

// New creates a new analyser.
Expand Down
15 changes: 12 additions & 3 deletions pkg/analyser/wechat/wechat.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package wechat

import (
"log"
"strings"

"github.com/deb-sig/double-entry-generator/pkg/config"
"github.com/deb-sig/double-entry-generator/pkg/ir"
Expand Down Expand Up @@ -37,8 +38,10 @@ 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) {
func (w Wechat) GetAccountsAndTags(o *ir.Order, cfg *config.Config, target, provider string) (string, string, map[ir.Account]string, []string) {
var resCommission string
var tags = make([]string, 0)

// check this tx whether has commission
if o.Commission != 0 {
if cfg.DefaultCommissionAccount == "" {
Expand All @@ -51,7 +54,7 @@ func (w Wechat) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st
if cfg.Wechat == nil || len(cfg.Wechat.Rules) == 0 {
return cfg.DefaultMinusAccount, cfg.DefaultPlusAccount, map[ir.Account]string{
ir.CommissionAccount: resCommission,
}
}, nil
}

resMinus := cfg.DefaultMinusAccount
Expand All @@ -62,6 +65,7 @@ func (w Wechat) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st
match := true
// get seperator
sep := ","

if r.Seperator != nil {
sep = *r.Seperator
}
Expand Down Expand Up @@ -118,11 +122,16 @@ func (w Wechat) GetAccounts(o *ir.Order, cfg *config.Config, target, provider st
if r.CommissionAccount != nil {
resCommission = *r.CommissionAccount
}

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

}

}

return resMinus, resPlus, map[ir.Account]string{
ir.CommissionAccount: resCommission,
}
}, tags
}
4 changes: 3 additions & 1 deletion pkg/compiler/beancount/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ func (b *BeanCount) Compile() error {
log.Printf("Getting the expected account for the bills")
for index, o := range b.IR.Orders {
// Get the expected accounts according to the configuration.
minusAccount, plusAccount, extraAccounts := b.GetAccounts(&o, b.Config, b.Provider, b.Target)
minusAccount, plusAccount, extraAccounts, tags := b.GetAccountsAndTags(&o, b.Config, b.Provider, b.Target)
b.IR.Orders[index].MinusAccount = minusAccount
b.IR.Orders[index].PlusAccount = plusAccount
b.IR.Orders[index].ExtraAccounts = extraAccounts
b.IR.Orders[index].Tags = tags
}

log.Printf("Writing to %s", b.Output)
Expand Down Expand Up @@ -185,6 +186,7 @@ func (b *BeanCount) writeBill(file *os.File, index int) error {
CommissionAccount: o.ExtraAccounts[ir.CommissionAccount],
Metadata: o.Metadata,
Currency: b.Config.DefaultCurrency,
Tags: o.Tags,
})
case ir.OrderTypeHuobiTrade: // Huobi trades
switch o.TxType {
Expand Down
3 changes: 2 additions & 1 deletion pkg/compiler/beancount/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
)

// 普通账单的模版(消费账)
var normalOrder = `{{ .PayTime.Format "2006-01-02" }} * "{{ EscapeString .Peer }}" "{{ EscapeString .Item }}"{{ if .Note }} ; {{ .Note }}{{ end }}
var normalOrder = `{{ .PayTime.Format "2006-01-02" }} * "{{ EscapeString .Peer }}" "{{ EscapeString .Item }}"{{ range .Tags }} #{{ . }}{{ end }}{{ if .Note }} ; {{ .Note }}{{ end }}
{{- range $key, $value := .Metadata }}{{ if $value }}{{ printf "\n" }} {{ $key }}: "{{ $value }}"{{end}}{{end}}
{{ .PlusAccount }} {{ .Money | printf "%.2f" }} {{ .Currency }}
{{ .MinusAccount }} -{{ .Money | printf "%.2f" }} {{ .Currency }}
Expand All @@ -29,6 +29,7 @@ type NormalOrderVars struct {
CommissionAccount string
Currency string
Metadata map[string]string // unordered metadata map
Tags []string
}

// 火币买入模版(手续费单位为购买单位货币)
Expand Down
12 changes: 6 additions & 6 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ type Config struct {
Title string `yaml:"title,omitempty"`
DefaultMinusAccount string `yaml:"defaultMinusAccount,omitempty"`
DefaultPlusAccount string `yaml:"defaultPlusAccount,omitempty"`
DefaultCashAccount string `yaml:"defaultCashAccount,omitempty`
DefaultPositionAccount string `yaml:"defaultPositionAccount,omitempty`
DefaultCommissionAccount string `yaml:"defaultCommissionAccount,omitempty`
DefaultPnlAccount string `yaml:"defaultPnlAccount,omitempty`
DefaultCashAccount string `yaml:"defaultCashAccount,omitempty"`
DefaultPositionAccount string `yaml:"defaultPositionAccount,omitempty"`
DefaultCommissionAccount string `yaml:"defaultCommissionAccount,omitempty"`
DefaultPnlAccount string `yaml:"defaultPnlAccount,omitempty"`
DefaultCurrency string `yaml:"defaultCurrency,omitempty"`
Alipay *alipay.Config `yaml:"alipay,omitempty"`
Wechat *wechat.Config `yaml:"wechat,omitempty"`
Huobi *huobi.Config `yaml:"huobi,omitempty`
Htsec *htsec.Config `yaml:"htsec,omitempty""`
Huobi *huobi.Config `yaml:"huobi,omitempty"`
Htsec *htsec.Config `yaml:"htsec,omitempty"`
}
1 change: 1 addition & 0 deletions pkg/ir/ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Order struct {
MinusAccount string
PlusAccount string
Metadata map[string]string
Tags []string
}

// Unit is the key commodity names
Expand Down
1 change: 1 addition & 0 deletions pkg/provider/alipay/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ type Rule struct {
TargetAccount *string `mapstructure:"targetAccount,omitempty"`
PnlAccount *string `mapstructure:"pnlAccount,omitempty"`
FullMatch bool `mapstructure:"fullMatch,omitempty"`
Tags *string `mapstructure:"tags,omitempty"`
}
1 change: 1 addition & 0 deletions pkg/provider/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package provider

import (
"fmt"

"github.com/deb-sig/double-entry-generator/pkg/consts"
"github.com/deb-sig/double-entry-generator/pkg/ir"
"github.com/deb-sig/double-entry-generator/pkg/provider/alipay"
Expand Down
1 change: 1 addition & 0 deletions pkg/provider/wechat/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ type Rule struct {
TargetAccount *string `mapstructure:"targetAccount,omitempty"`
CommissionAccount *string `mapstructure:"commissionAccount,omitempty"`
FullMatch bool `mapstructure:"fullMatch,omitempty"`
Tag *string `mapstructure:"tag,omitempty"`
}

0 comments on commit 59aa3fc

Please sign in to comment.