很多人把 Odoo 企业版现场服务里的“签字”理解成一个很轻的动作:客户在平板上写个名字,系统存一张图片,事情就结束了。源码里的设计要严格得多。
在企业版 FSM 里,签字不是孤立动作,而是依赖于报告是否可生成、任务是否具备最基本的服务内容、portal 是否有合法 access token、签完以后是否回写 PDF 留痕。也就是说,它处理的不是“收集一张签名图片”,而是把客户确认、现场报告和消息流审计串成一条链。
关键源码主要在:
enterprise/industry_fsm/models/project_task.pyenterprise/industry_fsm/models/ir_actions_report.pyenterprise/industry_fsm/controllers/portal.pyenterprise/industry_fsm/views/project_portal_templates.xml
一、为什么不是所有 FSM 任务都能直接点“Sign Report”
project.task 里最关键的两个判断方法是:
_is_fsm_report_available()_has_to_be_signed()
其中 _is_fsm_report_available() 在当前实现里非常克制:它要求任务至少有 timesheet_ids。随后 _has_to_be_signed() 又在此基础上要求:
- 这份 FSM 报告本身可用;
- 当前任务还没有
worksheet_signature。
所以系统的真实口径是:
不是“只要是 FSM task 就能签”,而是“这张 task 至少已经形成了一份可被称为服务报告的内容,而且还没签过”。
这比很多团队想象的严格,因为它避免了“什么都没做,只留一张签名图”的伪交付。
二、为什么没内容时连 PDF 都不让你生成
ir.actions.report._render_qweb_pdf() 对 industry_fsm.worksheet_custom 做了专门拦截。
它会先过滤 display_satisfied_conditions_count,只有满足条件的任务才允许渲染 PDF;否则直接抛 ValidationError:
所选任务没有工时、产品或 worksheet,现场服务报告不可用。
虽然 project.task._is_fsm_report_available() 当前最直接只看 timesheet_ids,但报告引擎这一层又多加了一道更广的安全阀。它强调的并不是“有没有签名按钮”,而是能不能生成一份像样的现场服务报告。
这就形成了双层边界:
- 业务对象层:task 是否具备可签署条件;
- 报告渲染层:PDF 是否真的有内容可出。
三、portal 里的签字并不是随便 POST 一下图片
controllers/portal.py 的 portal_worksheet_sign() 是真正的签署入口。它做了完整校验:
- 先从 query string 或 json 参数里取
access_token; - 用
_document_check_access()校验该 portal 访问是否合法; - 再确认
task_sudo._has_to_be_signed()仍然成立; - 如果没传
signature,直接返回错误; - 尝试写入
worksheet_signature与worksheet_signed_by; - 若签名数据非法,则返回
Invalid signature data。
这说明企业版并没有把客户签字做成一个前端装饰组件,而是当作一条需要权限、状态和数据格式同时成立的公开入口来处理。
也正因此,旧路径 /my/task/... 还会被强制重定向到 /my/tasks/...,避免 portal 链路混乱。
四、为什么签完以后还要重新生成 PDF 并回帖消息流
portal_worksheet_sign() 在签字成功后,并不会只保存二进制签名字段。它还会:
- 调用
ir.actions.report渲染industry_fsm.task_custom_report的 PDF; - 在任务 chatter 里
message_post(); - 把“Field Service Report - 任务名 - 客户名.pdf”作为附件一起贴回去。
这个设计特别像企业系统会做的事:
- 签名图只是原始证据;
- PDF 报告是可交付文档;
- chatter 附件是审计留痕和内部协作证据。
因此真正被保存下来的不是“客户签过字”这一个事实,而是:
某个有 access token 的客户,在某个可签状态下,对某张任务报告完成了签署,并生成了一份可回看的 PDF 文档。
五、为什么“Send Report”和“Sign Report”是两条不同状态链
project.task 里还有两条经常被混淆的字段:
fsm_is_sentworksheet_signature
_compute_show_customer_preview() 明确写了:只要任务已发送过报告,或者已经存在签名,就可以展示客户预览。
而 action_send_report() / _get_send_report_action() 做的是:
- 订阅客户 partner;
- 打开邮件发送向导;
- 使用
mail_template_data_task_report模板; - 通过上下文
fsm_mark_as_sent在发信后把fsm_is_sent标记为 True。
这意味着:
- 已发送 不等于 已签署;
- 已签署 也不一定意味着是通过同一次发送动作进入的;
- 客户预览资格则是两者的并集之一。
这组拆分很实用,因为现实业务里经常出现:
- 技术员先现场让客户直接签;
- 或者先发报告给客户,之后客户再远程签;
- 或者已发但客户迟迟未签。
六、portal 模板里真正暴露给客户的是什么
project_portal_templates.xml 里,签字按钮和弹窗都不是无条件渲染:
portal_task_sign_button只有在task._has_to_be_signed()时才显示;portal_task_sign_modal会把task.id、access_token、默认签名名和调用 URL 一并塞给前端;- 如果任务已经有
worksheet_signature,门户页面还会直接展示签名图片和worksheet_signed_by。
所以客户看到的 portal 并不是“一个随时可签的空白板”,而是一个受 task 状态严格控制的交付页面。
七、实战建议
- 如果客户反馈“为什么我的任务没有 Sign 按钮”,先检查 task 是否真的已有工时/worksheet 内容,而不是先怀疑 portal 权限。
- 想把 FSM 签字做成合规留痕时,不要只保存签名图片,应该同时保留 chatter PDF 附件与访问链路。
- 自定义“无工时也能签”的逻辑时,要同时考虑
_has_to_be_signed()与报告渲染拦截,不然容易出现按钮可见但 PDF 生成失败。 - 培训一线团队时,要明确区分“发送报告给客户”和“客户已经签字确认”。
八、结论
Odoo 企业版 FSM 的签字流程,本质不是“收一张电子签名图片”,而是任务内容校验、portal 访问控制、PDF 报告生成与 chatter 审计留痕的组合。也正因为如此,它比表面看上去更重,但这套重量恰恰是现场服务交付证据可信的来源。
DISCUSSION
评论区