hrms 培训反馈(Training Feedback)邮件通知模板与配置深度解析

原创2026-10-01 12:33:161,070 阅读
文章标签:企业应用后端前端移动开发

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 }}

逐行说明:

  1. <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 等)中均有对应翻译条目,验证了通知模板的多语言能力。
  2. <p>{{ message }}</p> —— 正文首段,输出通知上下文中的 message 变量。该变量由 Frappe 通知框架在渲染消息时注入,用于承载说明性文字。
  3. <h4>{{_("Details")}}</h4> —— "详情"小标题。
  4. {{_("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 相同)。
  5. <br>{{_("Event Location")}}: {{ location }} —— 培训地点,对应 Training Event 的 location 字段(必填,见 training_event.json 中 location 的 reqd: 1)。
  6. <br>{{_("Start Time")}}: {{ start_time }} —— 开始时间,对应 Training Event 的 start_time(Datetime 类型、必填)。
  7. <br>{{_("End Time")}}: {{ end_time }} —— 结束时间,对应 Training Event 的 end_time(Datetime 类型、必填)。
  8. <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。

整个生命周期可归纳为:

  1. HR 创建并提交 Training Event,员工邮箱自动汇入 employee_emails;
  2. 事件完成后 HR 提交 Training Result,参与者状态置为 Completed,同时触发 Training Feedback 通知,向员工发送反馈邀请邮件;
  3. 员工点击邮件中的链接进入 Training Feedback 新建反馈,提交后状态变为 Feedback Submitted,收集闭环完成。

六、收件人机制:employee_emails 的自动填充

通知配置中 recipients[0].email_by_document_field = "employee_emails" 意味着收件人完全来自单据字段。这个字段在两处被自动维护:

两者都调用 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