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 (没有这个命令的话可以通过包管理器安装 beanquery ),则可参考以下代码(LLM生成,已测试过可用):
:更新为依赖正则匹配的代码,速度比 bean-query 更快。
(defvar my/beancount-payee-cache nil
"缓存 Payee 列表以提高补全速度。")
(defvar my/beancount-payee-files (append '("ADDITIONAL/BEAN/FILE") (directory-files "BEAN/FILE/DIRECTORY" t "bean"))
"指向beancount记录的文件'.bean'")
(defun my/beancount-refresh-payee-cache ()
"强制刷新 Payee 缓存。通过遍历文件正则匹配获取 Payee,无需外部 bean-query。"
(interactive)
(message "正在从文件列表扫描收款人...")
(let ((payee-set (make-hash-table :test 'equal)))
(dolist (file-path my/beancount-payee-files)
(when (file-exists-p file-path)
(with-temp-buffer
(insert-file-contents file-path)
;; 遍历每一行
(while (not (eobp))
(let ((line (buffer-substring-no-properties (line-beginning-position) (line-end-position))))
;; 判断是否是交易行:以日期开头,并包含 * 或 !
(when (string-match "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\} [ *!]" line)
;; 在该行中查找所有引号内容
(let (matches)
(while (string-match "\"\\([^\"]+\\)\"" line (or (match-end 0) 0))
(push (match-string 1 line) matches))
;; 如果找到两个或以上引号内容,第一个通常是 Payee
(when (>= (length matches) 2)
(puthash (car (reverse matches)) t payee-set)))) ; matches 是反向 push 的
(forward-line 1))))))
;; 将 Hash 表转换为列表并存入缓存
(setq my/beancount-payee-cache nil)
(maphash (lambda (k v) (push k my/beancount-payee-cache)) payee-set)
(setq my/beancount-payee-cache (sort my/beancount-payee-cache #'string<))
(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默认行为)。