企业仪表板

Odoo 企业版把电子表格文档加入 Dashboard 时,真正发生的不是“复制一份”:快照冻结、评论迁移、权限组重挂与归档边界讲透

很多人以为 Odoo 企业版里把 Documents 里的 spreadsheet 加进 Dashboard,只是“把同一份表再显示一次”。但 `spreadsheet_dashboard_documents` 源码和测试说明,官方实现的是一次受控交接:先保存快照,再以 dashboard section 和 access groups 新建仪表板,把单元格评论线程从 document 侧改挂到 dashboard 侧,清空 JSON 里的 comments,并把原文档归档。

企业 其他
进阶 开发者 4 分钟阅读
0 评论 0 点赞 0 收藏 6 阅读

先说结论

在 Odoo 企业版里,把一份 Documents 里的电子表格 加进 Dashboard,不是“原地多开一个入口”,也不是“同一条记录换个菜单显示”。

/home/ubuntu/odoo-temp/enterprise/spreadsheet_dashboard_documents,再串上 documents_spreadsheetspreadsheet_dashboard_editionspreadsheet_edition 一起看,官方真正做的是一套 受控交接(handoff)

  1. 先把当前协作状态固化成 snapshot,避免把浏览器里尚未落盘的编辑丢掉;
  2. 按 dashboard group 和 access groups 新建一个 dashboard 对象,不继续沿用 document 的承载身份;
  3. 把单元格评论线程从 document_id 改挂到 dashboard_id,让讨论跟着新的承载对象走;
  4. 把 spreadsheet JSON / snapshot 里的 comments 清空,避免评论既存在线程表里、又残留在数据载荷里;
  5. 把原 document 归档,明确这不是“双活并行”,而是“从文档协作态切到仪表板消费态”。

所以更准确的理解应该是:

Odoo 把“文档里的协作型电子表格”转换成“仪表板里的发布型电子表格”时,做的是一次对象迁移和治理切换,而不是简单复制。

这也是为什么这个功能虽然挂在 Documents 和 Dashboard 之间,但本质上更像一套独立的轻 BI / 轻治理机制,很适合归到“企业, 其他”,而不是继续塞进通用协同办公主线。


一、入口为什么先判断你能不能“Add to Dashboard”

先看 models/documents_document.py

return dict(data, can_add_to_dashboard=self.env['spreadsheet.dashboard'].has_access('create'))

这行代码很短,但它揭示了一个关键设计:

  • 不是所有能编辑文档的人,天然都能把它升级成 dashboard;
  • 文档侧展示“Add to Dashboard”按钮前,系统会先问 你有没有创建 dashboard 的权限

测试 test_can_add_to_dashboard_admin() / test_can_add_to_dashboard_non_admin() 也把这个边界写死了:

  • spreadsheet_dashboard.group_dashboard_manager 的用户,接口返回 can_add_to_dashboard = true
  • 只有普通内部用户 + documents 用户组时,返回就是 false

这说明官方从一开始就把“文档协作”和“仪表板发布”当作两个治理层级:

  • 文档 更像工作区里的可编辑材料;
  • 仪表板 更像要被分发、展示、受控访问的发布对象。

所以这个动作不是一个纯 UI convenience,而是一道发布权限闸门。


二、真正的入口不在 Python 向导,而在前端先强制保存 snapshot

如果只看 Python,很容易误以为点击后直接弹一个向导完事。

但在 static/src/bundle/documents_spreadsheet_action.js 里,前端实际先做了一个非常重要的动作:

await this.env.services.orm.call("documents.document", "save_spreadsheet_snapshot", [
    resId,
    model.exportData(),
]);

也就是说,用户在 spreadsheet 里点 Add to Dashboard 时,系统第一步不是建 dashboard,而是先把浏览器中的当前模型 exportData() 保存成 snapshot。

这背后的原因很现实:

  • 电子表格是协作对象,用户当前看到的内容可能还停留在前端内存;
  • 如果不先 snapshot,后面新建 dashboard 时拿到的就可能是旧的 spreadsheet_data
  • 一旦转成 dashboard,又会归档原文档,丢快照的代价就更高。

所以官方设计不是:

  • “我猜你已经保存过了”;

而是:

  • “既然你要把它提升成一个 dashboard,我先替你冻结当前可见状态。”

这是很典型的发布前 freeze 思路。


三、为什么 list 视图的“Add a spreadsheet”与文档内的“Add to Dashboard”是两条入口

views/spreadsheet_dashboard_views.xml 里,dashboard list 的 control 区加了一个按钮:

  • Add a spreadsheet
  • 调用 action_add_document_spreadsheet_to_dashboard
  • 并把 dashboard_group_id 从父对象上下文传进去

models/spreadsheet_dashboard.py 返回的是一个 client action:

{
    "type": "ir.actions.client",
    "tag": "action_dashboard_add_spreadsheet",
    "params": {
        "dashboardGroupId": self.env.context.get("dashboard_group_id"),
    },
}

前端再通过 dashboard_add_spreadsheet_action.jsDocumentSelectorDialog,让你先选文档。

