Merge pull request #53711 from frappe/mergify/bp/version-16-hotfix/pr-52726

feat(employee): Create User button and form. (backport #52726)
This commit is contained in:
Rucha Mahabal
2026-03-24 11:06:15 +05:30
committed by GitHub
5 changed files with 288 additions and 47 deletions

View File

@@ -45,6 +45,64 @@ frappe.ui.form.on("Employee", {
refresh: function (frm) {
frm.fields_dict.date_of_birth.datepicker.update({ maxDate: new Date() });
if (!frm.is_new() && !frm.doc.user_id) {
frm.add_custom_button(__("Create User"), () => {
const dialog = new frappe.ui.Dialog({
title: __("Create User"),
fields: [
{
fieldtype: "Data",
fieldname: "email",
label: __("Email"),
reqd: 1,
default:
frm.doc.prefered_email || frm.doc.company_email || frm.doc.personal_email,
},
{
fieldtype: "Check",
fieldname: "create_user_permission",
label: __("Create User Permission"),
default: 1,
},
],
primary_action_label: __("Create"),
primary_action: (values) => {
if (!values.email) {
frappe.msgprint(__("Email is required to create a user."));
return;
}
frappe
.call({
method: "erpnext.setup.doctype.employee.employee.create_user",
args: {
employee: frm.doc.name,
email: values.email,
create_user_permission: values.create_user_permission ? 1 : 0,
},
freeze: true,
freeze_message: __("Creating User..."),
})
.then(() => {
dialog.hide();
frm.reload_doc();
});
},
});
dialog.show();
});
}
},
create_user_automatically: function (frm) {
if (frm.doc.create_user_automatically) {
frm.set_value("user_id", "");
frm.set_df_property("user_id", "read_only", 1);
} else {
frm.set_df_property("user_id", "read_only", 0);
}
},
prefered_contact_email: function (frm) {
@@ -77,24 +135,6 @@ frappe.ui.form.on("Employee", {
},
});
},
create_user: function (frm) {
if (!frm.doc.prefered_email) {
frappe.throw(__("Please enter Preferred Contact Email"));
}
frappe.call({
method: "erpnext.setup.doctype.employee.employee.create_user",
args: {
employee: frm.doc.name,
email: frm.doc.prefered_email,
},
freeze: true,
freeze_message: __("Creating User..."),
callback: function (r) {
frm.reload_doc();
},
});
},
});
cur_frm.cscript = new erpnext.setup.EmployeeController({

View File

@@ -28,8 +28,9 @@
"status",
"erpnext_user",
"user_id",
"create_user",
"create_user_permission",
"column_break_xwnm",
"create_user_automatically",
"company_details_section",
"company",
"department",
@@ -39,19 +40,11 @@
"reports_to",
"column_break_18",
"branch",
"employment_details",
"scheduled_confirmation_date",
"column_break_32",
"final_confirmation_date",
"contract_end_date",
"col_break_22",
"notice_number_of_days",
"date_of_retirement",
"contact_details",
"cell_number",
"column_break_40",
"personal_email",
"company_email",
"personal_email",
"column_break4",
"prefered_contact_email",
"prefered_email",
@@ -101,6 +94,14 @@
"external_work_history",
"history_in_company",
"internal_work_history",
"employment_details",
"scheduled_confirmation_date",
"column_break_32",
"final_confirmation_date",
"contract_end_date",
"col_break_22",
"notice_number_of_days",
"date_of_retirement",
"exit",
"resignation_letter_date",
"relieving_date",
@@ -273,6 +274,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.__islocal",
"fieldname": "erpnext_user",
"fieldtype": "Section Break",
"label": "User Details"
@@ -285,20 +287,23 @@
"label": "User ID",
"options": "User"
},
{
"depends_on": "eval:(!doc.user_id)",
"fieldname": "create_user",
"fieldtype": "Button",
"label": "Create User"
},
{
"default": "1",
"depends_on": "user_id",
"depends_on": "eval:doc.user_id || doc.create_user_automatically",
"description": "This will restrict user access to other employee records",
"fieldname": "create_user_permission",
"fieldtype": "Check",
"label": "Create User Permission"
},
{
"default": "0",
"depends_on": "eval:doc.__islocal && !doc.user_id",
"description": "Creates a User account for this employee using the Preferred, Company, or Personal email.",
"fieldname": "create_user_automatically",
"fieldtype": "Check",
"label": "Create User Automatically",
"set_only_once": 1
},
{
"allow_in_quick_entry": 1,
"collapsible": 1,
@@ -348,6 +353,7 @@
{
"fieldname": "department",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Department",
"oldfieldname": "department",
@@ -377,6 +383,7 @@
{
"fieldname": "branch",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Branch",
"oldfieldname": "branch",
"oldfieldtype": "Link",
@@ -600,7 +607,7 @@
"collapsible": 1,
"fieldname": "exit",
"fieldtype": "Tab Break",
"label": "Employee Exit",
"label": "Exit",
"oldfieldtype": "Section Break"
},
{
@@ -816,6 +823,10 @@
"fieldtype": "Data",
"label": "IBAN",
"options": "IBAN"
},
{
"fieldname": "column_break_xwnm",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-user",
@@ -823,7 +834,7 @@
"image_field": "image",
"is_tree": 1,
"links": [],
"modified": "2025-08-29 11:52:12.819878",
"modified": "2026-03-23 15:26:05.149280",
"modified_by": "Administrator",
"module": "Setup",
"name": "Employee",

View File

@@ -8,7 +8,7 @@ from frappe.permissions import (
get_doc_permissions,
remove_user_permission,
)
from frappe.utils import cstr, getdate, today, validate_email_address
from frappe.utils import cint, cstr, getdate, today, validate_email_address
from frappe.utils.nestedset import NestedSet
from erpnext.utilities.transaction_base import delete_events
@@ -23,6 +23,94 @@ class InactiveEmployeeStatusError(frappe.ValidationError):
class Employee(NestedSet):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.setup.doctype.employee_education.employee_education import EmployeeEducation
from erpnext.setup.doctype.employee_external_work_history.employee_external_work_history import (
EmployeeExternalWorkHistory,
)
from erpnext.setup.doctype.employee_internal_work_history.employee_internal_work_history import (
EmployeeInternalWorkHistory,
)
attendance_device_id: DF.Data | None
bank_ac_no: DF.Data | None
bank_name: DF.Data | None
bio: DF.TextEditor | None
blood_group: DF.Literal["", "A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"]
branch: DF.Link | None
cell_number: DF.Data | None
company: DF.Link
company_email: DF.Data | None
contract_end_date: DF.Date | None
create_user_automatically: DF.Check
create_user_permission: DF.Check
ctc: DF.Currency
current_accommodation_type: DF.Literal["", "Rented", "Owned"]
current_address: DF.SmallText | None
date_of_birth: DF.Date
date_of_issue: DF.Date | None
date_of_joining: DF.Date
date_of_retirement: DF.Date | None
department: DF.Link | None
designation: DF.Link | None
education: DF.Table[EmployeeEducation]
emergency_phone_number: DF.Data | None
employee: DF.Data | None
employee_name: DF.Data | None
employee_number: DF.Data | None
encashment_date: DF.Date | None
external_work_history: DF.Table[EmployeeExternalWorkHistory]
family_background: DF.SmallText | None
feedback: DF.SmallText | None
final_confirmation_date: DF.Date | None
first_name: DF.Data
gender: DF.Link
health_details: DF.SmallText | None
held_on: DF.Date | None
holiday_list: DF.Link | None
iban: DF.Data | None
image: DF.AttachImage | None
internal_work_history: DF.Table[EmployeeInternalWorkHistory]
last_name: DF.Data | None
leave_encashed: DF.Literal["", "Yes", "No"]
lft: DF.Int
marital_status: DF.Literal["", "Single", "Married", "Divorced", "Widowed"]
middle_name: DF.Data | None
naming_series: DF.Literal["HR-EMP-"]
new_workplace: DF.Data | None
notice_number_of_days: DF.Int
old_parent: DF.Data | None
passport_number: DF.Data | None
permanent_accommodation_type: DF.Literal["", "Rented", "Owned"]
permanent_address: DF.SmallText | None
person_to_be_contacted: DF.Data | None
personal_email: DF.Data | None
place_of_issue: DF.Data | None
prefered_contact_email: DF.Literal["", "Company Email", "Personal Email", "User ID"]
prefered_email: DF.Data | None
reason_for_leaving: DF.SmallText | None
relation: DF.Data | None
relieving_date: DF.Date | None
reports_to: DF.Link | None
resignation_letter_date: DF.Date | None
rgt: DF.Int
salary_currency: DF.Link | None
salary_mode: DF.Literal["", "Bank", "Cash", "Cheque"]
salutation: DF.Link | None
scheduled_confirmation_date: DF.Date | None
status: DF.Literal["Active", "Inactive", "Suspended", "Left"]
unsubscribed: DF.Check
user_id: DF.Link | None
valid_upto: DF.Date | None
# end: auto-generated types
nsm_parent_field = "reports_to"
def autoname(self):
@@ -72,6 +160,16 @@ class Employee(NestedSet):
self.validate_for_enabled_user_id(data.get("enabled", 0))
self.validate_duplicate_user_id()
def validate_auto_user_creation(self):
if self.create_user_automatically and not (
self.prefered_email or self.company_email or self.personal_email
):
frappe.throw(
_("Company or Personal Email is mandatory when 'Create User Automatically' is enabled"),
frappe.MandatoryError,
title=_("Auto User Creation Error"),
)
def update_nsm_model(self):
frappe.utils.nestedset.update_nsm(self)
@@ -83,6 +181,22 @@ class Employee(NestedSet):
self.update_user_permissions()
self.reset_employee_emails_cache()
def before_insert(self):
self.validate_auto_user_creation()
def after_insert(self):
if not self.create_user_automatically:
return
if self.user_id:
return
create_user(
employee=self.name,
email=self.prefered_email or self.company_email or self.personal_email,
create_user_permission=self.create_user_permission,
)
def update_user_permissions(self):
if not self.has_value_changed("user_id") and not self.has_value_changed("create_user_permission"):
return
@@ -310,10 +424,17 @@ def deactivate_sales_person(status=None, employee=None):
@frappe.whitelist()
def create_user(employee, user=None, email=None):
def create_user(employee: str, email: str | None = None, create_user_permission: int = 0) -> str:
emp = frappe.get_doc("Employee", employee)
if emp.user_id:
frappe.throw(_("Employee {0} already has a linked user").format(emp.name))
if not email:
frappe.throw(_("Email is required to create a user"))
email = validate_email_address(email, True)
employee_name = emp.employee_name.split(" ")
first_name = employee_name[0]
middle_name = last_name = ""
if len(employee_name) >= 3:
@@ -322,16 +443,10 @@ def create_user(employee, user=None, email=None):
elif len(employee_name) == 2:
last_name = employee_name[1]
first_name = employee_name[0]
if email:
emp.prefered_email = email
user = frappe.new_doc("User")
user.update(
{
"name": emp.employee_name,
"email": emp.prefered_email,
"email": email,
"enabled": 1,
"first_name": first_name,
"middle_name": middle_name,
@@ -342,9 +457,18 @@ def create_user(employee, user=None, email=None):
"bio": emp.bio,
}
)
emp.db_set("user_id", email)
user.append_roles("Employee")
user.insert()
emp.user_id = user.name
emp.create_user_permission = cint(create_user_permission)
emp.save()
if cint(create_user_permission):
add_user_permission("Employee", emp.name, user.name)
add_user_permission("Company", emp.company, user.name)
return user.name

View File

@@ -1,11 +1,25 @@
frappe.listview_settings["Employee"] = {
add_fields: ["status", "branch", "department", "designation", "image"],
filters: [["status", "=", "Active"]],
get_indicator: function (doc) {
get_indicator(doc) {
return [
__(doc.status, null, "Employee"),
{ Active: "green", Inactive: "red", Left: "gray", Suspended: "orange" }[doc.status],
"status,=," + doc.status,
];
},
onload(listview) {
if (frappe.perm.has_perm("Employee", 0, "create")) {
frappe.db.count("Employee").then((count) => {
if (count === 0) {
listview.page.add_inner_button(__("Import Employees"), () => {
frappe.new_doc("Data Import", {
reference_doctype: "Employee",
});
});
}
});
}
},
};

View File

@@ -64,6 +64,58 @@ class TestEmployee(ERPNextTestSuite):
self.assertEqual(qb_employee_list, employee_list)
frappe.set_user("Administrator")
def test_create_user_automatically(self):
def get_new_employee(email: str, create_user_permission: int):
return frappe.get_doc(
{
"doctype": "Employee",
"first_name": "Test Auto User 1",
"company": "_Test Company",
"date_of_birth": "2000-05-08",
"date_of_joining": "2013-01-01",
"gender": "Female",
"personal_email": email,
"status": "Active",
"create_user_automatically": 1,
"create_user_permission": create_user_permission,
}
).insert()
employee1 = get_new_employee("test_auto_user1@example.com", True)
user = frappe.db.get_value("User", "test_auto_user1@example.com")
self.assertTrue(user)
self.assertEqual(employee1.user_id, user)
# Verify user permissions are created
self.assertTrue(
frappe.db.exists(
"User Permission", {"allow": "Employee", "for_value": employee1.name, "user": user}
)
)
self.assertTrue(
frappe.db.exists(
"User Permission", {"allow": "Company", "for_value": employee1.company, "user": user}
)
)
# Test disabled create_user_permission
employee2 = get_new_employee("test_auto_user2@example.com", False)
user2 = frappe.db.get_value("User", "test_auto_user2@example.com")
self.assertTrue(user2)
self.assertEqual(employee2.user_id, user2)
# Verify user permissions are not created
self.assertFalse(
frappe.db.exists(
"User Permission", {"allow": "Employee", "for_value": employee2.name, "user": user2}
)
)
self.assertFalse(
frappe.db.exists(
"User Permission", {"allow": "Company", "for_value": employee2.company, "user": user2}
)
)
def make_employee(user, company=None, **kwargs):
if not frappe.db.get_value("User", user):