企业版文档会计

Odoo 企业版 Documents + Accounting 为什么不是“把发票扔进文件夹再点建账单”:Journal Folder Sync、XML 内嵌 PDF 与附件回写边界讲透

很多人把 Odoo 企业版 Documents + Accounting 理解成“在文档里存附件,然后手动生成一张 Vendor Bill”。但从 `documents_account` 源码看,官方真正设计的是一套会计文档编排层:按 Journal 自动建同步文件夹与标签、把可用动作嵌进 Finance 树、从附件创建单据后再持续回写文档位置与业务伙伴,并对 XML 发票里的内嵌 PDF 做预览兼容。

企业 会计
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 7 阅读

很多人第一次看到 Documents + Accounting,会把它想成一条很轻的链:

  • 把发票 PDF 丢进 Documents;
  • 点一下“Create Vendor Bill”;
  • 系统建出一张会计单据。

这个理解不算错,但太薄了。

如果你去读 /home/ubuntu/odoo-temp/enterprise/documents_account,会发现官方真正做的不是“给文档应用加一个建账按钮”,而是搭了一层 会计文档编排机制。这层机制至少在处理五件事:

  1. 每个 Journal 在 Documents 里应该落到哪个同步文件夹?
  2. 用户在文件夹里能看到哪些“创建会计单据”动作,而且这些动作是否按公司与 Journal 类型收口?
  3. 从附件生成 Vendor Bill / Customer Invoice / Bank Statement 后,文档怎么继续跟着单据走,而不是脱节?
  4. 如果主附件、Journal、Partner 后来变了,文档要不要跟着回写?
  5. 电子发票 XML 里如果塞了 PDF,Documents 预览到底按 XML 还是按 PDF 的体验走?

所以 documents_account 的重点不是“从文档生成账单”,而是“把会计附件、文件夹、动作和单据状态持续同步起来”。

这也是它为什么值得单独拆开讲:它不是一个按钮,而是一套边界管理。


一、入口不是“任意文件夹都能建账”,而是先按 Journal 自动铺好 Finance 树

先看 models/account_journal.py

account.journal.create() 被扩展后,会在 Journal 创建成功时直接调用 journals.sudo()._documents_configure_sync()。这一步很关键,因为它表明官方设计不是“等用户以后自己去配置文档目录”,而是:

  • 只要公司有 account_folder_id,并且 Journal 具备类型与名称;
  • 系统就会尝试自动完成 Documents 侧的同步准备。

_documents_configure_sync() 主要干三件事:

1)自动准备 Journal 对应文件夹

_documents_ensure_journal_folder_created() 会按 (company, journal.type) 维度找或建同步文件夹。

这里有两个值得注意的点:

  • 不是按“每个 Journal 一个文件夹”,而是先按 Journal 类型 组织主文件夹;
  • 但真正的同步设置 documents.account.folder.setting 仍然是 按具体 Journal 记录的。

这意味着官方把“目录拓扑”和“业务映射”拆成了两层:

  • 文件夹层:采购、销售、银行、总账这些大类;
  • 设置层:具体哪个 Journal 用哪个文件夹、打哪些标签。

所以 Documents 里的结构不会随着 Journal 数量无限炸开,但会计侧又保留了 Journal 级映射能力。

2)自动准备以 Journal 名称命名的标签

_documents_ensure_journal_tags_created() 会拿所有 Journal 的 name 去补齐 documents.tag,再用 _documents_sync_translations() 同步翻译。

这段设计非常像 Odoo 常见的“目录负责粗分流,标签负责细定位”。

也就是说:

  • 文件夹不必细到每本账都单独一层;
  • 但标签会把具体 Journal 身份带回来。

于是同为采购类文档,也能通过 tag 看出它原本更偏向哪个采购 Journal。

3)把会计动作嵌进正确的文件夹

_documents_get_embed_on_sync_actions() 直接定义了 Journal 类型与动作的映射关系:

  • purchase → Create Vendor Bill / Create Vendor Credit Note
  • sale → Create Customer Invoice / Create Credit Note
  • bank → Import Bank Statement
  • general → Create Misc Entry

然后 _documents_configure_sync() 会把这些动作同时嵌到:

  • 当前 Journal 的目标文件夹;
  • 公司级 Finance 根文件夹。

这说明官方不想让用户去记“这个动作只能去某个很深的子目录才能看到”,而是把可用动作在 局部文件夹Finance 父层 都铺出来。

所以 Documents 里的会计操作入口,本质上是由 Journal 类型驱动的,而不是由用户随便在任意文件夹点一个通用动作。


二、真正执行“建账”的不是普通按钮,而是受限于 documents.document 的专用 server action

再看 models/ir_actions_server.pydata/ir_actions_server_data.xml

documents_accountir.actions.server 新增了一个状态:

  • documents_account_record_create

并且新增了一组受控的目标模型:

  • account.move.in_invoice
  • account.move.out_invoice
  • account.move.in_refund
  • account.move.out_refund
  • account.move.entry
  • account.bank.statement
  • account.move.in_receipt

