- 企业应用
- 后端
- 前端
- 移动开发
【免费下载链接】hrms
Open Source HR and Payroll Software
本篇指南以 hrms(开源 HR 与薪资软件)中内置的 "Training Feedback" 标准通知为线索,完整拆解其通知定义(Notification JSON)、邮件正文模板(Jinja/HTML)、收件人机制与底层培训数据流转。读完你不仅能逐行理解该通知模板的每个占位符与配置字段,还能掌握 Frappe 框架下"基于文档字段派发邮件通知"的标准做法,以及 Training Event → Training Result → Training Feedback 三张单据之间的状态联动规则。
一、通知模板在 hrms 培训流程中的定位
在 hrms 中,培训(Training)相关的单据与通知分散在 hrms/hr/doctype/ 目录下,其中与"培训反馈收集"直接相关的三张核心单据是:
| 单据 | 作用 | 关键源码/定义 |
|---|---|---|
| Training Event | 培训事件主表,登记事件名称、类型、地点、起止时间与参与者 | training_event.py、training_event.json |
| Training Event Employee | 培训事件子表,逐人记录考勤(Present/Absent)与状态(Open/Invited/Completed/Feedback Submitted) | training_event_employee.json |
| Training Result | 培训结果单,提交后触发本通知 | training_result.py |
| Training Feedback | 培训反馈单,员工提交后状态回写为 Feedback Submitted | training_feedback.py |
本篇文章讨论的通知文件位于 hrms/hr/notification/training_feedback/,该目录共 5 个文件,各自职责如下:
training_feedback.json—— Notification 单据的完整定义(消息内容、收件人、触发事件等);training_feedback.md—— 邮件正文模板的源文件(与 JSON 中message字段内容一致);training_feedback.py—— 可选的通知上下文钩子(get_context),当前为空实现;training_feedback.html—— 另一份参考用的邮件模板(以doc前缀方式引用单据字段);__init__.py—— 空文件,标识包结构。
二、通知定义文件:training_feedback.json 字段逐项解读
Frappe 框架中,通知本身也是一种 DocType(Notification)。training_feedback.json给出了这条内置通知的完整配置,关键字段如下:
{ "attach_print": 0, "channel": "Email", "creation": "2017-08-11 03:17:11.769210", "days_in_advance": 0, "docstatus": 0, "doctype": "Notification", "document_type": "Training Result", "enabled": 1, "event": "Submit", "is_standard": 1, "message": "<h3>{{_(\"Training Event\")}}</h3>\n<p>{{ message }}</p>\n\n<h4>{{_(\"Details\")}}</h4>\n{{_(\"Event Name\")}}: <a href=\"{{ event_link }}\">{{ name }}</a>\n<br>{{_(\"Event Location\")}}: {{ location }}\n<br>{{_(\"Start Time\")}}: {{ start_time }}\n<br>{{_(\"End Time\")}}: {{ end_time }}\n<br>{{_(\"Attendance\")}}: {{ attendance }}\n", "name": "Training Feedback", "recipients": [ { "email_by_document_field": "employee_emails" } ], "subject": "Please Share your Feedback For {{ doc.training_event }}" }字段含义与配置要点:
| 字段 | 取值 | 说明 |
|---|---|---|
doctype | Notification | 通知记录本身是 Frappe 的 Notification 单据 |
name | Training Feedback | 通知名称,即这条标准通知的唯一标识 |
is_standard | 1 | 标准通知,随应用安装自动部署到站点 |
enabled | 1 | 通知处于启用状态 |
channel | Email | 发送渠道为邮件(Frappe 通知还支持 Slack、System Notification 等渠道) |
document_type | Training Result | 监听对象:Training Result 单据发生指定事件时触发 |
event | Submit | 触发事件:Training Result 提交(submit)时触发。可选的框架事件还包括 New、Save、Value Change、Days Before、Days After 等 |
attach_print | 0 | 不随邮件附加单据的 PDF 打印件 |
days_in_advance | 0 | 仅用于按日期提前触发的事件(Days Before/Days After),此处无实际作用 |
recipients[0].email_by_document_field | employee_emails | 收件人来源:从 Training Result 单据的employee_emails字段读取收件邮箱 |
subject | Please Share your Feedback For {{ doc.training_event }} | 邮件主题。注意主题里使用doc.前缀引用提交单据的字段(doc.training_event),而正文模板用的是裸变量(详见第四节) |
message | 见下文第三节 | 邮件正文模板,即training_feedback.md的内容(JSON 中为转义后的字符串形式) |
要点:通知的触发对象是Training Result,而不是 Training Event。也就是说,当 HR 将某次培训的结果单提交(Submit)后,系统自动向该结果单关联员工的邮箱发送"请提交培训反馈"的邮件。
三、邮件正文模板逐行拆解:training_feedback.md
training_feedback.md的完整原文如下,本文完整保留并逐行展开:
<h3>{{_("Training Event")}}</h3> <p>{{ message }}</p> <h4>{{_("Details")}}</h4> {{_("Event Name")}}: <a href="{{ event_link }}">{{ name }}</a> <br>{{_("Event Location")}}: {{ location }} <br>{{_("Start Time")}}: {{ start_time }} <br>{{_("End Time")}}: {{ end_time }} <br>{{_("Attendance")}}: {{ attendance }}逐行说明:
<h3>{{_("Training Event")}}</h3>—— 邮件标题段。{{ _("...") }}是 Frappe 的国际化(i18n)翻译函数,Jinja 渲染时会按当前站点语言从翻译表取词。该通知涉及的字符串("Training Event"、"Event Name"、"Event Location"、"Start Time"、"End Time"、"Attendance" 等)在 hrms/locale/ 下的各语言.po文件(如zh.po、zh_TW.po、de.po等)中均有对应翻译条目,验证了通知模板的多语言能力。<p>{{ message }}</p>—— 正文首段,输出通知上下文中的message变量。该变量由 Frappe 通知框架在渲染消息时注入,用于承载说明性文字。<h4>{{_("Details")}}</h4>—— "详情"小标题。{{_("Event Name")}}: <a href="{{ event_link }}">{{ name }}</a>—— 事件名称行。event_link为指向培训事件记录的链接地址,name为事件名称。从源码结构看,Training Event 采用"按字段自动命名"(autoname: field:event_name),event_name即事件名,见 training_event.json 的autoname配置;name即事件记录名(与event_name相同)。<br>{{_("Event Location")}}: {{ location }}—— 培训地点,对应 Training Event 的location字段(必填,见training_event.json中location的reqd: 1)。<br>{{_("Start Time")}}: {{ start_time }}—— 开始时间,对应 Training Event 的start_time(Datetime类型、必填)。<br>{{_("End Time")}}: {{ end_time }}—— 结束时间,对应 Training Event 的end_time(Datetime类型、必填)。<br>{{_("Attendance")}}: {{ attendance }}—— 参与者的考勤状态,对应 Training Event Employee 子表中的attendance字段,取值Present/Absent(见 training_event_employee.json)。
模板中占位符与底层字段的映射关系汇总:
| 模板变量 | 含义 | 对应字段/来源 |
|---|---|---|
message | 正文提示语 | Frappe 通知框架注入的上下文变量 |
event_link | 培训事件记录链接 | 指向 Training Event 表单的 URL |
name | 事件名称 | Training Event 的event_name(autoname 字段) |
location | 培训地点 | Training Event.location |
start_time | 开始时间 | Training Event.start_time |
end_time | 结束时间 | Training Event.end_time |
attendance | 考勤状态 | Training Event Employee.attendance(Present/Absent) |
四、上下文变量来源与两个值得注意的实践问题
4.1 get_context 钩子:当前为空实现
training_feedback.py提供了标准的上下文钩子,但目前是空实现:
def get_context(context): # do your magic here pass在 Frappe 通知机制中,get_context(context)允许为消息模板注入自定义上下文变量。当前为空,意味着模板中message、event_link、name、location等变量依赖框架默认上下文与关联单据数据提供;若某些变量未出现在渲染上下文中,Jinja 会将其渲染为空串,邮件中对应位置留白。需要为模板补充数据(如按参与员工定制考勤信息)时,可在get_context中填充context["..."]后由模板引用。
4.2 主题用doc.前缀、正文用裸变量:写法的对比
观察同一通知的两处模板写法可以发现一个对比:
- 邮件主题(JSON 中的
subject)使用了{{ doc.training_event }},显式引用提交单据(Training Result)上的training_event字段; - 邮件正文(
training_feedback.md)使用{{ event_link }}、{{ name }}等裸变量,未加doc.前缀。
4.3 同目录参考模板的推荐写法
training_feedback.html 是同一目录下的另一份参考模板,它展示了更"显式、可追溯"的写法:
<p>{{ _("Hello") }},</p> <p>You attended training {{ frappe.utils.get_link_to_form( "Training Event", doc.training_event) }}</p> <p>{{ _("Please share your feedback to the training by clicking on 'Training Feedback' and then 'New'") }}</p>这段模板直接通过frappe.utils.get_link_to_form("Training Event", doc.training_event)生成培训事件的可点击链接,并把操作引导(进入 Training Feedback、点击 New 新建反馈)写进邮件正文。对比可见:前者(.md)依赖注入变量、字段更省;后者(.html)显式引用单据字段、可读性更强。实际部署时若遇到正文占位符渲染为空,可参考.html的做法改用doc.前缀字段与frappe.utils.get_link_to_form组合。
五、底层数据流:从培训事件到反馈提交的状态机
通知只是"最后一公里",要真正理解它,需要看懂背后三张单据如何联动。以下链路均有源码佐证。
5.1 Training Event:参与者与邮箱的初始化
training_event.py 中:
def validate(self): self.set_employee_emails() self.validate_period() def set_employee_emails(self): self.employee_emails = ", ".join(get_employee_emails([d.employee for d in self.employees])) def validate_period(self): if time_diff_in_seconds(self.end_time, self.start_time) <= 0: frappe.throw(_("End time cannot be before start time")) def on_update_after_submit(self): self.set_status_for_attendees()set_employee_emails()遍历子表employees,通过 ERPNext 的get_employee_emails工具函数(erpnext.setup.doctype.employee.employee)拼装出逗号分隔的员工邮箱串,写入employee_emails隐藏字段;validate_period()校验结束时间不得早于开始时间;on_update_after_submit()中set_status_for_attendees()依据event_status(Scheduled/Completed/Cancelled,见training_event.json)批量更新参与者状态:事件 Completed 时,考勤为 Present 且尚未提交反馈的员工状态置为 Completed;事件 Scheduled 时全部重置为 Open。
5.2 Training Result:触发通知并回写参与者状态
training_result.py:
def validate(self): training_event = frappe.get_doc("Training Event", self.training_event) if training_event.docstatus != 1: frappe.throw(_("{0} must be submitted").format(_("Training Event"))) self.employee_emails = ", ".join(get_employee_emails([d.employee for d in self.employees])) def on_submit(self): training_event = frappe.get_doc("Training Event", self.training_event) training_event.status = "Completed" for e in self.employees: for e1 in training_event.employees: if e1.employee == e.employee: e1.status = "Completed" break training_event.save()要点:
validate()要求关联的 Training Event 必须已提交(docstatus == 1),同时自动填充employee_emails——这正是通知收件人(email_by_document_field)取值的来源;on_submit()提交结果单时,把 Training Event 中对应参与者的状态批量置为 Completed,形成状态闭环;- Training Result 提交时,Notification 监听
Submit事件即被触发,向employee_emails中的地址发送本模板邮件。
5.3 Training Feedback:校验规则与状态回写
training_feedback.py 定义了员工填写反馈时的业务约束:
def validate(self): training_event = frappe.get_doc("Training Event", self.training_event) if training_event.docstatus != 1: frappe.throw(_("{0} must be submitted").format(_("Training Event"))) emp_event_details = frappe.db.get_value( "Training Event Employee", {"parent": self.training_event, "employee": self.employee}, ["name", "attendance"], as_dict=True, ) if not emp_event_details: frappe.throw(_("Employee {0} not found in Training Event Participants.").format(...)) if emp_event_details.attendance == "Absent": frappe.throw(_("Feedback cannot be recorded for an absent Employee.")) def on_submit(self): # 将参与者的状态更新为 Feedback Submitted frappe.db.set_value("Training Event Employee", employee, "status", "Feedback Submitted") def on_cancel(self): # 取消反馈时回退为 Completed frappe.db.set_value("Training Event Employee", employee, "status", "Completed")三条核心规则:培训事件必须已提交;提交者必须是该事件的参与者;考勤为 Absent 的员工不允许记录反馈。反馈提交(submit)后参与者状态变为Feedback Submitted,取消则回退为Completed。
整个生命周期可归纳为:
- HR 创建并提交Training Event,员工邮箱自动汇入
employee_emails; - 事件完成后 HR 提交Training Result,参与者状态置为 Completed,同时触发 Training Feedback 通知,向员工发送反馈邀请邮件;
- 员工点击邮件中的链接进入Training Feedback新建反馈,提交后状态变为 Feedback Submitted,收集闭环完成。
六、收件人机制:employee_emails 的自动填充
通知配置中recipients[0].email_by_document_field = "employee_emails"意味着收件人完全来自单据字段。这个字段在两处被自动维护:
- Training Event 的
validate()(见 training_event.py); - Training Result 的
validate()(见 training_result.py)。
两者都调用 ERPNext 的get_employee_emails(employees)将员工列表转成邮箱串。因此:
- 只要在事件/结果单中维护好参与者子表(
employees),邮箱地址无需手工录入; - 通知发送时框架自动从
employee_emails字段解析收件人,一次向全部参与者群发同一封反馈邀请。
七、测试用例对业务规则的验证
test_training_feedback.py 用两个用例把上面第五节总结的规则固化为自动化测试,可作为理解通知背后业务约束的权威依据:
test_employee_validations_for_feedback:员工未登记为培训事件参与者时创建反馈应抛出frappe.ValidationError;将参与者考勤置为 Absent 后同样应被拒绝;test_training_feedback_status:反馈提交后,Training Event Employee 的status应更新为Feedback Submitted。
测试还演示了搭建场景的标准套路:create_training_program("Basic Training")创建培训项目、make_employee(...)创建员工、create_training_event(...)创建事件并submit()、将event_status置为 Completed 后保存(对应training_event.py的set_status_for_attendees逻辑)。
八、与 Training Scheduled 通知的对比
hrms/hr/notification/目录下还内置了另一条培训相关通知 training_scheduled(培训安排通知),其模板 training_scheduled.md 与反馈通知形成鲜明对照:
- Training Scheduled模板基于
doc上下文(doc.event_name、doc.introduction、doc.location),并使用 Jinja 条件语句按起止日期是否同一天分别渲染日期+时间段或起止时间,还通过frappe.utils.get_datetime格式化日期,通过frappe.utils.get_link_to_form生成事件链接; - Training Feedback模板则全部使用裸变量(
event_link/name/location/start_time/end_time/attendance)。
两者对比可以总结出 hrms 团队两种模板组织风格的并存:一种是"上下文变量注入式"(简练,依赖框架注入),一种是"doc 字段直引式"(显式、可控、便于排错)。对于希望二次开发通知模板的读者,推荐以 Training Scheduled 的写法为参照,在 Feedback 通知中改用doc前缀与frappe.utils.get_link_to_form,可显著降低占位符解析失败的风险。
小结
hrms 的 Training Feedback 通知是一个"配置 + 模板 + 数据流"三位一体的典型范本:training_feedback.json定义了监听 Training Result 提交事件、按employee_emails字段群发的规则;training_feedback.md提供了内嵌国际化标签的 HTML 邮件正文;而 Training Event、Training Result、Training Feedback 三张单据的校验与提交逻辑(training_event.py、training_result.py、training_feedback.py)保证了"只有真实参与者、考勤在场者才能反馈"的业务约束,测试用例 test_training_feedback.py 则让这些规则可持续验证。掌握这条通知的配置与数据链路后,你可以在 hrms 内快速复刻类似的"单据事件驱动邮件通知"能力。
- 企业应用
- 后端
- 前端
- 移动开发
【免费下载链接】hrms
Open Source HR and Payroll Software
相关推荐
HRMS 培训日程邮件通知(Training Scheduled)模板解析与底层实现指南
HRMS 培训日程邮件通知(Training Scheduled)模板解析与底层实现指南 导读 在 Frappe/HRMS(hrms)中,"培训日程已排定(Tr
企业应用后端前端移动开发INFINI Console邮件通知:SMTP配置与邮件模板
INFINI Console邮件通知:SMTP配置与邮件模板 概述 INFINI Console作为一款轻量级的多集群、跨版本统一Elasticsearch/O
后端数据库可观测性告警运维SBJson 5开发者深度指南:从源码解析到自定义扩展实现
SBJson 5开发者深度指南:从源码解析到自定义扩展实现 SBJson 5是一款高效可靠的JSON处理框架,专为iOS和macOS开发者设计。本指南将带您深入
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考