很多人第一次看到 Documents + Accounting,会把它想成一条很轻的链:
- 把发票 PDF 丢进 Documents;
- 点一下“Create Vendor Bill”;
- 系统建出一张会计单据。
这个理解不算错,但太薄了。
如果你去读 /home/ubuntu/odoo-temp/enterprise/documents_account,会发现官方真正做的不是“给文档应用加一个建账按钮”,而是搭了一层 会计文档编排机制。这层机制至少在处理五件事:
- 每个 Journal 在 Documents 里应该落到哪个同步文件夹?
- 用户在文件夹里能看到哪些“创建会计单据”动作,而且这些动作是否按公司与 Journal 类型收口?
- 从附件生成 Vendor Bill / Customer Invoice / Bank Statement 后,文档怎么继续跟着单据走,而不是脱节?
- 如果主附件、Journal、Partner 后来变了,文档要不要跟着回写?
- 电子发票 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 Notesale→ Create Customer Invoice / Create Credit Notebank→ Import Bank Statementgeneral→ Create Misc Entry
然后 _documents_configure_sync() 会把这些动作同时嵌到:
- 当前 Journal 的目标文件夹;
- 公司级
Finance根文件夹。
这说明官方不想让用户去记“这个动作只能去某个很深的子目录才能看到”,而是把可用动作在 局部文件夹 和 Finance 父层 都铺出来。
所以 Documents 里的会计操作入口,本质上是由 Journal 类型驱动的,而不是由用户随便在任意文件夹点一个通用动作。
二、真正执行“建账”的不是普通按钮,而是受限于 documents.document 的专用 server action
再看 models/ir_actions_server.py 和 data/ir_actions_server_data.xml。
documents_account 给 ir.actions.server 新增了一个状态:
documents_account_record_create
并且新增了一组受控的目标模型:
account.move.in_invoiceaccount.move.out_invoiceaccount.move.in_refundaccount.move.out_refundaccount.move.entryaccount.bank.statementaccount.move.in_receipt
这背后有两个边界控制很重要。
1)动作只能挂在 Documents 模型上
_check_document_account_check_model() 明确限制:
只要 state 是
documents_account_record_create,model_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 这类动作,往往会包含:
- 先把文档移动到
Taxes或Annual Closing; - 再执行创建会计单据。
如果顺序不对,就会出现一个很别扭的问题:
- 文档先进入一个“中间文件夹”;
- 但真正的同步逻辑又想把它放回 Journal 对应文件夹;
- 权限和嵌入动作也可能因为文件夹变化瞬间失效。
所以 IrActionsServer._run_action_multi() 专门覆盖了执行顺序。
源码注释已经讲得很直白:
- 有些 accounting multi-action 存在“不必要的中间移动”;
- 为了保证 synced doc 最终回到正确的 accounting journal folder;
- 要临时把动作 embed 到指定文件夹里,再按受控顺序运行,最后恢复嵌入状态。
_get_documents_account_actions_map() 还把这些需要特殊处理的动作映射到了两个关键文件夹:
TaxesAnnual 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_type 是 in_invoice 或 in_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.py 和 models/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_id 和 company_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 时继续;
- 先做字符串级快速过滤,检查
EmbeddedDocumentBinaryObject或Attachment; - 再用
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.pymodels/ir_actions_server.pymodels/documents_document.pymodels/account_move.pymodels/ir_attachment.pymodels/documents_account_folder_setting.pydata/ir_actions_server_data.xmlviews/ir_actions_server_views.xmlviews/documents_document_views.xml
DISCUSSION
评论区