在多个软件对账的时候需要看到特定期间的科目账目变动统计情况,而GnuCash官方并未提供类似报表,故进行报表设计并使用WorkBuddy实现,经人工测试,该报表在GnuCash 5.16显示正常,其他版本未测试。
该报表每行包含单个科目的科目代码、科目名称、期初余额、当期借合计、当期贷合计、期末余额,如有需要请自取。
报表文件
文件名称:account-change-summary.scm
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 科目余额变动表 / Account Change Summary
;;
;; 一个符合 GnuCash 报表规范的自定义报表(适配 GnuCash 5.x,已在 5.16 验证 API)。
;; 针对用户在选项中选定的【会计期间】与【科目】,逐科目汇总:
;; 期初余额 (opening balance) —— 期间开始前该科目的余额
;; 当期借方 (period debit) —— 期间内该科目的借方发生额
;; 当期贷方 (period credit) —— 期间内该科目的贷方发生额
;; 期末余额 (closing balance) —— 期初余额 + 当期借 - 当期贷(按科目性质自动处理)
;;
;; 会计恒等式(自动满足):
;; 期末余额 = 期初余额 + 当期借 - 当期贷
;; (资产/费用类科目“借”为增加;负债/权益/收入类科目“贷”为增加,报表已据此正确归类)
;;
;; 与早期版本相比,本版使用的均为 GnuCash 5.16 仍然支持且推荐的封装:
;; - gnc:options-add-date-interval! (会计期间起止日)
;; - gnc-register-account-list-option (科目多选,5.16 推荐直接注册)
;; - gnc:make-commodity-collector + 'getmonetary (按货币汇总,5.x 惯用取值方式)
;; - xaccAccountGetSplitList / xaccSplitGetAmount / xaccTransGetDate
;; (稳定的引擎层封装,用于拆出借/贷)
;; - qof-print-date (5.16 内置报表实际使用的日期格式化函数;
;; 注意 gnc-print-date 在 5.16 并不存在,勿用)
;;
;; copyright 2026 ocsxxi (with WorkBuddy in Hy3 & Hy4preview 2026.10.1 11:07)
;;
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 2 of
;; the License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, contact:
;;
;; Free Software Foundation Voice: +1-617-542-5942
;; 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652
;; Boston, MA 02110-1301, USA gnu@gnu.org
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define-module (gnucash reports custom account-change-summary))
(use-modules (gnucash engine))
(use-modules (gnucash utilities))
(use-modules (gnucash core-utils))
(use-modules (gnucash app-utils))
(use-modules (gnucash report))
(use-modules (gnucash html))
(use-modules (ice-9 format))
(use-modules (srfi srfi-1))
;; ---- 选项名(字符串) ----
(define optname-startdate (N_ "Start Date"))
(define optname-enddate (N_ "End Date"))
(define optname-accounts (N_ "Accounts"))
(define optname-show-full-name (N_ "Show full account name"))
(define optname-only-active (N_ "Only show accounts with activity"))
;; ---- 辅助函数 ----
;; 判断科目类型是否为“贷正常”(负债 / 权益 / 收入及其子类)
(define (credit-normal-type? type)
(or (eq? type ACCT-TYPE-LIABILITY)
(eq? type ACCT-TYPE-CREDIT)
(eq? type ACCT-TYPE-PAYABLE)
(eq? type ACCT-TYPE-EQUITY)
(eq? type ACCT-TYPE-INCOME)))
;; gnc-numeric 是否为负(5.16 用 gnc-numeric-negative-p,注意不是 gnc:gnc-numeric-num)
(define (amount-negative? amt)
(gnc-numeric-negative-p amt))
;; 取 commodity-collector 中某货币的金额对象(gnc-monetary);
;; 5.16 的 'getmonetary 签名为 (getmonetary 商品 sign?),sign?=#f 表示不取负。
;; 若该货币未记录,getmonetary 自动返回该货币下的零金额对象,无需额外兜底。
(define (collector-monetary coll comm)
(coll 'getmonetary comm #f))
;; 生成货币对象;若金额为负则取相反数,保证“借/贷”列始终为非负发生额
(define (non-negative-monetary comm amt)
(let ((m (gnc:make-gnc-monetary comm amt)))
(if (amount-negative? amt)
(gnc:monetary-neg m)
m)))
;; 取得账套中的所有科目(兜底:出错时返回空表,由渲染器提示用户手动选择)
(define (get-all-accounts)
(catch #t
(lambda ()
(gnc-account-get-descendants
(gnc-book-get-root-account (gnc-get-current-book))))
(lambda (key . args) '())))
;; 取得科目编码;GnuCash 允许科目不设编码(此时返回 ""),统一兜底为空字符串,
;; 避免 #f 参与字符串比较导致排序/显示报错
(define (account-code account)
(let ((c (xaccAccountGetCode account)))
(if c c "")))
;; 字符串升序比较:从左向右逐字符比较字符码值(char->integer),
;; 遇到第一个不同字符即定序;若一个是另一个的前缀,则较短者排在前。
;; 例:"1001" < "1001.01" < "1002",且 "1000" 排在 "999" 之前(首位 '1' < '9')。
(define (string-code<? a b)
(let ((la (string-length a))
(lb (string-length b)))
(let loop ((i 0))
(cond
((and (>= i la) (>= i lb)) #f) ; 完全相同:a 不小于 b
((>= i la) #t) ; a 是 b 的前缀:短者在前
((>= i lb) #f) ; b 是 a 的前缀
(else
(let ((va (char->integer (string-ref a i)))
(vb (char->integer (string-ref b i))))
(cond ((< va vb) #t)
((> va vb) #f)
(else (loop (+ i 1))))))))))
;; 科目编码升序比较:编码一律按字符串处理,逐字符比较(不做数值转换)
(define (account-code<? a b)
(string-code<? (account-code a) (account-code b)))
;; ---- 核心计算:单个科目的四列数据 ----
(define (compute-account-row account start-date end-date)
(let* ((comm (xaccAccountGetCommodity account))
(type (xaccAccountGetType account))
(open-coll (gnc:make-commodity-collector))
(debit-coll (gnc:make-commodity-collector))
(credit-coll (gnc:make-commodity-collector))
(close-coll (gnc:make-commodity-collector)))
(for-each
(lambda (split)
(let* ((txn (xaccSplitGetParent split))
(date (xaccTransGetDate txn))
(amt (xaccSplitGetAmount split)))
(cond
;; 期间之前的凭证 -> 计入期初(同时计入期末累计)
((< date start-date)
(open-coll 'add comm amt)
(close-coll 'add comm amt))
;; 期间之内(含起止日)-> 拆为借/贷,并计入期末累计
((<= date end-date)
(let* ((neg? (amount-negative? amt))
(debit? (if (credit-normal-type? type) neg? (not neg?))))
(if debit?
(debit-coll 'add comm amt)
(credit-coll 'add comm amt))
(close-coll 'add comm amt)))
(else #f)))) ; 期间之后 -> 忽略
(xaccAccountGetSplitList account))
;; 返回:(货币 期初 当期借 当期贷 期末 科目),均为 gnc-monetary 对象;末尾附科目便于渲染
(list comm
(collector-monetary open-coll comm)
(non-negative-monetary comm
(gnc:gnc-monetary-amount (collector-monetary debit-coll comm)))
(non-negative-monetary comm
(gnc:gnc-monetary-amount (collector-monetary credit-coll comm)))
(collector-monetary close-coll comm)
account)))
;; ---- 选项生成器 ----
(define (options-generator)
(let ((options (gnc-new-optiondb)))
;; 会计期间(起止日)—— 5.x 推荐封装
(gnc:options-add-date-interval!
options gnc:pagename-general optname-startdate optname-enddate "a")
;; 科目选择
(gnc-register-account-list-option
options
gnc:pagename-accounts optname-accounts "a"
(N_ "Select the accounts to include in the report. Leave as-is to include all accounts.")
(get-all-accounts))
;; 显示选项
(gnc-register-simple-boolean-option
options
gnc:pagename-display optname-show-full-name "a"
(N_ "Show the full account name (including parent accounts).") #t)
(gnc-register-simple-boolean-option
options
gnc:pagename-display optname-only-active "b"
(N_ "Only show accounts that had debit or credit activity in the period.")
#f)
(gnc:options-set-default-section options gnc:pagename-general)
options))
;; ---- 渲染器 ----
(define (renderer report-obj)
(define (get-op section name)
(gnc:option-value (gnc:lookup-option (gnc:report-options report-obj) section name)))
(let* ((start-date (gnc:time64-start-day-time
(gnc:date-option-absolute-time
(get-op gnc:pagename-general optname-startdate))))
(end-date (gnc:time64-end-day-time
(gnc:date-option-absolute-time
(get-op gnc:pagename-general optname-enddate))))
(selected (get-op gnc:pagename-accounts optname-accounts))
(show-full? (get-op gnc:pagename-display optname-show-full-name))
(only-active? (get-op gnc:pagename-display optname-only-active))
(accounts (if (null? selected) (get-all-accounts) selected))
(document (gnc:make-html-document)))
(gnc:html-document-set-title!
document (_ "科目余额变动表 / Account Change Summary"))
(gnc:html-document-add-object!
document
(gnc:make-html-text
(gnc:html-markup-p
(format #f (_ "会计期间:~a 至 ~a 所选科目数:~d")
(qof-print-date start-date)
(qof-print-date end-date)
(length accounts)))))
(if (null? accounts)
;; 未选择任何科目时的提示
(gnc:html-document-add-object!
document
(gnc:make-html-text
(gnc:html-markup-p
(_ "未选择任何科目。请在报表选项的“科目”页中选择需要统计的科目,"
"或保持默认以统计全部科目。"))))
;; 正常构建表格
(let* ((sorted (sort accounts
(lambda (a b) (account-code<? a b))))
(rows (map (lambda (a) (compute-account-row a start-date end-date))
sorted))
(g-open (gnc:make-commodity-collector))
(g-debit (gnc:make-commodity-collector))
(g-credit (gnc:make-commodity-collector))
(g-close (gnc:make-commodity-collector))
(comm-list '()))
;; 累计合计,并收集出现的不同货币
(for-each
(lambda (row)
(let ((comm (list-ref row 0))
(open (list-ref row 1))
(deb (list-ref row 2))
(cred (list-ref row 3))
(close (list-ref row 4)))
(g-open 'add comm (gnc:gnc-monetary-amount open))
(g-debit 'add comm (gnc:gnc-monetary-amount deb))
(g-credit 'add comm (gnc:gnc-monetary-amount cred))
(g-close 'add comm (gnc:gnc-monetary-amount close))
(if (not (any (lambda (c)
(string=? (gnc-commodity-get-mnemonic c)
(gnc-commodity-get-mnemonic comm)))
comm-list))
(set! comm-list (cons comm comm-list)))))
rows)
(let ((table (gnc:make-html-table)))
(gnc:html-table-set-col-headers!
table
(list (_ "科目编码")
(_ "科目")
(_ "期初余额")
(_ "当期借方")
(_ "当期贷方")
(_ "期末余额")))
;; 逐科目明细行
(for-each
(lambda (row)
(let ((account (list-ref row 5))
(comm (list-ref row 0))
(open (list-ref row 1))
(deb (list-ref row 2))
(cred (list-ref row 3))
(close (list-ref row 4)))
(let ((deb-amt (gnc:gnc-monetary-amount deb))
(cred-amt (gnc:gnc-monetary-amount cred)))
(when (or (not only-active?)
(not (and (gnc-numeric-zero-p deb-amt)
(gnc-numeric-zero-p cred-amt))))
(gnc:html-table-append-row!
table
(list
(account-code account)
(if show-full?
(gnc-account-get-full-name account)
(xaccAccountGetName account))
(gnc:make-html-table-cell/markup "number-cell" open)
(gnc:make-html-table-cell/markup "number-cell" deb)
(gnc:make-html-table-cell/markup "number-cell" cred)
(gnc:make-html-table-cell/markup "number-cell" close)))))))
rows)
;; 合计行(按货币分别汇总;单一货币时仅一行“合计”)
(for-each
(lambda (comm)
(let ((open (collector-monetary g-open comm))
(deb (non-negative-monetary comm
(gnc:gnc-monetary-amount (collector-monetary g-debit comm))))
(cred (non-negative-monetary comm
(gnc:gnc-monetary-amount (collector-monetary g-credit comm))))
(close (collector-monetary g-close comm))
(label (if (= (length comm-list) 1)
(_ "合计")
(format #f (_ "合计 (~a)")
(gnc-commodity-get-mnemonic comm)))))
(gnc:html-table-append-row!
table
(list
""
label
(gnc:make-html-table-cell/markup "number-cell" open)
(gnc:make-html-table-cell/markup "number-cell" deb)
(gnc:make-html-table-cell/markup "number-cell" cred)
(gnc:make-html-table-cell/markup "number-cell" close)))))
comm-list)
(gnc:html-document-add-object! document table))))
document))
;; ---- 注册报表 ----
;; 注意:report-guid 必须为 32 位十六进制、不含连字符。
;; 建议用 GnuCash 自带 gnucash-make-guids 重新生成一个唯一 GUID 以防与其它报表冲突。
(gnc:define-report
'version 1
'name (N_ "科目余额变动表 / Account Change Summary")
'report-guid "7e922f52d2b74401970da02a0ca91052"
'menu-tip (N_ "Select an accounting period and accounts to summarise opening balance, period debits, period credits and closing balance.")
'menu-path (list gnc:menuname-example)
'options-generator options-generator
'renderer renderer)
安装方法
将此文件复制到 GnuCash 用户数据目录: Linux :
~/.local/share/gnucash/Windows :%APPDATA%\GnuCash\macOS :~/Library/Application Support/GnuCash/在同目录下
config-user.scm中追加一行(若该文件不存在,新建一个即可,内容如下):
文件:config-user.scm
(load (gnc-build-userdata-path "account-change-summary.scm"))
- 重启 GnuCash。报表出现在:
报表 -> 示例 -> 科目余额变动表