emacs-让beancount插件可以补全收款人
最近在AI的指点下,更新了beancount的记账模式,从原来不断开子账户转变为大类账户+收款人+备注。由于这也是beancount官方推荐的备注模式,所以在诸如fava的地方也能很好地利用收款人和备注来匹配筛选出符合条件的交易。
注:beancount的一个交易一般如下:
2026-01-21 * "收款人" "备注"
账户1 1 CNY
账户2 -1 CNY
2026-01-21 * "备注"
账户1 1 CNY
账户2 -1 CNY
但是默认情况下,beancount官方的emacs插件 beancount.el 并未提供收款人的补全。(当然,他们也没有提供账户的指定补全文件功能,默认情况下只补全当前buffer的账户,可以通过这篇文章解决。)
1. 收款人补全代码
如果想要一个可以选择日期、补全收款人的函数,且你已经在电脑上安装了beancount,可调用 bean-query ,则可参考以下代码(LLM生成,已测试过可用):
(defvar my/beancount-payee-cache nil
"缓存 Payee 列表以提高补全速度。")
(defvar my/beancount-main-file "PATH/TO/YOUR/MAIN/BEANCOUNT/FILE"
"指向beancount主文件'*.bean'")
(defun my/beancount-refresh-payee-cache ()
"强制刷新 Payee 缓存。从 bean-query 获取最新数据并清理格式。"
(interactive)
(message "正在获取 Beancount 收款人列表...")
(let* ((file (file-truename my/beancount-main-file))
(query "SELECT distinct payee WHERE payee != ''")
(cmd (format "bean-query -f csv %s %S"
(shell-quote-argument file)
query))
;; 1. 执行命令并处理 Windows 换行符 (^M)
(raw-output (shell-command-to-string cmd))
(clean-output (replace-regexp-in-string "\r" "" raw-output))
(lines (split-string clean-output "\n" t))
;; 2. 提取数据
(payees (if (and lines (string= (car lines) "payee"))
(cdr lines) ;; 移除 CSV 表头
lines)))
;; 3. 清理每一行的双引号并存入缓存
(setq my/beancount-payee-cache
(mapcar (lambda (s)
(replace-regexp-in-string "^\"\\|\"$" "" s))
payees))
(message "收款人缓存刷新完毕,共 %d 个。" (length my/beancount-payee-cache))
my/beancount-payee-cache))
(defun my/beancount-get-payees-with-cache ()
"获取收款人列表。如果缓存为空,则进行第一次加载。"
(if my/beancount-payee-cache
my/beancount-payee-cache
(my/beancount-refresh-payee-cache)))
;; 利用calendar界面选择日期,快速插入beancount的entry
(defun my/beancount-insert-chosen-date ()
"插入 Beancount 格式的日期、Payee 和备注。
Payee 支持从历史记录中补全。"
(interactive)
(let* (;; 1. 获取日期 (保留你原来的 org-time-stamp 逻辑)
(date (substring
(with-temp-buffer
(org-time-stamp nil nil)
(buffer-string))
1 11))
;; 2. 获取 Payee 列表用于补全
(payee-list (my/beancount-get-payees-with-cache))
;; 1. 输入 Payee
(payee (completing-read "收款人: " payee-list))
;; 2. 输入 Narration
(narration (read-string "备 注: ")))
;; 插入日期和旗标
(insert date " * ")
;; 插入 Payee 和 Narration
;; 根据 Beancount 语法:如果两个都有,格式为 "Payee" "Narration"
;; 如果只有一个,默认为 Narration
(cond
((and (not (string-empty-p payee)) (not (string-empty-p narration)))
(insert (format "\"%s\" \"%s\"" payee narration)))
((not (string-empty-p payee))
(insert (format "\"%s\"" payee)))
(t
(insert "\"\""))) ; 均为空则插入空引号
;; 换行并缩进
(insert "\n ")))
然后再设置beancount,用这个函数替代默认的 beancount-insert-date 快捷键 M-RET
(use-package beancount
:mode
("\\.bean\\(?:count\\)?\\'" . beancount-mode)
:bind
(:map beancount-mode-map
;; 绑定一个快捷输入日期的自定义函数
("M-RET" . my/beancount-insert-chosen-date))
2. 使用说明
在beancount的文件中,如 2026.bean ,按下 M-RET ,会弹出org-mode的日期选择窗口。选择好日期后,会提示你输入收款人(带补全),再提示输入备注。如果收款人不为空且备注为空,则会用收款人接收到的字符串作为备注使用(beancount默认行为)。