From 755b773616cb7e037e2034d2bc0e396f36580a3f Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 25 Nov 2020 16:05:17 +0530 Subject: [PATCH] feat: Leave policy assignment (#23112) * feat: Leave Policy Assignment * feat: linking with leave allocation and valiations * style: removed old code from leave period * feat: Bulk Leave policy Assignment and grant Leaves * fix: overlap validation * feat: earned leaves based on joining date * feat: automatic grant leave based on leave policy * patch: create leave policy assignment based on employee current leave policy * fix: dependent test cases * test: Leave policy assignment * fix: some enhancement * style: break large function into small function * fix:requested Changes * fix(patch): Handled old Leave allocatioln * fix:codacy * fix: travis and sider,codacy * fix: codacy * fix: codacy * fix: requested changes and sider Co-authored-by: Nabin Hait --- erpnext/hooks.py | 4 +- erpnext/hr/doctype/employee/employee.json | 11 +- .../employee_grade/employee_grade.json | 130 ++------------ .../hr/doctype/hr_settings/hr_settings.json | 13 +- .../leave_allocation/leave_allocation.json | 16 +- .../leave_allocation/leave_allocation.py | 10 ++ .../test_leave_application.py | 37 ++-- .../leave_encashment/test_leave_encashment.py | 18 +- .../hr/doctype/leave_period/leave_period.js | 78 +-------- .../hr/doctype/leave_period/leave_period.py | 109 +----------- .../doctype/leave_period/test_leave_period.py | 34 +--- .../leave_policy_assignment/__init__.py | 0 .../leave_policy_assignment.js | 72 ++++++++ .../leave_policy_assignment.json | 160 +++++++++++++++++ .../leave_policy_assignment.py | 163 ++++++++++++++++++ .../leave_policy_assignment_list.js | 138 +++++++++++++++ .../test_leave_policy_assignment.py | 103 +++++++++++ erpnext/hr/doctype/leave_type/leave_type.json | 10 +- erpnext/hr/utils.py | 129 ++++++++------ erpnext/patches.txt | 1 + ..._based_on_employee_current_leave_policy.py | 77 +++++++++ 21 files changed, 899 insertions(+), 414 deletions(-) create mode 100644 erpnext/hr/doctype/leave_policy_assignment/__init__.py create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py create mode 100644 erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js create mode 100644 erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py create mode 100644 erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 741176f33f..726ab6e22a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -347,14 +347,16 @@ scheduler_events = { "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms", "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", + "erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy", "erpnext.hr.utils.generate_leave_encashment", + "erpnext.hr.utils.allocate_earned_leaves", + "erpnext.hr.utils.grant_leaves_automatically", "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.doctype.lead.lead.daily_open_lead" ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", - "erpnext.hr.utils.allocate_earned_leaves", "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" ] } diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 4cabe97cc4..4f1c04ff5d 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -57,7 +57,6 @@ "column_break_45", "shift_request_approver", "attendance_and_leave_details", - "leave_policy", "attendance_device_id", "column_break_44", "holiday_list", @@ -411,14 +410,6 @@ "oldfieldtype": "Link", "options": "Branch" }, - { - "fetch_from": "grade.default_leave_policy", - "fetch_if_empty": 1, - "fieldname": "leave_policy", - "fieldtype": "Link", - "label": "Leave Policy", - "options": "Leave Policy" - }, { "description": "Applicable Holiday List", "fieldname": "holiday_list", @@ -822,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-10-16 14:41:10.580897", + "modified": "2020-10-16 15:02:04.283657", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee_grade/employee_grade.json b/erpnext/hr/doctype/employee_grade/employee_grade.json index e63ffae0c4..88b061a3c3 100644 --- a/erpnext/hr/doctype/employee_grade/employee_grade.json +++ b/erpnext/hr/doctype/employee_grade/employee_grade.json @@ -1,167 +1,69 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 16:14:24.174138", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2018-04-13 16:14:24.174138", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "default_salary_structure" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_leave_policy", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Leave Policy", - "length": 0, - "no_copy": 0, - "options": "Leave Policy", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "default_salary_structure", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Default Salary Structure", - "length": 0, - "no_copy": 0, - "options": "Salary Structure", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Salary Structure" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-18 17:17:45.617624", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-26 13:12:07.815330", "modified_by": "Administrator", "module": "HR", "name": "Employee Grade", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 4374d2911a..f99963504a 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -21,6 +21,7 @@ "show_leaves_of_all_department_members_in_calendar", "auto_leave_encashment", "restrict_backdated_leave_application", + "automatically_allocate_leaves_based_on_leave_policy", "hiring_settings", "check_vacancies" ], @@ -41,7 +42,7 @@ "description": "Employee records are created using the selected field", "fieldname": "emp_created_by", "fieldtype": "Select", - "label": "Employee Records to Be Created By", + "label": "Employee Records to be created by", "options": "Naming Series\nEmployee Number\nFull Name" }, { @@ -117,7 +118,7 @@ "default": "0", "fieldname": "restrict_backdated_leave_application", "fieldtype": "Check", - "label": "Restrict Backdated Leave Applications" + "label": "Restrict Backdated Leave Application" }, { "depends_on": "eval:doc.restrict_backdated_leave_application == 1", @@ -125,13 +126,19 @@ "fieldtype": "Link", "label": "Role Allowed to Create Backdated Leave Application", "options": "Role" + }, + { + "default": "0", + "fieldname": "automatically_allocate_leaves_based_on_leave_policy", + "fieldtype": "Check", + "label": "Automatically Allocate Leaves Based On Leave Policy" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2020-10-13 11:49:46.168027", + "modified": "2020-08-27 14:30:28.995324", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 007497e34a..4b315014da 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-02-20 19:10:38", @@ -24,6 +25,7 @@ "compensatory_request", "leave_period", "leave_policy", + "leave_policy_assignment", "carry_forwarded_leaves_count", "expired", "amended_from", @@ -160,9 +162,10 @@ "read_only": 1 }, { - "fetch_from": "employee.leave_policy", + "fetch_from": "leave_policy_assignment.leave_policy", "fieldname": "leave_policy", "fieldtype": "Link", + "hidden": 1, "in_standard_filter": 1, "label": "Leave Policy", "options": "Leave Policy", @@ -209,12 +212,21 @@ "fieldtype": "Float", "label": "Carry Forwarded Leaves", "read_only": 1 + }, + { + "fieldname": "leave_policy_assignment", + "fieldtype": "Link", + "label": "Leave Policy Assignment", + "options": "Leave Policy Assignment", + "read_only": 1 } ], "icon": "fa fa-ok", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-08-08 15:08:42.440909", + "links": [], + "modified": "2020-08-20 14:25:10.314323", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 03fe3fa035..a09cd2ea11 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -51,9 +51,19 @@ class LeaveAllocation(Document): def on_cancel(self): self.create_leave_ledger_entry(submit=False) + if self.leave_policy_assignment: + self.update_leave_policy_assignments_when_no_allocations_left() if self.carry_forward: self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) + def update_leave_policy_assignments_when_no_allocations_left(self): + allocations = frappe.db.get_list("Leave Allocation", filters = { + "docstatus": 1, + "leave_policy_assignment": self.leave_policy_assignment + }) + if len(allocations) == 0: + frappe.db.set_value("Leave Policy Assignment", self.leave_policy_assignment ,"leaves_allocated", 0) + def validate_period(self): if date_diff(self.to_date, self.from_date) <= 0: frappe.throw(_("To date cannot be before from date")) diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index 6e909c3f01..53b7a39e51 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -10,6 +10,7 @@ from frappe.permissions import clear_user_permissions_for_doctype from frappe.utils import add_days, nowdate, now_datetime, getdate, add_months from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation +from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees test_dependencies = ["Leave Allocation", "Leave Block List"] @@ -410,25 +411,39 @@ class TestLeaveApplication(unittest.TestCase): self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21) def test_earned_leaves_creation(self): + + frappe.db.sql('''delete from `tabLeave Period`''') + frappe.db.sql('''delete from `tabLeave Policy Assignment`''') + frappe.db.sql('''delete from `tabLeave Allocation`''') + frappe.db.sql('''delete from `tabLeave Ledger Entry`''') + leave_period = get_leave_period() employee = get_employee() leave_type = 'Test Earned Leave Type' - if not frappe.db.exists('Leave Type', leave_type): - frappe.get_doc(dict( - leave_type_name = leave_type, - doctype = 'Leave Type', - is_earned_leave = 1, - earned_leave_frequency = 'Monthly', - rounding = 0.5, - max_leaves_allowed = 6 - )).insert() + frappe.delete_doc_if_exists("Leave Type", 'Test Earned Leave Type', force=1) + frappe.get_doc(dict( + leave_type_name = leave_type, + doctype = 'Leave Type', + is_earned_leave = 1, + earned_leave_frequency = 'Monthly', + rounding = 0.5, + max_leaves_allowed = 6 + )).insert() + leave_policy = frappe.get_doc({ "doctype": "Leave Policy", "leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}] }).insert() - frappe.db.set_value("Employee", employee.name, "leave_policy", leave_policy.name) - allocate_leaves(employee, leave_period, leave_type, 0, eligible_leaves = 12) + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee() from erpnext.hr.utils import allocate_earned_leaves i = 0 diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index 99f6463416..bbee18bb0a 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -9,6 +9,7 @@ from frappe.utils import today, add_months from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period +from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy\ test_dependencies = ["Leave Type"] @@ -16,6 +17,7 @@ test_dependencies = ["Leave Type"] class TestLeaveEncashment(unittest.TestCase): def setUp(self): frappe.db.sql('''delete from `tabLeave Period`''') + frappe.db.sql('''delete from `tabLeave Policy Assignment`''') frappe.db.sql('''delete from `tabLeave Allocation`''') frappe.db.sql('''delete from `tabLeave Ledger Entry`''') frappe.db.sql('''delete from `tabAdditional Salary`''') @@ -29,14 +31,22 @@ class TestLeaveEncashment(unittest.TestCase): # create employee, salary structure and assignment self.employee = make_employee("test_employee_encashment@example.com") - frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name) + self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": self.leave_period.name + } + + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee], frappe._dict(data)) salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee, other_details={"leave_encashment_amount_per_day": 50}) - # create the leave period and assign the leaves - self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) - self.leave_period.grant_leave_allocation(employee=self.employee) + #grant Leaves + frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee() + def test_leave_balance_value_and_amount(self): frappe.db.sql('''delete from `tabLeave Encashment`''') diff --git a/erpnext/hr/doctype/leave_period/leave_period.js b/erpnext/hr/doctype/leave_period/leave_period.js index bad2b8766c..0e88bc1671 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.js +++ b/erpnext/hr/doctype/leave_period/leave_period.js @@ -2,14 +2,6 @@ // For license information, please see license.txt frappe.ui.form.on('Leave Period', { - refresh: (frm)=>{ - frm.set_df_property("grant_leaves", "hidden", frm.doc.__islocal ? 1:0); - if(!frm.is_new()) { - frm.add_custom_button(__('Grant Leaves'), function () { - frm.trigger("grant_leaves"); - }); - } - }, from_date: (frm)=>{ if (frm.doc.from_date && !frm.doc.to_date) { var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12); @@ -22,73 +14,7 @@ frappe.ui.form.on('Leave Period', { "filters": { "company": frm.doc.company, } - } - }) - }, - grant_leaves: function(frm) { - var d = new frappe.ui.Dialog({ - title: __('Grant Leaves'), - fields: [ - { - "label": "Filter Employees By (Optional)", - "fieldname": "sec_break", - "fieldtype": "Section Break", - }, - { - "label": "Employee Grade", - "fieldname": "grade", - "fieldtype": "Link", - "options": "Employee Grade" - }, - { - "label": "Department", - "fieldname": "department", - "fieldtype": "Link", - "options": "Department" - }, - { - "fieldname": "col_break", - "fieldtype": "Column Break", - }, - { - "label": "Designation", - "fieldname": "designation", - "fieldtype": "Link", - "options": "Designation" - }, - { - "label": "Employee", - "fieldname": "employee", - "fieldtype": "Link", - "options": "Employee" - }, - { - "fieldname": "sec_break", - "fieldtype": "Section Break", - }, - { - "label": "Add unused leaves from previous allocations", - "fieldname": "carry_forward", - "fieldtype": "Check" - } - ], - primary_action: function() { - var data = d.get_values(); - - frappe.call({ - doc: frm.doc, - method: "grant_leave_allocation", - args: data, - callback: function(r) { - if(!r.exc) { - d.hide(); - frm.reload_doc(); - } - } - }); - }, - primary_action_label: __('Grant') + }; }); - d.show(); - } + }, }); diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py index 0973ac7198..28a33f6fac 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.py +++ b/erpnext/hr/doctype/leave_period/leave_period.py @@ -7,24 +7,10 @@ import frappe from frappe import _ from frappe.utils import getdate, cstr, add_days, date_diff, getdate, ceil from frappe.model.document import Document -from erpnext.hr.utils import validate_overlap, get_employee_leave_policy +from erpnext.hr.utils import validate_overlap from frappe.utils.background_jobs import enqueue -from six import iteritems class LeavePeriod(Document): - def get_employees(self, args): - conditions, values = [], [] - for field, value in iteritems(args): - if value: - conditions.append("{0}=%s".format(field)) - values.append(value) - - condition_str = " and " + " and ".join(conditions) if len(conditions) else "" - - employees = frappe._dict(frappe.db.sql("select name, date_of_joining from tabEmployee where status='Active' {condition}" #nosec - .format(condition=condition_str), tuple(values))) - - return employees def validate(self): self.validate_dates() @@ -33,96 +19,3 @@ class LeavePeriod(Document): def validate_dates(self): if getdate(self.from_date) >= getdate(self.to_date): frappe.throw(_("To date can not be equal or less than from date")) - - - def grant_leave_allocation(self, grade=None, department=None, designation=None, - employee=None, carry_forward=0): - employee_records = self.get_employees({ - "grade": grade, - "department": department, - "designation": designation, - "name": employee - }) - - if employee_records: - if len(employee_records) > 20: - frappe.enqueue(grant_leave_alloc_for_employees, timeout=600, - employee_records=employee_records, leave_period=self, carry_forward=carry_forward) - else: - grant_leave_alloc_for_employees(employee_records, self, carry_forward) - else: - frappe.msgprint(_("No Employee Found")) - -def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0): - leave_allocations = [] - existing_allocations_for = get_existing_allocations(list(employee_records.keys()), leave_period.name) - leave_type_details = get_leave_type_details() - count = 0 - for employee in employee_records.keys(): - if employee in existing_allocations_for: - continue - count +=1 - leave_policy = get_employee_leave_policy(employee) - if leave_policy: - for leave_policy_detail in leave_policy.leave_policy_details: - if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp: - leave_allocation = create_leave_allocation(employee, leave_policy_detail.leave_type, - leave_policy_detail.annual_allocation, leave_type_details, leave_period, carry_forward, employee_records.get(employee)) - leave_allocations.append(leave_allocation) - frappe.db.commit() - frappe.publish_progress(count*100/len(set(employee_records.keys()) - set(existing_allocations_for)), title = _("Allocating leaves...")) - - if leave_allocations: - frappe.msgprint(_("Leaves has been granted sucessfully")) - -def get_existing_allocations(employees, leave_period): - leave_allocations = frappe.db.sql_list(""" - SELECT DISTINCT - employee - FROM `tabLeave Allocation` - WHERE - leave_period=%s - AND employee in (%s) - AND carry_forward=0 - AND docstatus=1 - """ % ('%s', ', '.join(['%s']*len(employees))), [leave_period] + employees) - if leave_allocations: - frappe.msgprint(_("Skipping Leave Allocation for the following employees, as Leave Allocation records already exists against them. {0}") - .format("\n".join(leave_allocations))) - return leave_allocations - -def get_leave_type_details(): - leave_type_details = frappe._dict() - leave_types = frappe.get_all("Leave Type", - fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"]) - for d in leave_types: - leave_type_details.setdefault(d.name, d) - return leave_type_details - -def create_leave_allocation(employee, leave_type, new_leaves_allocated, leave_type_details, leave_period, carry_forward, date_of_joining): - ''' Creates leave allocation for the given employee in the provided leave period ''' - if carry_forward and not leave_type_details.get(leave_type).is_carry_forward: - carry_forward = 0 - - # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period - if getdate(date_of_joining) > getdate(leave_period.from_date): - remaining_period = ((date_diff(leave_period.to_date, date_of_joining) + 1) / (date_diff(leave_period.to_date, leave_period.from_date) + 1)) - new_leaves_allocated = ceil(new_leaves_allocated * remaining_period) - - # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0 - if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1: - new_leaves_allocated = 0 - - allocation = frappe.get_doc(dict( - doctype="Leave Allocation", - employee=employee, - leave_type=leave_type, - from_date=leave_period.from_date, - to_date=leave_period.to_date, - new_leaves_allocated=new_leaves_allocated, - leave_period=leave_period.name, - carry_forward=carry_forward - )) - allocation.save(ignore_permissions = True) - allocation.submit() - return allocation.name \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index 1762cf917a..b5857bcd8f 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -5,43 +5,11 @@ from __future__ import unicode_literals import frappe, erpnext import unittest -from frappe.utils import today, add_months -from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on test_dependencies = ["Employee", "Leave Type", "Leave Policy"] class TestLeavePeriod(unittest.TestCase): - def setUp(self): - frappe.db.sql("delete from `tabLeave Period`") - - def test_leave_grant(self): - leave_type = "_Test Leave Type" - - # create the leave policy - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{ - "leave_type": leave_type, - "annual_allocation": 20 - }] - }).insert() - leave_policy.submit() - - # create employee and assign the leave period - employee = "test_leave_period@employee.com" - employee_doc_name = make_employee(employee) - frappe.db.set_value("Employee", employee_doc_name, "leave_policy", leave_policy.name) - - # clear the already allocated leave - frappe.db.sql('''delete from `tabLeave Allocation` where employee=%s''', "test_leave_period@employee.com") - - # create the leave period - leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) - - # test leave_allocation - leave_period.grant_leave_allocation(employee=employee_doc_name) - self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20) + pass def create_leave_period(from_date, to_date, company=None): leave_period = frappe.db.get_value('Leave Period', diff --git a/erpnext/hr/doctype/leave_policy_assignment/__init__.py b/erpnext/hr/doctype/leave_policy_assignment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js new file mode 100644 index 0000000000..7c32a0dde0 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js @@ -0,0 +1,72 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Leave Policy Assignment', { + onload: function(frm) { + frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; + }, + + refresh: function(frm) { + if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) { + frm.add_custom_button(__("Grant Leave"), function() { + + frappe.call({ + doc: frm.doc, + method: "grant_leave_alloc_for_employee", + callback: function(r) { + let leave_allocations = r.message; + let msg = frm.events.get_success_message(leave_allocations); + frappe.msgprint(msg); + cur_frm.refresh(); + } + }); + }); + } + }, + + get_success_message: function(leave_allocations) { + let msg = __("Leaves has been granted successfully"); + msg += "
"; + msg += ""; + for (let key in leave_allocations) { + msg += ""; + } + msg += "
"+__('Leave Type')+""+__("Leave Allocation")+""+__("Leaves Granted")+"
"+key+""+leave_allocations[key]["name"]+""+leave_allocations[key]["leaves"]+"
"; + return msg; + }, + + assignment_based_on: function(frm) { + if (frm.doc.assignment_based_on) { + frm.events.set_effective_date(frm); + } else { + frm.set_value("effective_from", ''); + frm.set_value("effective_to", ''); + } + }, + + leave_period: function(frm) { + if (frm.doc.leave_period) { + frm.events.set_effective_date(frm); + } + }, + + set_effective_date: function(frm) { + if (frm.doc.assignment_based_on == "Leave Period" && frm.doc.leave_period) { + frappe.model.with_doc("Leave Period", frm.doc.leave_period, function () { + let from_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "from_date"); + let to_date = frappe.model.get_value("Leave Period", frm.doc.leave_period, "to_date"); + frm.set_value("effective_from", from_date); + frm.set_value("effective_to", to_date); + + }); + } else if (frm.doc.assignment_based_on == "Joining Date" && frm.doc.employee) { + frappe.model.with_doc("Employee", frm.doc.employee, function () { + let from_date = frappe.model.get_value("Employee", frm.doc.employee, "date_of_joining"); + frm.set_value("effective_from", from_date); + frm.set_value("effective_to", frappe.datetime.add_months(frm.doc.effective_from, 12)); + }); + } + frm.refresh(); + } + +}); diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json new file mode 100644 index 0000000000..ecebb3b7d6 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -0,0 +1,160 @@ +{ + "actions": [], + "autoname": "HR-LPOL-ASSGN-.#####", + "creation": "2020-08-19 13:02:43.343666", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "company", + "leave_policy", + "carry_forward", + "column_break_5", + "assignment_based_on", + "leave_period", + "effective_from", + "effective_to", + "leaves_allocated", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee name", + "read_only": 1 + }, + { + "fieldname": "leave_policy", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Leave Policy", + "options": "Leave Policy", + "reqd": 1 + }, + { + "fieldname": "assignment_based_on", + "fieldtype": "Select", + "label": "Assignment based on", + "options": "\nLeave Period\nJoining Date" + }, + { + "depends_on": "eval:doc.assignment_based_on == \"Leave Period\"", + "fieldname": "leave_period", + "fieldtype": "Link", + "label": "Leave Period", + "mandatory_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"", + "options": "Leave Period" + }, + { + "fieldname": "effective_from", + "fieldtype": "Date", + "label": "Effective From", + "read_only_depends_on": "eval:doc.assignment_based_on", + "reqd": 1 + }, + { + "fieldname": "effective_to", + "fieldtype": "Date", + "label": "Effective To", + "read_only_depends_on": "eval:doc.assignment_based_on == \"Leave Period\"", + "reqd": 1 + }, + { + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Leave Policy Assignment", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "carry_forward", + "fieldtype": "Check", + "label": "Add unused leaves from previous allocations" + }, + { + "default": "0", + "fieldname": "leaves_allocated", + "fieldtype": "Check", + "hidden": 1, + "label": "Leaves Allocated" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-10-15 15:18:15.227848", + "modified_by": "Administrator", + "module": "HR", + "name": "Leave Policy Assignment", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py new file mode 100644 index 0000000000..a5068bc26d --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe import _, bold +from frappe.utils import getdate, date_diff, comma_and, formatdate +from math import ceil +import json +from six import string_types + +class LeavePolicyAssignment(Document): + + def validate(self): + self.validate_policy_assignment_overlap() + self.set_dates() + + def set_dates(self): + if self.assignment_based_on == "Leave Period": + self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"]) + elif self.assignment_based_on == "Joining Date": + self.effective_from = frappe.db.get_value("Employee", self.employee, "date_of_joining") + + def validate_policy_assignment_overlap(self): + leave_policy_assignments = frappe.get_all("Leave Policy Assignment", filters = { + "employee": self.employee, + "name": ("!=", self.name), + "docstatus": 1, + "effective_to": (">=", self.effective_from), + "effective_from": ("<=", self.effective_to) + }) + + if len(leave_policy_assignments): + frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}") + .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to)))) + + def grant_leave_alloc_for_employee(self): + if self.leaves_allocated: + frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment")) + else: + leave_allocations = {} + leave_type_details = get_leave_type_details() + + leave_policy = frappe.get_doc("Leave Policy", self.leave_policy) + date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") + + for leave_policy_detail in leave_policy.leave_policy_details: + if not leave_type_details.get(leave_policy_detail.leave_type).is_lwp: + leave_allocation, new_leaves_allocated = self.create_leave_allocation( + leave_policy_detail.leave_type, leave_policy_detail.annual_allocation, + leave_type_details, date_of_joining + ) + + leave_allocations[leave_policy_detail.leave_type] = {"name": leave_allocation, "leaves": new_leaves_allocated} + + self.db_set("leaves_allocated", 1) + return leave_allocations + + def create_leave_allocation(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + # Creates leave allocation for the given employee in the provided leave period + carry_forward = self.carry_forward + if self.carry_forward and not leave_type_details.get(leave_type).is_carry_forward: + carry_forward = 0 + + new_leaves_allocated = self.get_new_leaves(leave_type, new_leaves_allocated, + leave_type_details, date_of_joining) + + allocation = frappe.get_doc(dict( + doctype="Leave Allocation", + employee=self.employee, + leave_type=leave_type, + from_date=self.effective_from, + to_date=self.effective_to, + new_leaves_allocated=new_leaves_allocated, + leave_period=self.leave_period or None, + leave_policy_assignment = self.name, + leave_policy = self.leave_policy, + carry_forward=carry_forward + )) + allocation.save(ignore_permissions = True) + allocation.submit() + return allocation.name, new_leaves_allocated + + def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining): + # Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period + if getdate(date_of_joining) > getdate(self.effective_from): + remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1)) + new_leaves_allocated = ceil(new_leaves_allocated * remaining_period) + + # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0 + if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1: + new_leaves_allocated = 0 + + return new_leaves_allocated + +@frappe.whitelist() +def grant_leave_for_multiple_employees(leave_policy_assignments): + leave_policy_assignments = json.loads(leave_policy_assignments) + not_granted = [] + for assignment in leave_policy_assignments: + try: + frappe.get_doc("Leave Policy Assignment", assignment).grant_leave_alloc_for_employee() + except Exception: + not_granted.append(assignment) + + if len(not_granted): + msg = _("Leave not Granted for Assignments:")+ bold(comma_and(not_granted)) + _(". Please Check documents") + else: + msg = _("Leave granted Successfully") + frappe.msgprint(msg) + +@frappe.whitelist() +def create_assignment_for_multiple_employees(employees, data): + + if isinstance(employees, string_types): + employees= json.loads(employees) + + if isinstance(data, string_types): + data = frappe._dict(json.loads(data)) + + docs_name = [] + for employee in employees: + assignment = frappe.new_doc("Leave Policy Assignment") + assignment.employee = employee + assignment.assignment_based_on = data.assignment_based_on or None + assignment.leave_policy = data.leave_policy + assignment.effective_from = getdate(data.effective_from) or None + assignment.effective_to = getdate(data.effective_to) or None + assignment.leave_period = data.leave_period or None + assignment.carry_forward = data.carry_forward + + assignment.save() + assignment.submit() + docs_name.append(assignment.name) + return docs_name + + +def automatically_allocate_leaves_based_on_leave_policy(): + today = getdate() + automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_single_value( + 'HR Settings', 'automatically_allocate_leaves_based_on_leave_policy' + ) + + pending_assignments = frappe.get_list( + "Leave Policy Assignment", + filters = {"docstatus": 1, "leaves_allocated": 0, "effective_from": today} + ) + + if len(pending_assignments) and automatically_allocate_leaves_based_on_leave_policy: + for assignment in pending_assignments: + frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee() + + +def get_leave_type_details(): + leave_type_details = frappe._dict() + leave_types = frappe.get_all("Leave Type", + fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"]) + for d in leave_types: + leave_type_details.setdefault(d.name, d) + return leave_type_details + diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js new file mode 100644 index 0000000000..468f243885 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js @@ -0,0 +1,138 @@ +frappe.listview_settings['Leave Policy Assignment'] = { + onload: function (list_view) { + let me = this; + list_view.page.add_inner_button(__("Bulk Leave Policy Assignment"), function () { + me.dialog = new frappe.ui.form.MultiSelectDialog({ + doctype: "Employee", + target: cur_list, + setters: { + company: '', + department: '', + }, + data_fields: [{ + fieldname: 'leave_policy', + fieldtype: 'Link', + options: 'Leave Policy', + label: __('Leave Policy'), + reqd: 1 + }, + { + fieldname: 'assignment_based_on', + fieldtype: 'Select', + options: ["", "Leave Period"], + label: __('Assignment Based On'), + onchange: () => { + if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period") { + cur_dialog.set_df_property("effective_from", "read_only", 1); + cur_dialog.set_df_property("leave_period", "reqd", 1); + cur_dialog.set_df_property("effective_to", "read_only", 1); + } else { + cur_dialog.set_df_property("effective_from", "read_only", 0); + cur_dialog.set_df_property("leave_period", "reqd", 0); + cur_dialog.set_df_property("effective_to", "read_only", 0); + cur_dialog.set_value("effective_from", ""); + cur_dialog.set_value("effective_to", ""); + } + } + }, + { + fieldname: "leave_period", + fieldtype: 'Link', + options: "Leave Period", + label: __('Leave Period'), + depends_on: doc => { + return doc.assignment_based_on == 'Leave Period'; + }, + onchange: () => { + if (cur_dialog.fields_dict.leave_period.value) { + me.set_effective_date(); + } + } + }, + { + fieldtype: "Column Break" + }, + { + fieldname: 'effective_from', + fieldtype: 'Date', + label: __('Effective From'), + reqd: 1 + }, + { + fieldname: 'effective_to', + fieldtype: 'Date', + label: __('Effective To'), + reqd: 1 + }, + { + fieldname: 'carry_forward', + fieldtype: 'Check', + label: __('Add unused leaves from previous allocations') + } + ], + get_query() { + return { + filters: { + status: ['=', 'Active'] + } + }; + }, + add_filters_group: 1, + primary_action_label: "Assign", + action(employees, data) { + frappe.call({ + method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.create_assignment_for_multiple_employees', + async: false, + args: { + employees: employees, + data: data + } + }); + cur_dialog.hide(); + } + }); + }); + + list_view.page.add_inner_button(__("Grant Leaves"), function () { + me.dialog = new frappe.ui.form.MultiSelectDialog({ + doctype: "Leave Policy Assignment", + target: cur_list, + setters: { + company: '', + employee: '', + }, + get_query() { + return { + filters: { + docstatus: ['=', 1], + leaves_allocated: ['=', 0] + } + }; + }, + add_filters_group: 1, + primary_action_label: "Grant Leaves", + action(leave_policy_assignments) { + frappe.call({ + method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.grant_leave_for_multiple_employees', + async: false, + args: { + leave_policy_assignments: leave_policy_assignments + } + }); + me.dialog.hide(); + } + }); + }); + }, + + set_effective_date: function () { + if (cur_dialog.fields_dict.assignment_based_on.value === "Leave Period" && cur_dialog.fields_dict.leave_period.value) { + frappe.model.with_doc("Leave Period", cur_dialog.fields_dict.leave_period.value, function () { + let from_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "from_date"); + let to_date = frappe.model.get_value("Leave Period", cur_dialog.fields_dict.leave_period.value, "to_date"); + cur_dialog.set_value("effective_from", from_date); + cur_dialog.set_value("effective_to", to_date); + }); + } + } +}; \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py new file mode 100644 index 0000000000..c7bc6fb775 --- /dev/null +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from erpnext.hr.doctype.leave_application.test_leave_application import get_leave_period, get_employee +from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees +from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy + +class TestLeavePolicyAssignment(unittest.TestCase): + + def setUp(self): + for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + + def test_grant_leaves(self): + leave_period = get_leave_period() + employee = get_employee() + + # create the leave policy with leave type "_Test Leave Type", allocation = 10 + leave_policy = create_leave_policy() + leave_policy.submit() + + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) + leave_policy_assignment_doc.grant_leave_alloc_for_employee() + leave_policy_assignment_doc.reload() + + self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) + + leave_allocation = frappe.get_list("Leave Allocation", filters={ + "employee": employee.name, + "leave_policy":leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1})[0] + + leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) + + self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) + self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type") + self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date) + self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date) + self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name) + self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0]) + + def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(self): + leave_period = get_leave_period() + employee = get_employee() + + # create the leave policy with leave type "_Test Leave Type", allocation = 10 + leave_policy = create_leave_policy() + leave_policy.submit() + + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) + leave_policy_assignment_doc.grant_leave_alloc_for_employee() + leave_policy_assignment_doc.reload() + + + # every leave is allocated no more leave can be granted now + self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) + + leave_allocation = frappe.get_list("Leave Allocation", filters={ + "employee": employee.name, + "leave_policy":leave_policy.name, + "leave_policy_assignment": leave_policy_assignments[0], + "docstatus": 1})[0] + + leave_alloc_doc = frappe.get_doc("Leave Allocation", leave_allocation) + + # User all allowed to grant leave when there is no allocation against assignment + leave_alloc_doc.cancel() + leave_alloc_doc.delete() + + leave_policy_assignment_doc.reload() + + + # User are now allowed to grant leave + self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0) + + def tearDown(self): + for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + + diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index 4a135e0ffe..a2092919f8 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -33,6 +33,7 @@ "is_earned_leave", "earned_leave_frequency", "column_break_22", + "based_on_date_of_joining", "rounding" ], "fields": [ @@ -189,6 +190,13 @@ }, { "default": "0", + "depends_on": "eval:doc.is_earned_leave", + "description": "If checked, leave will be granted on the day of joining every month.", + "fieldname": "based_on_date_of_joining", + "fieldtype": "Check", + "label": "Based On Date Of Joining" + }, + { "depends_on": "eval:doc.is_lwp == 0", "fieldname": "is_ppl", "fieldtype": "Check", @@ -205,7 +213,7 @@ "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2020-08-26 14:04:54.318687", + "modified": "2020-10-15 15:49:47.555105", "modified_by": "Administrator", "module": "HR", "name": "Leave Type", diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 8d95924681..d700e7fccf 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -215,19 +215,6 @@ def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date): + _(") for {0}").format(exists_for) frappe.throw(msg) -def get_employee_leave_policy(employee): - leave_policy = frappe.db.get_value("Employee", employee, "leave_policy") - if not leave_policy: - employee_grade = frappe.db.get_value("Employee", employee, "grade") - if employee_grade: - leave_policy = frappe.db.get_value("Employee Grade", employee_grade, "default_leave_policy") - if not leave_policy: - frappe.throw(_("Employee {0} of grade {1} have no default leave policy").format(employee, employee_grade)) - if leave_policy: - return frappe.get_doc("Leave Policy", leave_policy) - else: - frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee)) - def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee): existing_record = frappe.db.exists(doctype, { "payroll_period": payroll_period, @@ -300,43 +287,68 @@ def generate_leave_encashment(): def allocate_earned_leaves(): '''Allocate earned leaves to Employees''' - e_leave_types = frappe.get_all("Leave Type", - fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding"], - filters={'is_earned_leave' : 1}) + e_leave_types = get_earned_leaves() today = getdate() - divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12} for e_leave_type in e_leave_types: - leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where %s - between from_date and to_date and docstatus=1 and leave_type=%s""", (today, e_leave_type.name), as_dict=1) + + leave_allocations = get_leave_allocations(today, e_leave_type.name) + for allocation in leave_allocations: - leave_policy = get_employee_leave_policy(allocation.employee) - if not leave_policy: + + if not allocation.leave_policy_assignment and not allocation.leave_policy: continue - if not e_leave_type.earned_leave_frequency == "Monthly": - if not check_frequency_hit(allocation.from_date, today, e_leave_type.earned_leave_frequency): - continue + + leave_policy = allocation.leave_policy if allocation.leave_policy else frappe.db.get_value( + "Leave Policy Assignment", allocation.leave_policy_assignment, ["leave_policy"]) + annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={ - 'parent': leave_policy.name, + 'parent': leave_policy, 'leave_type': e_leave_type.name }, fieldname=['annual_allocation']) - if annual_allocation: - earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency] - if e_leave_type.rounding == "0.5": - earned_leaves = round(earned_leaves * 2) / 2 - else: - earned_leaves = round(earned_leaves) - allocation = frappe.get_doc('Leave Allocation', allocation.name) - new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) + from_date=allocation.from_date - if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: - new_allocation = e_leave_type.max_leaves_allowed + if e_leave_type.based_on_date_of_joining_date: + from_date = frappe.db.get_value("Employee", allocation.employee, "date_of_joining") - if new_allocation == allocation.total_leaves_allocated: - continue - allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) - create_additional_leave_ledger_entry(allocation, earned_leaves, today) + if check_effective_date(from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining_date): + update_previous_leave_allocation(allocation, annual_allocation, e_leave_type) + +def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type): + divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12} + if annual_allocation: + earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency] + if e_leave_type.rounding == "0.5": + earned_leaves = round(earned_leaves * 2) / 2 + else: + earned_leaves = round(earned_leaves) + + allocation = frappe.get_doc('Leave Allocation', allocation.name) + new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves) + + if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0: + new_allocation = e_leave_type.max_leaves_allowed + + if new_allocation != allocation.total_leaves_allocated: + allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) + today_date = today() + create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) + + +def get_leave_allocations(date, leave_type): + return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy + from `tabLeave Allocation` + where + %s between from_date and to_date and docstatus=1 + and leave_type=%s""", + (date, leave_type), as_dict=1) + + +def get_earned_leaves(): + return frappe.get_all("Leave Type", + fields=["name", "max_leaves_allowed", "earned_leave_frequency", "rounding", "based_on_date_of_joining"], + filters={'is_earned_leave' : 1}) def create_additional_leave_ledger_entry(allocation, leaves, date): ''' Create leave ledger entry for leave types ''' @@ -345,24 +357,32 @@ def create_additional_leave_ledger_entry(allocation, leaves, date): allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() -def check_frequency_hit(from_date, to_date, frequency): - '''Return True if current date matches frequency''' - from_dt = get_datetime(from_date) - to_dt = get_datetime(to_date) +def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining_date): + import calendar from dateutil import relativedelta - rd = relativedelta.relativedelta(to_dt, from_dt) - months = rd.months - if frequency == "Quarterly": - if not months % 3: + + from_date = get_datetime(from_date) + to_date = get_datetime(to_date) + rd = relativedelta.relativedelta(to_date, from_date) + #last day of month + last_day = calendar.monthrange(to_date.year, to_date.month)[1] + + if (from_date.day == to_date.day and based_on_date_of_joining_date) or (not based_on_date_of_joining_date and to_date.day == last_day): + if frequency == "Monthly": return True - elif frequency == "Half-Yearly": - if not months % 6: + elif frequency == "Quarterly" and rd.months % 3: return True - elif frequency == "Yearly": - if not months % 12: + elif frequency == "Half-Yearly" and rd.months % 6: return True + elif frequency == "Yearly" and rd.months % 12: + return True + + if frappe.flags.in_test: + return True + return False + def get_salary_assignment(employee, date): assignment = frappe.db.sql(""" select * from `tabSalary Structure Assignment` @@ -454,3 +474,10 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0: total_claimed_amount = sum_of_claimed_amount[0].total_amount return total_claimed_amount + +def grant_leaves_automatically(): + automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy") + if automatically_allocate_leaves_based_on_leave_policy: + lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0}) + for assignment in lpa: + frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 25be884117..98b2fcdcab 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -735,3 +735,4 @@ erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_custom_fields_for_shopify execute:frappe.delete_doc("Report", "Quoted Item Comparison") +erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py new file mode 100644 index 0000000000..80c9137653 --- /dev/null +++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py @@ -0,0 +1,77 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + if "leave_policy" in frappe.db.get_table_columns("Employee"): + employees_with_leave_policy = frappe.db.sql("SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''", as_dict = 1) + + employee_with_assignment = [] + leave_policy =[] + + #for employee + + for employee in employees_with_leave_policy: + alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": employee.leave_policy, "docstatus": 1}) + if not alloc: + create_assignment(employee.name, employee.leave_policy) + + employee_with_assignment.append(employee.name) + leave_policy.append(employee.leave_policy) + + + if "default_leave_policy" in frappe.db.get_table_columns("Employee"): + employee_grade_with_leave_policy = frappe.db.sql("SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''", as_dict = 1) + + #for whole employee Grade + + for grade in employee_grade_with_leave_policy: + employees = get_employee_with_grade(grade.name) + for employee in employees: + + if employee not in employee_with_assignment: #Will ensure no duplicate + alloc = frappe.db.exists("Leave Allocation", {"employee":employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1}) + if not alloc: + create_assignment(employee.name, grade.default_leave_policy) + leave_policy.append(grade.default_leave_policy) + + #for old Leave allocation and leave policy from allocation, which may got updated in employee grade. + leave_allocations = frappe.db.sql("SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ", as_dict = 1) + + for allocation in leave_allocations: + if allocation.leave_policy not in leave_policy: + create_assignment(allocation.employee, allocation.leave_policy, leave_period=allocation.leave_period, + allocation_exists=True) + +def create_assignment(employee, leave_policy, leave_period=None, allocation_exists = False): + + filters = {"employee":employee, "leave_policy": leave_policy} + if leave_period: + filters["leave_period"] = leave_period + + if not frappe.db.exists("Leave Policy Assignment" , filters): + lpa = frappe.new_doc("Leave Policy Assignment") + lpa.employee = employee + lpa.leave_policy = leave_policy + + lpa.flags.ignore_mandatory = True + if allocation_exists: + lpa.assignment_based_on = 'Leave Period' + lpa.leave_period = leave_period + lpa.leaves_allocated = 1 + + lpa.save() + if allocation_exists: + lpa.submit() + #Updating old Leave Allocation + frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name) + + +def get_employee_with_grade(grade): + return frappe.get_list("Employee", filters = {"grade": grade}) + + +