diff --git a/Makefile b/Makefile index 22d217a..3c13d9a 100644 --- a/Makefile +++ b/Makefile @@ -90,25 +90,31 @@ clean: ## Clean all the temporary files @rm -rf ./double-entry-generator @rm -rf ./wasm-dist -test: test-go test-alipay test-wechat test-huobi test-htsec test-icbc ## Run all tests +test: test-go test-alipay-beancount test-alipay-ledger test-wechat-beancount test-wechat-ledger test-huobi-beancount test-htsec-beancount test-icbc-beancount test-icbc-ledger ## Run all tests test-go: ## Run Golang tests @go test ./... -test-alipay: ## Run tests for Alipay provider - @$(SHELL) ./test/alipay-test.sh +test-alipay-beancount: ## Run tests for Alipay provider against beancount compiler + @$(SHELL) ./test/alipay-test-beancount.sh +test-alipay-ledger: ## Run tests for Alipay provider against ledger compiler + @$(SHELL) ./test/alipay-test-ledger.sh -test-wechat: ## Run tests for WeChat provider - @$(SHELL) ./test/wechat-test.sh +test-wechat-beancount: ## Run tests for WeChat provider against beancount compiler + @$(SHELL) ./test/wechat-test-beancount.sh +test-wechat-ledger: ## Run tests for WeChat provider against ledger compiler + @$(SHELL) ./test/wechat-test-ledger.sh -test-huobi: ## Run tests for huobi provider - @$(SHELL) ./test/huobi-test.sh +test-huobi-beancount: ## Run tests for huobi provider against beancount compiler + @$(SHELL) ./test/huobi-test-beancount.sh -test-htsec: ## Run tests for htsec provider - @$(SHELL) ./test/htsec-test.sh +test-htsec-beancount: ## Run tests for htsec provider against beancount compiler + @$(SHELL) ./test/htsec-test-beancount.sh -test-icbc: ## Run tests for ICBC provider - @$(SHELL) ./test/icbc-test.sh +test-icbc-beancount: ## Run tests for ICBC provider against beancount compiler + @$(SHELL) ./test/icbc-test-beancount.sh +test-icbc-ledger: ## Run tests for ICBC provider against ledger compiler + @$(SHELL) ./test/icbc-test-ledger.sh format: ## Format code @gofmt -s -w pkg diff --git a/example/alipay/example-alipay-output.ledger b/example/alipay/example-alipay-output.ledger new file mode 100644 index 0000000..ce3c9ff --- /dev/null +++ b/example/alipay/example-alipay-output.ledger @@ -0,0 +1,75 @@ +1970-01-01 * Open Balance + Assets:Alipay 0 CNY + Assets:Alipay:Invest:Fund 0 CNY + Assets:Alipay:Invest:Gold 0 CNY + Assets:FIXME 0 CNY + Expenses:Electronics 0 CNY + Expenses:FIXME 0 CNY + Expenses:Food:Dinner 0 CNY + Expenses:Food:Lunch 0 CNY + Expenses:Groceries 0 CNY + Expenses:Insurance 0 CNY + Income:Alipay:Invest:PnL 0 CNY + Income:Alipay:ShouKuanMa 0 CNY + Income:Alipay:YuEBao:PnL 0 CNY + Income:FIXME 0 CNY + Liabilities:CC:COMM:7449 0 CNY + Equity:Opening Balances +2023-01-18 * xxxx - 转账 + ; category: "转账红包" + ; method: "余额" + ; orderId: "2xxxxxxxxxxxxxxxxxxxxxxxxx9" + ; payTime: "2023-01-18 10:17:29 +0800 CST" + ; source: "支付宝" + ; status: "交易成功" + ; type: "收入" + Assets:Alipay 222228.50 CNY + Income:FIXME - 222228.50 CNY + +2023-02-02 * 蚂蚁财富-蚂蚁(杭州)基金销售有限公司 - 蚂蚁财富-交银定期支付双息平衡混合-卖出至余额宝 + ; category: "投资理财" + ; method: "余额宝" + ; orderId: "2xxxxxxxxxxxxxxxxxxxxxxxxxx8" + ; payTime: "2023-02-02 15:24:35 +0800 CST" + ; source: "支付宝" + ; status: "交易成功" + ; type: "不计收支" + Expenses:FIXME 99.34 CNY + Assets:Alipay - 99.34 CNY + +2023-02-04 * xxxxxxx - 退款-亲情卡 + ; category: "退款" + ; merchantId: "20xxxxxxxxxxxxxxxx5" + ; method: "交通银行信用卡(7449)" + ; orderId: "2xxxxxxxxxxxxxxxx8" + ; payTime: "2023-02-04 18:21:04 +0800 CST" + ; source: "支付宝" + ; status: "退款成功" + ; type: "不计收支" + Liabilities:CC:COMM:7449 16.03 CNY + Expenses:FIXME - 16.03 CNY + +2023-02-08 * x4***6 - 商品示例 + ; category: "日用百货" + ; merchantId: "Txxxxxxxxxxxxx0" + ; method: "余额" + ; orderId: "2xxxxxxxxxxxxxx0" + ; payTime: "2023-02-08 14:16:52 +0800 CST" + ; source: "支付宝" + ; status: "等待确认收货" + ; type: "支出" + Expenses:Groceries 20.00 CNY + Assets:Alipay - 20.00 CNY + +2023-02-12 * xxxxxxxxxxxx - 亲情卡 + ; category: "亲友代付" + ; merchantId: "20230xxxxxxx014741014xxxxxx" + ; method: "交通银行信用卡(7449)" + ; orderId: "202302xxxxxx0011000103xxxxxx" + ; payTime: "2023-02-12 21:32:14 +0800 CST" + ; source: "支付宝" + ; status: "交易成功" + ; type: "支出" + Expenses:FIXME 49.74 CNY + Liabilities:CC:COMM:7449 - 49.74 CNY + diff --git a/example/icbc/credit/example-icbc-credit-output.ledger b/example/icbc/credit/example-icbc-credit-output.ledger new file mode 100644 index 0000000..aaa4329 --- /dev/null +++ b/example/icbc/credit/example-icbc-credit-output.ledger @@ -0,0 +1,42 @@ +1970-01-01 * Open Balance + Assets:Bank:CN:ICBC:Savings 0 CNY + Assets:FIXME 0 CNY + Expenses:FIXME 0 CNY + Expenses:Transport:Highway 0 CNY + Equity:Opening Balances +2023-03-20 * 广东联合电子收费股份 + ; balances: "-16042.73" + ; currency: "人民币" + ; source: "中国工商银行" + ; txType: "********************" + ; type: "支出" + Expenses:Transport:Highway 29.45 CNY + Liabilities:Bank:CN:ICBC - 29.45 CNY + +2023-04-22 * XX旗舰店 + ; balances: "-6086.11" + ; currency: "人民币" + ; source: "中国工商银行" + ; txType: "银联在线支付" + ; type: "支出" + Expenses:FIXME 0.01 CNY + Liabilities:Bank:CN:ICBC - 0.01 CNY + +2023-04-25 * XX分行银行卡中心 + ; balances: "-1971.63" + ; currency: "人民币" + ; source: "中国工商银行" + ; txType: "人民币自动转帐还款" + ; type: "收入" + Liabilities:Bank:CN:ICBC 4621.01 CNY + Assets:Bank:CN:ICBC:Savings - 4621.01 CNY + +2023-05-04 * 广东联合电子收费股份 + ; balances: "-3684.62" + ; currency: "人民币" + ; source: "中国工商银行" + ; txType: "ETC" + ; type: "支出" + Expenses:Transport:Highway 12.35 CNY + Liabilities:Bank:CN:ICBC - 12.35 CNY + diff --git a/example/icbc/debit/example-icbc-debit-output.ledger b/example/icbc/debit/example-icbc-debit-output.ledger new file mode 100644 index 0000000..cfdae15 --- /dev/null +++ b/example/icbc/debit/example-icbc-debit-output.ledger @@ -0,0 +1,67 @@ +1970-01-01 * Open Balance + Assets:Borrow 0 CNY + Assets:FIXME 0 CNY + Expenses:FIXME 0 CNY + Income:Bank:ICBC:CreditCard 0 CNY + Liabilities:Bank:CMB:CreditCard 0 CNY + Equity:Opening Balances +2023-02-10 * 总行信用卡合伙人 + ; cardName: "这是卡别名" + ; currency: "人民币" + ; peerAccount: "总行信用卡合伙人" + ; source: "中国工商银行" + ; txType: "合伙人返现" + ; type: "收入" + Assets:Bank:CN:ICBC 30.00 CNY + Income:Bank:ICBC:CreditCard - 30.00 CNY + +2023-02-20 * 手机银行 张三 + ; cardName: "这是卡别名" + ; currency: "人民币" + ; peerAccount: "张三" + ; source: "中国工商银行" + ; txType: "跨行汇款" + ; type: "支出" + Assets:Borrow 1234.56 CNY + Assets:Bank:CN:ICBC - 1234.56 CNY + +2023-04-14 * ABC公司 + ; cardName: "这是卡别名" + ; currency: "人民币" + ; peerAccount: "ABC公司" + ; source: "中国工商银行" + ; txType: "工资" + ; type: "收入" + Assets:Bank:CN:ICBC 1234.56 CNY + Assets:FIXME - 1234.56 CNY + +2023-04-14 * 手机银行 王五 + ; cardName: "这是卡别名" + ; currency: "人民币" + ; peerAccount: "王五" + ; source: "中国工商银行" + ; txType: "网转" + ; type: "支出" + Assets:Borrow 500.00 CNY + Assets:Bank:CN:ICBC - 500.00 CNY + +2023-04-20 * 银联无卡支付业务((特约)掌上生活还款) + ; cardName: "这是卡别名" + ; currency: "人民币" + ; peerAccount: "银联无卡支付业务((特约)掌上生活还款)" + ; source: "中国工商银行" + ; txType: "银联消费" + ; type: "支出" + Liabilities:Bank:CMB:CreditCard 1234.56 CNY + Assets:Bank:CN:ICBC - 1234.56 CNY + +2023-04-22 * 李四 + ; cardName: "这是卡别名" + ; currency: "人民币" + ; peerAccount: "李四" + ; source: "中国工商银行" + ; txType: "他行汇入" + ; type: "收入" + Assets:Bank:CN:ICBC 1234.56 CNY + Assets:Borrow - 1234.56 CNY + diff --git a/example/wechat/example-wechat-output.ledger b/example/wechat/example-wechat-output.ledger new file mode 100644 index 0000000..959cfc2 --- /dev/null +++ b/example/wechat/example-wechat-output.ledger @@ -0,0 +1,264 @@ +1970-01-01 * Open Balance + Assets:Bank:CN:BOC:Savings 0 CNY + Assets:Bank:CN:ICBC:Savings 0 CNY + Assets:Digital:Wechat:Cash 0 CNY + Assets:FIXME 0 CNY + Assets:Trade:Tencent:LiCaiTong 0 CNY + Expenses:FIXME 0 CNY + Expenses:Food:Meal:Dinner 0 CNY + Expenses:Food:Meal:Lunch 0 CNY + Expenses:Food:Meal:MidNight 0 CNY + Expenses:Housing:Rent 0 CNY + Expenses:Travel 0 CNY + Expenses:Wechat:Commission 0 CNY + Expenses:Wechat:Sponsor 0 CNY + Income:Service 0 CNY + Income:Wechat:RedPacket 0 CNY + Liabilities:Bank:CN:CCB 0 CNY + Equity:Opening Balances +2017-10-20 * 建设银行信用卡还款 - / + ; merchantId: "1000019741201710201961089024" + ; method: "零钱" + ; orderId: "4200000069201710299246843141" + ; payTime: "2017-10-20 18:36:44 +0800 CST" + ; source: "微信支付" + ; status: "支付成功" + ; txType: "信用卡还款" + ; type: "/" + Liabilities:Bank:CN:CCB 548.58 CNY + Assets:Digital:Wechat:Cash - 548.58 CNY + +2019-04-16 * 工商银行(9876) - / + ; merchantId: "/" + ; method: "工商银行(9876)" + ; orderId: "110190416100031243293946287587" + ; payTime: "2019-04-16 10:28:55 +0800 CST" + ; source: "微信支付" + ; status: "充值完成" + ; txType: "零钱充值" + ; type: "/" + Assets:Digital:Wechat:Cash 1300.00 CNY + Assets:Bank:CN:ICBC:Savings - 1300.00 CNY + +2019-09-24 * 同性好友 - / + ; merchantId: "129847129" + ; method: "/" + ; orderId: "3985734" + ; payTime: "2019-09-24 10:10:11 +0800 CST" + ; source: "微信支付" + ; status: "已存入零钱" + ; txType: "微信红包" + ; type: "收入" + Assets:Digital:Wechat:Cash 0.35 CNY + Income:Wechat:RedPacket - 0.35 CNY + +2019-09-26 * 云膳过桥米线(传奇广场店) - 总共消费:28.16 + ; merchantId: "129847129" + ; method: "中国银行(1234)" + ; orderId: "3985734" + ; payTime: "2019-09-26 12:45:27 +0800 CST" + ; source: "微信支付" + ; status: "支付成功" + ; txType: "商户消费" + ; type: "支出" + Expenses:Food:Meal:Lunch 28.16 CNY + Assets:Bank:CN:BOC:Savings - 28.16 CNY + +2020-02-14 * 理财通 - 中欧医疗健康混合C(003096) + ; merchantId: "1800007030102002143310679508" + ; method: "工商银行" + ; orderId: "1800045030312002144223548046" + ; payTime: "2020-02-14 01:24:33 +0800 CST" + ; source: "微信支付" + ; status: "支付成功" + ; txType: "购买理财通" + ; type: "/" + Assets:Trade:Tencent:LiCaiTong 3000.00 CNY + Assets:Bank:CN:ICBC:Savings - 3000.00 CNY + +2020-02-14 * 理财通 - 鹏华增值宝(000569) + ; merchantId: "1800007308102002143430655989" + ; method: "工商银行" + ; orderId: "1800007367312002141165262215" + ; payTime: "2020-02-14 01:32:14 +0800 CST" + ; source: "微信支付" + ; status: "支付成功" + ; txType: "购买理财通" + ; type: "/" + Assets:Trade:Tencent:LiCaiTong 10000.00 CNY + Assets:Bank:CN:ICBC:Savings - 10000.00 CNY + +2020-07-06 * / - / + ; merchantId: "/" + ; method: "零钱通" + ; orderId: "18000070282007060048243102923587" + ; payTime: "2020-07-06 14:54:38 +0800 CST" + ; source: "微信支付" + ; status: "支付成功" + ; txType: "零钱通转出-到工商银行(9876)" + ; type: "/" + Assets:Bank:CN:ICBC:Savings 5505.00 CNY + Assets:Digital:Wechat:Cash - 5505.00 CNY + +2020-11-27 * 用户A - 收款方备注:二维码收款 + ; merchantId: "/" + ; method: "零钱" + ; orderId: "3985734" + ; payTime: "2020-11-27 19:29:00 +0800 CST" + ; source: "微信支付" + ; status: "已收钱" + ; txType: "二维码收款" + ; type: "收入" + Assets:Digital:Wechat:Cash 23.00 CNY + Income:Service - 23.00 CNY + +2021-01-17 * / - / + ; merchantId: "129847129" + ; method: "工商银行(9876)" + ; orderId: "3985734" + ; payTime: "2021-01-17 10:07:31 +0800 CST" + ; source: "微信支付" + ; status: "支付成功" + ; txType: "转入零钱通-来自工商银行(9876)" + ; type: "/" + Assets:Digital:Wechat:Cash 2000.00 CNY + Assets:Bank:CN:ICBC:Savings - 2000.00 CNY + +2021-01-17 * 某餐厅 - 收款方备注:二维码收款 + ; merchantId: "129847129" + ; method: "零钱通" + ; orderId: "3985734" + ; payTime: "2021-01-17 18:03:35 +0800 CST" + ; source: "微信支付" + ; status: "已转账" + ; txType: "扫二维码付款" + ; type: "支出" + Expenses:Food:Meal:Dinner 12.00 CNY + Assets:Digital:Wechat:Cash - 12.00 CNY + +2021-01-22 * 房东 - 转账备注:微信转账 + ; merchantId: "129847129" + ; method: "零钱通" + ; orderId: "3985734" + ; payTime: "2021-01-22 12:34:56 +0800 CST" + ; source: "微信支付" + ; status: "朋友已收钱" + ; txType: "转账" + ; type: "支出" + Expenses:Housing:Rent 500.00 CNY + Assets:Digital:Wechat:Cash - 500.00 CNY + +2021-07-11 * 招商银行() - / + ; merchantId: "/" + ; method: "中国银行" + ; orderId: "207210711100077147832088993175" + ; payTime: "2021-07-11 14:17:52 +0800 CST" + ; source: "微信支付" + ; status: "提现已到账" + ; txType: "零钱提现" + ; type: "/" + 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 + +2021-07-14 * 招商银行() - / + ; merchantId: "/" + ; method: "工商银行" + ; orderId: "207210714100077147459276708175" + ; payTime: "2021-07-14 22:18:10 +0800 CST" + ; source: "微信支付" + ; status: "提现已到账" + ; txType: "零钱提现" + ; type: "/" + 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 + +2021-07-15 * 招商银行() - / + ; merchantId: "/" + ; method: "工商银行" + ; orderId: "207210715100077148235523883175" + ; payTime: "2021-07-15 16:29:37 +0800 CST" + ; source: "微信支付" + ; status: "提现已到账" + ; txType: "零钱提现" + ; type: "/" + 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 + +2021-07-18 * 打开拼多多,点击底部\"多多视频\" - / + ; merchantId: "10101265586742107189275763431048" + ; method: "/" + ; orderId: "160572459521071810106004542906137497131422937" + ; payTime: "2021-07-18 10:48:09 +0800 CST" + ; source: "微信支付" + ; status: "充值成功" + ; txType: "商户消费" + ; type: "收入" + Assets:Digital:Wechat:Cash 0.07 CNY + Income:Wechat:RedPacket - 0.07 CNY + +2021-10-19 * 哈哈 - 亲属卡 + ; merchantId: "XSFF-SD2021101916055813620" + ; method: "零钱通" + ; orderId: "42000011202110191201583411" + ; payTime: "2021-10-19 16:08:00 +0800 CST" + ; source: "微信支付" + ; status: "支付成功" + ; txType: "亲属卡交易" + ; type: "支出" + Expenses:FIXME 2243.46 CNY + Assets:Digital:Wechat:Cash - 2243.46 CNY + +2021-12-15 * 某餐厅 - 测试 T+1 + ; merchantId: "129847129" + ; method: "零钱通" + ; orderId: "3985734" + ; payTime: "2021-12-15 00:06:35 +0800 CST" + ; source: "微信支付" + ; status: "已转账" + ; txType: "扫二维码付款" + ; type: "支出" + Expenses:Food:Meal:MidNight 12.00 CNY + Assets:Digital:Wechat:Cash - 12.00 CNY + +2021-12-15 * 某餐厅 - 测试 T-1 + ; merchantId: "129847129" + ; method: "零钱通" + ; orderId: "3985734" + ; payTime: "2021-12-15 23:51:35 +0800 CST" + ; source: "微信支付" + ; status: "已转账" + ; txType: "扫二维码付款" + ; type: "支出" + Expenses:Food:Meal:MidNight 12.00 CNY + Assets:Digital:Wechat:Cash - 12.00 CNY + +2022-07-18 * 测试时间戳,点击底部\"多多视频\" - / + ; merchantId: "10101265586742107189275763431049" + ; method: "/" + ; orderId: "160572459521071810106004542906137497131422938" + ; payTime: "2022-07-18 10:48:09 +0800 CST" + ; source: "微信支付" + ; status: "充值成功" + ; txType: "商户消费" + ; type: "收入" + Assets:Digital:Wechat:Cash 0.07 CNY + Expenses:Travel - 0.07 CNY + +2022-09-24 * YingDev - / + ; merchantId: "1000108101202209241820542253348" + ; method: "零钱" + ; orderId: "100010810122092400064222561891707533" + ; payTime: "2022-09-24 02:24:20 +0800 CST" + ; source: "微信支付" + ; status: "朋友已收钱" + ; txType: "赞赏码" + ; type: "支出" + Expenses:Wechat:Sponsor 36.99 CNY + Assets:Digital:Wechat:Cash - 36.99 CNY + diff --git a/pkg/compiler/interface.go b/pkg/compiler/interface.go index 1ef5bcd..8ead385 100644 --- a/pkg/compiler/interface.go +++ b/pkg/compiler/interface.go @@ -6,6 +6,7 @@ import ( "github.com/deb-sig/double-entry-generator/pkg/analyser" "github.com/deb-sig/double-entry-generator/pkg/compiler/beancount" + "github.com/deb-sig/double-entry-generator/pkg/compiler/ledger" "github.com/deb-sig/double-entry-generator/pkg/config" "github.com/deb-sig/double-entry-generator/pkg/consts" "github.com/deb-sig/double-entry-generator/pkg/ir" @@ -27,6 +28,8 @@ func New(providerName, targetName, output string, case consts.CompilerBeanCount: return beancount.New(providerName, targetName, output, appendMode, c, i, a) + case consts.CompilerLedger: + return ledger.New(providerName, targetName, output, appendMode, c, i, a) default: return nil, fmt.Errorf("Fail to create the compiler for the given name %s", targetName) } diff --git a/pkg/compiler/ledger/compiler.go b/pkg/compiler/ledger/compiler.go new file mode 100644 index 0000000..55e950c --- /dev/null +++ b/pkg/compiler/ledger/compiler.go @@ -0,0 +1,185 @@ +package ledger + +import ( + "bytes" + "fmt" + "html/template" + "io" + "log" + "sort" + + "github.com/deb-sig/double-entry-generator/pkg/analyser" + "github.com/deb-sig/double-entry-generator/pkg/config" + "github.com/deb-sig/double-entry-generator/pkg/io/writer" + "github.com/deb-sig/double-entry-generator/pkg/ir" + "github.com/deb-sig/double-entry-generator/pkg/util" +) + +// Ledger is the implementation +type Ledger struct { + Provider string + Target string + AppendMode bool + Output string + Config *config.Config + IR *ir.IR + + analyser.Interface +} + +func New(providerName, targetName, ouput string, appendMode bool, config *config.Config, + ir *ir.IR, analyser analyser.Interface) (*Ledger, error) { + ledger := &Ledger{ + Provider: providerName, + Target: targetName, + AppendMode: appendMode, + Output: ouput, + Config: config, + IR: ir, + Interface: analyser, + } + + err := ledger.initTemplates() + if err != nil { + return nil, err + } + + return ledger, nil +} + +func (ledger *Ledger) initTemplates() error { + funcMap := template.FuncMap{ + "EscapeString": util.EscapeString, + } + var err error + normalOrderTemplate, err = template.New("normalOrder").Funcs(funcMap).Parse(normalOrder) + + if err != nil { + return fmt.Errorf("Failed to init the normalOrder Template. %v", err) + } + return nil +} + +// Compile compiles IR to the given platform. +func (ledger *Ledger) Compile() error { + log.SetPrefix("[Compiler-Ledger] ") + log.Printf("Getting the expected account for the bills") + var orders []ir.Order + for _, order := range ledger.IR.Orders { + // Get the expected accounts according to the configuration. + ignore, minusAccount, plusAccount, extraAccounts, tags := ledger.GetAccountsAndTags(&order, ledger.Config, ledger.Provider, ledger.Target) + if ignore { + continue + } + order.MinusAccount = minusAccount + order.PlusAccount = plusAccount + order.ExtraAccounts = extraAccounts + order.Tags = tags + orders = append(orders, order) + } + + ledger.IR.Orders = orders + + outputWriter, err := writer.GetWriter(ledger.Output) + if err != nil { + return fmt.Errorf("can't get output writer, err: %v", err) + } + defer func(outputWriter writer.OutputWriter) { + err := outputWriter.Close() + if err != nil { + log.Printf("output writer close err: %v\n", err) + } + }(outputWriter) + + if !ledger.AppendMode { + if err := ledger.writeHeader(outputWriter); err != nil { + return err + } + } + + log.Printf("Finished to write to %s", ledger.Output) + return ledger.writeBills(outputWriter) +} + +// writeHeader writes the acounts and title into the file. +func (ledger *Ledger) writeHeader(file io.Writer) error { + var err error + + accounts := ledger.GetAllCandidateAccounts(ledger.Config) + var sortedAccounts []string + for k := range accounts { + if k != "" { + sortedAccounts = append(sortedAccounts, k) + } + } + sort.Strings(sortedAccounts) + + _, err = io.WriteString(file, "1970-01-01 * Open Balance\n") + if err != nil { + return fmt.Errorf("write open account error: %v", err) + } + + for _, k := range sortedAccounts { + _, err = io.WriteString(file, " "+k+" 0 "+ledger.Config.DefaultCurrency+"\n") + if err != nil { + return fmt.Errorf("write open account error: %v", err) + } + } + _, err = io.WriteString(file, " Equity:Opening Balances \n") + if err != nil { + return fmt.Errorf("write extra enter error: %v", err) + } + return nil +} + +// writeBills writes bills to the file. +func (ledger *Ledger) writeBills(file io.Writer) error { + // Sort the bills from earliest to lastest. + // If the bills are the same day, the transaction which has lower + // line number is considered happened earlier than the transaction + // which has a higher line number. + sort.Slice(ledger.IR.Orders, func(i, j int) bool { + return ledger.IR.Orders[i].PayTime.Before(ledger.IR.Orders[j].PayTime) + }) + + for i := range ledger.IR.Orders { + if err := ledger.writeBill(file, i); err != nil { + return err + } + } + return nil +} + +func (ledger *Ledger) writeBill(file io.Writer, index int) error { + order := ledger.IR.Orders[index] + + var buf bytes.Buffer + var err error + + switch order.OrderType { + default: + fallthrough + case ir.OrderTypeNormal: + err = normalOrderTemplate.Execute(&buf, &NormalOrderVars{ + PayTime: order.PayTime, + Peer: order.Peer, + Item: order.Item, + Note: order.Note, + Amount: order.Money, + Commission: order.Commission, + PlusAccount: order.PlusAccount, + MinusAccount: order.MinusAccount, + PnlAccount: order.ExtraAccounts[ir.PnlAccount], + CommissionAccount: order.ExtraAccounts[ir.CommissionAccount], + Metadata: order.Metadata, + Currency: ledger.Config.DefaultCurrency, + }) + } + if err != nil { + return err + } + if _, err := io.WriteString(file, buf.String()); err != nil { + return err + } + return nil +} diff --git a/pkg/compiler/ledger/template.go b/pkg/compiler/ledger/template.go new file mode 100644 index 0000000..4b26e8a --- /dev/null +++ b/pkg/compiler/ledger/template.go @@ -0,0 +1,55 @@ +package ledger + +import ( + "html/template" + "time" +) + +// 与 beancount相比, Ledger 格式简单许多, reference: +// - https://ledger-cli.org/doc/ledger3.html +// - https://devhints.io/ledger +/* +2013/01/03 * Rent for January + ; comment + Expenses:Rent $600.00 + Assets:Savings +*/ + +// 普通账单的模版(消费账) +var normalOrder = `{{ .PayTime.Format "2006-01-02" }} * {{ EscapeString .Peer }} {{- if .Item }} - {{ EscapeString .Item }} {{ end }} + {{- if .Note}}; {{ .Note }}{{ end }} + {{- range $key, $value := .Metadata }}{{ if $value }}{{ printf "\n" }} ; {{ $key }}: "{{ $value }}"{{end}}{{end}} + {{ .PlusAccount }} {{ .Amount | printf "%.2f" }} {{ .Currency }} + {{ .MinusAccount }} - {{ .Amount | printf "%.2f" }} {{ .Currency }} + {{- 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 }} + +` + +type NormalOrderVars struct { + PayTime time.Time // 支付时间 + Peer string // 交易对手 + Item string // 交易商品 + Note string // 说明 + Amount float64 // 金额 + Commission float64 // 手续费 + PlusAccount string // 入账账户 + MinusAccount string // 出账账户 + PnlAccount string // + CommissionAccount string // 佣金账户 + Metadata map[string]string // 元数据 + Currency string // 货币 +} + +var ( + normalOrderTemplate *template.Template +) + +// 火币买入模板(TODO) + +// 火币买入模板2(TODO) + +// 火币卖出模版(TODO) + +// 海通买入模版(TODO) diff --git a/pkg/consts/consts.go b/pkg/consts/consts.go index c87f576..f2e6e29 100644 --- a/pkg/consts/consts.go +++ b/pkg/consts/consts.go @@ -19,6 +19,8 @@ package consts const ( // CompilerBeanCount is the name for beancount backend. CompilerBeanCount = "beancount" + // CompilerLedger is the name for ledger backend. + CompilerLedger = "ledger" // ProviderAlipay is the name for alipay provider. ProviderAlipay = "alipay" diff --git a/test/alipay-test.sh b/test/alipay-test-beancount.sh similarity index 100% rename from test/alipay-test.sh rename to test/alipay-test-beancount.sh diff --git a/test/alipay-test-ledger.sh b/test/alipay-test-ledger.sh new file mode 100644 index 0000000..22f056f --- /dev/null +++ b/test/alipay-test-ledger.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# +# E2E test for alipay provider. + +# set -x # debug +set -eo errexit + +TEST_DIR=$(dirname "$(realpath $0)") +ROOT_DIR="$TEST_DIR/.." + +make -f "$ROOT_DIR/Makefile" build +mkdir -p "$ROOT_DIR/test/output" +OUTPUT="$ROOT_DIR/test/output/test-alipay-output.ledger" + +# generate alipay bills output in beancount format +"$ROOT_DIR/bin/double-entry-generator" translate \ + --provider alipay \ + --target ledger \ + --config "$ROOT_DIR/example/alipay/config.yaml" \ + --output "$OUTPUT" \ + "$ROOT_DIR/example/alipay/example-alipay-records.csv" + +diff -u --color \ + "$ROOT_DIR/example/alipay/example-alipay-output.ledger" \ + "$OUTPUT" + +if [ $? -ne 0 ]; then + echo "[FAIL] alipay provider output is different from expected output." + exit 1 +fi + +echo "[PASS] All alipay provider for ledger target tests!" diff --git a/test/htsec-test.sh b/test/htsec-test-beancount.sh similarity index 100% rename from test/htsec-test.sh rename to test/htsec-test-beancount.sh diff --git a/test/huobi-test.sh b/test/huobi-test-beancount.sh similarity index 100% rename from test/huobi-test.sh rename to test/huobi-test-beancount.sh diff --git a/test/icbc-test.sh b/test/icbc-test-beancount.sh similarity index 100% rename from test/icbc-test.sh rename to test/icbc-test-beancount.sh diff --git a/test/icbc-test-ledger.sh b/test/icbc-test-ledger.sh new file mode 100644 index 0000000..b82a34e --- /dev/null +++ b/test/icbc-test-ledger.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# E2E test for icbc provider. + +# set -x # debug +# set -eo errexit + +TEST_DIR=$(dirname "$(realpath $0)") +ROOT_DIR="$TEST_DIR/.." +CREDIT_OUTPUT="$ROOT_DIR/test/output/test-icbc-credit-output.ledger" +DEBIT_OUTPUT="$ROOT_DIR/test/output/test-icbc-debit-output.ledger" + +make -f "$ROOT_DIR/Makefile" build +mkdir -p "$ROOT_DIR/test/output" + +# generate icbc credit bills output in beancount format +"$ROOT_DIR/bin/double-entry-generator" translate \ + --provider icbc \ + --target ledger \ + --config "$ROOT_DIR/example/icbc/credit/config.yaml" \ + --output "$CREDIT_OUTPUT" \ + "$ROOT_DIR/example/icbc/credit/example-icbc-credit-records.csv" + +diff -u --color \ + "$ROOT_DIR/example/icbc/credit/example-icbc-credit-output.ledger" \ + "$CREDIT_OUTPUT" + +if [ $? -ne 0 ]; then + echo "[FAIL] ICBC provider (credit mode) output is different from expected output." + exit 1 +fi + +# generate icbc debit bills output in beancount format +"$ROOT_DIR/bin/double-entry-generator" translate \ + --provider icbc \ + --target ledger \ + --config "$ROOT_DIR/example/icbc/debit/config.yaml" \ + --output "$DEBIT_OUTPUT" \ + "$ROOT_DIR/example/icbc/debit/example-icbc-debit-records.csv" + +diff -u --color \ + "$ROOT_DIR/example/icbc/debit/example-icbc-debit-output.ledger" \ + "$DEBIT_OUTPUT" + +if [ $? -ne 0 ]; then + echo "[FAIL] ICBC provider (debit mode) output is different from expected output." + exit 1 +fi + +echo "[PASS] All ICBC provider tests!" diff --git a/test/wechat-test.sh b/test/wechat-test-beancount.sh similarity index 100% rename from test/wechat-test.sh rename to test/wechat-test-beancount.sh diff --git a/test/wechat-test-ledger.sh b/test/wechat-test-ledger.sh new file mode 100644 index 0000000..4afa5cf --- /dev/null +++ b/test/wechat-test-ledger.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# +# E2E test for wechat provider. + +# set -x # debug +set -eo errexit + +TEST_DIR=$(dirname "$(realpath $0)") +ROOT_DIR="$TEST_DIR/.." +OUTPUT="$ROOT_DIR/test/output/test-wechat-output.ledger" + +make -f "$ROOT_DIR/Makefile" build +mkdir -p "$ROOT_DIR/test/output" + +# generate wechat bills output in beancount format +"$ROOT_DIR/bin/double-entry-generator" translate \ + --provider wechat \ + --target ledger \ + --config "$ROOT_DIR/example/wechat/config.yaml" \ + --output "$OUTPUT" \ + "$ROOT_DIR/example/wechat/example-wechat-records.csv" + +diff -u --color \ + "$ROOT_DIR/example/wechat/example-wechat-output.ledger" \ + "$OUTPUT" + +if [ $? -ne 0 ]; then + echo "[FAIL] WeChat provider output is different from expected output." + exit 1 +fi + +echo "[PASS] All WeChat provider for ledger target tests!"