先说结论
Odoo 里把候选人加到别的岗位,不是把原申请上的 job_id 改一下。
源码真正表达的意思是:
- 一个岗位申请就是一条独立 applicant 事实
- 新岗位应该重新匹配自己的初始 stage
- 人才池语义不能自动继承到正式岗位申请里
所以 job_add_applicants 做的是复制申请并重新路由,而不是修改原申请归属。
为什么不能直接改 job_id
如果只从字段层面看,很多人会觉得:
- 候选人还是同一个人
- 只是从岗位 A 转去岗位 B
- 那直接把
job_id改成 B 不就好了?
但这会立刻带来几个问题:
- 原岗位的申请历史被覆盖
- 原阶段、原评估、原沟通上下文和新岗位混在一起
- 招聘漏斗统计会失真
- 同一人多岗位并行申请没法表达
Odoo 选择 copy_data() + create(),本质上是在承认:
候选人是同一个人,不代表申请是同一条业务记录。
这和 CRM 里“同一个联系人可以对应多条商机”是一个思路。
_add_applicants_to_job() 的核心:先复制,再按岗位展开
向导先做:
applicant_ids.copy_data()
得到的是申请记录的值快照,而不是直接修改原记录。
然后它对每个 applicant、每个目标 job 组合,生成新的 new_applicants_vals。
也就是说,如果:
- 2 个候选人
- 3 个目标岗位
结果不是 2 条更新,而是最多 6 条新申请。
这非常关键,因为它说明这不是“批量改字段”,而是一个申请复制工厂。
这也正符合招聘业务现实:一个候选人可以同时适合多个岗位,每个岗位都应该有自己独立的推进轨迹。
为什么 stage 要重新算,而且取“最早可用非 fold 阶段”
源码会先按岗位读取招聘阶段:
- 允许
job_ids包含该岗位 - 也允许取全局 stage
- 再过滤出可用集合
- 最后按
sequence取最小值
而 hr.recruitment.stage 本身还区分:
- 是否 job specific
- 是否
fold
这背后的逻辑很成熟:
新申请不应该继承旧申请在别的岗位上的阶段位置。
否则你会出现荒谬情况:
- 候选人在岗位 A 已经进到面试后期
- 转投岗位 B 时,系统也把他直接放到后期阶段
这显然不合理。岗位 B 需要自己的招聘语义和起点。所以 Odoo 会为新岗位重新找一个最早的、未折叠阶段,当作重新进入流程的入口。
为什么要清空 talent_pool_ids
新申请 vals 里有一行特别重要:
talent_pool_ids: False
这意味着从候选人复制到某个 job 的正式申请时,系统会显式把人才池关联清掉。
这不是遗漏,而是设计立场。
因为人才池是:
- 可复用的人才身份/储备语义
而岗位申请是:
- 针对具体 vacancy 的流程语义
如果不清掉,你很容易把“这个人属于人才池”误当成“这条岗位申请也天然属于同一池上下文”。
Odoo 在这里是故意拆边界:
人才池可以是来源或背景,但岗位申请本身必须重新成立。
为什么不是直接复用原申请的阶段和标签
copy_data() 的好处是带来大量已有字段,但 Odoo 仍然会明确覆盖:
job_idtalent_pool_idsstage_id
这正说明官方知道哪些字段是“可继承的上下文”,哪些是“必须重新定义的岗位语义”。
可继承的通常是:
- 候选人身份信息
- 联系方式
- 其他静态资料
必须重置的则是:
- 岗位归属
- 流程阶段
- 人才池关系
这比“全继承”或者“全清空”都更聪明。
成功后的返回动作,也在强调“新申请是新对象”
action_add_applicants_to_job() 完成后:
- 如果只生成一条,就直接打开那条新 applicant form
- 如果生成多条,则给出创建成功通知
这个返回动作很简单,但它传递的产品语言很明确:
系统希望你接下来去看的是新申请对象,不是回到旧申请上继续脑补。
换句话说,复制不是幕后实现细节,而是业务上有意义的新实体创建。
这套设计为什么比“移动候选人”更稳
很多团队内部会说“把这个人转到另一个岗位”。这个说法容易让系统做错。
因为业务上看似是“转”,数据上往往更接近:
- 保留原申请
- 为新岗位再建一条申请
- 两条申请并行存在,后续各走各的评估路径
Odoo 的实现正是站在这个更稳的建模上。
它保护了:
- 招聘漏斗真实度
- 岗位阶段独立性
- 人才池与岗位申请的边界
- 一个候选人多岗位并行的可能性
一句话记忆
Odoo 候选人转投岗位不是改 job_id,而是复制出新的 applicant,按新岗位重算起始阶段,并把 talent pool 语义和正式申请语义拆开。
DISCUSSION
评论区