会计附件

Odoo 会计附件上传为什么不是“拖进表单就结束”:document uploader、ir.attachment 与表单协作链路讲透

很多人以为 Odoo 会计里上传发票附件只是“前端传一个文件,后端存成 attachment”。但从 `document_file_uploader.js` 到 `account/models/ir_attachment.py` 的实现看,官方实际拆成了三段:先把原始文件安全落成附件,再把附件批量交给业务模型识别生成单据,最后由附件后处理把会计对象和附件关系补齐。本文把这条链路讲透。

会计 前端
进阶 开发者 1 分钟阅读
0 评论 0 点赞 0 收藏 9 阅读

很多实施顾问第一次看 Odoo 会计附件上传,都会把它想得很直:

  • 前端把 PDF 传上来;
  • 后端存一条 ir.attachment
  • 然后界面刷新一下。

但官方并不是这么做的。

document_file_uploader.jsaccount/models/ir_attachment.py 连起来看,Odoo 真正拆的是三层责任:

  1. 上传层:先把原始文件安全、独立地存成附件;
  2. 业务层:再让具体模型决定“这批附件该生成什么业务对象”;
  3. 后处理层:附件落到业务对象后,再补做解析、展开和关联修正。

这也是为什么它看起来只是“拖个文件进来”,背后却能接 OCR、文档识别、账单导入、审计轨迹等一整串能力。

一、前端第一步不是建账单,而是先建 ir.attachment

DocumentFileUploader.onFileUploaded() 做的事情很克制。

它先把浏览器拿到的文件整理成:

  • name
  • mimetype
  • datas

然后直接调 ORM 创建 ir.attachment

注意这里有个很关键的细节:它会先把当前搜索上下文里的 default_* 清掉,再把“干净上下文”传给 ir.attachment.create()

这不是代码洁癖,而是很现实的防呆。

因为会计看板、列表页、过滤器上经常带着很多 default_journal_iddefault_move_type 之类的上下文。如果这些默认值原封不动传给附件创建,很容易把本来只是“存文件”的动作污染成莫名其妙的业务创建失败。

所以官方明确地把“原始附件创建”从“业务默认值上下文”里隔离开了。

一句话总结就是:

先把文件变成一个中立附件,再谈业务识别。

二、真正的业务入口不是 ir.attachment.create(),而是 create_document_from_attachment

文件一条条上传完成后,onUploadComplete() 不会自己猜该建什么单据,而是统一调用业务模型上的:

create_document_from_attachment

这一步设计特别重要,因为它把职责切得很清楚:

  • 上传组件只负责“收集附件 id”;
  • 具体业务模型负责“解释这些附件”。

换句话说,前端上传器并不知道你现在想建的是:

  • 供应商账单;
  • 销售订单;
  • 还是别的支持附件导入的模型。

它只知道:这批附件上传完了,请目标模型接管。

sale.order 里就能看到一个很典型的实现:

  • browse(attachment_ids)
  • 没附件就直接报错;
  • 再调用 _create_records_from_attachments(attachments)
  • 最后返回一条 action,把新建出的记录打开。

这就是 Odoo 很典型的可扩展思路:

上传器不懂业务,业务模型借上传器拿到原材料。

三、为什么要分成“先附件、后业务”两步

这套分层不是为了优雅,而是为了解决现实问题。

1)上传成功不等于业务识别成功

发票文件能传上来,不代表:

  • 文件格式一定受支持;
  • OCR 一定识别正确;
  • 能匹配到供应商;
  • 能自动生成正确单据。

所以把“文件已进库”和“业务已创建”拆开,失败边界就清楚了。

2)一批附件可能对应一批业务对象

前端是逐个文件上传,但业务层经常要批处理。

例如:

  • 一次拖 10 张账单;
  • 统一识别;
  • 统一返回通知;
  • 最后跳到生成结果页。

因此 attachmentIdsToProcess 会先累积,等整批上传完成再统一交给业务模型。

