招聘场景里最常见的误会,是把 Odoo 企业版的 offer / contract signing 理解成“系统帮我发一份 PDF”。实际上,企业版把候选人、薪酬版本、合同模板与签署请求放在同一条入职主链路里。关键源码可以先看 enterprise/hr_contract_salary/models/hr_applicant.py、enterprise/hr_contract_salary/wizard/hr_contract_sign_document_wizard.py,再结合签署侧模型理解它怎么收口。
一、招聘不是直接生成合同,而是先生成 offer 版本
hr_applicant.action_generate_offer() 并不是创建最终雇佣关系,而是先生成 offer / version 数据。_get_offer_values() 会把公司、合同模板、年度成本等上下文一起带进去,_get_contract_template() 则负责从岗位或公司范围内找到合适的合同模板。
这意味着企业版把“签什么”放在 offer version 上,而不是直接塞进 applicant 本身。这样做的好处是:你可以保留多版报价、比较不同薪酬方案,也能在签署前明确当前生效的是哪一套模板与金额。
二、request template 的核心不是文件,而是版本语义
在 hr_contract_sign_document_wizard.py 里,sign_template_ids 的计算逻辑非常值得看。它不是简单给你一个可选模板列表,而是根据当前版本、历史版本、更新模板和正式签署模板,算出这次该走哪种签署文档。
这里反映的是企业场景常见需求:
- 首次录用时走正式合同模板;
- 合同更新时可能走 update template;
- 已归档版本的 sign request 需要同步取消或失效。
所以所谓 request template,不是“上传一份 Word 模板”那么轻,而是“这次人事动作对应哪类法律文书”。
三、候选人到入职的绑定点不在邮件,而在 hired stage 与后续对象迁移
hr_applicant._move_to_hired_stage() 说明一个很重要的点:签署成功后的业务意义,不只是状态改成 done,而是候选人流程要进入 hired stage,对后续员工创建、合同生效、归档旧版本形成明确交接。
也就是说,签署请求只是过程对象;真正要落到人事主数据里的,是 hired / onboarding 之后那组实体变化。你如果只盯 sign.request,会误以为“签完就结束了”,实际上那只是把候选人送进正式入职流程的闸门。
四、归档与取消逻辑是很多团队忽略的风控点
unlink_archived_versions() 里会把归档版本关联的 sign_request_ids 取消并失活。这个动作看起来很小,但很关键:它在防止组织里同时存在多份还可签署的旧合同版本。
对 HR 来说,最危险的不是“没有签出去”,而是“旧版合同还挂着,候选人可能签错”。企业版在这里做的,其实是文档状态治理,而不只是文件发送。
五、实战里要特别注意什么
1. 岗位模板要先于招聘流程设计
因为 _get_contract_template() 优先从岗位拿模板。岗位定义不清,后面招聘流程再顺,也会在合同阶段频繁人工修正。
2. 不要把 offer 版本当成无用中间层
这个版本层正是未来审计、复盘候选人谈薪过程的依据。
3. 签署完成不等于 onboarding 全部完成
还要看 hired stage、员工创建、合同正式生效、旧版本关闭是否都串起来了。
六、结论
Odoo 企业版里的合同签署真正管理的不是“文件发没发出去”,而是候选人从 offer 到正式入职过程中,每一个版本、模板和签署请求有没有被正确绑定、更新和关闭。理解这条链路后,你才会知道它为什么值得放在企业版 HR 主流程里,而不是当成一个附件插件。
主要源码锚点:
enterprise/hr_contract_salary/models/hr_applicant.pyenterprise/hr_contract_salary/wizard/hr_contract_sign_document_wizard.pyenterprise/hr_contract_salary/models/sign_request.py
DISCUSSION
评论区