于是这里其实存在两条不同入口:

入口 A:从文档里发起

先在文档内部工作,点 Add to Dashboard,然后命名 dashboard、选 section、设访问组。

入口 B:从 dashboard section 里发起

你已经站在某个 dashboard group 里,点 Add a spreadsheet,再反向挑一个 document 加进来。

这两条入口的共同点是:

  • 目标都不是“把文档嵌入 dashboard 菜单”;
  • 而是在 dashboard 侧新建一个独立对象

这也是很多人第一次用时最容易看错的地方:

UI 看着像“添加现有文档”,底层却是“把现有文档交接成新的 dashboard 记录”。


四、向导真正创建的不是链接,而是一张新的 spreadsheet.dashboard

核心逻辑在 wizard/documents_to_dashboard.pycreate_dashboard()

它做的事情非常直接:

dashboard = self.env["spreadsheet.dashboard"].create(
    {
        "name": self.name,
        "dashboard_group_id": self.dashboard_group_id.id,
        "group_ids": self.group_ids.ids,
        "spreadsheet_data": self.document_id._get_spreadsheet_serialized_snapshot(),
    }
)

这里至少能读出四个关键信号。

1. dashboard 有自己的名字

向导里的 name 默认取 document 名称,但可以改。

这说明 dashboard 不是 document 的只读镜像,而是允许在发布阶段重新命名的独立对象。

2. dashboard 必须挂进一个 section

dashboard_group_id 是必填。

所以 Dashboard 不是“有了对象再慢慢整理”,而是创建时就要求你明确它属于哪个展示分区。

3. access groups 在创建时重定义

group_ids 默认来自 spreadsheet.dashboard 的默认访问组,而不是简单沿用 document 原权限。

这点很重要。

Documents 世界常见的是:

  • 文件夹权限
  • internal / link / share 访问
  • 文档级编辑语义

Dashboard 世界则更像:

  • 面向哪些用户组展示
  • 谁能看到这个 section 里的内容
  • 谁能进入并继续协作

所以这里不是“继承”,而是重挂治理边界

4. 创建时吃的是 serialized snapshot,不是随便读个字段

spreadsheet_data 来自 _get_spreadsheet_serialized_snapshot()

spreadsheet_edition/models/spreadsheet_mixin.py 里这方法的逻辑是:

  • 如果附件里已有 spreadsheet_snapshot,优先读 snapshot;
  • 否则才回退到 spreadsheet_data

这与前端先 save_spreadsheet_snapshot() 完整闭环了:

  • 前端先冻结当前视图;
  • 后端创建 dashboard 时优先吃这份冻结结果。

这样你拿到的是“用户刚刚确认发布的状态”,而不是数据库里某个更早版本。


五、为什么评论不是复制,而是“线程迁移 + JSON 清洗”

这是整个功能里我最喜欢、也最容易被忽略的一层。

向导在创建 dashboard 后,紧接着会做:

self.env["spreadsheet.cell.thread"].sudo().search([
    ("document_id", "=", self.document_id.id)
]).write({"dashboard_id": dashboard.id, "document_id": False})
self.document_id._delete_comments_from_data()

也就是说,评论处理不是“保留原样”,而是两步:

  1. 把所有单元格评论线程从 document_id 改挂到 dashboard_id
  2. 再把原 document 数据里的 comments 节点清空。

为什么要这么做?

因为 Odoo 的 spreadsheet 评论不是只有一份 JSON 标记,它背后还有 spreadsheet.cell.thread 这张真实模型表,分别可挂到:

  • document_id
  • dashboard_id
  • template_id

一旦你把 spreadsheet 的承载对象从 document 换成 dashboard,如果线程还留在旧对象上,就会出现三个问题:

  • dashboard 打开后评论找不到归属;
  • 文档与 dashboard 可能各自都像“拥有”这串评论;
  • JSON 里的 comments 与数据库线程状态会发生双写或悬挂。

_delete_comments_from_data() 的做法更狠也更干净:

  • spreadsheet_data 里每个 sheet 的 comments 设成 {}
  • spreadsheet_snapshot 里也同步清空 comments。

换句话说,官方明确不想让“评论内容”继续作为 spreadsheet 数据载荷的一部分保留,而是要让它彻底回归到线程模型与访问规则上

这是一种典型的“把协作元数据从业务载荷里剥离出来”的工程思路。


六、为什么直接添加到 section 的那条路径,还要复制 revisions

除了向导 create_dashboard()models/spreadsheet_dashboard.py 里还有一个更底层的方法:

  • add_document_spreadsheet_to_dashboard(dashboard_group_id, document_id)

它创建 dashboard 时,不是只写 spreadsheet_data,而是:

  • spreadsheet_snapshot = document.spreadsheet_snapshot
  • spreadsheet_binary_data = document.datas
  • 然后调用 document._copy_revisions_to(dashboard)

再把 document 归档,最后 dashboard._delete_comments_from_data()

这里透露的重点是:

1. 不是只有最终快照,连 revision 历史也会被带过去