这背后有两个边界控制很重要。

1)动作只能挂在 Documents 模型上

_check_document_account_check_model() 明确限制:

只要 state 是 documents_account_record_createmodel_id 就必须是 documents.document

这不是小题大做。

它意味着“从文档创建会计单据”这件事在官方眼里是一个 Documents 场景专用动作,不是给别的模型随手复用的通用 server action。

2)Journal 不是任意可选,而是按目标单据类型重新筛选

_compute_documents_account_suitable_journal_ids() 会根据要创建的对象动态限制可选 Journal:

  • 如果目标是 account.bank.statement,只给 bank/credit 类型 Journal;
  • 如果目标是 account.move.*,就调用 _get_suitable_journal_ids() 取合适 Journal。

所以 UI 上看到的“Create Vendor Bill (某 Journal)”并不是展示层小把戏,而是底层已经在约束:

  • 这个动作能不能用;
  • 能选哪些 Journal;
  • 动作名该不该带上 Journal 名字。

_generate_action_name() 甚至会把 Journal 名字直接拼进动作标题里,形成这种语义:

  • Create Vendor Bill
  • Create Vendor Bill (Vendor Purchases)

这说明官方想把“生成什么单据、落到哪本账”在动作层就说清楚,而不是等建单后再让财务去猜。


三、多动作链里最怕的不是“能不能建单”,而是文档会不会先被移错文件夹

这一点是 documents_account 里最容易被忽略、但最体现工程味的地方。

data/ir_actions_server_data.xml 里很多动作不是一个 code action 单独跑,而是一个 multi action。比如 Create Vendor Bill 这类动作,往往会包含:

  1. 先把文档移动到 TaxesAnnual Closing
  2. 再执行创建会计单据。

如果顺序不对,就会出现一个很别扭的问题:

  • 文档先进入一个“中间文件夹”;
  • 但真正的同步逻辑又想把它放回 Journal 对应文件夹;
  • 权限和嵌入动作也可能因为文件夹变化瞬间失效。

所以 IrActionsServer._run_action_multi() 专门覆盖了执行顺序。

源码注释已经讲得很直白:

  • 有些 accounting multi-action 存在“不必要的中间移动”;
  • 为了保证 synced doc 最终回到正确的 accounting journal folder;
  • 要临时把动作 embed 到指定文件夹里,再按受控顺序运行,最后恢复嵌入状态。

_get_documents_account_actions_map() 还把这些需要特殊处理的动作映射到了两个关键文件夹:

  • Taxes
  • Annual Closing/<current year>

这说明官方非常清楚:

Documents + Accounting 的难点不是“建不出会计单据”,而是“多步动作执行时,文档最终归档位置和权限语义不能乱”。

这也是为什么我更愿意把它叫“文档编排层”,而不是“建账入口”。


四、从文档创建单据时,系统顺手处理了活动完成、币种默认和批量跳转

再看 models/documents_document.py 里的 account_create_account_move()

这段方法并不是简单把附件 ID 传给 _create_document_from_attachment() 就结束,它还顺手处理了三类很实用的收尾动作。

1)先把当前文档上的活动标记为完成

如果没有 skip_activities,方法一开始就会对 record.activity_ids.action_feedback(feedback="completed")

这意味着从 Documents 里点击“Create Vendor Bill”,不仅是建了一张账单,也是在语义上结束一次“待处理财务文档”任务。

所以 Documents 里的待办和会计单据创建,不是两条完全无关的链。

2)供应商单据会尝试带出采购币种

move_typein_invoicein_refund 时,如果文档或传入参数上有 partner,源码会读取:

  • partner.with_company(document.company_id).property_purchase_currency_id

如果供应商设了采购币种,就把它塞进 default_currency_id

这段逻辑很有代表性:

  • Documents 层只是入口;
  • 但在真正建 account.move 时,会尽量把供应商会计语义带进去;
  • 避免“附件来源正确、账单创建成功,但币种默认错了”这种半成功状态。

3)批量创建时,返回的不是最后一张单,而是整批结果

如果动作是在多个 Documents 上批量执行,documents_active_ids 会被拿来汇总所有 res_id,最后返回列表页,而不是只打开最后一张单据。

这同样是很典型的 Odoo 企业版体验优化:

  • 单个文档执行 → 直接进表单;
  • 多个文档执行 → 回到列表看整批结果。

它让 Documents 真正适合做财务收件箱,而不只是单文件试验场。


五、单据创建后不会“各走各路”,主附件、Journal、Partner 还会继续反向同步 Documents

如果只做到“从文档生成账单”,那链路还是断的。

models/account_move.pymodels/ir_attachment.py 补上的,正是这条后续同步。

1)主附件改了,文档也跟着切换附件

AccountMove.write() 会盯住 message_main_attachment_id

如果主附件变了:

  • 它先尝试按旧附件或 previous_attachment_ids 找已有 documents.document
  • 找到就把 document.attachment_id 改成新附件;
  • 找不到就标记为需要新建文档;
  • 写入完成后再调用 _update_or_create_document()

