diff --git a/README.md b/README.md index 766f6b0..50ec3ee 100644 --- a/README.md +++ b/README.md @@ -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` 配置中的第一条开始匹配,如果匹配成功仍继续匹配。也就是后面的规则优先级要**高于**前面的规则。 diff --git a/example/wechat/config.yaml b/example/wechat/config.yaml index a14d9c3..fcf5973 100644 --- a/example/wechat/config.yaml +++ b/example/wechat/config.yaml @@ -51,6 +51,7 @@ wechat: - peer: 用户 type: 收入 targetAccount: Income:Service + tag: income,service - peer: 理财通 type: / diff --git a/example/wechat/example-wechat-output.beancount b/example/wechat/example-wechat-output.beancount index 1f154e3..97c9281 100644 --- a/example/wechat/example-wechat-output.beancount +++ b/example/wechat/example-wechat-output.beancount @@ -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" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..96e4296 --- /dev/null +++ b/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1663761423, + "narHash": "sha256-bDLXl2BVq7eIQz/8CduZI1SLyhG9u/CrckHd6f7bwPE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d6490a0bd9dfb298fcd8382d3363b86870dc7340", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..bacfea3 --- /dev/null +++ b/flake.nix @@ -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 ${./.} + ''; + }; + } + ); +} diff --git a/pkg/analyser/alipay/alipay.go b/pkg/analyser/alipay/alipay.go index 77c2725..6fda4f4 100644 --- a/pkg/analyser/alipay/alipay.go +++ b/pkg/analyser/alipay/alipay.go @@ -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 { @@ -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 } diff --git a/pkg/analyser/htsec/htsec.go b/pkg/analyser/htsec/htsec.go index 771ebb1..7158341 100644 --- a/pkg/analyser/htsec/htsec.go +++ b/pkg/analyser/htsec/htsec.go @@ -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 @@ -101,6 +101,7 @@ func (h Htsec) GetAccounts(o *ir.Order, cfg *config.Config, target, provider str if r.PnlAccount != nil { pnlAccount = *r.PnlAccount } + } } @@ -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 } diff --git a/pkg/analyser/huobi/huobi.go b/pkg/analyser/huobi/huobi.go index 81d8043..1b01124 100644 --- a/pkg/analyser/huobi/huobi.go +++ b/pkg/analyser/huobi/huobi.go @@ -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 @@ -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 } diff --git a/pkg/analyser/interface.go b/pkg/analyser/interface.go index d21be03..f5ef787 100644 --- a/pkg/analyser/interface.go +++ b/pkg/analyser/interface.go @@ -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" @@ -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. diff --git a/pkg/analyser/wechat/wechat.go b/pkg/analyser/wechat/wechat.go index f6d454e..a568b47 100644 --- a/pkg/analyser/wechat/wechat.go +++ b/pkg/analyser/wechat/wechat.go @@ -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" @@ -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 == "" { @@ -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 @@ -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 } @@ -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 } diff --git a/pkg/compiler/beancount/compiler.go b/pkg/compiler/beancount/compiler.go index a89f051..60e095e 100644 --- a/pkg/compiler/beancount/compiler.go +++ b/pkg/compiler/beancount/compiler.go @@ -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) @@ -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 { diff --git a/pkg/compiler/beancount/template.go b/pkg/compiler/beancount/template.go index de00ce5..9a0f312 100644 --- a/pkg/compiler/beancount/template.go +++ b/pkg/compiler/beancount/template.go @@ -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 }} @@ -29,6 +29,7 @@ type NormalOrderVars struct { CommissionAccount string Currency string Metadata map[string]string // unordered metadata map + Tags []string } // 火币买入模版(手续费单位为购买单位货币) diff --git a/pkg/config/config.go b/pkg/config/config.go index 8ee7f45..d972ded 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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"` } diff --git a/pkg/ir/ir.go b/pkg/ir/ir.go index a2ac873..60a526e 100644 --- a/pkg/ir/ir.go +++ b/pkg/ir/ir.go @@ -49,6 +49,7 @@ type Order struct { MinusAccount string PlusAccount string Metadata map[string]string + Tags []string } // Unit is the key commodity names diff --git a/pkg/provider/alipay/config.go b/pkg/provider/alipay/config.go index f7e3f1b..50f2020 100644 --- a/pkg/provider/alipay/config.go +++ b/pkg/provider/alipay/config.go @@ -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"` } diff --git a/pkg/provider/interface.go b/pkg/provider/interface.go index 5fb3664..3432e1d 100644 --- a/pkg/provider/interface.go +++ b/pkg/provider/interface.go @@ -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" diff --git a/pkg/provider/wechat/config.go b/pkg/provider/wechat/config.go index 37a65cc..211ef24 100644 --- a/pkg/provider/wechat/config.go +++ b/pkg/provider/wechat/config.go @@ -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"` }