很多团队第一次接触 Odoo 企业版的 Documents + Sign,脑子里的流程图通常都很短:
- 在 Documents 里选一个 PDF;
- 点一下创建签署模板;
- 发给签署人;
- 签完后系统生成 signed PDF;
- 文件回到某个文件夹。
这个理解不算错,但太“表面 UI”了。
如果你认真看 /home/ubuntu/odoo-temp/enterprise/documents_sign,会发现官方真正要解决的不是“签完以后保存一个 PDF”,而是下面这些更棘手的问题:
- 原始附件如果本来挂在别的业务对象上,能不能直接拿来做
sign.document? - 签署请求到底应该回链到“某个原文档”还是“某个文件夹”?
- 完成件在生成和邮件发送两个阶段,为什么要一会儿
no_document=False,一会儿no_document=True? - 签署人和发起人为什么能自动看到完成件,但又不会因此获得过大的编辑权限?
- 为什么 Sign 专用文件夹会被禁止删除、归档、甚至禁止绑定公司?
所以 Documents + Sign 的重点,不是“生成一份签过的 PDF”,而是“让签署产物在文档系统里可回流、可定位、可授权、还不重复建档”。
这才是 documents_sign 真正有意思的地方。
一、入口不是“把 PDF 丢给 Sign”,而是先重写附件归属关系
先看 documents_sign/models/documents_document.py 里的 document_sign_create_sign_template(),以及 documents_sign/models/sign_document.py 的 create()。
从 Documents 发起签署模板时,系统会:
- 先创建
sign.template; - 把当前文档的标签同步到
documents_tag_ids; - 再为每个选中的文档创建
sign.document。
但真正关键的是 sign.document.create() 里这段判断:
- 如果
attachment.res_model已经指向别的业务模型,而且 不是documents.document,就 复制一份附件; - 如果附件本来就属于
documents.document,就先把它解绑成res_model=False, res_id=0; - 等
sign.document创建完成后,再把附件重新绑定到sign.document本身。
这段代码解决的是一个非常现实的冲突:
同一份
ir.attachment不能在“原业务对象 / Documents / Sign”之间随意裸奔,否则所有权会乱。
为什么不能直接复用原附件?
因为附件在 Odoo 里不是纯文件句柄,它还带着:
res_modelres_id- 访问控制语义
- 垃圾回收与引用关系
如果一个附件本来挂在 sale.order、hr.employee 或别的模型上,你直接把它塞进 sign.document,就会把“原对象附件”和“待签文档附件”这两层语义混在一起。
所以官方的策略很清楚:
- 外部附件有主人的,复制;
- Documents 自己托管的附件,可以移交;
- 最终待签附件必须明确归
sign.document。
这不是多此一举,而是在做附件所有权隔离。
二、签署请求不是随便回链,而是优先绑定“具体原文档”,其次才绑定“共同文件夹”
documents_sign/models/sign_request.py 重写了 create()。
创建 sign.request 后,它会拿模板里的 document_ids.attachment_id 去 Documents 里反查对应 documents.document,然后决定 reference_doc 怎么填。
规则并不复杂,但非常讲究:
- 如果模板附件最终只对应 1 个 文档记录,
reference_doc直接绑到这个具体文档; - 如果对应多个文档,但它们都在 同一个文件夹,
reference_doc就绑到那个文件夹; - 如果既不是单文档、也没有共同文件夹,就不乱绑。
这个设计特别值得抄。
因为在真实系统里,同内容 PDF、复制件、重复校验和附件并不少见。官方测试 test_correct_reference_doc_set() 甚至专门验证了:
- 就算两个文档内容完全相同;
- 也不能因为 checksum 相近,就把签署请求错连到“不相关的另一份文档”。
为什么要这样保守?
因为 reference_doc 不只是个展示字段,它影响后续:
- 从签署请求跳回 Documents 时,应该打开哪个上下文;
- 完成件该往哪个文件夹回流;
- 使用者理解这次签署“属于哪一堆材料”。
一旦回链错了,用户看到的就不是简单的“入口点错了”,而是:
- 签署请求挂到了错误文档;
- 完成件回到了错误目录;
- 权限和协作上下文也跟着漂移。
所以官方宁可少绑,也不乱绑。
reference_doc的本质不是导航 convenience,而是签署链路在文档系统里的定位锚点。
三、为什么 _get_linked_record_action() 要把你送回 Documents,而不是普通表单页
还是 documents_sign/models/sign_request.py。
当 reference_doc 是 documents.document 时,_get_linked_record_action() 不走父类默认动作,而是组一个 /odoo/action-documents.document_action_preference?... 的 URL,并且带上:
preview_idview_idmenu_idfolder_id
这说明官方想让用户回到的不是“某条记录 form view”,而是 Documents 的工作语境:
- 左边还是文件夹;
- 中间还是文档列表 / 预览;
- 当前文档被高亮预览;
- 所属目录上下文还在。
这个细节很像产品体验问题,但本质上是建模选择:
签署请求虽然是 Sign 的业务对象,但它在文档侧的落点,仍应该回到文档工作台,而不是脱离上下文的孤立记录。
这就是“集成做得深”和“只是互相加个 Many2one”的区别。
四、完成件生成和完成件发送,为什么要故意分成 no_document=False 与 no_document=True
这是整篇最核心的源码点。
documents_sign/models/sign_request.py 里有两个重写:
_generate_completed_documents()
这里明确用:
with_context(no_document=False)
注释写得很直白:
Ensure document are created by the super method which only create the related attachment.
意思是:
- 生成完成件时,不能只停留在附件层;
- 还得让 Documents 那边真正生成对应的
documents.document; - 这样签完的 PDF 才是文档系统里正式可管理的对象。
_send_completed_documents()
这里反过来故意使用:
with_context(no_document=True)
注释解释得更重要:
- 父类在发送完成件前,会先调用
_generate_completed_documents(); - 如果此时再次允许“附件自动转文档”,系统就会试图把 已经被文档引用的完成件附件 再转一遍;
- 结果就可能撞上 duplicate key constraint violation。
换句话说,官方把“完成件对象化”和“完成件邮件分发”强行切成了两个阶段:
- 生成阶段:允许把附件提升成 Documents 文档;
- 发送阶段:禁止附件再被二次文档化。
这个设计非常像处理消息幂等时的经典思路。
这在防什么?
它防的不是“多发一封邮件”,而是更底层的:
同一份完成件附件,被不同链路重复解释为“还需要再建一个 documents.document”。
如果不切开这两个阶段,最终会出现:
- 邮件发出去了;
- 文档也有了;
- 但发送动作又把附件重新送回 Documents 建档;
- 然后唯一约束、引用关系、访问关系全部开始打架。
这就是为什么 no_document 在这里不是小开关,而是 幂等保护阀门。
五、签署完成后,系统为什么只补 view 权限,而不是直接给 edit
documents_sign/models/sign_request.py 对 sign.request.item._sign() 的重写也很耐看。
签署动作完成后,它会:
- 找出这次请求对应的完成件文档;
- 取出两个关键伙伴:签署人 和 发起人;
- 检查他们是否已经拥有更大的权限(尤其是
edit); - 对还没有较大权限的人,自动补一条
view; - 同时把文档的
partner_id指到当前签署人。
官方测试 test_signed_documents_access_rights() 与 test_signed_document_requester_access() 也明确验证了:
- 签署人至少能看;
- 发起人至少能看;
- 若某个经理本来就对目标文件夹有
edit,不会被降权; - 普通签署人没有因为完成签署就自动获得编辑文件夹的能力。
为什么这里非常克制?
因为完成件和待签模板不是一回事。
- 待签模板 是流程设计对象;
- 完成件 PDF 是签署结果对象;
- Documents 文件夹编辑权 则是文档治理权限。
这三层权限不能因为“你签了这个文件”就自动塌成一层。
所以官方只补最小必要权限:
让你能读到你参与产生的结果,但不默认让你修改文档治理结构。
这才是企业系统里更稳的做法。
六、为什么 Sign 专用文件夹被视作“基础设施”,而不是普通业务文件夹
documents_sign/models/sign_template.py 和 documents_sign/models/documents_document.py 一起看,会发现官方给 Sign 留了一个默认文件夹:
documents_sign.document_sign_folder
而且对它做了三重保护:
- 不能删除:
unlink_except_sign_folder() - 不能归档:
_archive_except_sign_folder() - 不能设置 company_id:
_check_no_company_on_sign_folder()
这三条组合起来很有意思。
为什么不能删除或归档?
因为它不是“某个部门自己建的目录”,而是 Sign 模块的默认落点。
一旦删掉或归档:
- 新模板默认目录会失效;
- 完成件默认回流会失去稳定锚点;
- 配置上下文会开始出现悬空引用。
为什么连公司都不能设?
因为官方把它当成一个 跨公司可引用的基础设施文件夹,而不是业务数据本身。
如果把这个根目录绑到某家公司:
- 多公司环境下默认模板目录会天然偏向单公司;
- 某些共享流程或初始化配置会出现不必要的公司边界;
- 你会把“基础设施容器”误当成“业务归属容器”。
这也是企业源码里很典型的一种思路:
有些对象虽然看起来是记录,但本质上是平台级支撑位。
对这类对象,限制越早越好。
七、垃圾回收为什么要排除 sign.request 和 sign.document
documents_document._get_gc_clear_bin_domain() 也被重写了。
它在 Documents 自己的清理域上,又额外排除了:
res_model != sign.requestres_model != sign.document
配合测试 test_gc_clear_bin(),官方明确要保证:
- 在垃圾桶里的普通废弃文档,该删还是删;
- 但和签署请求、签署文档绑定的记录,即便处于 inactive / trash 状态,也不能被 GC 顺手清掉。
为什么?
因为签署链路里的附件和文档,往往还承担:
- 审计凭证;
- 法务留档;
- 完成件下载;
- 签署过程回溯。
所以“在垃圾桶里”并不等于“可以像普通文档一样物理清理”。
同理,test_signed_document_unlink() 也验证了直接删这类文档可能触发外键约束冲突。
这说明官方不是简单地“懒得删”,而是在用数据库约束和 GC 域一起表达:
签署产物的生命周期,不服从普通附件垃圾回收规则。
八、最容易误解的 5 个点
误区 1:Documents 发起签署只是打开了另一个应用
不是。
它同时改写了附件归属、模板目录、标签继承和后续完成件路由。
误区 2:reference_doc 只是一个方便跳转的字段
也不是。
它实际上决定了签署请求在文档系统里的定位语义,影响回链和目录上下文。
误区 3:完成件发送时再自动生成文档也没关系
不行。
源码明确用 no_document=True 把“发送邮件”和“建文档”切开,就是为了防重复建档和唯一约束冲突。
误区 4:签署人签完就该拥有编辑权
也不对。
官方只自动补最小可用的 view,编辑权仍由原文件夹治理模型控制。
误区 5:Sign 文件夹只是一个默认值,不重要
恰恰相反。
它更像集成基础设施,所以才被禁止删除、归档和绑定公司。
九、二开时最值得抄的不是 UI,而是这 4 条边界
如果你要在 Odoo 里自己做“文档系统 + 流程引擎 + 外部签署 / 审批结果回流”,documents_sign 最值得抄作业的是这四条:
- 附件先做归属隔离:别让原业务附件、流程附件、结果附件混成一团。
- 回链必须保守:能精确绑具体文档就绑具体文档,否则退回共同文件夹,不要猜。
- 生成与发送拆阶段:凡是“既要建对象、又要分发附件”的链路,都要考虑幂等保护。
- 结果可见 ≠ 结果可改:参与流程的人,未必应该自动拥有治理权限。
这四条都是平台级经验,不只是 Sign 模块专属技巧。
结语
documents_sign 真正厉害的地方,不在于它把 Documents 和 Sign 接在一起,而在于它把“签署完成件回流到文档系统”这件事做成了一条 有归属、有锚点、有授权边界、还能防重复建档 的稳定链路。
所以 Odoo 企业版 Documents + Sign 的本质,不是“签完存个 PDF”,而是“把法律/协作文档的结果产物,安全地重新纳入企业文档治理体系”。
理解了这一点,你以后再看审批归档、合同归档、电子回执、外部平台回传文件,就不会只问“文件最后存哪”,而会先问:
- 这份附件现在到底归谁?
- 结果对象应该回链到哪?
- 发送动作会不会重复建档?
- 谁应该看到它,谁又不该改它?
这些,才是企业版框架源码真正想教你的东西。
DISCUSSION
评论区