diff --git a/erpnext/setup/doctype/employee/employee.js b/erpnext/setup/doctype/employee/employee.js index 21d67d70e78..b4adc01b102 100755 --- a/erpnext/setup/doctype/employee/employee.js +++ b/erpnext/setup/doctype/employee/employee.js @@ -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({ diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index e663913e8d7..03f68b91dc5 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -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", diff --git a/erpnext/setup/doctype/employee/employee.py b/erpnext/setup/doctype/employee/employee.py index 1ecbb4b9ac7..d66d091320b 100755 --- a/erpnext/setup/doctype/employee/employee.py +++ b/erpnext/setup/doctype/employee/employee.py @@ -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 diff --git a/erpnext/setup/doctype/employee/employee_list.js b/erpnext/setup/doctype/employee/employee_list.js index b50eb381c95..33cf7225626 100644 --- a/erpnext/setup/doctype/employee/employee_list.js +++ b/erpnext/setup/doctype/employee/employee_list.js @@ -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", + }); + }); + } + }); + } + }, }; diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py index c022f724a66..b553898dc49 100644 --- a/erpnext/setup/doctype/employee/test_employee.py +++ b/erpnext/setup/doctype/employee/test_employee.py @@ -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):