3)审计和权限更好控

附件先独立落到 ir.attachment,后面无论识别成功、失败、部分成功,都还有一个清晰的原始文件留痕点。

对会计场景来说,这很重要。

四、action.context.notifications 说明“上传完成”后还有业务反馈层

onUploadComplete() 里有个容易被忽略的设计:

如果返回的 action 里带 context.notifications,前端会把这些通知逐条弹出来。

这说明官方认为批量附件导入的结果,天然就不该只有“全成”或“全错”两种。

现实里更常见的是:

  • A 文件识别成功;
  • B 文件格式不支持;
  • C 文件重复;
  • D 文件已生成草稿但有警告。

所以业务模型可以把逐文件反馈挂回 action,由上传器统一展示。

这比“抛一个总异常”对实施和操作员友好多了。

五、ir.attachment 在 account 里并不是被动文件柜

很多人把 ir.attachment 理解成一个纯存储表,但 account/models/ir_attachment.py 明显不是这个定位。

1)write() 会拦审计轨迹破坏

如果你改的是:

  • res_id
  • res_model
  • 原始内容字段
  • 公司字段

account 扩展会先尝试 _except_audit_trail()

也就是说,在启用了严格审计轨迹的公司里,某些已入账单据上的 PDF/XML 并不能随便删、随便换、随便挪。

这正好回答了一个常见误解:

会计附件不是普通业务附件,它可能已经成为审计链的一部分。

对于像 invoice_pdf_report_fileubl_cii_xml_file 这样的关键发票附件,unlink() 遇到限制审计轨迹时并不会简单删除。

它会:

  • res_field 清掉;
  • 给附件改名,标记是谁在什么时候 detach;
  • 但文件本体继续保留在数据库里。

这非常会计。

系统允许你“取消字段上的直接绑定”,但不允许你无痕抹掉审计证据。

3)_post_add_create() 说明附件创建后还会继续喂给业务对象

当附件是挂到 account.move 上时,_post_add_create() 会把附件整理成 files_data,再交给 move 去:

  • _to_files_data()
  • _unwrap_attachments()
  • _extend_with_attachments()

这说明附件落账后,仍然可能继续触发:

  • 附件展开;
  • 文档元数据抽取;
  • 业务记录扩展。

所以附件不是终点,它本身就是业务链的一环。

六、实施时最容易踩的几个坑

误区 1:把上传器当成“建账单按钮”

其实它只是附件收集器 + 业务入口转发器。

误区 2:在上传上下文里乱塞 default_*

这很容易导致附件创建阶段就被业务默认值污染。

误区 3:以为附件删了就等于痕迹没了

在 restrictive audit trail 下,经常只是“解绑”,不是“消失”。

误区 4:上传失败只查前端

很多问题其实发生在 create_document_from_attachment 或附件后处理阶段,而不是 HTTP 上传本身。

七、排错顺序怎么更高效

如果现场出现“附件传上去了,但单据没出来”这类问题,我建议按这个顺序查:

  1. 浏览器侧确认文件是否真的建成了 ir.attachment
  2. create_document_from_attachment 有没有返回 action 或 notification;
  3. 检查目标模型的附件导入实现是否支持当前文件类型;
  4. 如果是会计单据,再检查 audit trail / field detach / 后处理有没有拦住后续动作。

这样查,比一上来就怀疑 OCR 或前端按钮靠谱得多。

八、结论

Odoo 会计附件上传之所以能同时承接“拖拽上传、自动识别、会计留痕、审计保护”,靠的不是一个更花哨的上传按钮,而是清晰的三段式分层:

  • DocumentFileUploader 先安全落附件;
  • 业务模型用 create_document_from_attachment 决定生成什么;
  • accountir.attachment 的扩展继续负责审计保护和后处理。

所以真正该记住的不是“附件怎么传”,而是:

在 Odoo 里,附件上传是业务入口的一部分,但它绝不等于业务本身。

DISCUSSION

评论区

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