先说结论
采购里那个“从附件创建单据”的入口,很容易让人误以为 Odoo 自带一种万能 OCR / PDF 识别能力。
其实不是。
purchase.order.create_document_from_attachment() 本身只做两件事:
- 校验你确实传了附件;
- 调用
_create_records_from_attachments()去创建记录并尝试解码。
所以真正决定结果的关键不在按钮,而在后面那套导入框架:
- 附件会不会被拆包;
- 会被分成几组;
- 哪个 decoder 优先命中;
- 命中后能不能成功解码;
- 失败了是报错、留消息,还是只挂附件。
换句话说:
采购附件导入的核心不是“上传”,而是“识别器链路能否为这类文件找到可信解释”。
第一步:采购模块自己不懂附件内容,它只提供业务入口
在 /addons/purchase/models/purchase_order.py 里,create_document_from_attachment() 非常克制。
它拿到 attachment_ids 后:
- browse 附件;
- 如果为空,抛
No attachment was provided; - 否则调用
self.with_context(default_partner_id=当前用户伙伴)._create_records_from_attachments(attachments); - 最后返回“Generated Orders”的 action。
这段代码传递了一个很重要的设计信号:
- 采购模块负责定义“我要生成 purchase.order”;
- 但它不负责理解附件具体内容;
- 内容识别交给抽象导入框架与具体 EDI / 文档模块。
这也是为什么你不能简单说“采购支持导入 PDF”。
更准确的说法应该是:
采购支持把附件送进统一文档导入框架,再由适配的 decoder 决定能不能落成采购单。
第二步:真正的主链路在 account.document.import.mixin
_create_records_from_attachments() 定义在 account.document.import.mixin,它的流程很值得开发者背下来:
1. 先把 ir.attachment 变成 files_data
系统会把每个附件转成带元数据的中间结构,包括:
- 文件名;
- 原始二进制;
- mimetype;
- 原始 attachment;
- XML tree;
- import file type。
所以后续所有判断都不是直接围绕 attachment 本身转,而是围绕 file_data 转。
2. 尝试解包嵌套附件
某些格式可能包含嵌套文件,框架会先 _unwrap_attachments(files_data)。
这一步意味着一个上传动作,后面不一定只对应一个可识别文件。
3. 按 grouping 规则决定要建几张单
默认 grouping 是按 origin attachment 分组。
所以你上传几个根附件,通常就可能建几张单; 但如果某个附件内又拆出多个文档,情况就会更复杂。
4. 先创建空记录,再把附件挂上去
这点很容易被忽略。
框架会先 create([{}] * len(file_data_groups)) 创建记录,然后把附件写到这些新记录上,并发一条 chatter 消息说:
- 这些记录是从哪些附件创建出来的。
也就是说,记录创建和附件解码不是同一步发生。
5. 最后才 _extend_with_attachments() 执行真正导入
这一步才会:
- 找 decoder;
- 按 priority 选最合适的文件;
- 调用 decoder;
- 成功则把数据写进单据;
- 失败则 message_post 原因。
这也是为什么某些场景下你会看到:
- 采购单记录被建出来了;
- 附件也挂上了;
- 但行项目并没有正确解析。
不是“半成功”,而是框架本来就把“建壳”和“填内容”拆开了。
第三步:采购默认没有 builder,识别能力靠扩展模块注入
采购模块自己的 _get_edi_builders() 返回的是空列表。
这句话的潜台词很重要:
- 基础 purchase 模块没有声明任何内建采购 EDI builder;
- 如果没有别的模块覆盖,你就不能指望它凭空识别各种采购附件。
例如 purchase_edi_ubl_bis3 就扩展了这件事:
_get_edi_builders()在父类结果上追加purchase.edi.xml.ubl_bis3;_get_import_file_type()通过 XML 的CustomizationID识别 UBL BIS3 采购单;_get_edi_decoder()命中后返回 priority 更高的 decoder;- decoder 最终会调用
_import_order_ubl把 XML 转成采购内容。
所以“采购附件导入”真正可用的前提是:
系统里已经安装了能识别你这类文档的 builder / decoder 模块。
没有扩展模块时,上传只是上传; 有扩展模块时,上传才有机会变成结构化导入。
第四步:priority 决定谁来解释附件,不是所有文件都平权
_extend_with_attachments() 会先给每个 file_data 求 decoder_info,然后按:
- 有没有 decoder;
- decoder 的
priority
倒序排序,选出最优解释器。
这意味着如果同一批附件里有:
- 一个高置信度 XML;
- 一个普通 PDF;
- 一个图片附件;
系统通常会优先让更结构化、优先级更高的格式先说话。
这其实是对的。
因为采购导入最怕的是:
- 明明有结构化电子单据,
- 却让不稳定的弱识别文件抢先占位。
所以 priority 不是技术细节,而是业务可信度排序。
第五步:识别失败不等于“整笔操作回滚消失”
框架里 decoder 如果没找到,日志会记:
- no suitable decoder found
如果 decoder 找到了,但抛异常,则会:
- 捕获异常;
- 在目标记录 message_post 错误说明;
- 保留附件和记录上下文。
这套设计其实很实用,因为它把失败从“黑盒失败”变成了“带证据的失败”:
- 你知道附件是什么;
- 知道创建出了哪张记录;
- 知道哪一步识别失败;
- 后续可以补人工维护。
所以采购附件导入的正确理解不是“成或败二选一”,而是:
它把‘收到文件’、‘建业务壳’、‘识别并写字段’这三件事拆开,允许失败发生在可追踪的位置。
最容易误解的 4 个点
1. 以为上传 PDF 就等于系统会识别采购行
错。能不能识别,取决于有没有对应 decoder,而不是文件能不能上传。
2. 以为 purchase 自己就内建完整导入能力
错。基础 purchase 的 _get_edi_builders() 默认就是空的。
3. 以为识别失败时不会留下任何记录
错。框架可能已经建了记录并挂好附件,只是内容没填完整。
4. 以为附件导入只是一段采购模块小逻辑
本质上它是 account 文档导入框架 + purchase 业务对象 + 具体 EDI 扩展模块协作完成的。
实战排错顺序
如果用户说“我上传了附件,但没生成采购单内容”,建议按这个顺序排:
- 看系统里是否安装了对应采购 EDI / 文档识别模块;
- 检查文件被识别成什么
import_file_type; - 确认
_get_edi_decoder()是否返回了 decoder 与 priority; - 查看 chatter / 日志里是否出现 decoder 失败原因;
- 确认记录是没创建,还是已经创建但没正确填行。
一句话记忆法
Odoo 采购附件导入不是“上传即识别”,而是“上传后进入统一导入框架,再由安装好的 decoder 争夺解释权”。
DISCUSSION
评论区