从 Beancount 迁移
张记账可以直接读取 Beancount 账本。你可以把它指向你的 main.bean,照旧编辑你的文件:张记账在网页界面中显示它们,你在网页界面中记录的内容会以 Beancount 语法写回。
本页介绍如何运行 Beancount 账本,并在兼容性一节中列出张记账与 Beancount 的不同之处。
运行 Beancount 账本
Section titled “运行 Beancount 账本”把账本目录和主文件名交给 zhang serve:
zhang serve /path/to/ledger --endpoint main.bean使用 Docker 时,把目录挂载到 /data,并在镜像名之后加上 --endpoint:
docker run --name zhang -v "/path/to/ledger:/data" -p "8000:8000" kilerd/zhang:latest --endpoint main.bean- 主文件以
.bean、.bc或.beancount结尾时,张记账会把整个账本都按 Beancount 读取:主文件以及它引入的每个文件,无论扩展名是什么。一个账本要么是 Beancount 语法,要么是张记账语法,不能混用。 - 打开
http://localhost:8000。侧边栏中的总览项显示张记账发现了多少问题,总览页面列出这些问题。请先逐一处理它们:大部分来自下文所述的差异。 - 你在网页界面中记录的内容会写入一个与主文件扩展名相同的文件,默认是
data/{{year}}/{{month_str}}.bean,使用 Beancount 语法。见记录交易。
安装列出了 zhang serve 的所有选项。
记账行元数据
Section titled “记账行元数据”张记账按 Beancount 和 Fava 的方式读取 Beancount 文件中交易的元数据:第一条记账行之前的元数据属于交易,记账行之后的每一行元数据都属于该记账行,无论缩进多少。
2024-01-02 * "Cafe" "lunch" invoice: "2024-001" ; 属于交易 Assets:Cash -10 CNY receipt: "r-17" ; 属于 Assets:Cash 记账行 Expenses:Food 10 CNY category: "meals" ; 属于 Expenses:Food 记账行旧版本的张记账把交易的元数据写在记账行之后。这样的文件现在在张记账中的读法不同了:这些元数据属于最后一条记账行,Fava 一直是这样读取的。有两个键仍照旧工作:
time:写在最后一条记账行之后、与记账行缩进相同的time(旧版本张记账就是这样写的),仍然是交易的时间,除非交易自己有time,或者其他记账行有time。缩进比其记账行更深的time仍属于该记账行。document:记账行上的文档是其所属交易的文档,因此附加到交易上的文档仍然附加在交易上。
张记账写入交易时,例如你在网页界面中编辑交易之后,会把交易的元数据写在记账行之前,并把每条记账行的元数据紧跟在该记账行之下,因此文件在张记账、Beancount 和 Fava 中的读法都一样。见交易。
| Beancount | 在张记账中 |
|---|---|
open |
读取,包括其商品和记账方法(open Assets:Broker HOOL "FIFO")。这些商品必须已声明,但使用其他商品的记账行不会被报告。 |
close |
读取。关闭仍有余额的账户会被报告为 CloseNonZeroAccount。 |
commodity |
读取,并且是必需的:见商品必须声明。 |
| 交易 | 读取,支持标记 *、! 和其他 Beancount 标记(在交易上和记账行上)、txn 关键字、标签、链接、成本({}、{{}},可带日期和批次标签)以及价格(@、@@)。 |
balance |
读取,可以带 ~ 容差。没有容差时精确匹配:见余额断言是精确的。 |
pad |
读取,并与它所服务的 balance 配对:见补齐。 |
note、event |
读取。 |
document |
读取。路径与 Beancount 一样相对于该指令所在的文件:见文档路径。 |
price |
读取,用于查询和货币页面中的估值。 |
query |
读取:这些查询出现在查询页面的已保存菜单中。 |
custom |
读取。custom "budget" … 定义预算:见预算。 |
option |
读取。只有部分选项起作用:见选项。 |
plugin |
Python 插件不会运行:见插件。 |
include |
读取,包括 * 模式,例如 include "2024/*.bean"。 |
pushtag、poptag、pushmeta、popmeta |
读取。 |
time: "HH:MM:SS" 元数据可以为指令加上一天中的时间,但 balance 和 pad 除外:张记账与 Beancount 一样忽略它们的时间,见余额断言的时间。
Beancount 不加引号就能读取的元数据值——账户、货币、数字或算式、金额、日期、标签、TRUE、FALSE 和 NULL——在交易、记账行和其他所有指令上都按书写原样读取并保存为文本。counterpart: Assets:Bank 是文本 Assets:Bank,limit: 10.00 USD 是文本 10.00 USD,1 + 2 仍是 1 + 2,而 Beancount 会算出 3。标签值保留它的 #,TRUE、FALSE 和 NULL 保持原词,而 beanquery 显示的是 True、False 和空值。张记账写回这些值时保持原样。
张记账无法读取以下内容。使用了它们的文件完全无法加载:
- 只有日期或只有批次标签的成本,例如
{2024-01-01}或{"lot-1"}。请先写成本:{100.00 USD, 2024-01-01}; - 复合成本
{100 # 9.95 USD},以及{*}。
张记账使用 Beancount 账本的以下选项:
title,显示在网页界面中;operating_currency,但只能有一个:张记账保留最后一个,并把其余的都报告为MultipleOperatingCurrencyDetect;account_previous_balances、account_previous_earnings、account_previous_conversions、account_current_earnings、account_current_conversions和conversion_currency,用于查询的会计期间。
其他所有 Beancount 选项,例如 booking_method、inferred_tolerance_default 或 documents,都会列在设置页面上,除此之外被忽略。张记账也有自己的选项,例如 default_booking_method 和 timezone,Beancount 同样会忽略它们。见选项。
商品必须声明
Section titled “商品必须声明”记账行计算权重所用的每种商品、price 提到的每种商品以及 open 列出的每种商品,都必须有一条 commodity 指令。主货币由它的选项声明。Beancount 不要求 commodity 指令;张记账会把缺少的声明报告为 CommodityDoesNotDefine。请为每种商品添加一条,日期早于它的首次使用:
1970-01-01 commodity HOOL余额断言是精确的
Section titled “余额断言是精确的”Beancount 允许 balance 在差额不超过某个容差时通过,这个容差由小数位数推导而来:balance Assets:A 10.00 USD 在余额为 10.004 USD 时通过。张记账不这样做:只有金额完全相等,或在你写在 ~ 之后的容差之内(例如 10.00 ~ 0.005 USD),断言才成立。见余额。
交易按商品的精度平衡
Section titled “交易按商品的精度平衡”当一笔交易在每种商品上的权重之和按该商品的精度舍入后为零时,交易就是平衡的。精度是其 commodity 指令的 precision 元数据,没有时为 2 位小数。Beancount 则根据交易中写出的数字推导这个容差。请为金额更精细的商品(例如 BTC)设置自己的 precision。见批次与成本。
除非另行指定,张记账按 FIFO 记账,而 Beancount 的默认方法是 STRICT。要得到 Beancount 的行为,请添加 option "default_booking_method" "STRICT";Beancount 自己的 booking_method 选项不会被读取。NONE、AVERAGE 和 AVERAGE_ONLY 尚未实现:使用其中之一的账户会得到一个错误,并按默认方法记账。批次标签与 Beancount 一样用于选择批次:写成 {, "first"} 的卖出会扣减标签为 first 的批次。见批次与成本。
张记账像 Beancount 一样,把每条 pad 与它所服务的 balance 条目配对:该账户在每种商品上、日期更晚的下一条 balance,直到该账户的下一条 pad 为止,但绝不包括与 pad 同一天的 balance。补齐交易的日期是 pad 的日期,pad 和它的 balance 可以在不同的文件中,不服务任何 balance 的 pad 会报告为 UnusedPad。不同之处在于:
- 补齐总是使账户精确等于断言金额,即使差额在明确写出的
~容差之内,而 Beancount 此时不补齐。 - 只有对被补齐账户本身的断言会使用这条
pad:Beancount 还允许子账户上的断言用掉其父账户的pad。 - 同一账户在不同文件中同一天的两条
pad,由张记账排在最后的那条补齐,它可能不是 Beancount 使用的那条。
余额断言的时间
Section titled “余额断言的时间”张记账与 Beancount 一样,在日期开始时、当天的交易之前检查 balance,并忽略它的 time 元数据。早期版本的张记账会在那个时刻检查它,即在当天该时刻之前的交易之后:当这改变了一条余额断言所检查的金额时,它会附带一条 BalanceTimeIgnored 提示。要在当天的交易之后检查,请把这条余额断言的日期写成下一天。
张记账与 Beancount 一样,相对于 document 所在的文件读取它的路径,你上传的文档也是这样写入的。早期版本的张记账把上传文档的路径写成相对于账本根目录,写入 data/2026/10.bean 这类文件中,Beancount 会报告这些文件不存在。张记账仍然能打开这些文档。在本地磁盘上,它会在每一条上附带一条 DocumentPathRelativeToRoot 提示,给出应改写成的路径,并把在任何位置都找不到的文档报告为 DocumentNotFound。见文档。
只有 price 指令会给出价格。用 @ 和 @@ 写在记账行上的价格不会,而在 Beancount 中使用 implicit_prices 插件时它们会。
- 关闭仍有余额的账户会被报告为错误。Beancount 允许这样做。
- 商品不在账户
open列表中的记账行或余额断言,与 Beancount 一样会被报告为CommodityNotAllowed,但每个写下的记账行只报告一次:对按多个批次记账的卖出,Beancount 每个批次报告一次。 - 账户名必须以
Assets、Liabilities、Equity、Income或Expenses开头。Beancount 中用来重命名它们的name_assets… 等选项不会被读取。 - 在带引号的字符串中,不构成转义的反斜杠会被保留:
"\d"仍是\d,而 Beancount 会丢掉这个反斜杠。
不支持的功能
Section titled “不支持的功能”Beancount 的插件是 Python 代码,张记账不运行它们,包括 Beancount 自带的插件,例如 auto_accounts 或 implicit_prices。没有它们时:
- 显式开设每个账户,而不是依赖
auto_accounts; - 检查其他插件替你做了什么,然后在账本中或用查询完成这些工作。
只要账本没有启用张记账自己的插件,plugin 行就不会造成影响:使用 option "features.plugin" "true" 时,张记账会尝试把每个 plugin 都作为 WASM 模块加载,Python 模块名会让整个账本无法加载。张记账的插件是 WASM 模块:见插件。
张记账读取按 Beancount 接受的写法写成 custom 指令的预算:custom "budget" "Food" "CNY"、custom "budget-add" "Food" 2000 CNY、custom "budget-transfer" "Fun" "Food" 300 CNY 和 custom "budget-close" "Food",类型和名称都加引号;张记账也以这种方式写入预算,所以 bean-check 和 Fava 都接受这样的账本。早期版本的张记账写出的不加引号的写法 custom budget Food CNY 仍然可以读取,但 Beancount 会以 Invalid token 拒绝它:请改用加引号的写法。见预算指令。
张记账独有的功能
Section titled “张记账独有的功能”以下功能在张记账中可用,在 Beancount 中没有对应:
张记账账本(main.zhang)还有更多自己的语法,例如 balance … with pad 和带时间的日期;Beancount 无法读取它。张记账没有在两种语法之间转换的命令,所以如果你想继续使用 Beancount 的工具,请让账本保持 Beancount 语法。
