hrms 培训反馈(Training Feedback)邮件通知模板与配置深度解析
hrms 培训反馈(Training Feedback)邮件通知模板与配置深度解析
本篇指南以 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:参与者与邮箱的初始化
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:触发通知并回写参与者状态
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 内快速复刻类似的"单据事件驱动邮件通知"能力。