先说结论
Odoo 的招聘问卷,不是“发封邮件让候选人填一下”。
源码真正处理的是一条小而完整的业务链:
- 候选人如果还没有
partner_id,先补一个联系人 - 同一岗位问卷如果已经有答卷,决定是打印现有结果还是重发邀请
- 发送动作不只发邮件,还会把操作写进 applicant chatter
- 问卷和候选人的关系不是临时 URL,而是
response_ids这条结构化连接
所以它管理的不是“邮件动作”,而是候选人与问卷答题过程之间的可持续关系。
为什么 action_send_survey() 第一步是补 partner_id
在 hr_recruitment_survey.models.hr_applicant 里,action_send_survey() 先检查:
- applicant 有没有
partner_id
如果没有,它不会直接硬发,而是:
- 要求至少有
partner_name - 再用姓名、邮箱、电话创建一个
res.partner
这一步特别有代表性。
因为 Odoo 并不把候选人问卷视为“一次匿名链接投递”。它更想把这件事绑定到可通信、可跟踪、可在后续复用的联系人对象上。
没有 partner_id,很多后续能力都会变弱:
- 邀请目标不稳定
- 历史答卷不好归集
- 邮件与 chatter 之间难以形成完整记录
所以先补 partner,不是为了表单好看,而是在给整条招聘沟通链打地基。
response_ids 说明问卷不是外部附件,而是 applicant 的一部分
模型上,hr.applicant 被扩展了:
survey_id:来自job_id.survey_idresponse_ids:候选人的问卷答卷集合
这就决定了一个很重要的事实:
候选人的问卷不是独立漂浮在 survey 模块里,而是被挂回招聘对象本身。
这意味着招聘负责人以后看 applicant,不只是看邮件、阶段、面试,还能看:
- 这人是否被发过问卷
- 发的是哪一份
- 回了几次
- 哪一份已完成
换句话说,Odoo 在这里做的是把“测评”变成招聘对象的延伸事实,而不是一个外链工具。
为什么打印问卷优先取“最近完成”的答卷
action_print_survey() 的逻辑非常值得注意。
它会先:
- 从
response_ids里筛出当前survey_id相关答卷 - 按
create_date倒序排序
然后分三种情况:
- 没答卷:打印空白 survey form
- 有已完成答卷:打印最近完成的那份
- 没完成但已有答卷:打印最近一份进行中的答卷
这背后的业务选择很清楚:
打印不是为了展示“曾经发过几次”,而是优先拿出最可用、最接近最终结果的那一份。
这和很多系统单纯“总是打印最新记录”不一样。Odoo 在这里把“已完成”看成比“最新创建”更重要的优先级。
_get_done_partners_emails():为什么重发不是重新发给所有人
在 survey invite 向导扩展里,_get_done_partners_emails() 会检查:
- applicant 在当前 survey 下是否已有
response_ids - 如果存在,并且
existing_mode == 'resend' - 就把 applicant 的 partner 归入 done/resend 的逻辑里
这说明 Odoo 对“重复发送”并不是完全放开。
它知道招聘问卷里最常见的几种情况:
- 候选人已经答过了
- 候选人答到一半
- HR 想催一次或补发一次
所以 resend 不只是“再点一遍发送按钮”,而是在已有 answer 语义上继续推进,而不是把历史状态抹掉重来。
action_invite():真正建立的是 applicant 与 answer 的绑定
action_invite() 的关键,不在于发邮件,而在于它会先判断:
- applicant 在当前 survey 下有没有答卷
如果没有,就通过 survey._create_answer(...) 创建 answer,并把结果写回:
applicant.response_ids
这一步很关键。
因为如果系统只是临时发一个 survey 链接,后面 applicant 记录和答卷之间就会很松散;而现在 Odoo 先把 answer 建出来再发,意味着:
- 这份问卷从一开始就是“属于这个 applicant 的答卷”
- 后续邮件、查看、打印、重发,都围着同一条业务主线在走
所以问卷邀请的本质不是 email send,而是先创建可追踪答卷,再把入口发出去。
为什么发完邮件还要 message_post()
向导里还有两处 chatter 留痕:
1)action_invite() 里记录“问卷已发给谁”
它会生成带 HTML link 的提示,回写到 applicant chatter。
2)_send_mail() 里把实际邮件正文也 message_post() 到 applicant
也就是说,Odoo 不是只关心“邮件是否被 SMTP 发走”,而是要让招聘负责人在 applicant 页面里看到:
- 发过哪份问卷
- 发给了谁
- 邮件大致内容是什么
这对团队协作非常重要。因为招聘流程不一定由一个人跟到底,后续接手的人如果只能去邮件系统查历史,会非常痛苦。
而 chatter 留痕把问卷沟通纳入了招聘主记录。
为什么这套设计比“Survey + 链接”更稳
很多公司会说:
- Survey 模块本来就能发链接
- 招聘模块本来就有 applicant
- 为什么还要多做一层集成?
答案就在源码里:
这层集成解决的是身份、历史和上下文一致性。
没有它,你通常会遇到这几类问题:
- 候选人邮箱变动后,旧答卷不好追
- 同一个 applicant 的问卷和招聘沟通割裂
- 招聘负责人看不到是否已经发过/回过
- 打印结果时,不知道该拿空白卷、草稿还是最终卷
Odoo 在这里补的不是一个按钮,而是一整套问卷在招聘流程中的归档方式。
实施时最容易踩的坑
误区一:把 partner 看成可有可无
如果你跳过 partner,只靠邮箱字段发问卷,后面答卷和 applicant 的关系会弱很多。
误区二:重发等于重建答卷
源码里不是这么干的。已有答卷时,系统会尽量延续已有 answer 语义。
误区三:邮件发出就算完成
不够。message_post() 才让团队后续看得懂这件事发生过什么。
误区四:打印总是取最新记录
Odoo 更看重“最近已完成”的答卷,因为那才最接近可评审结果。
我会怎么跟招聘团队解释
如果招聘负责人问:“这不就是给候选人发个表单吗?”
我会说:
不是。Odoo 在做的是把问卷答题过程挂回 applicant 本身,让这份测评以后还能被查、被重发、被打印、被团队接力理解。
一句话记忆
Odoo 招聘问卷不是发链接,而是先把 applicant、partner、answer 和 chatter 串起来,再让邮件成为其中一个动作。
DISCUSSION
评论区