diff --git a/erpnext/config/hr.py b/erpnext/config/hr.py index 0d05cb183d4..eae937c4493 100644 --- a/erpnext/config/hr.py +++ b/erpnext/config/hr.py @@ -134,6 +134,12 @@ def get_data(): "name": "Employee Leave Balance", "doctype": "Leave Application" }, + { + "type": "report", + "is_query_report": True, + "name": "Leave Ledger Entry", + "doctype": "Leave Ledger Entry" + }, ] }, { diff --git a/erpnext/hooks.py b/erpnext/hooks.py index be9a4fb2648..7e33a14d518 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -283,7 +283,9 @@ scheduler_events = { "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status" ], "daily_long": [ - "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms" + "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.utils.generate_leave_encashment" ], "monthly_long": [ "erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income", diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 8dd0acf4551..a41c887852e 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -24,6 +24,7 @@ "column_break_18", "leave_approver_mandatory_in_leave_application", "show_leaves_of_all_department_members_in_calendar", + "auto_leave_encashment", "hiring_settings", "check_vacancies" ], @@ -153,12 +154,18 @@ "fieldname": "check_vacancies", "fieldtype": "Check", "label": "Check Vacancies On Job Offer Creation" + }, + { + "default": "0", + "fieldname": "auto_leave_encashment", + "fieldtype": "Check", + "label": "Auto Leave Encashment" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, - "modified": "2019-07-01 18:59:55.256878", + "modified": "2019-08-05 13:07:17.993968", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js index 4b4bfafd161..210a73cfe55 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js @@ -21,11 +21,41 @@ frappe.ui.form.on("Leave Allocation", { }) }, + refresh: function(frm) { + if(frm.doc.docstatus === 1 && frm.doc.expired) { + var valid_expiry = moment(frappe.datetime.get_today()).isBetween(frm.doc.from_date, frm.doc.to_date); + if(valid_expiry) { + // expire current allocation + frm.add_custom_button(__('Expire Allocation'), function() { + frm.trigger("expire_allocation"); + }); + } + } + }, + + expire_allocation: function(frm) { + frappe.call({ + method: 'erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.expire_allocation', + args: { + 'allocation': frm.doc, + 'expiry_date': frappe.datetime.get_today() + }, + freeze: true, + callback: function(r){ + if(!r.exc){ + frappe.msgprint(__("Allocation Expired!")); + } + frm.refresh(); + } + }); + }, + employee: function(frm) { frm.trigger("calculate_total_leaves_allocated"); }, leave_type: function(frm) { + frm.trigger("leave_policy"); frm.trigger("calculate_total_leaves_allocated"); }, @@ -33,37 +63,38 @@ frappe.ui.form.on("Leave Allocation", { frm.trigger("calculate_total_leaves_allocated"); }, - carry_forwarded_leaves: function(frm) { + unused_leaves: function(frm) { frm.set_value("total_leaves_allocated", - flt(frm.doc.carry_forwarded_leaves) + flt(frm.doc.new_leaves_allocated)); + flt(frm.doc.unused_leaves) + flt(frm.doc.new_leaves_allocated)); }, new_leaves_allocated: function(frm) { frm.set_value("total_leaves_allocated", - flt(frm.doc.carry_forwarded_leaves) + flt(frm.doc.new_leaves_allocated)); + flt(frm.doc.unused_leaves) + flt(frm.doc.new_leaves_allocated)); }, + leave_policy: function(frm) { + if(frm.doc.leave_policy && frm.doc.leave_type) { + frappe.db.get_value("Leave Policy Detail",{ + 'parent': frm.doc.leave_policy, + 'leave_type': frm.doc.leave_type + }, 'annual_allocation', (r) => { + if (r && !r.exc) frm.set_value("new_leaves_allocated", flt(r.annual_allocation)); + }, "Leave Policy"); + } + }, calculate_total_leaves_allocated: function(frm) { if (cint(frm.doc.carry_forward) == 1 && frm.doc.leave_type && frm.doc.employee) { return frappe.call({ - method: "erpnext.hr.doctype.leave_allocation.leave_allocation.get_carry_forwarded_leaves", - args: { - "employee": frm.doc.employee, - "date": frm.doc.from_date, - "leave_type": frm.doc.leave_type, - "carry_forward": frm.doc.carry_forward - }, + method: "set_total_leaves_allocated", + doc: frm.doc, callback: function(r) { - if (!r.exc && r.message) { - frm.set_value('carry_forwarded_leaves', r.message); - frm.set_value("total_leaves_allocated", - flt(r.message) + flt(frm.doc.new_leaves_allocated)); - } + frm.refresh_fields(); } }) } else if (cint(frm.doc.carry_forward) == 0) { - frm.set_value("carry_forwarded_leaves", 0); + frm.set_value("unused_leaves", 0); frm.set_value("total_leaves_allocated", flt(frm.doc.new_leaves_allocated)); } } -}) +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 6d61fe3d5c2..007497e34a5 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -1,683 +1,220 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, "allow_import": 1, - "allow_rename": 0, "autoname": "naming_series:", - "beta": 0, "creation": "2013-02-20 19:10:38", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "naming_series", + "employee", + "employee_name", + "department", + "column_break1", + "leave_type", + "from_date", + "to_date", + "section_break_6", + "new_leaves_allocated", + "carry_forward", + "unused_leaves", + "total_leaves_allocated", + "total_leaves_encashed", + "column_break_10", + "compensatory_request", + "leave_period", + "leave_policy", + "carry_forwarded_leaves_count", + "expired", + "amended_from", + "notes", + "description" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", "fieldname": "naming_series", "fieldtype": "Select", - "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": "Series", - "length": 0, "no_copy": 1, "options": "HR-LAL-.YYYY.-", - "permlevel": 0, - "precision": "", "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 + "set_only_once": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "employee", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, "in_list_view": 1, "in_standard_filter": 1, "label": "Employee", - "length": 0, - "no_copy": 0, "oldfieldname": "employee", "oldfieldtype": "Link", "options": "Employee", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "employee.employee_name", "fieldname": "employee_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, "in_list_view": 1, - "in_standard_filter": 0, "label": "Employee Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "employee.department", "fieldname": "department", "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": "Department", - "length": 0, - "no_copy": 0, "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break1", "fieldtype": "Column Break", - "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, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, "width": "50%" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "leave_type", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Leave Type", - "length": 0, - "no_copy": 0, "oldfieldname": "leave_type", "oldfieldtype": "Link", "options": "Leave Type", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_date", "fieldtype": "Date", - "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": "From Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "to_date", "fieldtype": "Date", - "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": "To Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_6", "fieldtype": "Section Break", - "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": "Allocation", - "length": 0, - "no_copy": 0, - "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 + "label": "Allocation" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, "bold": 1, - "collapsible": 0, - "columns": 0, "fieldname": "new_leaves_allocated", "fieldtype": "Float", - "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": "New Leaves Allocated", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "New Leaves Allocated" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", + "default": "0", "fieldname": "carry_forward", "fieldtype": "Check", - "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": "Add unused leaves from previous allocations", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "label": "Add unused leaves from previous allocations" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "carry_forward", - "fieldname": "carry_forwarded_leaves", + "fieldname": "unused_leaves", "fieldtype": "Float", - "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": "Unused leaves", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "total_leaves_allocated", "fieldtype": "Float", - "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": "Total Leaves Allocated", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.total_leaves_encashed>0", "fieldname": "total_leaves_encashed", "fieldtype": "Float", - "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": "Total Leaves Encashed", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_10", - "fieldtype": "Column Break", - "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, - "length": 0, - "no_copy": 0, - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "compensatory_request", "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": "Compensatory Leave Request", - "length": 0, - "no_copy": 0, "options": "Compensatory Leave Request", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "leave_period", "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": 1, "label": "Leave Period", - "length": 0, - "no_copy": 0, "options": "Leave Period", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 + }, + { + "fetch_from": "employee.leave_policy", + "fieldname": "leave_policy", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Leave Policy", + "options": "Leave Policy", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "expired", + "fieldtype": "Check", + "hidden": 1, + "in_standard_filter": 1, + "label": "Expired", + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "amended_from", "fieldtype": "Link", - "hidden": 0, "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Amended From", - "length": 0, "no_copy": 1, "oldfieldname": "amended_from", "oldfieldtype": "Data", "options": "Leave Allocation", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "fieldname": "notes", "fieldtype": "Section Break", - "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": "Notes", - "length": 0, - "no_copy": 0, - "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 + "label": "Notes" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "description", "fieldtype": "Small Text", - "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": "Description", - "length": 0, - "no_copy": 0, "oldfieldname": "reason", "oldfieldtype": "Small Text", - "permlevel": 0, - "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, "width": "300px" + }, + { + "depends_on": "carry_forwarded_leaves_count", + "fieldname": "carry_forwarded_leaves_count", + "fieldtype": "Float", + "label": "Carry Forwarded Leaves", + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-ok", "idx": 1, - "image_view": 0, - "in_create": 0, "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-30 11:28:09.360525", + "modified": "2019-08-08 15:08:42.440909", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", @@ -689,15 +226,10 @@ "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR User", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 @@ -709,28 +241,19 @@ "delete": 1, "email": 1, "export": 1, - "if_owner": 0, "import": 1, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, "search_fields": "employee,employee_name,leave_type,total_leaves_allocated", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "timeline_field": "employee", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "timeline_field": "employee" } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index dc270dba412..296a52c2c70 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -3,11 +3,11 @@ from __future__ import unicode_literals import frappe -from frappe.utils import flt, date_diff, formatdate +from frappe.utils import flt, date_diff, formatdate, add_days, today, getdate from frappe import _ from frappe.model.document import Document from erpnext.hr.utils import set_employee_name, get_leave_period -from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry class OverlapError(frappe.ValidationError): pass class BackDatedAllocationError(frappe.ValidationError): pass @@ -40,14 +40,18 @@ class LeaveAllocation(Document): frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period")\ .format(self.leave_type, self.employee)) - def on_update_after_submit(self): - self.validate_new_leaves_allocated_value() - self.set_total_leaves_allocated() + def on_submit(self): + self.create_leave_ledger_entry() - frappe.db.set(self,'carry_forwarded_leaves', flt(self.carry_forwarded_leaves)) - frappe.db.set(self,'total_leaves_allocated',flt(self.total_leaves_allocated)) + # expire all unused leaves in the ledger on creation of carry forward allocation + allocation = get_previous_allocation(self.from_date, self.leave_type, self.employee) + if self.carry_forward and allocation: + expire_allocation(allocation) - self.validate_against_leave_applications() + def on_cancel(self): + self.create_leave_ledger_entry(submit=False) + if self.carry_forward: + self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True) def validate_period(self): if date_diff(self.to_date, self.from_date) <= 0: @@ -87,13 +91,32 @@ class LeaveAllocation(Document): BackDatedAllocationError) def set_total_leaves_allocated(self): - self.carry_forwarded_leaves = get_carry_forwarded_leaves(self.employee, + self.unused_leaves = get_carry_forwarded_leaves(self.employee, self.leave_type, self.from_date, self.carry_forward) - self.total_leaves_allocated = flt(self.carry_forwarded_leaves) + flt(self.new_leaves_allocated) + self.total_leaves_allocated = flt(self.unused_leaves) + flt(self.new_leaves_allocated) + + if self.carry_forward: + self.maintain_carry_forwarded_leaves() + self.set_carry_forwarded_leaves_in_previous_allocation() if not self.total_leaves_allocated and not frappe.db.get_value("Leave Type", self.leave_type, "is_earned_leave") and not frappe.db.get_value("Leave Type", self.leave_type, "is_compensatory"): - frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}".format(self.leave_type))) + frappe.throw(_("Total leaves allocated is mandatory for Leave Type {0}").format(self.leave_type)) + + def maintain_carry_forwarded_leaves(self): + ''' Reduce the carry forwarded leaves to be within the maximum allowed leaves ''' + + max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed") + if self.new_leaves_allocated <= max_leaves_allowed <= self.total_leaves_allocated: + self.unused_leaves = max_leaves_allowed - flt(self.new_leaves_allocated) + self.total_leaves_allocated = flt(max_leaves_allowed) + + def set_carry_forwarded_leaves_in_previous_allocation(self, on_cancel=False): + ''' Set carry forwarded leaves in previous allocation ''' + previous_allocation = get_previous_allocation(self.from_date, self.leave_type, self.employee) + if on_cancel: + self.unused_leaves = 0.0 + frappe.db.set_value("Leave Allocation", previous_allocation.name, 'carry_forwarded_leaves_count', self.unused_leaves) def validate_total_leaves_allocated(self): # Adding a day to include To Date in the difference @@ -101,15 +124,37 @@ class LeaveAllocation(Document): if date_difference < self.total_leaves_allocated: frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError) - def validate_against_leave_applications(self): - leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type, - self.from_date, self.to_date) + def create_leave_ledger_entry(self, submit=True): + if self.unused_leaves: + expiry_days = frappe.db.get_value("Leave Type", self.leave_type, "expire_carry_forwarded_leaves_after_days") + end_date = add_days(self.from_date, expiry_days - 1) if expiry_days else self.to_date + args = dict( + leaves=self.unused_leaves, + from_date=self.from_date, + to_date= min(getdate(end_date), getdate(self.to_date)), + is_carry_forward=1 + ) + create_leave_ledger_entry(self, args, submit) - if flt(leaves_taken) > flt(self.total_leaves_allocated): - if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): - frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken)) - else: - frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError) + args = dict( + leaves=self.new_leaves_allocated, + from_date=self.from_date, + to_date=self.to_date, + is_carry_forward=0 + ) + create_leave_ledger_entry(self, args, submit) + +def get_previous_allocation(from_date, leave_type, employee): + ''' Returns document properties of previous allocation ''' + return frappe.db.get_value("Leave Allocation", + filters={ + 'to_date': ("<", from_date), + 'leave_type': leave_type, + 'employee': employee, + 'docstatus': 1 + }, + order_by='to_date DESC', + fieldname=['name', 'from_date', 'to_date', 'employee', 'leave_type'], as_dict=1) def get_leave_allocation_for_period(employee, leave_type, from_date, to_date): leave_allocated = 0 @@ -136,25 +181,28 @@ def get_leave_allocation_for_period(employee, leave_type, from_date, to_date): @frappe.whitelist() def get_carry_forwarded_leaves(employee, leave_type, date, carry_forward=None): - carry_forwarded_leaves = 0 - - if carry_forward: + ''' Returns carry forwarded leaves for the given employee ''' + unused_leaves = 0.0 + previous_allocation = get_previous_allocation(date, leave_type, employee) + if carry_forward and previous_allocation: validate_carry_forward(leave_type) + unused_leaves = get_unused_leaves(employee, leave_type, previous_allocation.from_date, previous_allocation.to_date) - previous_allocation = frappe.db.sql(""" - select name, from_date, to_date, total_leaves_allocated - from `tabLeave Allocation` - where employee=%s and leave_type=%s and docstatus=1 and to_date < %s - order by to_date desc limit 1 - """, (employee, leave_type, date), as_dict=1) - if previous_allocation: - leaves_taken = get_approved_leaves_for_period(employee, leave_type, - previous_allocation[0].from_date, previous_allocation[0].to_date) + return unused_leaves - carry_forwarded_leaves = flt(previous_allocation[0].total_leaves_allocated) - flt(leaves_taken) - - return carry_forwarded_leaves +def get_unused_leaves(employee, leave_type, from_date, to_date): + ''' Returns unused leaves between the given period while skipping leave allocation expiry ''' + leaves = frappe.get_all("Leave Ledger Entry", filters={ + 'employee': employee, + 'leave_type': leave_type, + 'from_date': ('>=', from_date), + 'to_date': ('<=', to_date) + }, or_filters={ + 'is_expired': 0, + 'is_carry_forward': 1 + }, fields=['sum(leaves) as leaves']) + return flt(leaves[0]['leaves']) def validate_carry_forward(leave_type): if not frappe.db.get_value("Leave Type", leave_type, "is_carry_forward"): - frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) + frappe.throw(_("Leave Type {0} cannot be carry-forwarded").format(leave_type)) \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py index 72a1b7c1948..7456aebb457 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation_dashboard.py @@ -12,4 +12,9 @@ def get_data(): 'items': ['Leave Encashment'] } ], + 'reports': [ + { + 'items': ['Employee Leave Balance'] + } + ] } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js b/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js new file mode 100644 index 00000000000..93f7b8356b3 --- /dev/null +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js @@ -0,0 +1,11 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +// render +frappe.listview_settings['Leave Allocation'] = { + get_indicator: function(doc) { + if(doc.status==="Expired") { + return [__("Expired"), "darkgrey", "expired, =, 1"]; + } + }, +}; diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js index b8f4fafa6d8..0ef78f2f883 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.js @@ -34,7 +34,7 @@ QUnit.test("Test: Leave allocation [HR]", function (assert) { () => assert.equal(today_date, cur_frm.doc.from_date, "from date correctly set"), // check for total leaves - () => assert.equal(cur_frm.doc.carry_forwarded_leaves + 2, cur_frm.doc.total_leaves_allocated, + () => assert.equal(cur_frm.doc.unused_leaves + 2, cur_frm.doc.total_leaves_allocated, "total leave calculation is correctly set"), () => done() ]); diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py index 3b22eb2e44c..bdba8c9f8f6 100644 --- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py @@ -1,12 +1,14 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import getdate +from frappe.utils import nowdate, add_months, getdate, add_days +from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation class TestLeaveAllocation(unittest.TestCase): def test_overlapping_allocation(self): frappe.db.sql("delete from `tabLeave Allocation`") - + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) leaves = [ { @@ -18,7 +20,7 @@ class TestLeaveAllocation(unittest.TestCase): "from_date": getdate("2015-10-01"), "to_date": getdate("2015-10-31"), "new_leaves_allocated": 5, - "docstatus": 1 + "docstatus": 1 }, { "doctype": "Leave Allocation", @@ -28,17 +30,17 @@ class TestLeaveAllocation(unittest.TestCase): "leave_type": "_Test Leave Type", "from_date": getdate("2015-09-01"), "to_date": getdate("2015-11-30"), - "new_leaves_allocated": 5 + "new_leaves_allocated": 5 } ] frappe.get_doc(leaves[0]).save() self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save) - - def test_invalid_period(self): + + def test_invalid_period(self): employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) - - d = frappe.get_doc({ + + doc = frappe.get_doc({ "doctype": "Leave Allocation", "__islocal": 1, "employee": employee.name, @@ -46,15 +48,15 @@ class TestLeaveAllocation(unittest.TestCase): "leave_type": "_Test Leave Type", "from_date": getdate("2015-09-30"), "to_date": getdate("2015-09-1"), - "new_leaves_allocated": 5 + "new_leaves_allocated": 5 }) - + #invalid period - self.assertRaises(frappe.ValidationError, d.save) - + self.assertRaises(frappe.ValidationError, doc.save) + def test_allocated_leave_days_over_period(self): employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) - d = frappe.get_doc({ + doc = frappe.get_doc({ "doctype": "Leave Allocation", "__islocal": 1, "employee": employee.name, @@ -62,10 +64,102 @@ class TestLeaveAllocation(unittest.TestCase): "leave_type": "_Test Leave Type", "from_date": getdate("2015-09-1"), "to_date": getdate("2015-09-30"), - "new_leaves_allocated": 35 + "new_leaves_allocated": 35 }) - - #allocated leave more than period - self.assertRaises(frappe.ValidationError, d.save) - + #allocated leave more than period + self.assertRaises(frappe.ValidationError, doc.save) + + def test_carry_forward_calculation(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1) + leave_type.submit() + + # initial leave allocation + leave_allocation = create_leave_allocation( + leave_type="_Test_CF_leave", + from_date=add_months(nowdate(), -12), + to_date=add_months(nowdate(), -1), + carry_forward=0) + leave_allocation.submit() + + # leave allocation with carry forward from previous allocation + leave_allocation_1 = create_leave_allocation( + leave_type="_Test_CF_leave", + carry_forward=1) + leave_allocation_1.submit() + + self.assertEquals(leave_allocation.total_leaves_allocated, leave_allocation_1.unused_leaves) + + def test_carry_forward_leaves_expiry(self): + frappe.db.sql("delete from `tabLeave Allocation`") + frappe.db.sql("delete from `tabLeave Ledger Entry`") + leave_type = create_leave_type( + leave_type_name="_Test_CF_leave_expiry", + is_carry_forward=1, + expire_carry_forwarded_leaves_after_days=90) + leave_type.submit() + + # initial leave allocation + leave_allocation = create_leave_allocation( + leave_type="_Test_CF_leave_expiry", + from_date=add_months(nowdate(), -24), + to_date=add_months(nowdate(), -12), + carry_forward=0) + leave_allocation.submit() + + leave_allocation = create_leave_allocation( + leave_type="_Test_CF_leave_expiry", + from_date=add_days(nowdate(), -90), + to_date=add_days(nowdate(), 100), + carry_forward=1) + leave_allocation.submit() + + # expires all the carry forwarded leaves after 90 days + process_expired_allocation() + + # leave allocation with carry forward of only new leaves allocated + leave_allocation_1 = create_leave_allocation( + leave_type="_Test_CF_leave_expiry", + carry_forward=1, + from_date=add_months(nowdate(), 6), + to_date=add_months(nowdate(), 12)) + leave_allocation_1.submit() + + self.assertEquals(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated) + + def test_creation_of_leave_ledger_entry_on_submit(self): + frappe.db.sql("delete from `tabLeave Allocation`") + + leave_allocation = create_leave_allocation() + leave_allocation.submit() + + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name)) + + self.assertEquals(len(leave_ledger_entry), 1) + self.assertEquals(leave_ledger_entry[0].employee, leave_allocation.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, leave_allocation.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, leave_allocation.new_leaves_allocated) + + # check if leave ledger entry is deleted on cancellation + leave_allocation.cancel() + self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name})) + +def create_leave_allocation(**args): + args = frappe._dict(args) + + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + leave_allocation = frappe.get_doc({ + "doctype": "Leave Allocation", + "__islocal": 1, + "employee": args.employee or employee.name, + "employee_name": args.employee_name or employee.employee_name, + "leave_type": args.leave_type or "_Test Leave Type", + "from_date": args.from_date or nowdate(), + "new_leaves_allocated": args.new_leaves_created or 15, + "carry_forward": args.carry_forward or 0, + "to_date": args.to_date or add_months(nowdate(), 12) + }) + return leave_allocation + test_dependencies = ["Employee", "Leave Type"] \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 5bce348489e..174641048b8 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -49,7 +49,7 @@ frappe.ui.form.on("Leave Application", { async: false, args: { employee: frm.doc.employee, - date: frm.doc.posting_date + date: frm.doc.from_date || frm.doc.posting_date }, callback: function(r) { if (!r.exc && r.message['leave_allocation']) { @@ -60,9 +60,8 @@ frappe.ui.form.on("Leave Application", { } } }); - $("div").remove(".form-dashboard-section"); - let section = frm.dashboard.add_section( + frm.dashboard.add_section( frappe.render_template('leave_application_dashboard', { data: leave_details }) @@ -115,6 +114,7 @@ frappe.ui.form.on("Leave Application", { }, from_date: function(frm) { + frm.trigger("make_dashboard"); frm.trigger("half_day_datepicker"); frm.trigger("calculate_total_days"); }, @@ -138,12 +138,13 @@ frappe.ui.form.on("Leave Application", { }, get_leave_balance: function(frm) { - if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date) { + if(frm.doc.docstatus==0 && frm.doc.employee && frm.doc.leave_type && frm.doc.from_date && frm.doc.to_date) { return frappe.call({ method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_balance_on", args: { employee: frm.doc.employee, date: frm.doc.from_date, + to_date: frm.doc.to_date, leave_type: frm.doc.leave_type, consider_all_leaves_in_the_allocation_period: true }, diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json index b3800e01bd4..cdb1add3918 100644 --- a/erpnext/hr/doctype/leave_application/leave_application.json +++ b/erpnext/hr/doctype/leave_application/leave_application.json @@ -1,332 +1,332 @@ { - "allow_import": 1, - "autoname": "naming_series:", - "creation": "2013-02-20 11:18:11", - "description": "Apply / Approve Leaves", - "doctype": "DocType", - "document_type": "Document", - "engine": "InnoDB", - "field_order": [ - "naming_series", - "employee", - "employee_name", - "column_break_4", - "leave_type", - "department", - "leave_balance", - "section_break_5", - "from_date", - "to_date", - "half_day", - "half_day_date", - "total_leave_days", - "column_break1", - "description", - "section_break_7", - "leave_approver", - "leave_approver_name", - "column_break_18", - "status", - "salary_slip", - "sb10", - "posting_date", - "follow_via_email", - "color", - "column_break_17", - "company", - "letter_head", - "amended_from" - ], - "fields": [ - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "no_copy": 1, - "options": "HR-LAP-.YYYY.-", - "print_hide": 1, - "reqd": 1, - "set_only_once": 1 - }, - { - "fieldname": "employee", - "fieldtype": "Link", - "in_global_search": 1, - "in_standard_filter": 1, - "label": "Employee", - "options": "Employee", - "reqd": 1, - "search_index": 1 - }, - { - "fieldname": "employee_name", - "fieldtype": "Data", - "in_global_search": 1, - "label": "Employee Name", - "read_only": 1 - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "leave_type", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "in_standard_filter": 1, - "label": "Leave Type", - "options": "Leave Type", - "reqd": 1, - "search_index": 1 - }, - { - "fetch_from": "employee.department", - "fieldname": "department", - "fieldtype": "Link", - "label": "Department", - "options": "Department", - "read_only": 1 - }, - { - "fieldname": "leave_balance", - "fieldtype": "Float", - "label": "Leave Balance Before Application", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "section_break_5", - "fieldtype": "Section Break" - }, - { - "fieldname": "from_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "From Date", - "reqd": 1, - "search_index": 1 - }, - { - "fieldname": "to_date", - "fieldtype": "Date", - "label": "To Date", - "reqd": 1, - "search_index": 1 - }, - { - "default": "0", - "fieldname": "half_day", - "fieldtype": "Check", - "label": "Half Day" - }, - { - "depends_on": "eval:doc.half_day && (doc.from_date != doc.to_date)", - "fieldname": "half_day_date", - "fieldtype": "Date", - "label": "Half Day Date" - }, - { - "fieldname": "total_leave_days", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Total Leave Days", - "no_copy": 1, - "precision": "1", - "read_only": 1 - }, - { - "fieldname": "column_break1", - "fieldtype": "Column Break", - "print_width": "50%", - "width": "50%" - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "label": "Reason" - }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, - { - "fieldname": "leave_approver", - "fieldtype": "Link", - "label": "Leave Approver", - "options": "User" - }, - { - "fieldname": "leave_approver_name", - "fieldtype": "Data", - "label": "Leave Approver Name", - "read_only": 1 - }, - { - "fieldname": "column_break_18", - "fieldtype": "Column Break" - }, - { - "default": "Open", - "fieldname": "status", - "fieldtype": "Select", - "in_standard_filter": 1, - "label": "Status", - "no_copy": 1, - "options": "Open\nApproved\nRejected\nCancelled" - }, - { - "fieldname": "salary_slip", - "fieldtype": "Link", - "label": "Salary Slip", - "options": "Salary Slip", - "print_hide": 1 - }, - { - "fieldname": "sb10", - "fieldtype": "Section Break" - }, - { - "default": "Today", - "fieldname": "posting_date", - "fieldtype": "Date", - "label": "Posting Date", - "no_copy": 1, - "reqd": 1 - }, - { - "allow_on_submit": 1, - "default": "1", - "fieldname": "follow_via_email", - "fieldtype": "Check", - "label": "Follow via Email", - "print_hide": 1 - }, - { - "allow_on_submit": 1, - "fieldname": "color", - "fieldtype": "Color", - "label": "Color", - "print_hide": 1 - }, - { - "fieldname": "column_break_17", - "fieldtype": "Column Break" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company", - "remember_last_selected_value": 1, - "reqd": 1 - }, - { - "allow_on_submit": 1, - "fieldname": "letter_head", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Letter Head", - "options": "Letter Head", - "print_hide": 1 - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Amended From", - "no_copy": 1, - "options": "Leave Application", - "print_hide": 1, - "read_only": 1 - } - ], - "icon": "fa fa-calendar", - "idx": 1, - "is_submittable": 1, - "max_attachments": 3, - "modified": "2019-08-11 19:13:53.603011", - "modified_by": "Administrator", - "module": "HR", - "name": "Leave Application", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "share": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 1, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "permlevel": 1, - "read": 1, - "role": "All" - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 1, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Leave Approver", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "permlevel": 1, - "read": 1, - "report": 1, - "role": "HR User", - "write": 1 - }, - { - "permlevel": 1, - "read": 1, - "report": 1, - "role": "Leave Approver", - "write": 1 - } - ], - "search_fields": "employee,employee_name,leave_type,from_date,to_date,total_leave_days", - "sort_field": "modified", - "sort_order": "DESC", - "timeline_field": "employee", - "title_field": "employee_name" -} \ No newline at end of file + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2013-02-20 11:18:11", + "description": "Apply / Approve Leaves", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "naming_series", + "employee", + "employee_name", + "column_break_4", + "leave_type", + "department", + "leave_balance", + "section_break_5", + "from_date", + "to_date", + "half_day", + "half_day_date", + "total_leave_days", + "column_break1", + "description", + "section_break_7", + "leave_approver", + "leave_approver_name", + "column_break_18", + "status", + "salary_slip", + "sb10", + "posting_date", + "follow_via_email", + "color", + "column_break_17", + "company", + "letter_head", + "amended_from" + ], + "fields": [ + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "HR-LAP-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "employee", + "fieldtype": "Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "employee_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "leave_type", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_standard_filter": 1, + "label": "Leave Type", + "options": "Leave Type", + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fieldname": "leave_balance", + "fieldtype": "Float", + "label": "Leave Balance Before Application", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "From Date", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date", + "reqd": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "half_day", + "fieldtype": "Check", + "label": "Half Day" + }, + { + "depends_on": "eval:doc.half_day && (doc.from_date != doc.to_date)", + "fieldname": "half_day_date", + "fieldtype": "Date", + "label": "Half Day Date" + }, + { + "fieldname": "total_leave_days", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Total Leave Days", + "no_copy": 1, + "precision": "1", + "read_only": 1 + }, + { + "fieldname": "column_break1", + "fieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Reason" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "fieldname": "leave_approver", + "fieldtype": "Link", + "label": "Leave Approver", + "options": "User" + }, + { + "fieldname": "leave_approver_name", + "fieldtype": "Data", + "label": "Leave Approver Name", + "read_only": 1 + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Open\nApproved\nRejected\nCancelled" + }, + { + "fieldname": "sb10", + "fieldtype": "Section Break" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "no_copy": 1, + "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, + { + "allow_on_submit": 1, + "default": "1", + "fieldname": "follow_via_email", + "fieldtype": "Check", + "label": "Follow via Email", + "print_hide": 1 + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "fieldname": "salary_slip", + "fieldtype": "Link", + "label": "Salary Slip", + "options": "Salary Slip", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "color", + "fieldtype": "Color", + "label": "Color", + "print_hide": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Leave Application", + "print_hide": 1, + "read_only": 1 + } + ], + "icon": "fa fa-calendar", + "idx": 1, + "is_submittable": 1, + "max_attachments": 3, + "modified": "2019-08-13 13:32:04.860848", + "modified_by": "Administrator", + "module": "HR", + "name": "Leave Application", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "set_user_permissions": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "role": "All" + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "set_user_permissions": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Leave Approver", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "report": 1, + "role": "HR User", + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "report": 1, + "role": "Leave Approver", + "write": 1 + } + ], + "search_fields": "employee,employee_name,leave_type,from_date,to_date,total_leave_days", + "sort_field": "modified", + "sort_order": "DESC", + "timeline_field": "employee", + "title_field": "employee_name" + } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 1ef8ee00ae4..0aa8849e879 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -5,11 +5,12 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \ - comma_or, get_fullname, add_days, nowdate + comma_or, get_fullname, add_days, nowdate, get_datetime_str from erpnext.hr.utils import set_employee_name, get_leave_period from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry class LeaveDayBlockedError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass @@ -50,6 +51,7 @@ class LeaveApplication(Document): # notify leave applier about approval self.notify_employee() + self.create_leave_ledger_entry() self.reload() def on_cancel(self): @@ -57,6 +59,7 @@ class LeaveApplication(Document): # notify leave applier about cancellation self.notify_employee() self.cancel_attendance() + self.create_leave_ledger_entry(submit=False) def validate_applicable_after(self): if self.leave_type: @@ -193,9 +196,9 @@ class LeaveApplication(Document): frappe.throw(_("The day(s) on which you are applying for leave are holidays. You need not apply for leave.")) if not is_lwp(self.leave_type): - self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, docname=self.name, + self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, self.from_date, self.to_date, consider_all_leaves_in_the_allocation_period=True) - if self.status != "Rejected" and self.leave_balance < self.total_leave_days: + if self.status != "Rejected" and (self.leave_balance < self.total_leave_days or not self.leave_balance): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): frappe.msgprint(_("Note: There is not enough leave balance for Leave Type {0}") .format(self.leave_type)) @@ -347,6 +350,54 @@ class LeaveApplication(Document): except frappe.OutgoingEmailError: pass + def create_leave_ledger_entry(self, submit=True): + expiry_date = get_allocation_expiry(self.employee, self.leave_type, + self.to_date, self.from_date) + + lwp = frappe.db.get_value("Leave Type", self.leave_type, "is_lwp") + + if expiry_date: + self.create_ledger_entry_for_intermediate_allocation_expiry(expiry_date, submit, lwp) + else: + args = dict( + leaves=self.total_leave_days * -1, + from_date=self.from_date, + to_date=self.to_date, + is_lwp=lwp + ) + create_leave_ledger_entry(self, args, submit) + + def create_ledger_entry_for_intermediate_allocation_expiry(self, expiry_date, submit, lwp): + ''' splits leave application into two ledger entries to consider expiry of allocation ''' + args = dict( + from_date=self.from_date, + to_date=expiry_date, + leaves=(date_diff(expiry_date, self.from_date) + 1) * -1, + is_lwp=lwp + ) + create_leave_ledger_entry(self, args, submit) + + if getdate(expiry_date) != getdate(self.to_date): + start_date = add_days(expiry_date, 1) + args.update(dict( + from_date=start_date, + to_date=self.to_date, + leaves=date_diff(self.to_date, expiry_date) * -1 + )) + create_leave_ledger_entry(self, args, submit) + +def get_allocation_expiry(employee, leave_type, to_date, from_date): + ''' Returns expiry of carry forward allocation in leave ledger entry ''' + expiry = frappe.get_all("Leave Ledger Entry", + filters={ + 'employee': employee, + 'leave_type': leave_type, + 'is_carry_forward': 1, + 'transaction_type': 'Leave Allocation', + 'to_date': ['between', (from_date, to_date)] + },fields=['to_date']) + return expiry[0]['to_date'] if expiry else None + @frappe.whitelist() def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None): number_of_days = 0 @@ -364,14 +415,16 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day @frappe.whitelist() def get_leave_details(employee, date): - allocation_records = get_leave_allocation_records(date, employee).get(employee, frappe._dict()) + allocation_records = get_leave_allocation_records(employee, date) leave_allocation = {} for d in allocation_records: allocation = allocation_records.get(d, frappe._dict()) - date = allocation.to_date - leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, date, status="Approved") - leaves_pending = get_leaves_for_period(employee, d, allocation.from_date, date, status="Open") - remaining_leaves = allocation.total_leaves_allocated - leaves_taken - leaves_pending + remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date, + consider_all_leaves_in_the_allocation_period=True) + end_date = allocation.to_date + leaves_taken = get_leaves_for_period(employee, d, allocation.from_date, end_date) * -1 + leaves_pending = get_pending_leaves_for_period(employee, d, allocation.from_date, end_date) + leave_allocation[d] = { "total_leaves": allocation.total_leaves_allocated, "leaves_taken": leaves_taken, @@ -386,27 +439,131 @@ def get_leave_details(employee, date): return ret @frappe.whitelist() -def get_leave_balance_on(employee, leave_type, date, allocation_records=None, docname=None, - consider_all_leaves_in_the_allocation_period=False, consider_encashed_leaves=True): +def get_leave_balance_on(employee, leave_type, date, to_date=nowdate(), consider_all_leaves_in_the_allocation_period=False): + ''' + Returns leave balance till date + :param employee: employee name + :param leave_type: leave type + :param date: date to check balance on + :param to_date: future date to check for allocation expiry + :param consider_all_leaves_in_the_allocation_period: consider all leaves taken till the allocation end date + ''' - if allocation_records == None: - allocation_records = get_leave_allocation_records(date, employee).get(employee, frappe._dict()) + allocation_records = get_leave_allocation_records(employee, date, leave_type) allocation = allocation_records.get(leave_type, frappe._dict()) - if consider_all_leaves_in_the_allocation_period: - date = allocation.to_date - leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, date, status="Approved", docname=docname) - leaves_encashed = 0 - if frappe.db.get_value("Leave Type", leave_type, 'allow_encashment') and consider_encashed_leaves: - leaves_encashed = flt(allocation.total_leaves_encashed) - return flt(allocation.total_leaves_allocated) - (flt(leaves_taken) + flt(leaves_encashed)) + end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date + expiry = get_allocation_expiry(employee, leave_type, to_date, date) -def get_leaves_for_period(employee, leave_type, from_date, to_date, status, docname=None): - leave_applications = frappe.db.sql(""" - select name, employee, leave_type, from_date, to_date, total_leave_days - from `tabLeave Application` + leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date) + + return get_remaining_leaves(allocation, leaves_taken, date, expiry) + +def get_leave_allocation_records(employee, date, leave_type=None): + ''' returns the total allocated leaves and carry forwarded leaves based on ledger entries ''' + + conditions = ("and leave_type='%s'" % leave_type) if leave_type else "" + allocation_details = frappe.db.sql(""" + SELECT + SUM(CASE WHEN is_carry_forward = 1 THEN leaves ELSE 0 END) as cf_leaves, + SUM(CASE WHEN is_carry_forward = 0 THEN leaves ELSE 0 END) as new_leaves, + MIN(from_date) as from_date, + MAX(to_date) as to_date, + leave_type + FROM `tabLeave Ledger Entry` + WHERE + from_date <= %(date)s + AND to_date >= %(date)s + AND docstatus=1 + AND transaction_type="Leave Allocation" + AND employee=%(employee)s + AND is_expired=0 + AND is_lwp=0 + {0} + GROUP BY employee, leave_type + """.format(conditions), dict(date=date, employee=employee), as_dict=1) #nosec + + allocated_leaves = frappe._dict() + for d in allocation_details: + allocated_leaves.setdefault(d.leave_type, frappe._dict({ + "from_date": d.from_date, + "to_date": d.to_date, + "total_leaves_allocated": flt(d.cf_leaves) + flt(d.new_leaves), + "unused_leaves": d.cf_leaves, + "new_leaves_allocated": d.new_leaves, + "leave_type": d.leave_type + })) + return allocated_leaves + +def get_pending_leaves_for_period(employee, leave_type, from_date, to_date): + ''' Returns leaves that are pending approval ''' + return frappe.db.get_value("Leave Application", + filters={ + "employee": employee, + "leave_type": leave_type, + "from_date": ("<=", from_date), + "to_date": (">=", to_date), + "status": "Open" + }, fieldname=['SUM(total_leave_days)']) or flt(0) + +def get_remaining_leaves(allocation, leaves_taken, date, expiry): + ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry ''' + def _get_remaining_leaves(allocated_leaves, end_date): + remaining_leaves = flt(allocated_leaves) + flt(leaves_taken) + + if remaining_leaves > 0: + remaining_days = date_diff(end_date, date) + 1 + remaining_leaves = min(remaining_days, remaining_leaves) + + return remaining_leaves + + total_leaves = allocation.total_leaves_allocated + + if expiry and allocation.unused_leaves: + remaining_leaves = _get_remaining_leaves(allocation.unused_leaves, expiry) + + total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves) + + return _get_remaining_leaves(total_leaves, allocation.to_date) + +def get_leaves_for_period(employee, leave_type, from_date, to_date): + leave_entries = get_leave_entries(employee, leave_type, from_date, to_date) + leave_days = 0 + + for leave_entry in leave_entries: + inclusive_period = leave_entry.from_date >= getdate(from_date) and leave_entry.to_date <= getdate(to_date) + + if inclusive_period and leave_entry.transaction_type == 'Leave Encashment': + leave_days += leave_entry.leaves + + elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \ + and not skip_expiry_leaves(leave_entry, to_date): + leave_days += leave_entry.leaves + + else: + if leave_entry.from_date < getdate(from_date): + leave_entry.from_date = from_date + if leave_entry.to_date > getdate(to_date): + leave_entry.to_date = to_date + + leave_days += get_number_of_leave_days(employee, leave_type, + leave_entry.from_date, leave_entry.to_date) * -1 + + return leave_days + +def skip_expiry_leaves(leave_entry, date): + ''' Checks whether the expired leaves coincide with the to_date of leave balance check ''' + end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date']) + return True if end_date == date and not leave_entry.is_carry_forward else False + +def get_leave_entries(employee, leave_type, from_date, to_date): + ''' Returns leave entries between from_date and to_date ''' + return frappe.db.sql(""" + select employee, leave_type, from_date, to_date, leaves, transaction_type, is_carry_forward + from `tabLeave Ledger Entry` where employee=%(employee)s and leave_type=%(leave_type)s - and status = %(status)s and docstatus != 2 + and docstatus=1 + and leaves<0 and (from_date between %(from_date)s and %(to_date)s or to_date between %(from_date)s and %(to_date)s or (from_date < %(from_date)s and to_date > %(to_date)s)) @@ -414,43 +571,8 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, status, docn "from_date": from_date, "to_date": to_date, "employee": employee, - "status": status, "leave_type": leave_type }, as_dict=1) - leave_days = 0 - for leave_app in leave_applications: - if docname and leave_app.name == docname: - continue - if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date): - leave_days += leave_app.total_leave_days - else: - if leave_app.from_date < getdate(from_date): - leave_app.from_date = from_date - if leave_app.to_date > getdate(to_date): - leave_app.to_date = to_date - - leave_days += get_number_of_leave_days(employee, leave_type, - leave_app.from_date, leave_app.to_date) - - return leave_days - -def get_leave_allocation_records(date, employee=None): - conditions = (" and employee='%s'" % employee) if employee else "" - - leave_allocation_records = frappe.db.sql(""" - select employee, leave_type, total_leaves_allocated, total_leaves_encashed, from_date, to_date - from `tabLeave Allocation` - where %s between from_date and to_date and docstatus=1 {0}""".format(conditions), (date), as_dict=1) - - allocated_leaves = frappe._dict() - for d in leave_allocation_records: - allocated_leaves.setdefault(d.employee, frappe._dict()).setdefault(d.leave_type, frappe._dict({ - "from_date": d.from_date, - "to_date": d.to_date, - "total_leaves_allocated": d.total_leaves_allocated, - "total_leaves_encashed":d.total_leaves_encashed - })) - return allocated_leaves @frappe.whitelist() def get_holidays(employee, from_date, to_date): @@ -629,4 +751,4 @@ def get_leave_approver(employee, department=None): if department: return frappe.db.get_value('Department Approver', {'parent': department, - 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') + 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py new file mode 100644 index 00000000000..8075b7b5c57 --- /dev/null +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py @@ -0,0 +1,14 @@ +from __future__ import unicode_literals + +from frappe import _ + + +def get_data(): + return { + 'reports': [ + { + 'label': _('Reports'), + 'items': ['Employee Leave Balance'] + } + ] + } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index d3dcca1da05..ad141a57482 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -7,7 +7,9 @@ import unittest from erpnext.hr.doctype.leave_application.leave_application import LeaveDayBlockedError, OverlapError, NotAnOptionalHoliday, get_leave_balance_on from frappe.permissions import clear_user_permissions_for_doctype -from frappe.utils import add_days, nowdate, now_datetime, getdate +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 test_dependencies = ["Leave Allocation", "Leave Block List"] @@ -17,6 +19,7 @@ _test_records = [ "doctype": "Leave Application", "employee": "_T-Employee-00001", "from_date": "2013-05-01", + "description": "_Test Reason", "leave_type": "_Test Leave Type", "posting_date": "2013-01-02", "to_date": "2013-05-05" @@ -26,6 +29,7 @@ _test_records = [ "doctype": "Leave Application", "employee": "_T-Employee-00002", "from_date": "2013-05-01", + "description": "_Test Reason", "leave_type": "_Test Leave Type", "posting_date": "2013-01-02", "to_date": "2013-05-05" @@ -35,6 +39,7 @@ _test_records = [ "doctype": "Leave Application", "employee": "_T-Employee-00001", "from_date": "2013-01-15", + "description": "_Test Reason", "leave_type": "_Test Leave Type LWP", "posting_date": "2013-01-02", "to_date": "2013-01-15" @@ -44,8 +49,8 @@ _test_records = [ class TestLeaveApplication(unittest.TestCase): def setUp(self): - for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]: - frappe.db.sql("delete from `tab%s`" % dt) + for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]: + frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec @classmethod def setUpClass(cls): @@ -268,13 +273,14 @@ class TestLeaveApplication(unittest.TestCase): doctype = 'Leave Application', employee = employee.name, company = '_Test Company', + description = "_Test Reason", leave_type = leave_type, from_date = date, to_date = date, )) # can only apply on optional holidays - self.assertTrue(NotAnOptionalHoliday, leave_application.insert) + self.assertRaises(NotAnOptionalHoliday, leave_application.insert) leave_application.from_date = today leave_application.to_date = today @@ -285,7 +291,6 @@ class TestLeaveApplication(unittest.TestCase): # check leave balance is reduced self.assertEqual(get_leave_balance_on(employee.name, leave_type, today), 9) - def test_leaves_allowed(self): employee = get_employee() leave_period = get_leave_period() @@ -301,24 +306,25 @@ class TestLeaveApplication(unittest.TestCase): allocate_leaves(employee, leave_period, leave_type.name, 5) leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', + doctype = 'Leave Application', employee = employee.name, leave_type = leave_type.name, + description = "_Test Reason", from_date = date, to_date = add_days(date, 2), company = "_Test Company", docstatus = 1, status = "Approved" )) - - self.assertTrue(leave_application.insert()) + leave_application.submit() leave_application = frappe.get_doc(dict( doctype = 'Leave Application', employee = employee.name, leave_type = leave_type.name, + description = "_Test Reason", from_date = add_days(date, 4), - to_date = add_days(date, 7), + to_date = add_days(date, 8), company = "_Test Company", docstatus = 1, status = "Approved" @@ -342,6 +348,7 @@ class TestLeaveApplication(unittest.TestCase): doctype = 'Leave Application', employee = employee.name, leave_type = leave_type.name, + description = "_Test Reason", from_date = date, to_date = add_days(date, 4), company = "_Test Company", @@ -363,6 +370,7 @@ class TestLeaveApplication(unittest.TestCase): doctype = 'Leave Application', employee = employee.name, leave_type = leave_type_1.name, + description = "_Test Reason", from_date = date, to_date = add_days(date, 4), company = "_Test Company", @@ -392,6 +400,7 @@ class TestLeaveApplication(unittest.TestCase): doctype = 'Leave Application', employee = employee.name, leave_type = leave_type.name, + description = "_Test Reason", from_date = date, to_date = add_days(date, 4), company = "_Test Company", @@ -401,6 +410,18 @@ class TestLeaveApplication(unittest.TestCase): self.assertRaises(frappe.ValidationError, leave_application.insert) + def test_leave_balance_near_allocaton_expiry(self): + employee = get_employee() + leave_type = create_leave_type( + leave_type_name="_Test_CF_leave_expiry", + is_carry_forward=1, + expire_carry_forwarded_leaves_after_days=90) + leave_type.submit() + + create_carry_forwarded_allocation(employee, leave_type) + + self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21) + def test_earned_leave(self): leave_period = get_leave_period() employee = get_employee() @@ -444,9 +465,10 @@ class TestLeaveApplication(unittest.TestCase): allocation.insert(ignore_permissions=True) allocation.submit() leave_application = frappe.get_doc(dict( - doctype = 'Leave Application', + doctype = 'Leave Application', employee = employee.name, leave_type = leave_type, + description = "_Test Reason", from_date = '2018-10-02', to_date = '2018-10-02', company = '_Test Company', @@ -457,9 +479,103 @@ class TestLeaveApplication(unittest.TestCase): leave_application.submit() self.assertEqual(leave_application.docstatus, 1) -def make_allocation_record(employee=None, leave_type=None): - frappe.db.sql("delete from `tabLeave Allocation`") + def test_creation_of_leave_ledger_entry_on_submit(self): + employee = get_employee() + leave_type = create_leave_type(leave_type_name = 'Test Leave Type 1') + leave_type.save() + + leave_allocation = create_leave_allocation(employee=employee.name, employee_name=employee.employee_name, + leave_type=leave_type.name) + leave_allocation.submit() + + leave_application = frappe.get_doc(dict( + doctype = 'Leave Application', + employee = employee.name, + leave_type = leave_type.name, + from_date = add_days(nowdate(), 1), + to_date = add_days(nowdate(), 4), + description = "_Test Reason", + company = "_Test Company", + docstatus = 1, + status = "Approved" + )) + leave_application.submit() + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name)) + + self.assertEquals(leave_ledger_entry[0].employee, leave_application.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, leave_application.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, leave_application.total_leave_days * -1) + + # check if leave ledger entry is deleted on cancellation + leave_application.cancel() + self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_application.name})) + + def test_ledger_entry_creation_on_intermediate_allocation_expiry(self): + employee = get_employee() + leave_type = create_leave_type( + leave_type_name="_Test_CF_leave_expiry", + is_carry_forward=1, + expire_carry_forwarded_leaves_after_days=90) + leave_type.submit() + + create_carry_forwarded_allocation(employee, leave_type) + + leave_application = frappe.get_doc(dict( + doctype = 'Leave Application', + employee = employee.name, + leave_type = leave_type.name, + from_date = add_days(nowdate(), -3), + to_date = add_days(nowdate(), 7), + description = "_Test Reason", + company = "_Test Company", + docstatus = 1, + status = "Approved" + )) + leave_application.submit() + + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', '*', filters=dict(transaction_name=leave_application.name)) + + self.assertEquals(len(leave_ledger_entry), 2) + self.assertEquals(leave_ledger_entry[0].employee, leave_application.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, leave_application.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, -9) + self.assertEquals(leave_ledger_entry[1].leaves, -2) + + def test_leave_application_creation_after_expiry(self): + # test leave balance for carry forwarded allocation + employee = get_employee() + leave_type = create_leave_type( + leave_type_name="_Test_CF_leave_expiry", + is_carry_forward=1, + expire_carry_forwarded_leaves_after_days=90) + leave_type.submit() + + create_carry_forwarded_allocation(employee, leave_type) + + self.assertEquals(get_leave_balance_on(employee.name, leave_type.name, add_days(nowdate(), -85), add_days(nowdate(), -84)), 0) + +def create_carry_forwarded_allocation(employee, leave_type): + # initial leave allocation + leave_allocation = create_leave_allocation( + leave_type="_Test_CF_leave_expiry", + employee=employee.name, + employee_name=employee.employee_name, + from_date=add_months(nowdate(), -24), + to_date=add_months(nowdate(), -12), + carry_forward=0) + leave_allocation.submit() + + leave_allocation = create_leave_allocation( + leave_type="_Test_CF_leave_expiry", + employee=employee.name, + employee_name=employee.employee_name, + from_date=add_days(nowdate(), -84), + to_date=add_days(nowdate(), 100), + carry_forward=1) + leave_allocation.submit() + +def make_allocation_record(employee=None, leave_type=None): allocation = frappe.get_doc({ "doctype": "Leave Allocation", "employee": employee or "_T-Employee-00001", @@ -513,4 +629,4 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el "docstatus": 1 }).insert() - allocate_leave.submit() + allocate_leave.submit() \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 9944bc53806..42f0179baf5 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -10,6 +10,8 @@ from frappe.utils import getdate, nowdate, flt from erpnext.hr.utils import set_employee_name from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry +from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves class LeaveEncashment(Document): def validate(self): @@ -25,7 +27,7 @@ class LeaveEncashment(Document): def on_submit(self): if not self.leave_allocation: - self.leave_allocation = self.get_leave_allocation() + self.leave_allocation = self.get_leave_allocation().get('name') additional_salary = frappe.new_doc("Additional Salary") additional_salary.company = frappe.get_value("Employee", self.employee, "company") additional_salary.employee = self.employee @@ -40,6 +42,8 @@ class LeaveEncashment(Document): frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') + self.encashable_days) + self.create_leave_ledger_entry() + def on_cancel(self): if self.additional_salary: frappe.get_doc("Additional Salary", self.additional_salary).cancel() @@ -48,6 +52,7 @@ class LeaveEncashment(Document): if self.leave_allocation: frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days) + self.create_leave_ledger_entry(submit=False) def get_leave_details_for_encashment(self): salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate())) @@ -57,8 +62,10 @@ class LeaveEncashment(Document): if not frappe.db.get_value("Leave Type", self.leave_type, 'allow_encashment'): frappe.throw(_("Leave Type {0} is not encashable").format(self.leave_type)) - self.leave_balance = get_leave_balance_on(self.employee, self.leave_type, - self.encashment_date or getdate(nowdate()), consider_all_leaves_in_the_allocation_period=True) + allocation = self.get_leave_allocation() + + self.leave_balance = allocation.total_leaves_allocated - allocation.carry_forwarded_leaves_count\ + - get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date) encashable_days = self.leave_balance - frappe.db.get_value('Leave Type', self.leave_type, 'encashment_threshold_days') self.encashable_days = encashable_days if encashable_days > 0 else 0 @@ -66,12 +73,47 @@ class LeaveEncashment(Document): per_day_encashment = frappe.db.get_value('Salary Structure', salary_structure , 'leave_encashment_amount_per_day') self.encashment_amount = self.encashable_days * per_day_encashment if per_day_encashment > 0 else 0 - self.leave_allocation = self.get_leave_allocation() + self.leave_allocation = allocation.name return True def get_leave_allocation(self): - leave_allocation = frappe.db.sql("""select name from `tabLeave Allocation` where '{0}' + leave_allocation = frappe.db.sql("""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}' between from_date and to_date and docstatus=1 and leave_type='{1}' - and employee= '{2}'""".format(self.encashment_date or getdate(nowdate()), self.leave_type, self.employee)) + and employee= '{2}'""".format(self.encashment_date or getdate(nowdate()), self.leave_type, self.employee), as_dict=1) #nosec - return leave_allocation[0][0] if leave_allocation else None + return leave_allocation[0] if leave_allocation else None + + def create_leave_ledger_entry(self, submit=True): + args = frappe._dict( + leaves=self.encashable_days * -1, + from_date=self.encashment_date, + to_date=self.encashment_date, + is_carry_forward=0 + ) + create_leave_ledger_entry(self, args, submit) + + # create reverse entry for expired leaves + to_date = self.get_leave_allocation().get('to_date') + if to_date < getdate(nowdate()): + args = frappe._dict( + leaves=self.encashable_days, + from_date=to_date, + to_date=to_date, + is_carry_forward=0 + ) + create_leave_ledger_entry(self, args, submit) + + +def create_leave_encashment(leave_allocation): + ''' Creates leave encashment for the given allocations ''' + for allocation in leave_allocation: + if not get_assigned_salary_structure(allocation.employee, allocation.to_date): + continue + leave_encashment = frappe.get_doc(dict( + doctype="Leave Encashment", + leave_period=allocation.leave_period, + employee=allocation.employee, + leave_type=allocation.leave_type, + encashment_date=allocation.to_date + )) + leave_encashment.insert(ignore_permissions=True) \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index ef5c2aa6f39..e5bd170bc4a 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -9,42 +9,43 @@ from frappe.utils import today, add_months from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.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.test_leave_policy import create_leave_policy\ test_dependencies = ["Leave Type"] class TestLeaveEncashment(unittest.TestCase): def setUp(self): frappe.db.sql('''delete from `tabLeave Period`''') - def test_leave_balance_value_and_amount(self): - employee = "test_employee_encashment@salary.com" - leave_type = "_Test Leave Type Encashment" + frappe.db.sql('''delete from `tabLeave Allocation`''') + frappe.db.sql('''delete from `tabLeave Ledger Entry`''') + frappe.db.sql('''delete from `tabAdditional Salary`''') # create the leave policy - leave_policy = frappe.get_doc({ - "doctype": "Leave Policy", - "leave_policy_details": [{ - "leave_type": leave_type, - "annual_allocation": 10 - }] - }).insert() + leave_policy = create_leave_policy( + leave_type="_Test Leave Type Encashment", + annual_allocation=10) leave_policy.submit() # create employee, salary structure and assignment - employee = make_employee(employee) - frappe.db.set_value("Employee", employee, "leave_policy", leave_policy.name) - salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", employee, + self.employee = make_employee("test_employee_encashment@example.com") + + frappe.db.set_value("Employee", self.employee, "leave_policy", leave_policy.name) + + 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 - leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) - leave_period.grant_leave_allocation(employee=employee) + self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3)) + self.leave_period.grant_leave_allocation(employee=self.employee) + def test_leave_balance_value_and_amount(self): + frappe.db.sql('''delete from `tabLeave Encashment`''') leave_encashment = frappe.get_doc(dict( - doctype = 'Leave Encashment', - employee = employee, - leave_type = leave_type, - leave_period = leave_period.name, - payroll_date = today() + doctype='Leave Encashment', + employee=self.employee, + leave_type="_Test Leave Type Encashment", + leave_period=self.leave_period.name, + payroll_date=today() )).insert() self.assertEqual(leave_encashment.leave_balance, 10) @@ -53,3 +54,26 @@ class TestLeaveEncashment(unittest.TestCase): leave_encashment.submit() self.assertTrue(frappe.db.get_value("Leave Encashment", leave_encashment.name, "additional_salary")) + + def test_creation_of_leave_ledger_entry_on_submit(self): + frappe.db.sql('''delete from `tabLeave Encashment`''') + leave_encashment = frappe.get_doc(dict( + doctype='Leave Encashment', + employee=self.employee, + leave_type="_Test Leave Type Encashment", + leave_period=self.leave_period.name, + payroll_date=today() + )).insert() + + leave_encashment.submit() + + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_encashment.name)) + + self.assertEquals(len(leave_ledger_entry), 1) + self.assertEquals(leave_ledger_entry[0].employee, leave_encashment.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, leave_encashment.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, leave_encashment.encashable_days * -1) + + # check if leave ledger entry is deleted on cancellation + leave_encashment.cancel() + self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_encashment.name})) diff --git a/erpnext/hr/doctype/leave_ledger_entry/__init__.py b/erpnext/hr/doctype/leave_ledger_entry/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.js b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.js new file mode 100644 index 00000000000..c68d518f670 --- /dev/null +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Leave Ledger Entry', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json new file mode 100644 index 00000000000..c11422211ca --- /dev/null +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json @@ -0,0 +1,168 @@ +{ + "creation": "2019-05-09 15:47:39.760406", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "leave_type", + "transaction_type", + "transaction_name", + "leaves", + "column_break_7", + "from_date", + "to_date", + "is_carry_forward", + "is_expired", + "is_lwp", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Employee", + "options": "Employee" + }, + { + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name" + }, + { + "fieldname": "leave_type", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Leave Type", + "options": "Leave Type" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Leave Ledger Entry", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "transaction_type", + "fieldtype": "Link", + "label": "Transaction Type", + "options": "DocType" + }, + { + "fieldname": "transaction_name", + "fieldtype": "Dynamic Link", + "label": "Transaction Name", + "options": "transaction_type" + }, + { + "fieldname": "leaves", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Leaves" + }, + { + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date" + }, + { + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, + { + "default": "0", + "fieldname": "is_carry_forward", + "fieldtype": "Check", + "label": "Is Carry Forward" + }, + { + "default": "0", + "fieldname": "is_expired", + "fieldtype": "Check", + "label": "Is Expired" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_lwp", + "fieldtype": "Check", + "label": "Is Leave Without Pay" + } + ], + "in_create": 1, + "is_submittable": 1, + "modified": "2019-06-21 00:37:07.782810", + "modified_by": "Administrator", + "module": "HR", + "name": "Leave Ledger Entry", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "title_field": "employee" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py new file mode 100644 index 00000000000..c82114e6d5e --- /dev/null +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 _ +from frappe.utils import add_days, today, flt, DATE_FORMAT, getdate + +class LeaveLedgerEntry(Document): + def validate(self): + if getdate(self.from_date) > getdate(self.to_date): + frappe.throw(_("To date needs to be before from date")) + + def on_cancel(self): + # allow cancellation of expiry leaves + if self.is_expired: + frappe.db.set_value("Leave Allocation", self.transaction_name, "expired", 0) + else: + frappe.throw(_("Only expired allocation can be cancelled")) + +def validate_leave_allocation_against_leave_application(ledger): + ''' Checks that leave allocation has no leave application against it ''' + leave_application_records = frappe.db.sql_list(""" + SELECT transaction_name + FROM `tabLeave Ledger Entry` + WHERE + employee=%s + AND leave_type=%s + AND transaction_type='Leave Application' + AND from_date>=%s + AND to_date<=%s + """, (ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date)) + + if leave_application_records: + frappe.throw(_("Leave allocation %s is linked with leave application %s" + % (ledger.transaction_name, ', '.join(leave_application_records)))) + +def create_leave_ledger_entry(ref_doc, args, submit=True): + ledger = frappe._dict( + doctype='Leave Ledger Entry', + employee=ref_doc.employee, + employee_name=ref_doc.employee_name, + leave_type=ref_doc.leave_type, + transaction_type=ref_doc.doctype, + transaction_name=ref_doc.name, + is_carry_forward=0, + is_expired=0, + is_lwp=0 + ) + ledger.update(args) + + if submit: + frappe.get_doc(ledger).submit() + else: + delete_ledger_entry(ledger) + +def delete_ledger_entry(ledger): + ''' Delete ledger entry on cancel of leave application/allocation/encashment ''' + if ledger.transaction_type == "Leave Allocation": + validate_leave_allocation_against_leave_application(ledger) + + expired_entry = get_previous_expiry_ledger_entry(ledger) + frappe.db.sql("""DELETE + FROM `tabLeave Ledger Entry` + WHERE + `transaction_name`=%s + OR `name`=%s""", (ledger.transaction_name, expired_entry)) + +def get_previous_expiry_ledger_entry(ledger): + ''' Returns the expiry ledger entry having same creation date as the ledger entry to be cancelled ''' + creation_date = frappe.db.get_value("Leave Ledger Entry", filters={ + 'transaction_name': ledger.transaction_name, + 'is_expired': 0, + 'transaction_type': 'Leave Allocation' + }, fieldname=['creation']) + + creation_date = creation_date.strftime(DATE_FORMAT) if creation_date else '' + + return frappe.db.get_value("Leave Ledger Entry", filters={ + 'creation': ('like', creation_date+"%"), + 'employee': ledger.employee, + 'leave_type': ledger.leave_type, + 'is_expired': 1, + 'docstatus': 1, + 'is_carry_forward': 0 + }, fieldname=['name']) + +def process_expired_allocation(): + ''' Check if a carry forwarded allocation has expired and create a expiry ledger entry ''' + + # fetch leave type records that has carry forwarded leaves expiry + leave_type_records = frappe.db.get_values("Leave Type", filters={ + 'expire_carry_forwarded_leaves_after_days': (">", 0) + }, fieldname=['name']) + + if leave_type_records: + leave_type = [record[0] for record in leave_type_records] + + expired_allocation = frappe.db.sql_list("""SELECT name + FROM `tabLeave Ledger Entry` + WHERE + `transaction_type`='Leave Allocation' + AND `is_expired`=1""") + + expire_allocation = frappe.get_all("Leave Ledger Entry", + fields=['leaves', 'to_date', 'employee', 'leave_type', 'is_carry_forward', 'transaction_name as name', 'transaction_type'], + filters={ + 'to_date': ("<", today()), + 'transaction_type': 'Leave Allocation', + 'transaction_name': ('not in', expired_allocation) + }, + or_filters={ + 'is_carry_forward': 0, + 'leave_type': ('in', leave_type) + }) + + if expire_allocation: + create_expiry_ledger_entry(expire_allocation) + +def create_expiry_ledger_entry(allocations): + ''' Create ledger entry for expired allocation ''' + for allocation in allocations: + if allocation.is_carry_forward: + expire_carried_forward_allocation(allocation) + else: + expire_allocation(allocation) + +def get_remaining_leaves(allocation): + ''' Returns remaining leaves from the given allocation ''' + return frappe.db.get_value("Leave Ledger Entry", + filters={ + 'employee': allocation.employee, + 'leave_type': allocation.leave_type, + 'to_date': ('<=', allocation.to_date), + }, fieldname=['SUM(leaves)']) + +@frappe.whitelist() +def expire_allocation(allocation, expiry_date=None): + ''' expires non-carry forwarded allocation ''' + leaves = get_remaining_leaves(allocation) + expiry_date = expiry_date if expiry_date else allocation.to_date + + if leaves: + args = dict( + leaves=flt(leaves) * -1, + transaction_name=allocation.name, + transaction_type='Leave Allocation', + from_date=expiry_date, + to_date=expiry_date, + is_carry_forward=0, + is_expired=1 + ) + create_leave_ledger_entry(allocation, args) + + frappe.db.set_value("Leave Allocation", allocation.name, "expired", 1) + +def expire_carried_forward_allocation(allocation): + ''' Expires remaining leaves in the on carried forward allocation ''' + from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period + leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, allocation.from_date, allocation.to_date) + leaves = flt(allocation.leaves) + flt(leaves_taken) + if leaves > 0: + args = frappe._dict( + transaction_name=allocation.name, + transaction_type="Leave Allocation", + leaves=allocation.leaves * -1, + is_carry_forward=allocation.is_carry_forward, + is_expired=1, + from_date=allocation.to_date, + to_date=allocation.to_date + ) + create_leave_ledger_entry(allocation, args) \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_ledger_entry/test_leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/test_leave_ledger_entry.py new file mode 100644 index 00000000000..6f7725c254e --- /dev/null +++ b/erpnext/hr/doctype/leave_ledger_entry/test_leave_ledger_entry.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLeaveLedgerEntry(unittest.TestCase): + pass diff --git a/erpnext/hr/doctype/leave_period/leave_period.js b/erpnext/hr/doctype/leave_period/leave_period.js index b8c5f716f5f..bad2b8766c8 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.js +++ b/erpnext/hr/doctype/leave_period/leave_period.js @@ -68,7 +68,7 @@ frappe.ui.form.on('Leave Period', { }, { "label": "Add unused leaves from previous allocations", - "fieldname": "carry_forward_leaves", + "fieldname": "carry_forward", "fieldtype": "Check" } ], diff --git a/erpnext/hr/doctype/leave_period/leave_period.json b/erpnext/hr/doctype/leave_period/leave_period.json index df28763c79c..9e895c34fb2 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.json +++ b/erpnext/hr/doctype/leave_period/leave_period.json @@ -1,294 +1,294 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "HR-LPR-.YYYY.-.#####", - "beta": 0, - "creation": "2018-04-13 15:20:52.864288", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 1, + "allow_rename": 1, + "autoname": "HR-LPR-.YYYY.-.#####", + "beta": 0, + "creation": "2018-04-13 15:20:52.864288", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "From Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "from_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "From Date", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "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": "to_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "To Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "to_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "To Date", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "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": "column_break_3", - "fieldtype": "Column Break", - "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, - "length": 0, - "no_copy": 0, - "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, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_active", + "fieldtype": "Check", + "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": "Is Active", + "length": 0, + "no_copy": 0, + "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": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_3", + "fieldtype": "Column Break", + "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, + "length": 0, + "no_copy": 0, + "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": "is_active", - "fieldtype": "Check", - "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": "Is Active", - "length": 0, - "no_copy": 0, - "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, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Company", + "length": 0, + "no_copy": 0, + "options": "Company", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "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": "optional_holiday_list", - "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": "Holiday List for Optional Leave", - "length": 0, - "no_copy": 0, - "options": "Holiday List", - "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, + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "optional_holiday_list", + "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": "Holiday List for Optional Leave", + "length": 0, + "no_copy": 0, + "options": "Holiday List", + "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 } - ], - "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-08-21 16:15:43.305502", - "modified_by": "Administrator", - "module": "HR", - "name": "Leave Period", - "name_case": "", - "owner": "Administrator", + ], + "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": "2019-05-30 16:15:43.305502", + "modified_by": "Administrator", + "module": "HR", + "name": "Leave Period", + "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, + "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, + "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, + "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, + ], + "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 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_period/leave_period.py b/erpnext/hr/doctype/leave_period/leave_period.py index 15fa8d6f8cf..a8566c4ffb0 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.py +++ b/erpnext/hr/doctype/leave_period/leave_period.py @@ -5,9 +5,10 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import getdate, cstr +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.doctype.leave_allocation.leave_allocation import get_carry_forwarded_leaves from frappe.utils.background_jobs import enqueue from six import iteritems @@ -21,8 +22,8 @@ class LeavePeriod(Document): condition_str = " and " + " and ".join(conditions) if len(conditions) else "" - employees = frappe.db.sql_list("select name from tabEmployee where status='Active' {condition}" - .format(condition=condition_str), tuple(values)) + 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 @@ -36,29 +37,29 @@ class LeavePeriod(Document): def grant_leave_allocation(self, grade=None, department=None, designation=None, - employee=None, carry_forward_leaves=0): - employees = self.get_employees({ + employee=None, carry_forward=0): + employee_records = self.get_employees({ "grade": grade, - "department": department, - "designation": designation, + "department": department, + "designation": designation, "name": employee }) - if employees: - if len(employees) > 20: + if employee_records: + if len(employee_records) > 20: frappe.enqueue(grant_leave_alloc_for_employees, timeout=600, - employees=employees, leave_period=self, carry_forward_leaves=carry_forward_leaves) + employee_records=employee_records, leave_period=self, carry_forward=carry_forward) else: - grant_leave_alloc_for_employees(employees, self, carry_forward_leaves) + grant_leave_alloc_for_employees(employee_records, self, carry_forward) else: frappe.msgprint(_("No Employee Found")) -def grant_leave_alloc_for_employees(employees, leave_period, carry_forward_leaves=0): +def grant_leave_alloc_for_employees(employee_records, leave_period, carry_forward=0): leave_allocations = [] - existing_allocations_for = get_existing_allocations(employees, leave_period.name) + 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 employees: + count = 0 + for employee in employee_records.keys(): if employee in existing_allocations_for: continue count +=1 @@ -67,18 +68,24 @@ def grant_leave_alloc_for_employees(employees, leave_period, carry_forward_leave 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_leaves) + 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(employees) - set(existing_allocations_for)), title = _("Allocating leaves...")) + 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 docstatus=1 + 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}") @@ -87,28 +94,36 @@ def get_existing_allocations(employees, leave_period): 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"]) + 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_leaves): - allocation = frappe.new_doc("Leave Allocation") - allocation.employee = employee - allocation.leave_type = leave_type - allocation.from_date = leave_period.from_date - allocation.to_date = leave_period.to_date +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.new_leaves_allocated = new_leaves_allocated - allocation.leave_period = leave_period.name - if carry_forward_leaves: - if leave_type_details.get(leave_type).is_carry_forward: - allocation.carry_forward = carry_forward_leaves + 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 - - + return allocation.name \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py index f97d2855a4f..48a204596c3 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py +++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py @@ -12,6 +12,9 @@ def get_data(): }, { 'items': ['Employee Grade'] - } + }, + { + 'items': ['Leave Allocation'] + }, ] } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy/test_leave_policy.py b/erpnext/hr/doctype/leave_policy/test_leave_policy.py index 2c6f1d0a21a..fc868ea15a6 100644 --- a/erpnext/hr/doctype/leave_policy/test_leave_policy.py +++ b/erpnext/hr/doctype/leave_policy/test_leave_policy.py @@ -12,16 +12,20 @@ class TestLeavePolicy(unittest.TestCase): if random_leave_type: random_leave_type = random_leave_type[0] leave_type = frappe.get_doc("Leave Type", random_leave_type.name) - old_max_leaves_allowed = leave_type.max_leaves_allowed leave_type.max_leaves_allowed = 2 leave_type.save() - leave_policy_details = { - "doctype": "Leave Policy", - "leave_policy_details": [{ - "leave_type": leave_type.name, - "annual_allocation": leave_type.max_leaves_allowed + 1 - }] - } + leave_policy = create_leave_policy(leave_type=leave_type.name, annual_allocation=leave_type.max_leaves_allowed + 1) - self.assertRaises(frappe.ValidationError, frappe.get_doc(leave_policy_details).insert) + self.assertRaises(frappe.ValidationError, leave_policy.insert) + +def create_leave_policy(**args): + ''' Returns an object of leave policy ''' + args = frappe._dict(args) + return frappe.get_doc({ + "doctype": "Leave Policy", + "leave_policy_details": [{ + "leave_type": args.leave_type or "_Test Leave Type", + "annual_allocation": args.annual_allocation or 10 + }] + }) \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index 6a7a80a7845..2f15e3b3c1f 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 1, @@ -14,10 +15,12 @@ "fields": [ { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "leave_type_name", "fieldtype": "Data", "hidden": 0, @@ -42,15 +45,17 @@ "search_index": 0, "set_only_once": 0, "translatable": 0, - "unique": 0 + "unique": 1 }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "depends_on": "", + "fetch_if_empty": 0, "fieldname": "max_leaves_allowed", "fieldtype": "Int", "hidden": 0, @@ -78,10 +83,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "applicable_after", "fieldtype": "Int", "hidden": 0, @@ -109,10 +116,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "max_continuous_days_allowed", "fieldtype": "Int", "hidden": 0, @@ -141,10 +150,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "column_break_3", "fieldtype": "Column Break", "hidden": 0, @@ -171,10 +182,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "is_carry_forward", "fieldtype": "Check", "hidden": 0, @@ -203,10 +216,13 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "depends_on": "", + "fetch_if_empty": 0, "fieldname": "is_lwp", "fieldtype": "Check", "hidden": 0, @@ -233,10 +249,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "is_optional_leave", "fieldtype": "Check", "hidden": 0, @@ -264,10 +282,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "allow_negative", "fieldtype": "Check", "hidden": 0, @@ -294,10 +314,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "include_holiday", "fieldtype": "Check", "hidden": 0, @@ -324,10 +346,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "is_compensatory", "fieldtype": "Check", "hidden": 0, @@ -355,10 +379,81 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 1, "columns": 0, + "depends_on": "eval: doc.is_carry_forward == 1", + "fetch_if_empty": 0, + "fieldname": "carry_forward_section", + "fieldtype": "Section Break", + "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": "Carry Forward", + "length": 0, + "no_copy": 0, + "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, + "depends_on": "", + "description": "Calculated in days", + "fetch_if_empty": 0, + "fieldname": "expire_carry_forwarded_leaves_after_days", + "fieldtype": "Int", + "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": "Expire Carry Forwarded Leaves (Days)", + "length": 0, + "no_copy": 0, + "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": 1, + "columns": 0, + "fetch_if_empty": 0, "fieldname": "encashment", "fieldtype": "Section Break", "hidden": 0, @@ -386,10 +481,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "allow_encashment", "fieldtype": "Check", "hidden": 0, @@ -417,11 +514,13 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "depends_on": "allow_encashment", + "fetch_if_empty": 0, "fieldname": "encashment_threshold_days", "fieldtype": "Int", "hidden": 0, @@ -449,11 +548,13 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "depends_on": "allow_encashment", + "fetch_if_empty": 0, "fieldname": "earning_component", "fieldtype": "Link", "hidden": 0, @@ -482,10 +583,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 1, "columns": 0, + "fetch_if_empty": 0, "fieldname": "earned_leave", "fieldtype": "Section Break", "hidden": 0, @@ -513,10 +616,12 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, + "fetch_if_empty": 0, "fieldname": "is_earned_leave", "fieldtype": "Check", "hidden": 0, @@ -544,11 +649,13 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "depends_on": "is_earned_leave", + "fetch_if_empty": 0, "fieldname": "earned_leave_frequency", "fieldtype": "Select", "hidden": 0, @@ -577,12 +684,14 @@ }, { "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "default": "0.5", "depends_on": "is_earned_leave", + "fetch_if_empty": 0, "fieldname": "rounding", "fieldtype": "Select", "hidden": 0, @@ -611,17 +720,15 @@ } ], "has_web_view": 0, - "hide_heading": 0, "hide_toolbar": 0, "icon": "fa fa-flag", "idx": 1, - "image_view": 0, "in_create": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-06-03 18:32:51.803472", + "modified": "2019-08-02 15:38:39.334283", "modified_by": "Administrator", "module": "HR", "name": "Leave Type", @@ -687,8 +794,8 @@ ], "quick_entry": 0, "read_only": 0, - "read_only_onload": 0, "show_name_in_global_search": 0, "track_changes": 0, - "track_seen": 0 -} + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_type/leave_type.py b/erpnext/hr/doctype/leave_type/leave_type.py index e0127e5e973..c0d12968416 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.py +++ b/erpnext/hr/doctype/leave_type/leave_type.py @@ -2,9 +2,22 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals +import calendar import frappe +from datetime import datetime +from frappe.utils import today +from frappe import _ from frappe.model.document import Document class LeaveType(Document): - pass \ No newline at end of file + def validate(self): + if self.is_lwp: + leave_allocation = frappe.get_all("Leave Allocation", filters={ + 'leave_type': self.name, + 'from_date': ("<=", today()), + 'to_date': (">=", today()) + }, fields=['name']) + leave_allocation = [l['name'] for l in leave_allocation] + if leave_allocation: + frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec diff --git a/erpnext/hr/doctype/leave_type/test_leave_type.py b/erpnext/hr/doctype/leave_type/test_leave_type.py index b844e49e7c4..0c4f435860a 100644 --- a/erpnext/hr/doctype/leave_type/test_leave_type.py +++ b/erpnext/hr/doctype/leave_type/test_leave_type.py @@ -2,6 +2,25 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals - import frappe -test_records = frappe.get_test_records('Leave Type') \ No newline at end of file +from frappe import _ + +test_records = frappe.get_test_records('Leave Type') + +def create_leave_type(**args): + args = frappe._dict(args) + if frappe.db.exists("Leave Type", args.leave_type_name): + return frappe.get_doc("Leave Type", args.leave_type_name) + leave_type = frappe.get_doc({ + "doctype": "Leave Type", + "leave_type_name": args.leave_type_name or "_Test Leave Type", + "include_holiday": args.include_holidays or 1, + "allow_encashment": args.allow_encashment or 0, + "is_earned_leave": args.is_earned_leave or 0, + "is_lwp": args.is_lwp or 0, + "is_carry_forward": args.is_carry_forward or 0, + "expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0, + "encashment_threshold_days": args.encashment_threshold_days or 5, + "earning_component": "Leave Encashment" + }) + return leave_type \ No newline at end of file diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js index 59c25608c24..05728a297b2 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js @@ -24,6 +24,18 @@ frappe.query_reports["Employee Leave Balance"] = { "options": "Company", "reqd": 1, "default": frappe.defaults.get_user_default("Company") + }, + { + "fieldname":"department", + "label": __("Department"), + "fieldtype": "Link", + "options": "Department", + }, + { + "fieldname":"employee", + "label": __("Employee"), + "fieldtype": "Link", + "options": "Employee", } ] } diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 18431768528..66e3614982e 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -4,8 +4,9 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.utils import flt from erpnext.hr.doctype.leave_application.leave_application \ - import get_leave_allocation_records, get_leave_balance_on, get_approved_leaves_for_period + import get_leave_balance_on, get_leaves_for_period def execute(filters=None): @@ -30,17 +31,28 @@ def get_columns(leave_types): return columns +def get_conditions(filters): + conditions = { + "status": "Active", + "company": filters.company, + } + if filters.get("department"): + conditions.update({"department": filters.get("department")}) + if filters.get("employee"): + conditions.update({"employee": filters.get("employee")}) + + return conditions + def get_data(filters, leave_types): user = frappe.session.user - allocation_records_based_on_to_date = get_leave_allocation_records(filters.to_date) - allocation_records_based_on_from_date = get_leave_allocation_records(filters.from_date) + conditions = get_conditions(filters) if filters.to_date <= filters.from_date: frappe.throw(_("From date can not be greater than than To date")) active_employees = frappe.get_all("Employee", - filters = { "status": "Active", "company": filters.company}, - fields = ["name", "employee_name", "department", "user_id"]) + filters=conditions, + fields=["name", "employee_name", "department", "user_id"]) data = [] for employee in active_employees: @@ -50,16 +62,14 @@ def get_data(filters, leave_types): for leave_type in leave_types: # leaves taken - leaves_taken = get_approved_leaves_for_period(employee.name, leave_type, - filters.from_date, filters.to_date) + leaves_taken = get_leaves_for_period(employee.name, leave_type, + filters.from_date, filters.to_date) * -1 # opening balance - opening = get_leave_balance_on(employee.name, leave_type, filters.from_date, - allocation_records_based_on_to_date.get(employee.name, frappe._dict())) + opening = get_total_allocated_leaves(employee.name, leave_type, filters.from_date, filters.to_date) # closing balance - closing = get_leave_balance_on(employee.name, leave_type, filters.to_date, - allocation_records_based_on_to_date.get(employee.name, frappe._dict())) + closing = flt(opening) - flt(leaves_taken) row += [opening, leaves_taken, closing] @@ -84,3 +94,19 @@ def get_approvers(department): where parent = %s and parentfield = 'leave_approvers'""", (d), as_dict=True)]) return approvers + +def get_total_allocated_leaves(employee, leave_type, from_date, to_date): + ''' Returns leave allocation between from date and to date ''' + filters= { + 'from_date': ['between', (from_date, to_date)], + 'to_date': ['between', (from_date, to_date)], + 'docstatus': 1, + 'is_expired': 0, + 'leave_type': leave_type, + 'employee': employee, + 'transaction_type': 'Leave Allocation' + } + + leave_allocation_records = frappe.db.get_all('Leave Ledger Entry', filters=filters, fields=['SUM(leaves) as leaves']) + + return flt(leave_allocation_records[0].get('leaves')) if leave_allocation_records else flt(0) \ No newline at end of file diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index de2b0909dbd..1464a779368 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr +from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr, add_days, today from frappe.model.document import Document from frappe.desk.form import assign_to from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee @@ -270,6 +270,21 @@ def get_leave_period(from_date, to_date, company): if leave_period: return leave_period +def generate_leave_encashment(): + ''' Generates a draft leave encashment on allocation expiry ''' + from erpnext.hr.doctype.leave_encashment.leave_encashment import create_leave_encashment + + if frappe.db.get_single_value('HR Settings', 'auto_leave_encashment'): + leave_type = frappe.get_all('Leave Type', filters={'allow_encashment': 1}, fields=['name']) + leave_type=[l['name'] for l in leave_type] + + leave_allocation = frappe.get_all("Leave Allocation", filters={ + 'to_date': add_days(today(), -1), + 'leave_type': ('in', leave_type) + }, fields=['employee', 'leave_period', 'leave_type', 'to_date', 'total_leaves_allocated', 'new_leaves_allocated']) + + create_leave_encashment(leave_allocation=leave_allocation) + def allocate_earned_leaves(): '''Allocate earned leaves to Employees''' e_leave_types = frappe.get_all("Leave Type", @@ -277,31 +292,43 @@ def allocate_earned_leaves(): filters={'is_earned_leave' : 1}) today = getdate() divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12} - if e_leave_types: - for e_leave_type in e_leave_types: - leave_allocations = frappe.db.sql("""select name, employee, from_date, to_date from `tabLeave Allocation` where '{0}' - between from_date and to_date and docstatus=1 and leave_type='{1}'""" - .format(today, e_leave_type.name), as_dict=1) - for allocation in leave_allocations: - leave_policy = get_employee_leave_policy(allocation.employee) - if not 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 - annual_allocation = frappe.db.sql("""select annual_allocation from `tabLeave Policy Detail` - where parent=%s and leave_type=%s""", (leave_policy.name, e_leave_type.name)) - if annual_allocation and annual_allocation[0]: - earned_leaves = flt(annual_allocation[0][0]) / 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) - allocated_leaves = frappe.db.get_value('Leave Allocation', allocation.name, 'total_leaves_allocated') - new_allocation = flt(allocated_leaves) + flt(earned_leaves) - new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed - frappe.db.set_value('Leave Allocation', allocation.name, 'total_leaves_allocated', new_allocation) + 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) + for allocation in leave_allocations: + leave_policy = get_employee_leave_policy(allocation.employee) + if not 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 + annual_allocation = frappe.db.get_value("Leave Policy Detail", filters={ + 'parent': leave_policy.name, + '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) + new_allocation = new_allocation if new_allocation <= e_leave_type.max_leaves_allowed else e_leave_type.max_leaves_allowed + + if new_allocation == allocation.total_leaves_allocated: + continue + allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) + create_earned_leave_ledger_entry(allocation, earned_leaves, today) + +def create_earned_leave_ledger_entry(allocation, earned_leaves, date): + ''' Create leave ledger entry based on the earned leave frequency ''' + allocation.new_leaves_allocated = earned_leaves + allocation.from_date = 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''' diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b6ea542554e..7b8280975d7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -626,3 +626,4 @@ erpnext.patches.v12_0.add_default_buying_selling_terms_in_company erpnext.patches.v12_0.update_ewaybill_field_position erpnext.patches.v12_0.create_accounting_dimensions_in_missing_doctypes erpnext.patches.v11_1.set_status_for_material_request_type_manufacture +erpnext.patches.v12_0.generate_leave_ledger_entries \ No newline at end of file diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py new file mode 100644 index 00000000000..44b59bf09b0 --- /dev/null +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -0,0 +1,86 @@ +# Copyright (c) 2018, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import getdate + +def execute(): + """ Generates leave ledger entries for leave allocation/application/encashment + for last allocation """ + frappe.reload_doc("HR", "doctype", "Leave Ledger Entry") + frappe.reload_doc("HR", "doctype", "Leave Encashment") + if frappe.db.a_row_exists("Leave Ledger Entry"): + return + + if not frappe.get_meta("Leave Allocation").has_field("unused_leaves"): + frappe.reload_doc("HR", "doctype", "Leave Allocation") + update_leave_allocation_fieldname() + + generate_allocation_ledger_entries() + generate_application_leave_ledger_entries() + generate_encashment_leave_ledger_entries() + generate_expiry_allocation_ledger_entries() + +def update_leave_allocation_fieldname(): + ''' maps data from old field to the new field ''' + frappe.db.sql(""" + UPDATE `tabLeave Allocation` + SET `unused_leaves` = `carry_forwarded_leaves` + """) + +def generate_allocation_ledger_entries(): + ''' fix ledger entries for missing leave allocation transaction ''' + allocation_list = get_allocation_records() + + for allocation in allocation_list: + if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}): + allocation.update(dict(doctype="Leave Allocation")) + allocation_obj = frappe.get_doc(allocation) + allocation_obj.create_leave_ledger_entry() + +def generate_application_leave_ledger_entries(): + ''' fix ledger entries for missing leave application transaction ''' + leave_applications = get_leaves_application_records() + + for application in leave_applications: + if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}): + application.update(dict(doctype="Leave Application")) + frappe.get_doc(application).create_leave_ledger_entry() + +def generate_encashment_leave_ledger_entries(): + ''' fix ledger entries for missing leave encashment transaction ''' + leave_encashments = get_leave_encashment_records() + + for encashment in leave_encashments: + if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): + encashment.update(dict(doctype="Leave Encashment")) + frappe.get_doc(encashment).create_leave_ledger_entry() + +def generate_expiry_allocation_ledger_entries(): + ''' fix ledger entries for missing leave allocation transaction ''' + from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation + allocation_list = get_allocation_records() + + for allocation in allocation_list: + if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}): + allocation.update(dict(doctype="Leave Allocation")) + allocation_obj = frappe.get_doc(allocation) + expire_allocation(allocation_obj) + +def get_allocation_records(): + return frappe.get_all("Leave Allocation", filters={ + "docstatus": 1 + }, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated', + 'unused_leaves', 'from_date', 'to_date', 'carry_forward' + ], order_by='to_date ASC') + +def get_leaves_application_records(): + return frappe.get_all("Leave Application", filters={ + "docstatus": 1 + }, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date']) + +def get_leave_encashment_records(): + return frappe.get_all("Leave Encashment", filters={ + "docstatus": 1 + }, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date']) \ No newline at end of file