这段逻辑解决的是一个很实际的问题:

财务单据的主附件不是永远不变的,可能发生替换、版本升级、重新上传。

如果 Documents 不跟着变,最终就会出现:

  • 会计单据上的主附件是新版;
  • Documents 里却还挂着旧版。

官方显然不接受这种脱节,所以直接把它做成 write 时的同步约束。

2)Journal 改了,文档也会被重新挂到新的同步文件夹

_update_or_create_document() 会根据新的 journal_idcompany_id 去找 documents.account.folder.setting,然后把文档更新为:

  • 新的 folder_id
  • 对应的 tag_ids
  • 当前 partner_id
  • 当前创建人 owner
  • 若文档已归档则重新激活

这说明 Documents 里的归档位置并不是“创建瞬间定终身”。

只要会计单据后来换了 Journal,Documents 归属也要重算。

3)Partner 改了,文档上的 Partner 也会同步

_sync_partner_on_document() 很直接:

  • 找到主附件对应的 documents.document
  • 如果文档上的 partner 和会计单据不一致,就把文档改过去。

这又是一条很容易被低估的边界。

因为在真实业务里,附件先到、供应商后认领非常常见。官方这里的选择不是“那就让 Documents 自己保留旧 partner 吧”,而是让文档元数据持续服从会计单据。

4)甚至从附件层新建/改写,也会反补 Documents

models/ir_attachment.py 更进一步:

  • attachment create() 时,如果它已经挂在有效的 account.move 上,且不是 misc operation、不是 XML 二次注册场景,就会触发 move._update_or_create_document()
  • attachment write() 时,如果附件被重新挂到某个有效 move,也会补同步。

这说明官方不是只盯表单按钮,而是连“附件对象自身的生命周期”也纳入文档同步链。

换句话说,在 documents_account 里,Documents 不是单次导入入口,而是会计附件的持续镜像层。


六、XML 发票的重点不只是能导入,还要在 Documents 里“看起来像一份 PDF”

这是这个模块里另一个特别漂亮的小设计。

DocumentsDocument 上新增了 has_embedded_pdf,并通过 _extract_pdf_from_xml() 去解析 XML 附件,看里面是否存在 base64 的 PDF。

源码支持的关键思路是:

  • 只在 mimetype 像 XML 时继续;
  • 先做字符串级快速过滤,检查 EmbeddedDocumentBinaryObjectAttachment
  • 再用 ElementTree 解析;
  • 遍历相关节点并尝试 base64 解码;
  • 只有当解码后二进制头是 %PDF- 时,才认定这是一份可预览 PDF。

然后 _compute_thumbnail() 会对这类 XML 文档特殊处理:

  • 清空原本缩略图;
  • thumbnail_status 标成 client_generated
  • 让前端按类似 PDF 的方式生成预览体验。

这个设计非常能说明官方的思路:

  • 对会计来说,电子发票 XML 是合规载体;
  • 但对日常审核人员来说,更自然的阅读介质往往还是 PDF 版票据;
  • 所以 Documents 不能只停留在“附件存下来了”,还得把 查看体验 调整到更接近业务习惯。

这也是为什么我会说 documents_account 处理的不只是归档,而是“会计文档工作台”的体验闭环。


七、这个模块真正建立的是“文档先行、单据跟进、归档持续对齐”的会计工作台

把整个模块串起来看,它表达的设计非常清楚:

1)Documents 不是会计附件垃圾桶,而是收件与分流入口

Journal 创建时自动铺文件夹、标签和动作,就是为了让财务团队先在 Documents 接住文件,再决定生成什么单据。

2)建单动作不是万能按钮,而是带 Journal 约束的专用动作

目标模型、可选 Journal、动作命名、公司边界,都被锁在 documents_account_record_create 这套机制里。

3)建单之后,文档不会退出舞台

主附件变化、Partner 变化、Journal 变化、附件后补,都会继续回写 Documents。

4)电子发票体验也被纳入闭环

XML 里内嵌 PDF 的情况不是“反正存起来就行”,而是继续兼顾预览体验与审核效率。

所以如果你问:

Odoo 企业版 Documents + Accounting 到底在解决什么问题?

我会给一个更准确的答案:

它解决的不是“能不能从文档生成账单”,而是“财务文档从进入系统,到生成会计对象,再到后续归档与元数据变更,能不能始终保持同一套语义”。

而这,恰恰就是很多团队在“共享文件夹 + 手工建账”模式里最容易散掉的地方。


源码依据

本文只依据 /home/ubuntu/odoo-temp/enterprise/documents_account 模块撰写,核心参考包括:

  • models/account_journal.py
  • models/ir_actions_server.py
  • models/documents_document.py
  • models/account_move.py
  • models/ir_attachment.py
  • models/documents_account_folder_setting.py
  • data/ir_actions_server_data.xml
  • views/ir_actions_server_views.xml
  • views/documents_document_views.xml

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。