先说结论
如果你把 Odoo 企业版 Documents 里的在线 spreadsheet 理解成“谁拿到链接谁都能一起改的云表格”,那会误解这个模块的产品边界。
从 /home/ubuntu/odoo-temp/enterprise/documents_spreadsheet 的模型、控制器和测试一起看,官方真正实现的是两种完全不同的对象语义:
- 在线 spreadsheet:面向内部协作,允许 revision、snapshot、贡献者追踪;
- frozen spreadsheet:面向外部分发或归档,只允许受控只读访问和 Excel 下载;
- 外部访问 token 只是访问入口,不等于获得协作写权限;
- 修订历史本身不是共享对象,即便文档可读,也不会把 revision ORM 能力一并放出去;
- live data 也不是随便公开渲染,前台分享前还会额外拦一道。
所以更准确的理解是:
Odoo 企业版把 spreadsheet 当成“内部协作工件”,而不是“默认可外包编辑的公共云文档”。对外分享时,官方更偏向先冻结成发布副本,而不是继续开放在线编辑。
这也是为什么它很适合归到 企业, 协同办公:重点不在 BI 展示,而在协同边界治理。
一、数据库约束先把路堵死:在线 spreadsheet 不能 link-edit,冻结 spreadsheet 彻底不能 edit
先看 models/documents_document.py 里的两个 SQL 级约束:
_spreadsheet_access_via_link = models.Constraint(
"CHECK((handler != 'spreadsheet') OR access_via_link != 'edit')",
"To share a spreadsheet in edit mode, add the user in the accesses",
)
_frozen_spreadsheet_access_via_link_access_internal = models.Constraint(
"CHECK((handler != 'frozen_spreadsheet') OR (access_via_link != 'edit' AND access_internal != 'edit'))",
"A frozen spreadsheet can not be editable",
)
这两条约束非常关键:
- 普通在线 spreadsheet 不能通过链接直接给 edit;
- frozen spreadsheet 不只是“默认只读”,而是 从 link 和 internal 两个维度都禁止 edit。
测试 test_spreadsheet_can_not_share_write_access_to_portal() 进一步把这件事钉死:
access_via_link = 'edit'会触发数据库层CheckViolation;- frozen 副本如果想改成
access_internal='edit',同样直接失败。
这说明官方不是靠前端按钮灰掉来“劝你不要这么做”,而是把规则下沉到数据层:
在线协作可以发生,但必须绑定明确内部身份;任何“拿个链接就能改”的分享方式,都不被允许。
二、外部人为什么不能被赋予 edit:因为 Odoo 把编辑权视作“内部身份能力”
再看 models/documents_access.py:
if (
access.document_id.handler == 'spreadsheet'
and (not user_ids or all(user_ids.mapped("share")))
and access.role == 'edit'
):
raise ValidationError(_('Spreadsheets can not be shared in edit mode to non-internal users.'))
意思很直接:
- 如果对方没有用户,或者只有 portal/share 用户;
- 你又想给 spreadsheet 的
edit; - 系统直接拒绝。
测试里覆盖了三种情况:
- portal user;
- 没有内部用户的 partner;
- 归档的内部用户仍然允许保留内部身份语义。
这背后的产品判断很明确:
- 编辑 spreadsheet 不只是“能不能改 JSON”;
- 它还意味着进入协同会话、触发 revision、参与 snapshot、影响 live 数据与共享状态;
- 所以它被归类为一种 内部协作权限,不是外部访客权限。
也就是说,Odoo 并不想把 Documents Spreadsheet 做成 Google Sheets 式的匿名外部协作。
三、真正对外分发的正确姿势,不是继续开放原表,而是 action_freeze_and_copy()
action_freeze_and_copy() 是整个模块最值得注意的方法。
它的流程不是“生成一个分享链接”那么简单,而是:
- 先检查你是否有创建文档和读取当前表格的权限;
- 如果原文件夹下还没有
Frozen spreadsheets文件夹,就 sudo 建一个专用文件夹; - 复制当前 spreadsheet,名称改成
Frozen at <date>: <name>; - 把新副本的
handler改成frozen_spreadsheet; - 自动把原有 access 中的
edit全降成view; access_via_link固定给view;- 同时保存一份
excel_export,供外部下载。
这里最有意思的地方有两个。
1)它不是“分享当前对象”,而是“复制成另一种对象”
冻结分享并不延续原 spreadsheet 的对象身份,而是复制一条新文档记录,放进专门的 frozen folder。
这意味着官方明确区分:
- 原对象:继续内部协作;
- 新对象:负责对外阅读、下载、归档。
2)权限会自动降级,而不是原样照搬
复制 access 时,源码用了:
'role': 'view' if access.role == 'edit' else access.role,
也就是:
- 原来你在协作表上有 edit;
- 到冻结副本后也只剩 view。
这说明 frozen 的目标不是保留协作现场,而是发布一份稳定只读版。
freeze-and-copy 的本质,是把“内部持续演进中的表格”切换成“对外可分发的快照制品”。
四、token 只是访问凭证,不是写权限万能钥匙
很多人会下意识以为:既然有 access_token,是不是拿到 token 就能推 revision?
_check_spreadsheet_share() 和测试 test_collaborative_dispatch_spreadsheet_with_token() / test_collaborative_readonly_dispatch_spreadsheet_with_token() 给出的答案是否定的。
写入要同时满足几件事:
- token 必须正确;
- 文档必须允许该操作;
- 如果是写操作,
access_via_link还得是edit; - 但 spreadsheet 本身又被 SQL 约束禁止
access_via_link='edit'。
于是结论就出来了:
- 对普通 spreadsheet,token 可以辅助受控访问,但不会把公开链接升级成开放编辑;
- 对 frozen spreadsheet,非 sudo 写入直接被拒绝:
if not self.env.su and operation == 'write' and self.sudo().handler == 'frozen_spreadsheet':
raise AccessError(_("You can not edit a frozen spreadsheet"))
这就是非常典型的“凭证 != 权限”设计。
官方允许 token 解决公开入口问题,但协作写权限仍然被牢牢拴在对象类型和访问规则上。
五、为什么 frozen spreadsheet 能下载 Excel,而原始在线表反而没有普通内容流
控制器 controllers/documents.py 还有一个很漂亮的边界设计。
在 _documents_content_stream() 里:
- frozen spreadsheet 下载时走
excel_export; - 普通 spreadsheet 反而直接抛错:
ValueError("non-frozen spreadsheets have no content")。
这意味着在线 spreadsheet 的“真实载体”不是一个稳定二进制文件下载口,而是:
- snapshot
- revision
- JSON 数据
- 协作状态
而 frozen spreadsheet 则被重新包装成适合共享/下载的制品。
这和前面的 freeze-and-copy 完全一致:
- 在线对象强调协作;
- 冻结对象强调交付。
六、前台公开查看前还要再检查一次:含 live data 的表不能随便公开渲染
同一个控制器在 _documents_render_portal_view() 里还有一道保护:
if document._contains_live_data():
return request.render("documents_spreadsheet.documents_error_live_data")
这说明即便文档可分享,前台渲染也不是无条件放行。
为什么?因为 spreadsheet 里可能带有:
- pivot / list 等动态数据源;
- 依赖后端权限和会话上下文的 live 数据;
- 不适合公开页面直接暴露的查询结果。
所以 Odoo 宁可提示错误,也不把一个需要内部上下文的数据表硬塞给公共页面。
这体现出官方对“可看”和“可安全公开渲染”做了进一步区分。
七、贡献者追踪和 revision ORM 也说明它是内部协作工件,不是公共文件
spreadsheet.contributor 会在读取序列化数据和元数据时更新最近贡献者;
test_spreadsheet_collaborative.py 还明确验证了一个更重要的点:
- 有文档读写权,不代表能直接读写
spreadsheet.revisionORM; - revision 记录的 ORM 访问被严格收口;
- 普通有权限用户协作,是通过既定消息/协同入口进行,而不是开放底层 revision 表。
这说明官方把 revision 看成内部协同引擎的一部分,而不是共享 API。
如果把这层也公开出去,就会出现两个问题:
- 外部人可能绕过文档级约束直接碰 revision;
- 协作协议会暴露成低层存储接口,治理会失控。
八、实战里最容易误解的 4 件事
误区 1:有分享链接,就应该能在线共同编辑
在 Odoo 企业版里不是这样。
分享链接解决的是访问入口,不是开放协作编辑。
误区 2:给 portal 用户加个 edit 就行
不行。模型约束和测试都说明,portal / 非内部用户不能拿 spreadsheet edit。
误区 3:冻结副本只是 UI 上的“另存为”
也不是。它会重建对象类型、切换文件夹、降级权限、保存 Excel 导出,本质上是发布工件化。
误区 4:文档可读,revision 也应该能看
官方明确没这么做。协作底层记录不是普通共享资产。
九、这套设计对实施和二开的启发
如果你在做企业内部表格协同,documents_spreadsheet 最值得借鉴的是这几条:
- 把在线协作态和对外发布态拆成两类对象;
- 对外分享优先走只读快照,而不是继续开放主对象编辑;
- 公开 token 只解决入口,不提升写权限等级;
- 协作底层 revision 要和普通文档权限分层治理;
- 涉及 live data 时,宁可拒绝公开渲染,也不要误把内部数据视图暴露出去。
很多系统失败,就是因为把“内部共创文档”和“外部可分享文件”当成同一件事。
Odoo 企业版在这里的态度反而很克制:
真正允许多人实时改的,是内部协作表;真正允许公开带走的,是冻结后的只读副本。
这条边界一旦想清楚,很多权限设计就顺了。
源码依据
enterprise/documents_spreadsheet/models/documents_document.pyenterprise/documents_spreadsheet/models/documents_access.pyenterprise/documents_spreadsheet/controllers/documents.pyenterprise/documents_spreadsheet/models/spreadsheet_contributor.pyenterprise/documents_spreadsheet/tests/test_spreadsheet_share.pyenterprise/documents_spreadsheet/tests/test_spreadsheet_documents_sharing.pyenterprise/documents_spreadsheet/tests/test_spreadsheet_collaborative.py
DISCUSSION
评论区