UP | HOME

▼ 本文更新于 [2026-01-22 周四 10:03]

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

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