企业版签署链路

Odoo 企业版 Documents + Sign 为什么不是“签完生成个 PDF”而已:folder 继承、reference_doc 路由与 no_document 防重复讲透

很多人把企业版 Documents 与 Sign 的集成理解成“从文档里发起签署,签完把 PDF 丢回文件夹”。但从 `documents_sign` 源码看,官方真正解决的是附件所有权迁移、签署请求如何绑定原文档或文件夹、完成件生成与邮件发送为何要区分 `no_document`、以及签署人/发起人如何自动获得受控访问权。

企业 框架
进阶 开发者 3 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

很多团队第一次接触 Odoo 企业版的 Documents + Sign,脑子里的流程图通常都很短:

  • 在 Documents 里选一个 PDF;
  • 点一下创建签署模板;
  • 发给签署人;
  • 签完后系统生成 signed PDF;
  • 文件回到某个文件夹。

这个理解不算错,但太“表面 UI”了。

如果你认真看 /home/ubuntu/odoo-temp/enterprise/documents_sign,会发现官方真正要解决的不是“签完以后保存一个 PDF”,而是下面这些更棘手的问题:

  1. 原始附件如果本来挂在别的业务对象上,能不能直接拿来做 sign.document
  2. 签署请求到底应该回链到“某个原文档”还是“某个文件夹”?
  3. 完成件在生成和邮件发送两个阶段,为什么要一会儿 no_document=False,一会儿 no_document=True
  4. 签署人和发起人为什么能自动看到完成件,但又不会因此获得过大的编辑权限?
  5. 为什么 Sign 专用文件夹会被禁止删除、归档、甚至禁止绑定公司?

所以 Documents + Sign 的重点,不是“生成一份签过的 PDF”,而是“让签署产物在文档系统里可回流、可定位、可授权、还不重复建档”。

这才是 documents_sign 真正有意思的地方。


一、入口不是“把 PDF 丢给 Sign”,而是先重写附件归属关系

先看 documents_sign/models/documents_document.py 里的 document_sign_create_sign_template(),以及 documents_sign/models/sign_document.pycreate()

从 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_model
  • res_id
  • 访问控制语义
  • 垃圾回收与引用关系

如果一个附件本来挂在 sale.orderhr.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_docdocuments.document 时,_get_linked_record_action() 不走父类默认动作,而是组一个 /odoo/action-documents.document_action_preference?... 的 URL,并且带上:

  • preview_id
  • view_id
  • menu_id
  • folder_id

这说明官方想让用户回到的不是“某条记录 form view”,而是 Documents 的工作语境

  • 左边还是文件夹;
  • 中间还是文档列表 / 预览;
  • 当前文档被高亮预览;
  • 所属目录上下文还在。

这个细节很像产品体验问题,但本质上是建模选择:

签署请求虽然是 Sign 的业务对象,但它在文档侧的落点,仍应该回到文档工作台,而不是脱离上下文的孤立记录。

这就是“集成做得深”和“只是互相加个 Many2one”的区别。


四、完成件生成和完成件发送,为什么要故意分成 no_document=Falseno_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

换句话说,官方把“完成件对象化”和“完成件邮件分发”强行切成了两个阶段:

  1. 生成阶段:允许把附件提升成 Documents 文档;
  2. 发送阶段:禁止附件再被二次文档化。

这个设计非常像处理消息幂等时的经典思路。

这在防什么?

它防的不是“多发一封邮件”,而是更底层的:

同一份完成件附件,被不同链路重复解释为“还需要再建一个 documents.document”。

如果不切开这两个阶段,最终会出现:

  • 邮件发出去了;
  • 文档也有了;
  • 但发送动作又把附件重新送回 Documents 建档;
  • 然后唯一约束、引用关系、访问关系全部开始打架。

这就是为什么 no_document 在这里不是小开关,而是 幂等保护阀门


五、签署完成后,系统为什么只补 view 权限,而不是直接给 edit

documents_sign/models/sign_request.pysign.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.pydocuments_sign/models/documents_document.py 一起看,会发现官方给 Sign 留了一个默认文件夹:

  • documents_sign.document_sign_folder

而且对它做了三重保护:

  1. 不能删除unlink_except_sign_folder()
  2. 不能归档_archive_except_sign_folder()
  3. 不能设置 company_id_check_no_company_on_sign_folder()

这三条组合起来很有意思。

为什么不能删除或归档?

因为它不是“某个部门自己建的目录”,而是 Sign 模块的默认落点。

一旦删掉或归档:

  • 新模板默认目录会失效;
  • 完成件默认回流会失去稳定锚点;
  • 配置上下文会开始出现悬空引用。

为什么连公司都不能设?

因为官方把它当成一个 跨公司可引用的基础设施文件夹,而不是业务数据本身。

如果把这个根目录绑到某家公司:

  • 多公司环境下默认模板目录会天然偏向单公司;
  • 某些共享流程或初始化配置会出现不必要的公司边界;
  • 你会把“基础设施容器”误当成“业务归属容器”。

这也是企业源码里很典型的一种思路:

有些对象虽然看起来是记录,但本质上是平台级支撑位。

对这类对象,限制越早越好。


七、垃圾回收为什么要排除 sign.requestsign.document

documents_document._get_gc_clear_bin_domain() 也被重写了。

它在 Documents 自己的清理域上,又额外排除了:

  • res_model != sign.request
  • res_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 最值得抄作业的是这四条:

  1. 附件先做归属隔离:别让原业务附件、流程附件、结果附件混成一团。
  2. 回链必须保守:能精确绑具体文档就绑具体文档,否则退回共同文件夹,不要猜。
  3. 生成与发送拆阶段:凡是“既要建对象、又要分发附件”的链路,都要考虑幂等保护。
  4. 结果可见 ≠ 结果可改:参与流程的人,未必应该自动拥有治理权限。

这四条都是平台级经验,不只是 Sign 模块专属技巧。


结语

documents_sign 真正厉害的地方,不在于它把 Documents 和 Sign 接在一起,而在于它把“签署完成件回流到文档系统”这件事做成了一条 有归属、有锚点、有授权边界、还能防重复建档 的稳定链路。

所以 Odoo 企业版 Documents + Sign 的本质,不是“签完存个 PDF”,而是“把法律/协作文档的结果产物,安全地重新纳入企业文档治理体系”。

理解了这一点,你以后再看审批归档、合同归档、电子回执、外部平台回传文件,就不会只问“文件最后存哪”,而会先问:

  • 这份附件现在到底归谁?
  • 结果对象应该回链到哪?
  • 发送动作会不会重复建档?
  • 谁应该看到它,谁又不该改它?

这些,才是企业版框架源码真正想教你的东西。

DISCUSSION

评论区

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