_copy_revisions_to() 会复制 spreadsheet_revision_ids,并把:

  • res_model
  • res_id
  • parent_revision_id

全部改挂到新的 dashboard 上。

2. 复制 revision 时还会删掉评论类命令

spreadsheet_mixin.py 里,_copy_revisions_to() 会先走 _delete_comments_from_commands(),把:

  • ADD_COMMENT_THREAD
  • DELETE_COMMENT_THREAD
  • EDIT_COMMENT_THREAD

从 revision commands 中剔掉。

这和前面的线程迁移逻辑是完全一致的。

官方态度很明确:

电子表格的公式、单元格结构、数据演进可以跟着 dashboard 走;但评论行为不应继续以命令流方式混在 revision 历史里。

所以评论相关元数据会被治理性地抽走。


七、为什么原 document 一定会被 archive,而不是保留“双活”

无论走向导还是走 dashboard model 里的添加方法,都会看到同一个动作:

self.document_id.action_archive()

或:

document.action_archive()

测试也明确断言:

  • The original document should be archived

这句话其实非常值得品。

如果官方允许 document 和 dashboard 长期双活,会发生什么?

  • 用户可能继续在 document 里改;
  • 管理层在 dashboard 里看的是另一套节奏;
  • 评论线程与 revision 归属会持续分叉;
  • 你根本说不清“哪一个才是正式发布对象”。

所以 Odoo 直接给出产品立场:

  • 文档态 适合协作准备;
  • dashboard 态 适合发布展示;
  • 一旦切换,就让原文档进归档,不鼓励并行漂移。

这其实很像内容管理里的“草稿转发布版”,只是这里的对象不是网页,而是电子表格。


八、为什么 dashboard 侧的评论权限反而更严格

tests/test_access_rights.py 很有意思。

它证明了:

对 document 侧评论线程

普通内部用户在文档可访问的前提下,可以:

  • message_ids
  • message_post
  • 新建 thread

但不能随便 unlink()

对 dashboard 侧评论线程

如果 dashboard 的 group_ids 不包含该用户,则普通内部用户:

  • 不能读消息
  • 不能发消息
  • 不能删线程
  • 不能新建线程

只有 dashboard 对其可访问时,才允许创建评论线程。

这说明“转成 dashboard”不只是换展示入口,而是通常伴随着更收敛的讨论边界

文档世界偏开放协作; Dashboard 世界偏受控消费与定向协作。

所以在实施里,如果你发现:

  • “原来文档里大家都能聊,为什么进 dashboard 后就不行了?”

答案多半不在 bug,而在这里:

因为评论线程已经跟着对象迁移到了 dashboard,而 dashboard 的访问规则本来就不是 documents 的那套。


九、新建空白 dashboard 的方法,也能看出“仪表板是第一类对象”

action_open_new_dashboard() 里,官方甚至直接可以:

  • 新建一个 Untitled dashboard
  • 返回 action_edit_dashboard

这意味着 dashboard 从来不是 document 的附属壳子。

它是一个可以独立创建、独立编辑、独立授权的第一类对象。

spreadsheet_dashboard_documents 这个模块做的,只是把 document → dashboard 的交接路径补齐,而不是把 dashboard 降级成 document 的一个视图模式。

这点一旦看明白,很多误解都会消失。


十、最容易误解的四个点

误区一:这是“复制一份电子表格”

不准确。它同时牵涉 snapshot、revision、评论线程、权限组和归档动作,是一套迁移链,而不是 copy/paste。

误区二:文档权限会自动原样继承到 dashboard

不一定。dashboard 创建时会重新挂 group_ids,治理边界是重建的。

误区三:评论还留在 spreadsheet JSON 里

不是。线程会迁移,JSON / snapshot 里的 comments 会被清空。

误区四:原文档会继续和 dashboard 长期同步

不会。官方明确 archive 原 document,不鼓励双活。


实战上该怎么理解最稳

如果你在做企业版 Documents + Spreadsheet + Dashboard 方案,最稳的理解是:

  1. 把 document 当作协作编辑阶段:多人整理、补注释、迭代公式;
  2. 把 add to dashboard 当作发布动作:先冻结快照,再选 section 与可见组;
  3. 不要期待双向同步:转过去后,默认以 dashboard 作为展示对象;
  4. 评论问题先查线程归属:看 spreadsheet.cell.thread 究竟挂在 document_id 还是 dashboard_id
  5. 权限问题先查 group_ids:别只盯 documents 文件夹权限。

这样你在做培训、实施或排错时,就不会把它误教成“文档多一个菜单入口”。


最后总结

spreadsheet_dashboard_documents 最有价值的地方,不是让 Dashboard 能“挑一个现成文档来显示”,而是把一份协作中的 spreadsheet,正式交接成一个受 section、group、revision 和评论线程共同治理的 dashboard 对象。

所以这套机制真正讲的不是“显示位置变化”,而是:

同一份电子表格,从协作工件变成发布工件时,Odoo 企业版如何完成一次对象身份、访问边界和讨论归属的整体切换。

这才是它最值得读源码的地方。

DISCUSSION

评论区

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