UP | HOME

▼ 本文更新于 [2026-03-08 周日 23:56]

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默认行为)。

© Published by Emacs 31.0.50 (Org mode 10.0-pre) | RSS English-Index