diff --git a/erpnext/domains/hr_asistencia_vacaciones.py b/erpnext/domains/hr_asistencia_vacaciones_rendimiento_gastos.py similarity index 55% rename from erpnext/domains/hr_asistencia_vacaciones.py rename to erpnext/domains/hr_asistencia_vacaciones_rendimiento_gastos.py index 0be4450b30bd..e3b5f28384a2 100644 --- a/erpnext/domains/hr_asistencia_vacaciones.py +++ b/erpnext/domains/hr_asistencia_vacaciones_rendimiento_gastos.py @@ -2,6 +2,6 @@ data = { 'modules': [ - 'HR Asistencia y Vacaciones' + 'HR Asistencia, Vacaciones y Rendimiento de Gastos' ], } \ No newline at end of file diff --git a/erpnext/domains/hr_reclutamiento_capacitacion_gastos.py b/erpnext/domains/hr_reclutamiento_capacitacion_gastos.py deleted file mode 100644 index 57acc2d8ad4b..000000000000 --- a/erpnext/domains/hr_reclutamiento_capacitacion_gastos.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import unicode_literals - -data = { - 'modules': [ - 'HR Reclutamiento, Capacitacion y Gastos' - ], -} \ No newline at end of file diff --git a/erpnext/education/doctype/student_attendance/student_attendance.py b/erpnext/education/doctype/student_attendance/student_attendance.py index 7c61cbeb6ce4..a6f6d51c3c88 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance.py +++ b/erpnext/education/doctype/student_attendance/student_attendance.py @@ -92,7 +92,7 @@ def validate_is_holiday(self): frappe.bold(formatdate(self.date)))) def get_holiday_list(company=None): - if not frappe.db.exists("Has Domain", {"domain": 'HR Asistencia y Vacaciones'}): + if not frappe.db.exists("Has Domain", {"domain": 'HR Asistencia, Vacaciones y Rendimiento de Gastos'}): return None if not company: diff --git a/erpnext/hooks.py b/erpnext/hooks.py index c35b45093a1b..8d20c2551814 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -115,8 +115,6 @@ 'RMA': 'erpnext.domains.rma', 'Retail Avanzado': 'erpnext.domains.retail_avanzado', 'Servicios Google': 'erpnext.domains.servicios_google', - 'HR Asistencia y Vacaciones': 'erpnext.domains.hr_asistencia_vacaciones', - 'HR Reclutamiento, Capacitacion y Gastos': 'erpnext.domains.hr_reclutamiento_capacitacion_gastos', 'Build': 'erpnext.domains.build', 'Pagos360': 'erpnext.domains.pagos360', 'Mercadolibre': 'erpnext.domains.mercadolibre', @@ -128,7 +126,8 @@ 'Compreahora': 'erpnext.domains.compreahora', 'Usuario de Ventas Reducido': 'erpnext.domains.usuario_ventas_reducido', 'Usuario de Soporte Reducido': 'erpnext.domains.usuario_soporte_reducido', - 'Usuario de Proyecto Reducido':'erpnext.domains.usuario_proyecto_reducido', + 'Usuario de Proyecto Reducido': 'erpnext.domains.usuario_proyecto_reducido', + 'HR Asistencia, Vacaciones y Rendimiento de Gastos': 'erpnext.domains.hr_asistencia_vacaciones_rendimiento_gastos', } website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner", diff --git a/erpnext/hr/doctype/appraisal/appraisal.json b/erpnext/hr/doctype/appraisal/appraisal.json index 9e233b8a39b9..26bfb359f353 100644 --- a/erpnext/hr/doctype/appraisal/appraisal.json +++ b/erpnext/hr/doctype/appraisal/appraisal.json @@ -201,7 +201,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:01:58.902166", + "modified": "2021-05-12 21:01:58.902166", "modified_by": "Administrator", "module": "HR", "name": "Appraisal", @@ -246,7 +246,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Reclutamiento, Capacitacion y Gastos", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "search_fields": "status, employee, employee_name", "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/hr/doctype/appraisal_template/appraisal_template.json b/erpnext/hr/doctype/appraisal_template/appraisal_template.json index 48eac2757800..d06a9ff15aa0 100644 --- a/erpnext/hr/doctype/appraisal_template/appraisal_template.json +++ b/erpnext/hr/doctype/appraisal_template/appraisal_template.json @@ -46,7 +46,7 @@ "icon": "icon-file-text", "idx": 1, "links": [], - "modified": "2021-05-12 20:01:59.271381", + "modified": "2021-05-12 21:01:59.271381", "modified_by": "Administrator", "module": "HR", "name": "Appraisal Template", @@ -67,7 +67,7 @@ "role": "Employee" } ], - "restrict_to_domain": "HR Reclutamiento, Capacitacion y Gastos", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index 64e91e4dd56e..1c659ee54e30 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -205,7 +205,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:01:54.473452", + "modified": "2021-05-12 21:01:54.473452", "modified_by": "Administrator", "module": "HR", "name": "Attendance", @@ -253,7 +253,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "search_fields": "employee,employee_name,attendance_date,status", "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/hr/doctype/attendance_request/attendance_request.json b/erpnext/hr/doctype/attendance_request/attendance_request.json index 98b5e0370dc2..77721245ecd0 100644 --- a/erpnext/hr/doctype/attendance_request/attendance_request.json +++ b/erpnext/hr/doctype/attendance_request/attendance_request.json @@ -119,7 +119,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:01:54.807226", + "modified": "2021-05-12 21:01:54.807226", "modified_by": "Administrator", "module": "HR", "name": "Attendance Request", @@ -183,7 +183,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "title_field": "employee_name", diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.json b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.json index 828d0cd17409..3caaafac17d9 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.json +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.json @@ -114,7 +114,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:01:53.391272", + "modified": "2021-05-12 21:01:53.391272", "modified_by": "Administrator", "module": "HR", "name": "Compensatory Leave Request", @@ -172,7 +172,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "title_field": "employee_name", diff --git a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.json b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.json index 4c8e862f9957..615e2f405520 100644 --- a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.json +++ b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.json @@ -65,7 +65,7 @@ } ], "links": [], - "modified": "2021-05-12 20:01:56.379840", + "modified": "2021-05-12 21:01:56.379840", "modified_by": "Administrator", "module": "HR", "name": "Daily Work Summary Group", @@ -85,7 +85,7 @@ } ], "quick_entry": 1, - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/department/department.json b/erpnext/hr/doctype/department/department.json index 549e04927f1d..d8ad2d3761d9 100644 --- a/erpnext/hr/doctype/department/department.json +++ b/erpnext/hr/doctype/department/department.json @@ -73,7 +73,6 @@ "description": "Days for which Holidays are blocked for this department.", "fieldname": "leave_block_list", "fieldtype": "Link", - "hidden": 1, "in_list_view": 1, "label": "Leave Block List", "options": "Leave Block List" @@ -145,7 +144,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2023-01-09 14:57:33.875494", + "modified": "2023-07-31 12:58:08.462456", "modified_by": "Administrator", "module": "HR", "name": "Department", diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index 113ea1887fcf..78ea4a92a2e9 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -44,13 +44,13 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" - field_name = "Leave Approver" + field_name = _("Leave Approver") elif filters.get("doctype") == "Expense Claim": parentfield = "expense_approvers" - field_name = "Expense Approver" + field_name = _("Expense Approver") elif filters.get("doctype") == "Shift Request": parentfield = "shift_request_approver" - field_name = "Shift Request Approver" + field_name = _("Shift Request Approver") if department_list: for d in department_list: approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from @@ -63,7 +63,8 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): if len(approvers) == 0: error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name)) if department_list: - error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department)) - frappe.throw(error_msg, title=_(field_name + " Missing")) + error_msg += " " + _("or for Department: {0}").format(frappe.bold(employee_department)) + frappe.throw(error_msg, title=_("{0} Missing").format(field_name)) + return set(tuple(approver) for approver in approvers) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 4a4add8e5104..81da178fee15 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -756,7 +756,6 @@ "collapsible": 1, "fieldname": "attendance_and_leave_details", "fieldtype": "Section Break", - "hidden": 1, "label": "Attendance and Leave Details" }, { @@ -829,7 +828,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2022-08-11 15:28:59.782180", + "modified": "2023-07-31 12:56:37.591515", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index dd3f384a9371..8cd6bec46d9c 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -30,7 +30,8 @@ "amended_from", "column_break_18", "advance_account", - "mode_of_payment" + "mode_of_payment", + "travel_request" ], "fields": [ { @@ -196,11 +197,18 @@ "precision": "9", "print_hide": 1, "reqd": 1 + }, + { + "fieldname": "travel_request", + "fieldtype": "Link", + "label": "Travel Request", + "options": "Travel Request", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2021-09-11 18:38:38.617478", + "modified": "2023-08-01 08:07:59.242651", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", @@ -233,7 +241,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Reclutamiento, Capacitacion y Gastos", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "search_fields": "employee,employee_name", "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index dbcfe71c4685..81ffc7c645c3 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -27,6 +27,10 @@ def validate(self): validate_active_employee(self.employee) self.set_status() + def on_submit(self): + if self.travel_request: + frappe.db.set_value('Travel Request', self.travel_request, 'employee_advance', self.name) + def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry') diff --git a/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py index 17d5bd27a6ef..72c88e77d940 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py @@ -6,14 +6,14 @@ def get_data(): 'fieldname': 'employee_advance', 'non_standard_fieldnames': { 'Payment Entry': 'reference_name', - 'Journal Entry': 'reference_name' + 'Journal Entry': 'reference_name', }, 'transactions': [ { 'items': ['Expense Claim'] }, { - 'items': ['Payment Entry', 'Journal Entry'] + 'items': ['Payment Entry', 'Journal Entry', 'Travel Request'] } ] } diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.json b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.json index 249b4f6b7fb1..d5c6658e47c4 100644 --- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.json +++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.json @@ -71,7 +71,7 @@ "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2021-05-12 20:01:54.184685", + "modified": "2021-05-12 21:01:54.184685", "modified_by": "Administrator", "module": "HR", "name": "Employee Attendance Tool", @@ -84,7 +84,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.json b/erpnext/hr/doctype/employee_checkin/employee_checkin.json index a408a1ea880e..4b726ff4b17b 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.json +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.json @@ -107,7 +107,7 @@ } ], "links": [], - "modified": "2021-05-12 20:01:55.298451", + "modified": "2021-05-12 21:01:55.298451", "modified_by": "Administrator", "module": "HR", "name": "Employee Checkin", @@ -201,7 +201,7 @@ "role": "Employee" } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "ASC", "title_field": "employee_name", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 7b525ab7dba7..ef091214cd89 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -379,7 +379,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-11-22 16:26:57.787838", + "modified": "2021-11-23 16:26:57.787838", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", @@ -441,7 +441,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Reclutamiento, Capacitacion y Gastos", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "search_fields": "employee,employee_name", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json index 8d4a39cec42c..8fa36a2a642c 100644 --- a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json +++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json @@ -29,7 +29,7 @@ "fieldname": "account_head", "fieldtype": "Link", "in_list_view": 1, - "label": "Account Head", + "label": "Cuenta contable", "oldfieldname": "account_head", "oldfieldtype": "Link", "options": "Account", @@ -61,7 +61,7 @@ "fieldname": "rate", "fieldtype": "Float", "in_list_view": 1, - "label": "Rate", + "label": "Al\u00edcuota", "oldfieldname": "rate", "oldfieldtype": "Currency" }, @@ -102,7 +102,7 @@ ], "istable": 1, "links": [], - "modified": "2021-05-12 19:59:12.974975", + "modified": "2023-08-01 11:13:52.620713", "modified_by": "Administrator", "module": "HR", "name": "Expense Taxes and Charges", diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.json b/erpnext/hr/doctype/holiday_list/holiday_list.json index 65e6c687ea0c..45f783fa1b00 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.json +++ b/erpnext/hr/doctype/holiday_list/holiday_list.json @@ -112,10 +112,10 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2022-06-22 15:13:55.735097", + "modified": "2022-06-22 16:13:55.735097", "modified_by": "Administrator", "module": "HR", - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "name": "Holiday List", "owner": "Administrator", "permissions": [ diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.py b/erpnext/hr/doctype/holiday_list/test_holiday_list.py index 27131932db48..b5530669d17e 100644 --- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import unittest +from contextlib import contextmanager from datetime import timedelta import frappe @@ -31,3 +32,24 @@ def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getd "holidays" : holiday_dates }).insert() return doc + + +@contextmanager +def set_holiday_list(holiday_list, company_name): + """ + Context manager for setting holiday list in tests + """ + try: + company = frappe.get_doc('Company', company_name) + previous_holiday_list = company.default_holiday_list + + company.default_holiday_list = holiday_list + company.save() + + yield + + finally: + # restore holiday list setup + company = frappe.get_doc('Company', company_name) + company.default_holiday_list = previous_holiday_list + company.save() diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 14a2f03e6aaa..e73a4f5871e0 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -60,15 +60,14 @@ "default": "1", "fieldname": "expense_approver_mandatory_in_expense_claim", "fieldtype": "Check", - "label": "Expense Approver Mandatory In Expense Claim", - "hidden": 1 + "hidden": 1, + "label": "Expense Approver Mandatory In Expense Claim" }, { "collapsible": 1, "fieldname": "leave_settings", "fieldtype": "Section Break", - "label": "Leave Settings", - "hidden": 1 + "label": "Leave Settings" }, { "depends_on": "eval: doc.send_leave_notification == 1", @@ -189,7 +188,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2021-08-25 15:54:12.834162", + "modified": "2023-07-31 12:57:33.002058", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json index 170c087b0c01..e48112986045 100644 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json @@ -236,7 +236,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-06-03 15:28:26.335104", + "modified": "2021-06-03 16:28:26.335104", "modified_by": "Administrator", "module": "HR", "name": "Leave Allocation", @@ -273,7 +273,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "search_fields": "employee,employee_name,leave_type,total_leaves_allocated", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 9ccb915908ff..00e1964aaeaa 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -53,7 +53,7 @@ frappe.ui.form.on("Leave Application", { make_dashboard: function(frm) { var leave_details; let lwps; - if (frm.doc.employee) { + if (frm.doc.employee && frm.doc.from_date) { frappe.call({ method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details", async: false, @@ -149,6 +149,7 @@ frappe.ui.form.on("Leave Application", { }, to_date: function(frm) { + frm.trigger("make_dashboard"); frm.trigger("half_day_datepicker"); frm.trigger("calculate_total_days"); }, diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json index c75490669965..c62cae008d61 100644 --- a/erpnext/hr/doctype/leave_application/leave_application.json +++ b/erpnext/hr/doctype/leave_application/leave_application.json @@ -214,6 +214,7 @@ { "fieldname": "salary_slip", "fieldtype": "Link", + "hidden": 1, "label": "Salary Slip", "options": "Salary Slip", "print_hide": 1 @@ -249,8 +250,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "max_attachments": 0, - "modified": "2021-05-13 20:01:52.079355", + "modified": "2023-07-31 12:55:18.574877", "modified_by": "Administrator", "module": "HR", "name": "Leave Application", @@ -330,7 +330,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "search_fields": "employee,employee_name,leave_type,from_date,to_date,total_leave_days", "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 9e6fc6d0f144..5290464d73f7 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -1,7 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals +from typing import Dict import frappe from frappe import _ @@ -34,6 +34,10 @@ class LeaveDayBlockedError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass class AttendanceAlreadyMarkedError(frappe.ValidationError): pass class NotAnOptionalHoliday(frappe.ValidationError): pass +class InsufficientLeaveBalanceError(frappe.ValidationError): + pass +class LeaveAcrossAllocationsError(frappe.ValidationError): + pass from frappe.model.document import Document @@ -127,21 +131,35 @@ def validate_dates(self): def validate_dates_across_allocation(self): if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): return + + alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() + + if not (alloc_on_from_date or alloc_on_to_date): + frappe.throw(_("Application period cannot be outside leave allocation period")) + elif self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date): + frappe.throw(_("Application period cannot be across two allocation records"), exc=LeaveAcrossAllocationsError) + + def get_allocation_based_on_application_dates(self): + """Returns allocation name, from and to dates for application dates""" def _get_leave_allocation_record(date): - allocation = frappe.db.sql("""select name from `tabLeave Allocation` - where employee=%s and leave_type=%s and docstatus=1 - and %s between from_date and to_date""", (self.employee, self.leave_type, date)) + LeaveAllocation = frappe.qb.DocType("Leave Allocation") + allocation = ( + frappe.qb.from_(LeaveAllocation) + .select(LeaveAllocation.name, LeaveAllocation.from_date, LeaveAllocation.to_date) + .where( + (LeaveAllocation.employee == self.employee) + & (LeaveAllocation.leave_type == self.leave_type) + & (LeaveAllocation.docstatus == 1) + & ((date >= LeaveAllocation.from_date) & (date <= LeaveAllocation.to_date)) + ) + ).run(as_dict=True) - return allocation and allocation[0][0] + return allocation and allocation[0] allocation_based_on_from_date = _get_leave_allocation_record(self.from_date) allocation_based_on_to_date = _get_leave_allocation_record(self.to_date) - if not (allocation_based_on_from_date or allocation_based_on_to_date): - frappe.throw(_("Application period cannot be outside leave allocation period")) - - elif allocation_based_on_from_date != allocation_based_on_to_date: - frappe.throw(_("Application period cannot be across two allocation records")) + return allocation_based_on_from_date, allocation_based_on_to_date def validate_back_dated_application(self): future_allocation = frappe.db.sql("""select name, from_date from `tabLeave Allocation` @@ -229,15 +247,34 @@ def validate_balance_leaves(self): 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, self.to_date, - consider_all_leaves_in_the_allocation_period=True) - 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)) - else: - frappe.throw(_("There is not enough leave balance for Leave Type {0}") - .format(self.leave_type)) + 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, for_consumption=True) + self.leave_balance = leave_balance.get("leave_balance") + leave_balance_for_consumption = leave_balance.get("leave_balance_for_consumption") + + if self.status != "Rejected" and (leave_balance_for_consumption < self.total_leave_days or not leave_balance_for_consumption): + self.show_insufficient_balance_message(leave_balance_for_consumption) + + def show_insufficient_balance_message(self, leave_balance_for_consumption): + alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() + + if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"): + if leave_balance_for_consumption != self.leave_balance: + msg = _("Warning: Insufficient leave balance for Leave Type {0} in this allocation.").format(frappe.bold(self.leave_type)) + msg += "

" + msg += _("Actual leave balance is {0} but only {1} leave(s) can be consumed between {2} (Application Date) and {3} (Allocation Expiry).").format( + frappe.bold(self.leave_balance), frappe.bold(leave_balance_for_consumption), + frappe.bold(formatdate(self.from_date)), + frappe.bold(formatdate(alloc_on_from_date.to_date))) + msg += "
" + msg += _("Remaining leaves would be compensated in the next allocation.") + else: + msg = _("Warning: Insufficient leave balance for Leave Type {0}.").format(frappe.bold(self.leave_type)) + + frappe.msgprint(msg, title=_("Warning"), indicator="orange") + else: + frappe.throw(_("Insufficient leave balance for Leave Type {0}").format(frappe.bold(self.leave_type)), + exc=InsufficientLeaveBalanceError, title=_("Insufficient Balance")) def validate_leave_overlap(self): if not self.name: @@ -394,54 +431,111 @@ def create_leave_ledger_entry(self, submit=True): if self.status != 'Approved' and submit: return - expiry_date = get_allocation_expiry(self.employee, self.leave_type, + expiry_date = get_allocation_expiry_for_cf_leaves(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: - raise_exception = True - if frappe.flags.in_patch: - raise_exception=False - - args = dict( - leaves=self.total_leave_days * -1, - from_date=self.from_date, - to_date=self.to_date, - is_lwp=lwp, - holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' - ) - 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 ''' + alloc_on_from_date, alloc_on_to_date = self.get_allocation_based_on_application_dates() + if self.is_separate_ledger_entry_required(alloc_on_from_date, alloc_on_to_date): + # required only if negative balance is allowed for leave type + # else will be stopped in validation itself + self.create_separate_ledger_entries(alloc_on_from_date, alloc_on_to_date, submit, lwp) + else: + raise_exception = False if frappe.flags.in_patch else True + args = dict( + leaves=self.total_leave_days * -1, + from_date=self.from_date, + to_date=self.to_date, + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' + ) + create_leave_ledger_entry(self, args, submit) + + def is_separate_ledger_entry_required(self, alloc_on_from_date=None, alloc_on_to_date=None) -> bool: + """Checks if application dates fall in separate allocations""" + if ((alloc_on_from_date and not alloc_on_to_date) + or (not alloc_on_from_date and alloc_on_to_date) + or (alloc_on_from_date and alloc_on_to_date and alloc_on_from_date.name != alloc_on_to_date.name)): + return True + return False + + def create_separate_ledger_entries(self, alloc_on_from_date, alloc_on_to_date, submit, lwp): + """Creates separate ledger entries for application period falling into separate allocations""" + # for creating separate ledger entries existing allocation periods should be consecutive + if submit and alloc_on_from_date and alloc_on_to_date and add_days(alloc_on_from_date.to_date, 1) != alloc_on_to_date.from_date: + frappe.throw(_("Leave Application period cannot be across two non-consecutive leave allocations {0} and {1}.").format( + get_link_to_form("Leave Allocation", alloc_on_from_date.name), get_link_to_form("Leave Allocation", alloc_on_to_date))) + + raise_exception = False if frappe.flags.in_patch else True + + if alloc_on_from_date: + first_alloc_end = alloc_on_from_date.to_date + second_alloc_start = add_days(alloc_on_from_date.to_date, 1) + else: + first_alloc_end = add_days(alloc_on_to_date.from_date, -1) + second_alloc_start = alloc_on_to_date.from_date - raise_exception = True - if frappe.flags.in_patch: - raise_exception=False + leaves_in_first_alloc = get_number_of_leave_days(self.employee, self.leave_type, + self.from_date, first_alloc_end, self.half_day, self.half_day_date) + leaves_in_second_alloc = get_number_of_leave_days(self.employee, self.leave_type, + second_alloc_start, self.to_date, self.half_day, self.half_day_date) args = dict( - from_date=self.from_date, - to_date=expiry_date, - leaves=(date_diff(expiry_date, self.from_date) + 1) * -1, is_lwp=lwp, holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' ) - create_leave_ledger_entry(self, args, submit) - if getdate(expiry_date) != getdate(self.to_date): - start_date = add_days(expiry_date, 1) + if leaves_in_first_alloc: + args.update(dict( + from_date=self.from_date, + to_date=first_alloc_end, + leaves=leaves_in_first_alloc * -1 + )) + create_leave_ledger_entry(self, args, submit) + + if leaves_in_second_alloc: args.update(dict( - from_date=start_date, + from_date=second_alloc_start, to_date=self.to_date, - leaves=date_diff(self.to_date, expiry_date) * -1 + leaves=leaves_in_second_alloc * -1 )) 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""" + raise_exception = False if frappe.flags.in_patch else True + + leaves = get_number_of_leave_days(self.employee, self.leave_type, + self.from_date, expiry_date, self.half_day, self.half_day_date) -def get_allocation_expiry(employee, leave_type, to_date, from_date): + if leaves: + args = dict( + from_date=self.from_date, + to_date=expiry_date, + leaves=leaves * -1, + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee, raise_exception=raise_exception) or '' + ) + create_leave_ledger_entry(self, args, submit) + + if getdate(expiry_date) != getdate(self.to_date): + start_date = add_days(expiry_date, 1) + leaves = get_number_of_leave_days(self.employee, self.leave_type, + start_date, self.to_date, self.half_day, self.half_day_date) + + if leaves: + args.update(dict( + from_date=start_date, + to_date=self.to_date, + leaves=leaves * -1 + )) + create_leave_ledger_entry(self, args, submit) + + +def get_allocation_expiry_for_cf_leaves(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={ @@ -449,7 +543,8 @@ def get_allocation_expiry(employee, leave_type, to_date, from_date): 'leave_type': leave_type, 'is_carry_forward': 1, 'transaction_type': 'Leave Allocation', - 'to_date': ['between', (from_date, to_date)] + 'to_date': ['between', (from_date, to_date)], + 'docstatus': 1 },fields=['to_date']) return expiry[0]['to_date'] if expiry else None @@ -483,6 +578,7 @@ def get_leave_details(employee, date): 'to_date': ('>=', date), 'employee': employee, 'leave_type': allocation.leave_type, + 'docstatus': 1 }, 'SUM(total_leaves_allocated)') or 0 remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date, @@ -490,29 +586,28 @@ def get_leave_details(employee, date): 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) + leaves_pending = get_leaves_pending_approval_for_period(employee, d, allocation.from_date, end_date) leave_allocation[d] = { "total_leaves": total_allocated_leaves, "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken), "leaves_taken": leaves_taken, - "pending_leaves": leaves_pending, + "leaves_pending_approval": leaves_pending, "remaining_leaves": remaining_leaves} #is used in set query - lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1}) - lwps = [lwp.name for lwp in lwps] + lwp = frappe.get_list("Leave Type", filters={"is_lwp": 1}, pluck="name") - ret = { - 'leave_allocation': leave_allocation, - 'leave_approver': get_leave_approver(employee), - 'lwps': lwps + return { + "leave_allocation": leave_allocation, + "leave_approver": get_leave_approver(employee), + "lwps": lwp } - return ret @frappe.whitelist() -def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_leaves_in_the_allocation_period=False): +def get_leave_balance_on(employee, leave_type, date, to_date=None, + consider_all_leaves_in_the_allocation_period=False, for_consumption=False): ''' Returns leave balance till date :param employee: employee name @@ -520,6 +615,11 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_ :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 + :param for_consumption: flag to check if leave balance is required for consumption or display + eg: employee has leave balance = 10 but allocation is expiring in 1 day so employee can only consume 1 leave + in this case leave_balance = 10 but leave_balance_for_consumption = 1 + if True, returns a dict eg: {'leave_balance': 10, 'leave_balance_for_consumption': 1} + else, returns leave_balance (in this case 10) ''' if not to_date: @@ -529,11 +629,17 @@ def get_leave_balance_on(employee, leave_type, date, to_date=None, consider_all_ allocation = allocation_records.get(leave_type, frappe._dict()) 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) + cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date) leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date) - return get_remaining_leaves(allocation, leaves_taken, date, expiry) + remaining_leaves = get_remaining_leaves(allocation, leaves_taken, date, cf_expiry) + + if for_consumption: + return remaining_leaves + else: + return remaining_leaves.get('leave_balance') + def get_leave_allocation_records(employee, date, leave_type=None): ''' returns the total allocated leaves and carry forwarded leaves based on ledger entries ''' @@ -571,7 +677,7 @@ def get_leave_allocation_records(employee, date, leave_type=None): })) return allocated_leaves -def get_pending_leaves_for_period(employee, leave_type, from_date, to_date): +def get_leaves_pending_approval_for_period(employee, leave_type, from_date, to_date): ''' Returns leaves that are pending approval ''' leaves = frappe.get_all("Leave Application", filters={ @@ -585,38 +691,44 @@ def get_pending_leaves_for_period(employee, leave_type, from_date, to_date): }, fields=['SUM(total_leave_days) as leaves'])[0] return leaves['leaves'] if leaves['leaves'] else 0.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(allocation, leaves_taken, date, cf_expiry) -> Dict[str, float]: + '''Returns a dict of leave_balance and leave_balance_for_consumption + leave_balance returns the available leave balance + leave_balance_for_consumption returns the minimum leaves remaining after comparing with remaining days for allocation expiry + ''' def _get_remaining_leaves(remaining_leaves, end_date): - + ''' Returns minimum leaves remaining after comparing with remaining days for allocation expiry ''' if remaining_leaves > 0: remaining_days = date_diff(end_date, date) + 1 remaining_leaves = min(remaining_days, remaining_leaves) return remaining_leaves - total_leaves = flt(allocation.total_leaves_allocated) + flt(leaves_taken) + leave_balance = leave_balance_for_consumption = flt(allocation.total_leaves_allocated) + flt(leaves_taken) - if expiry and allocation.unused_leaves: - remaining_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) - remaining_leaves = _get_remaining_leaves(remaining_leaves, expiry) + # balance for carry forwarded leaves + if cf_expiry and allocation.unused_leaves: + cf_leaves = flt(allocation.unused_leaves) + flt(leaves_taken) + remaining_cf_leaves = _get_remaining_leaves(cf_leaves, cf_expiry) - total_leaves = flt(allocation.new_leaves_allocated) + flt(remaining_leaves) + leave_balance = flt(allocation.new_leaves_allocated) + flt(cf_leaves) + leave_balance_for_consumption = flt(allocation.new_leaves_allocated) + flt(remaining_cf_leaves) - return _get_remaining_leaves(total_leaves, allocation.to_date) + remaining_leaves = _get_remaining_leaves(leave_balance_for_consumption, allocation.to_date) + return frappe._dict(leave_balance=leave_balance, leave_balance_for_consumption=remaining_leaves) -def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_expired_leaves=False): +def get_leaves_for_period(employee, leave_type, from_date, to_date, skip_expired_leaves=True): 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': + 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 leave_entry.is_expired \ - and (do_not_skip_expired_leaves or not skip_expiry_leaves(leave_entry, to_date)): + and not skip_expired_leaves: leave_days += leave_entry.leaves elif leave_entry.transaction_type == 'Leave Application': @@ -638,11 +750,6 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date, do_not_skip_ return leave_days -def skip_expiry_leaves(leave_entry, date): - ''' Checks whether the expired leaves coincide with the to_date of leave balance check. - This allows backdated leave entry creation for non carry forwarded allocation ''' - 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. ''' diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html index 9f667a68356a..8091697a7df6 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html @@ -4,11 +4,11 @@ {{ __("Leave Type") }} - {{ __("Total Allocated Leave") }} - {{ __("Expired Leave") }} - {{ __("Used Leave") }} - {{ __("Pending Leave") }} - {{ __("Available Leave") }} + {{ __("Total Allocated Leave(s)") }} + {{ __("Expired Leave(s)") }} + {{ __("Used Leave(s)") }} + {{ __("Leave(s) Pending Approval") }} + {{ __("Available Leave(s)") }} @@ -18,12 +18,12 @@ {%= value["total_leaves"] %} {%= value["expired_leaves"] %} {%= value["leaves_taken"] %} - {%= value["pending_leaves"] %} + {%= value["leaves_pending_approval"] %} {%= value["remaining_leaves"] %} {% } %} {% else %} -

No Leave has been allocated.

+

{{ __("No Leave has been allocated.") }}

{% endif %} diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py index c45717f5870f..3878fcd641fd 100644 --- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.py +++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.py @@ -14,7 +14,7 @@ def get_data(): 'reports': [ { 'label': _('Reports'), - 'items': ['Employee Leave Balance'] + 'items': [_('Employee Leave Balance')] } ] } diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py index b9c785a8a9c9..abb71fc69fce 100644 --- a/erpnext/hr/doctype/leave_application/test_leave_application.py +++ b/erpnext/hr/doctype/leave_application/test_leave_application.py @@ -9,19 +9,23 @@ from frappe.utils import add_days, add_months, getdate, nowdate from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation from erpnext.hr.doctype.leave_application.leave_application import ( + InsufficientLeaveBalanceError, + LeaveAcrossAllocationsError, LeaveDayBlockedError, NotAnOptionalHoliday, OverlapError, get_leave_balance_on, + get_leave_details, ) from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( create_assignment_for_multiple_employees, ) from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type -test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"] +test_dependencies = ["Leave Type", "Leave Allocation", "Leave Block List", "Employee"] _test_records = [ { @@ -60,14 +64,23 @@ class TestLeaveApplication(unittest.TestCase): def setUp(self): for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]: - frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec + frappe.db.delete(dt) @classmethod def setUpClass(cls): set_leave_approver() - frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'") + frappe.db.delete("Attendance", {"employee": "_T-Employee-00001"}) + self.holiday_list = make_holiday_list() + + if not frappe.db.exists("Leave Type", "_Test Leave Type"): + frappe.get_doc(dict( + leave_type_name="_Test Leave Type", + doctype="Leave Type", + include_holiday=True + )).insert() def tearDown(self): + frappe.db.rollback() frappe.set_user("Administrator") def _clear_roles(self): @@ -83,6 +96,132 @@ def get_application(self, doc): application.to_date = "2013-01-05" return application + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_validate_application_across_allocations(self): + # Test validation for application dates when negative balance is disabled + frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Leave Validation", + doctype="Leave Type", + allow_negative=False + )).insert() + + employee = get_employee() + date = getdate() + first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date)) + + leave_application = frappe.get_doc(dict( + doctype='Leave Application', + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, 1), + to_date=add_days(first_sunday, 4), + company="_Test Company", + status="Approved", + leave_approver = 'test@example.com' + )) + # Application period cannot be outside leave allocation period + self.assertRaises(frappe.ValidationError, leave_application.insert) + + make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + + leave_application = frappe.get_doc(dict( + doctype='Leave Application', + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, -10), + to_date=add_days(first_sunday, 1), + company="_Test Company", + status="Approved", + leave_approver = 'test@example.com' + )) + + # Application period cannot be across two allocation records + self.assertRaises(LeaveAcrossAllocationsError, leave_application.insert) + + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_insufficient_leave_balance_validation(self): + # CASE 1: Validation when allow negative is disabled + frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Leave Validation", + doctype="Leave Type", + allow_negative=False + )).insert() + + employee = get_employee() + date = getdate() + first_sunday = get_first_sunday(self.holiday_list, for_date=get_year_start(date)) + + # allocate 2 leaves, apply for more + make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date), leaves=2) + leave_application = frappe.get_doc(dict( + doctype='Leave Application', + employee=employee.name, + leave_type=leave_type.name, + from_date=add_days(first_sunday, 1), + to_date=add_days(first_sunday, 3), + company="_Test Company", + status="Approved", + leave_approver = 'test@example.com' + )) + self.assertRaises(InsufficientLeaveBalanceError, leave_application.insert) + + # CASE 2: Allows creating application with a warning message when allow negative is enabled + frappe.db.set_value("Leave Type", "Test Leave Validation", "allow_negative", True) + make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 3), leave_type.name) + + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_separate_leave_ledger_entry_for_boundary_applications(self): + # When application falls in 2 different allocations and Allow Negative is enabled + # creates separate leave ledger entries + frappe.delete_doc_if_exists("Leave Type", "Test Leave Validation", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Leave Validation", + doctype="Leave Type", + allow_negative=True + )).insert() + + employee = get_employee() + date = getdate() + year_start = getdate(get_year_start(date)) + year_end = getdate(get_year_ending(date)) + + make_allocation_record(leave_type=leave_type.name, from_date=year_start, to_date=year_end) + # application across allocations + + # CASE 1: from date has no allocation, to date has an allocation / both dates have allocation + application = make_leave_application(employee.name, add_days(year_start, -10), add_days(year_start, 3), leave_type.name) + + # 2 separate leave ledger entries + ledgers = frappe.db.get_all("Leave Ledger Entry", { + "transaction_type": "Leave Application", + "transaction_name": application.name + }, ["leaves", "from_date", "to_date"], order_by="from_date") + self.assertEqual(len(ledgers), 2) + + self.assertEqual(ledgers[0].from_date, application.from_date) + self.assertEqual(ledgers[0].to_date, add_days(year_start, -1)) + + self.assertEqual(ledgers[1].from_date, year_start) + self.assertEqual(ledgers[1].to_date, application.to_date) + + # CASE 2: from date has an allocation, to date has no allocation + application = make_leave_application(employee.name, add_days(year_end, -3), add_days(year_end, 5), leave_type.name) + + # 2 separate leave ledger entries + ledgers = frappe.db.get_all("Leave Ledger Entry", { + "transaction_type": "Leave Application", + "transaction_name": application.name + }, ["leaves", "from_date", "to_date"], order_by="from_date") + self.assertEqual(len(ledgers), 2) + + self.assertEqual(ledgers[0].from_date, application.from_date) + self.assertEqual(ledgers[0].to_date, year_end) + + self.assertEqual(ledgers[1].from_date, add_days(year_end, 1)) + self.assertEqual(ledgers[1].to_date, application.to_date) + def test_overwrite_attendance(self): '''check attendance is automatically created on leave approval''' make_allocation_record() @@ -107,6 +246,77 @@ def test_overwrite_attendance(self): for d in ('2018-01-01', '2018-01-02', '2018-01-03'): self.assertTrue(getdate(d) in dates) + + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_attendance_for_include_holidays(self): + # Case 1: leave type with 'Include holidays within leaves as leaves' enabled + frappe.delete_doc_if_exists("Leave Type", "Test Include Holidays", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Include Holidays", + doctype="Leave Type", + include_holiday=True + )).insert() + + date = getdate() + make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + + employee = get_employee() + first_sunday = get_first_sunday(self.holiday_list) + + leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), leave_type.name) + leave_application.reload() + self.assertEqual(leave_application.total_leave_days, 4) + self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4) + + leave_application.cancel() + + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_attendance_update_for_exclude_holidays(self): + # Case 2: leave type with 'Include holidays within leaves as leaves' disabled + frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1) + leave_type = frappe.get_doc(dict( + leave_type_name="Test Do Not Include Holidays", + doctype="Leave Type", + include_holiday=False + )).insert() + + date = getdate() + make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) + + employee = get_employee() + first_sunday = get_first_sunday(self.holiday_list) + + # already marked attendance on a holiday should be deleted in this case + config = { + "doctype": "Attendance", + "employee": employee.name, + "status": "Present" + } + attendance_on_holiday = frappe.get_doc(config) + attendance_on_holiday.attendance_date = first_sunday + attendance_on_holiday.flags.ignore_validate = True + attendance_on_holiday.save() + + # already marked attendance on a non-holiday should be updated + attendance = frappe.get_doc(config) + attendance.attendance_date = add_days(first_sunday, 3) + attendance.flags.ignore_validate = True + attendance.save() + + leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company) + leave_application.reload() + # holiday should be excluded while marking attendance + self.assertEqual(leave_application.total_leave_days, 3) + self.assertEqual(frappe.db.count("Attendance", {"leave_application": leave_application.name}), 3) + + # attendance on holiday deleted + self.assertFalse(frappe.db.exists("Attendance", attendance_on_holiday.name)) + + # attendance on non-holiday updated + self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave") + + frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list) + def test_block_list(self): self._clear_roles() @@ -237,11 +447,15 @@ def test_overlap_with_half_day_3(self): application.half_day_date = "2013-01-05" application.insert() + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') def test_optional_leave(self): leave_period = get_leave_period() today = nowdate() holiday_list = 'Test Holiday List for Optional Holiday' - optional_leave_date = add_days(today, 7) + employee = get_employee() + + first_sunday = get_first_sunday(self.holiday_list) + optional_leave_date = add_days(first_sunday, 1) if not frappe.db.exists('Holiday List', holiday_list): frappe.get_doc(dict( @@ -415,11 +629,13 @@ def test_leave_balance_near_allocaton_expiry(self): leave_type_name="_Test_CF_leave_expiry", is_carry_forward=1, expire_carry_forwarded_leaves_after_days=90) - leave_type.submit() + leave_type.insert() create_carry_forwarded_allocation(employee, leave_type) + details = get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8), for_consumption=True) - self.assertEqual(get_leave_balance_on(employee.name, leave_type.name, nowdate(), add_days(nowdate(), 8)), 21) + self.assertEqual(details.leave_balance_for_consumption, 21) + self.assertEqual(details.leave_balance, 30) def test_earned_leaves_creation(self): @@ -472,7 +688,14 @@ def test_earned_leaves_creation(self): # test to not consider current leave in leave balance while submitting def test_current_leave_on_submit(self): employee = get_employee() - leave_type = 'Sick leave' + + leave_type = 'Sick Leave' + if not frappe.db.exists('Leave Type', leave_type): + frappe.get_doc(dict( + leave_type_name=leave_type, + doctype='Leave Type' + )).insert() + allocation = frappe.get_doc(dict( doctype = 'Leave Allocation', employee = employee.name, @@ -615,6 +838,35 @@ def test_leave_approver_perms(self): employee.leave_approver = "" employee.save() + @set_holiday_list('Salary Slip Test Holiday List', '_Test Company') + def test_get_leave_details_for_dashboard(self): + employee = get_employee() + date = getdate() + year_start = getdate(get_year_start(date)) + year_end = getdate(get_year_ending(date)) + + # ALLOCATION = 30 + allocation = make_allocation_record(employee=employee.name, from_date=year_start, to_date=year_end) + + # USED LEAVES = 4 + first_sunday = get_first_sunday(self.holiday_list) + leave_application = make_leave_application(employee.name, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application.reload() + + # LEAVES PENDING APPROVAL = 1 + leave_application = make_leave_application(employee.name, add_days(first_sunday, 5), add_days(first_sunday, 5), + '_Test Leave Type', submit=False) + leave_application.status = 'Open' + leave_application.save() + + details = get_leave_details(employee.name, allocation.from_date) + leave_allocation = details['leave_allocation']['_Test Leave Type'] + self.assertEqual(leave_allocation['total_leaves'], 30) + self.assertEqual(leave_allocation['leaves_taken'], 4) + self.assertEqual(leave_allocation['expired_leaves'], 0) + self.assertEqual(leave_allocation['leaves_pending_approval'], 1) + self.assertEqual(leave_allocation['remaining_leaves'], 26) + def create_carry_forwarded_allocation(employee, leave_type): # initial leave allocation @@ -636,19 +888,23 @@ def create_carry_forwarded_allocation(employee, leave_type): carry_forward=1) leave_allocation.submit() -def make_allocation_record(employee=None, leave_type=None): + +def make_allocation_record(employee=None, leave_type=None, from_date=None, to_date=None, carry_forward=False, leaves=None): allocation = frappe.get_doc({ "doctype": "Leave Allocation", "employee": employee or "_T-Employee-00001", "leave_type": leave_type or "_Test Leave Type", - "from_date": "2013-01-01", - "to_date": "2019-12-31", - "new_leaves_allocated": 30 + "from_date": from_date or "2013-01-01", + "to_date": to_date or "2019-12-31", + "new_leaves_allocated": leaves or 30, + "carry_forward": carry_forward }) allocation.insert(ignore_permissions=True) allocation.submit() + return allocation + def get_employee(): return frappe.get_doc("Employee", "_T-Employee-00001") diff --git a/erpnext/hr/doctype/leave_block_list/leave_block_list.json b/erpnext/hr/doctype/leave_block_list/leave_block_list.json index 298a8f38033e..a0eb8e0f93e8 100644 --- a/erpnext/hr/doctype/leave_block_list/leave_block_list.json +++ b/erpnext/hr/doctype/leave_block_list/leave_block_list.json @@ -70,7 +70,7 @@ "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2021-05-12 20:01:53.938972", + "modified": "2021-05-12 21:01:53.938972", "modified_by": "Administrator", "module": "HR", "name": "Leave Block List", @@ -86,7 +86,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.json b/erpnext/hr/doctype/leave_encashment/leave_encashment.json index b31b82f7d58a..d6564dd088bb 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.json +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.json @@ -154,7 +154,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:01:53.652300", + "modified": "2021-05-12 21:01:53.652300", "modified_by": "Administrator", "module": "HR", "name": "Leave Encashment", @@ -218,7 +218,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py index 6cf96851556c..139b958c7de7 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py @@ -173,7 +173,7 @@ 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, do_not_skip_expired_leaves=True) + allocation.from_date, allocation.to_date, skip_expired_leaves=False) leaves = flt(allocation.leaves) + flt(leaves_taken) # allow expired leaves entry to be created diff --git a/erpnext/hr/doctype/leave_period/leave_period.json b/erpnext/hr/doctype/leave_period/leave_period.json index 728c74e65311..875297284e9a 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.json +++ b/erpnext/hr/doctype/leave_period/leave_period.json @@ -56,7 +56,7 @@ } ], "links": [], - "modified": "2021-05-13 19:59:23.030463", + "modified": "2021-05-14 19:59:23.030463", "modified_by": "Administrator", "module": "HR", "name": "Leave Period", @@ -99,7 +99,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/hr/doctype/leave_policy/leave_policy.json b/erpnext/hr/doctype/leave_policy/leave_policy.json index bc20c572027b..84a0804ba240 100644 --- a/erpnext/hr/doctype/leave_policy/leave_policy.json +++ b/erpnext/hr/doctype/leave_policy/leave_policy.json @@ -36,7 +36,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:01:52.959552", + "modified": "2021-05-12 21:01:52.959552", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy", @@ -88,7 +88,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json index 4123e0055f8f..b1d7319c6261 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -113,7 +113,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-05-13 19:59:10.997369", + "modified": "2021-05-14 19:59:10.997369", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy Assignment", @@ -162,7 +162,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index f62b3002194b..d15c6185db81 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -133,6 +133,8 @@ def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_t monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding) new_leaves_allocated = monthly_earned_leave * months_passed + else: + new_leaves_allocated = 0 return new_leaves_allocated diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js index 8b954c46a10e..c482cad4eb25 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js @@ -79,7 +79,7 @@ frappe.listview_settings['Leave Policy Assignment'] = { }; }, add_filters_group: 1, - primary_action_label: "Assign", + primary_action_label: __("Assign"), action(employees, data) { frappe.call({ method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.create_assignment_for_multiple_employees', diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index cbb26a1e285a..0668044bc12d 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -6,6 +6,7 @@ import unittest import frappe +from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate from erpnext.hr.doctype.leave_application.test_leave_application import ( get_employee, @@ -100,8 +101,208 @@ def test_allow_to_grant_all_leave_after_cancellation_of_every_leave_allocation(s leave_policy_assignment_doc.reload() - # User are now allowed to grant leave - self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0) + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + + # second last day of the month + # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency + frappe.flags.current_date = add_days(get_last_day(getdate()), -1) + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 0) + + def test_earned_leave_alloc_for_passed_months_based_on_leave_period(self): + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -1))) + + # Case 1: assignment created one month after the leave period, should allocate 1 leave + frappe.flags.current_date = get_first_day(getdate()) + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 1) + + def test_earned_leave_alloc_for_passed_months_on_month_end_based_on_leave_period(self): + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) + # Case 2: assignment created on the last day of the leave period's latter month + # should allocate 1 leave for current month even though the month has not ended + # since the daily job might have already executed + frappe.flags.current_date = get_last_day(getdate()) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # if the daily job is not completed yet, there is another check present + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self): + from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation + + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2))) + # initial leave allocation = 5 + leave_allocation = create_leave_allocation(employee=self.employee.name, employee_name=self.employee.employee_name, leave_type="Test Earned Leave", + from_date=add_months(getdate(), -12), to_date=add_months(getdate(), -3), new_leaves_allocated=5, carry_forward=0) + leave_allocation.submit() + + # Case 3: assignment created on the last day of the leave period's latter month with carry forwarding + frappe.flags.current_date = get_last_day(add_months(getdate(), -1)) + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name, + "carry_forward": 1 + } + # carry forwarded leaves = 5, 3 leaves allocated for passed months + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + + details = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, ["total_leaves_allocated", "new_leaves_allocated", "unused_leaves", "name"], as_dict=True) + self.assertEqual(details.new_leaves_allocated, 2) + self.assertEqual(details.unused_leaves, 5) + self.assertEqual(details.total_leaves_allocated, 7) + + # if the daily job is not completed yet, there is another check present + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import is_earned_leave_already_allocated + frappe.flags.current_date = get_last_day(getdate()) + + allocation = frappe.get_doc("Leave Allocation", details.name) + # 1 leave is still pending to be allocated, irrespective of carry forwarded leaves + self.assertFalse(is_earned_leave_already_allocated(allocation, leave_policy.leave_policy_details[0].annual_allocation)) + + def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self): + # tests leave alloc for earned leaves for assignment based on joining date in policy assignment + leave_type = create_earned_leave_type("Test Earned Leave") + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).submit() + + # joining date set to 2 months back + self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) + self.employee.save() + + # assignment created on the last day of the current month + frappe.flags.current_date = get_last_day(getdate()) + data = { + "assignment_based_on": "Joining Date", + "leave_policy": leave_policy.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + self.assertEqual(effective_from, self.employee.date_of_joining) + self.assertEqual(leaves_allocated, 3) + + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_last_day(getdate()) + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): + # tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type + leave_period, leave_policy = setup_leave_period_and_policy(get_first_day(add_months(getdate(), -2)), based_on_doj=True) + + # joining date set to 2 months back + self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) + self.employee.save() + + # assignment created on the same day of the current month, should allocate leaves including the current month + frappe.flags.current_date = get_first_day(getdate()) + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + # if the daily job is not completed yet, there is another check present + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) + + def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): + # tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type + leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "title": "Test Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 12}] + }).submit() + + # joining date set to 2 months back + # leave should be allocated for current month too since this day is same as the joining day + self.employee.date_of_joining = get_first_day(add_months(getdate(), -2)) + self.employee.save() + + # assignment created on the first day of the current month + frappe.flags.current_date = get_first_day(getdate()) + data = { + "assignment_based_on": "Joining Date", + "leave_policy": leave_policy.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([self.employee.name], frappe._dict(data)) + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + effective_from = frappe.db.get_value("Leave Policy Assignment", leave_policy_assignments[0], "effective_from") + self.assertEqual(effective_from, self.employee.date_of_joining) + self.assertEqual(leaves_allocated, 3) + + # to ensure leave is not already allocated to avoid duplication + from erpnext.hr.utils import allocate_earned_leaves + frappe.flags.current_date = get_first_day(getdate()) + allocate_earned_leaves() + + leaves_allocated = frappe.db.get_value("Leave Allocation", {"leave_policy_assignment": leave_policy_assignments[0]}, + "total_leaves_allocated") + self.assertEqual(leaves_allocated, 3) def tearDown(self): for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json index 75f689114e8b..8d711171a4fd 100644 --- a/erpnext/hr/doctype/leave_type/leave_type.json +++ b/erpnext/hr/doctype/leave_type/leave_type.json @@ -126,6 +126,7 @@ "collapsible": 1, "fieldname": "encashment", "fieldtype": "Section Break", + "hidden": 1, "label": "Encashment" }, { @@ -214,7 +215,7 @@ "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2021-08-13 16:10:36.464690", + "modified": "2023-07-31 12:54:34.847134", "modified_by": "Administrator", "module": "HR", "name": "Leave Type", @@ -247,7 +248,7 @@ "role": "Employee" } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.json b/erpnext/hr/doctype/shift_assignment/shift_assignment.json index d7800f2fcd69..42fb6525eab6 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.json +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.json @@ -101,7 +101,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:01:56.161205", + "modified": "2021-05-13 20:01:56.161205", "modified_by": "Administrator", "module": "HR", "name": "Shift Assignment", @@ -144,7 +144,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "title_field": "employee_name", diff --git a/erpnext/hr/doctype/shift_request/shift_request.json b/erpnext/hr/doctype/shift_request/shift_request.json index 1fee05f7eee2..b78ee7d3f571 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.json +++ b/erpnext/hr/doctype/shift_request/shift_request.json @@ -103,7 +103,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:01:55.912706", + "modified": "2021-05-13 20:01:55.912706", "modified_by": "Administrator", "module": "HR", "name": "Shift Request", @@ -148,7 +148,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "title_field": "employee_name", diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json index 99e876d7ca6c..648fefb903e5 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.json +++ b/erpnext/hr/doctype/shift_type/shift_type.json @@ -160,7 +160,7 @@ } ], "links": [], - "modified": "2021-05-12 20:01:55.578405", + "modified": "2021-05-13 20:01:55.578405", "modified_by": "Administrator", "module": "HR", "name": "Shift Type", @@ -200,7 +200,7 @@ } ], "quick_entry": 1, - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/hr/doctype/travel_itinerary/travel_itinerary.json b/erpnext/hr/doctype/travel_itinerary/travel_itinerary.json index f280b0591243..505d5efb5a8c 100644 --- a/erpnext/hr/doctype/travel_itinerary/travel_itinerary.json +++ b/erpnext/hr/doctype/travel_itinerary/travel_itinerary.json @@ -39,13 +39,13 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Mode of Travel", - "options": "\nFlight\nTrain\nTaxi\nRented Car" + "options": "\nAvi\u00f3n\nTren\nTaxi\nAuto\nBarco/Catamar\u00e1n" }, { "fieldname": "meal_preference", "fieldtype": "Select", "label": "Meal Preference", - "options": "\nVegetarian\nNon-Vegetarian\nGluten Free\nNon Diary" + "options": "\nVegetariano\nNo Vegetariano\nSin Gluten\nNo Lacteos\nVegano\nOtro" }, { "default": "0", @@ -110,7 +110,7 @@ ], "istable": 1, "links": [], - "modified": "2021-05-12 19:59:28.401414", + "modified": "2023-08-01 11:04:32.045630", "modified_by": "Administrator", "module": "HR", "name": "Travel Itinerary", @@ -118,6 +118,7 @@ "permissions": [], "quick_entry": 1, "restrict_to_domain": "HR", + "route": "app/doctype/Travel%20Itinerary", "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/hr/doctype/travel_request/travel_request.js b/erpnext/hr/doctype/travel_request/travel_request.js index 9dd48eb38e9a..f39de9599332 100644 --- a/erpnext/hr/doctype/travel_request/travel_request.js +++ b/erpnext/hr/doctype/travel_request/travel_request.js @@ -3,6 +3,22 @@ frappe.ui.form.on('Travel Request', { refresh: function(frm) { + if (frm.doc.docstatus == 1) { + frm.add_custom_button(__("Employee Advance"), function() { + frappe.call({ + method: "erpnext.hr.doctype.travel_request.travel_request.make_employee_advance", + args: { + "dt": frm.doc.doctype, + "dn": frm.doc.name + }, + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + }, __('Create')); - } + frm.page.set_inner_btn_group_as_primary(__('Create')); + } + }, }); diff --git a/erpnext/hr/doctype/travel_request/travel_request.json b/erpnext/hr/doctype/travel_request/travel_request.json index 137dd0e02bd0..a56a5051400f 100644 --- a/erpnext/hr/doctype/travel_request/travel_request.json +++ b/erpnext/hr/doctype/travel_request/travel_request.json @@ -35,7 +35,8 @@ "name_of_organizer", "address_of_organizer", "other_details", - "amended_from" + "amended_from", + "employee_advance" ], "fields": [ { @@ -212,11 +213,19 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "employee_advance", + "fieldtype": "Link", + "hidden": 1, + "label": "Employee Advance", + "options": "Employee Advance", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2021-05-13 19:59:20.945318", + "modified": "2023-08-01 08:15:45.860681", "modified_by": "Administrator", "module": "HR", "name": "Travel Request", @@ -235,7 +244,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Reclutamiento, Capacitacion y Gastos", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/hr/doctype/travel_request/travel_request.py b/erpnext/hr/doctype/travel_request/travel_request.py index b10333fd2d61..de60bfa11757 100644 --- a/erpnext/hr/doctype/travel_request/travel_request.py +++ b/erpnext/hr/doctype/travel_request/travel_request.py @@ -3,12 +3,22 @@ # For license information, please see license.txt from __future__ import unicode_literals - +import frappe from frappe.model.document import Document - from erpnext.hr.utils import validate_active_employee class TravelRequest(Document): def validate(self): validate_active_employee(self.employee) + + +@frappe.whitelist() +def make_employee_advance(dt, dn): + travel_request = frappe.get_doc(dt, dn) + employee_advance = frappe.new_doc('Employee Advance') + employee_advance.employee = travel_request.employee + employee_advance.advance_amount = sum(c.total_amount for c in travel_request.costings) + employee_advance.purpose = travel_request.name + employee_advance.travel_request = travel_request.name + return employee_advance \ No newline at end of file diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.json b/erpnext/hr/doctype/upload_attendance/upload_attendance.json index 665eecefc2cd..689cef1a0f9b 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.json +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.json @@ -63,7 +63,7 @@ "issingle": 1, "links": [], "max_attachments": 0, - "modified": "2021-05-13 20:01:55.082864", + "modified": "2021-05-13 21:01:55.082864", "modified_by": "Administrator", "module": "HR", "name": "Upload Attendance", @@ -82,7 +82,7 @@ "write": 1 } ], - "restrict_to_domain": "HR Asistencia y Vacaciones", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC" } \ No newline at end of file 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 d463b9b62a87..57e52eb7beef 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -4,18 +4,21 @@ from __future__ import unicode_literals from itertools import groupby +from typing import Dict, List, Tuple import frappe from frappe import _ -from frappe.utils import add_days +from frappe.utils import add_days, getdate +from erpnext.hr.doctype.leave_allocation.leave_allocation import get_previous_allocation from erpnext.hr.doctype.leave_application.leave_application import ( get_leave_balance_on, get_leaves_for_period, ) +Filters = frappe._dict -def execute(filters=None): +def execute(filters: Filters = None) -> Tuple: if filters.to_date <= filters.from_date: frappe.throw(_('"From Date" can not be greater than or equal to "To Date"')) @@ -24,8 +27,9 @@ def execute(filters=None): charts = get_chart_data(data) return columns, data, None, charts -def get_columns(): - columns = [{ + +def get_columns() -> List[Dict]: + return [{ 'label': _('Leave Type'), 'fieldtype': 'Link', 'fieldname': 'leave_type', @@ -47,32 +51,31 @@ def get_columns(): 'label': _('Opening Balance'), 'fieldtype': 'float', 'fieldname': 'opening_balance', - 'width': 130, + 'width': 150, }, { - 'label': _('Leave Allocated'), + 'label': _('New Leave(s) Allocated'), 'fieldtype': 'float', 'fieldname': 'leaves_allocated', - 'width': 130, + 'width': 200, }, { - 'label': _('Leave Taken'), + 'label': _('Leave(s) Taken'), 'fieldtype': 'float', 'fieldname': 'leaves_taken', - 'width': 130, + 'width': 150, }, { - 'label': _('Leave Expired'), + 'label': _('Leave(s) Expired'), 'fieldtype': 'float', 'fieldname': 'leaves_expired', - 'width': 130, + 'width': 150, }, { 'label': _('Closing Balance'), 'fieldtype': 'float', 'fieldname': 'closing_balance', - 'width': 130, + 'width': 150, }] - return columns -def get_data(filters): +def get_data(filters: Filters) -> List: leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name') conditions = get_conditions(filters) @@ -103,19 +106,18 @@ def get_data(filters): or ("HR Manager" in frappe.get_roles(user)): if len(active_employees) > 1: row = frappe._dict() - row.employee = employee.name, + row.employee = employee.name row.employee_name = employee.employee_name leaves_taken = get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1 - new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type) - - - opening = get_leave_balance_on(employee.name, leave_type, add_days(filters.from_date, -1)) #allocation boundary condition + new_allocation, expired_leaves, carry_forwarded_leaves = get_allocated_and_expired_leaves( + filters.from_date, filters.to_date, employee.name, leave_type) + opening = get_opening_balance(employee.name, leave_type, filters, carry_forwarded_leaves) row.leaves_allocated = new_allocation - row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0 + row.leaves_expired = expired_leaves row.opening_balance = opening row.leaves_taken = leaves_taken @@ -126,7 +128,26 @@ def get_data(filters): return data -def get_conditions(filters): + +def get_opening_balance(employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float) -> float: + # allocation boundary condition + # opening balance is the closing leave balance 1 day before the filter start date + opening_balance_date = add_days(filters.from_date, -1) + allocation = get_previous_allocation(filters.from_date, leave_type, employee) + + if allocation and allocation.get("to_date") and opening_balance_date and \ + getdate(allocation.get("to_date")) == getdate(opening_balance_date): + # if opening balance date is same as the previous allocation's expiry + # then opening balance should only consider carry forwarded leaves + opening_balance = carry_forwarded_leaves + else: + # else directly get leave balance on the previous day + opening_balance = get_leave_balance_on(employee, leave_type, opening_balance_date) + + return opening_balance + + +def get_conditions(filters: Filters) -> Dict: conditions={ 'status': 'Active', } @@ -141,29 +162,26 @@ def get_conditions(filters): return conditions -def get_department_leave_approver_map(department=None): +def get_department_leave_approver_map(department: str = None): # get current department and all its child department_list = frappe.get_list('Department', - filters={ - 'disabled': 0 - }, - or_filters={ - 'name': department, - 'parent_department': department - }, - fields=['name'], - pluck='name' - ) + filters={'disabled': 0}, + or_filters={ + 'name': department, + 'parent_department': department + }, + pluck='name' + ) # retrieve approvers list from current department and from its subsequent child departments approver_list = frappe.get_all('Department Approver', - filters={ - 'parentfield': 'leave_approvers', - 'parent': ('in', department_list) - }, - fields=['parent', 'approver'], - as_list=1 - ) + filters={ + 'parentfield': 'leave_approvers', + 'parent': ('in', department_list) + }, + fields=['parent', 'approver'], + as_list=True + ) approvers = {} @@ -172,41 +190,61 @@ def get_department_leave_approver_map(department=None): return approvers -def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): - - from frappe.utils import getdate +def get_allocated_and_expired_leaves(from_date: str, to_date: str, employee: str, leave_type: str) -> Tuple[float, float, float]: new_allocation = 0 expired_leaves = 0 + carry_forwarded_leaves = 0 - records= frappe.db.sql(""" - SELECT - employee, leave_type, from_date, to_date, leaves, transaction_name, - transaction_type, is_carry_forward, is_expired - FROM `tabLeave Ledger Entry` - WHERE employee=%(employee)s AND leave_type=%(leave_type)s - AND docstatus=1 - AND transaction_type = 'Leave Allocation' - 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)) - """, { - "from_date": from_date, - "to_date": to_date, - "employee": employee, - "leave_type": leave_type - }, as_dict=1) + records = get_leave_ledger_entries(from_date, to_date, employee, leave_type) for record in records: + # new allocation records with `is_expired=1` are created when leave expires + # these new records should not be considered, else it leads to negative leave balance + if record.is_expired: + continue + if record.to_date < getdate(to_date): + # leave allocations ending before to_date, reduce leaves taken within that period + # since they are already used, they won't expire expired_leaves += record.leaves + expired_leaves += get_leaves_for_period(employee, leave_type, + record.from_date, record.to_date) if record.from_date >= getdate(from_date): - new_allocation += record.leaves - - return new_allocation, expired_leaves - -def get_chart_data(data): + if record.is_carry_forward: + carry_forwarded_leaves += record.leaves + else: + new_allocation += record.leaves + + return new_allocation, expired_leaves, carry_forwarded_leaves + + +def get_leave_ledger_entries(from_date: str, to_date: str, employee: str, leave_type: str) -> List[Dict]: + ledger = frappe.qb.DocType('Leave Ledger Entry') + records = ( + frappe.qb.from_(ledger) + .select( + ledger.employee, ledger.leave_type, ledger.from_date, ledger.to_date, + ledger.leaves, ledger.transaction_name, ledger.transaction_type, + ledger.is_carry_forward, ledger.is_expired + ).where( + (ledger.docstatus == 1) + & (ledger.transaction_type == 'Leave Allocation') + & (ledger.employee == employee) + & (ledger.leave_type == leave_type) + & ( + (ledger.from_date[from_date: to_date]) + | (ledger.to_date[from_date: to_date]) + | ((ledger.from_date < from_date) & (ledger.to_date > to_date)) + ) + ) + ).run(as_dict=True) + + return records + + +def get_chart_data(data: List) -> Dict: labels = [] datasets = [] employee_data = data @@ -225,7 +263,8 @@ def get_chart_data(data): return chart -def get_dataset_for_chart(employee_data, datasets, labels): + +def get_dataset_for_chart(employee_data: List, datasets: List, labels: List) -> List: leaves = [] employee_data = sorted(employee_data, key=lambda k: k['employee_name']) diff --git a/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py new file mode 100644 index 000000000000..b2ed72c04d7e --- /dev/null +++ b/erpnext/hr/report/employee_leave_balance/test_employee_leave_balance.py @@ -0,0 +1,161 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +import unittest + +import frappe +from frappe.utils import add_days, add_months, flt, get_year_ending, get_year_start, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list +from erpnext.hr.doctype.leave_application.test_leave_application import ( + get_first_sunday, + make_allocation_record, +) +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation +from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type +from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) + +test_records = frappe.get_test_records('Leave Type') + +class TestEmployeeLeaveBalance(unittest.TestCase): + def setUp(self): + for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']: + frappe.db.delete(dt) + + frappe.set_user('Administrator') + + self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') + + self.date = getdate() + self.year_start = getdate(get_year_start(self.date)) + self.mid_year = add_months(self.year_start, 6) + self.year_end = getdate(get_year_ending(self.date)) + + self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end) + + def tearDown(self): + frappe.db.rollback() + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_employee_leave_balance(self): + frappe.get_doc(test_records[0]).insert() + + # 5 leaves + allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11), + to_date=add_days(self.year_start, -1), leaves=5) + # 30 leaves + allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + # expires 5 leaves + process_expired_allocation() + + # 4 days leave + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application.reload() + + filters = frappe._dict({ + 'from_date': allocation1.from_date, + 'to_date': allocation2.to_date, + 'employee': self.employee_id + }) + + report = execute(filters) + + expected_data = [{ + 'leave_type': '_Test Leave Type', + 'employee': self.employee_id, + 'employee_name': 'test_emp_leave_balance@example.com', + 'leaves_allocated': flt(allocation1.new_leaves_allocated + allocation2.new_leaves_allocated), + 'leaves_expired': flt(allocation1.new_leaves_allocated), + 'opening_balance': flt(0), + 'leaves_taken': flt(leave_application.total_leave_days), + 'closing_balance': flt(allocation2.new_leaves_allocated - leave_application.total_leave_days), + 'indent': 1 + }] + + self.assertEqual(report[1], expected_data) + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_opening_balance_on_alloc_boundary_dates(self): + frappe.get_doc(test_records[0]).insert() + + # 30 leaves allocated + allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + # 4 days leave application in the first allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application.reload() + + # Case 1: opening balance for first alloc boundary + filters = frappe._dict({ + 'from_date': self.year_start, + 'to_date': self.year_end, + 'employee': self.employee_id + }) + report = execute(filters) + self.assertEqual(report[1][0].opening_balance, 0) + + # Case 2: opening balance after leave application date + filters = frappe._dict({ + 'from_date': add_days(leave_application.to_date, 1), + 'to_date': self.year_end, + 'employee': self.employee_id + }) + report = execute(filters) + self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days)) + + # Case 3: leave balance shows actual balance and not consumption balance as per remaining days near alloc end date + # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 + filters = frappe._dict({ + 'from_date': add_days(self.year_end, -3), + 'to_date': self.year_end, + 'employee': self.employee_id + }) + report = execute(filters) + self.assertEqual(report[1][0].opening_balance, (allocation1.new_leaves_allocated - leave_application.total_leave_days)) + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_opening_balance_considers_carry_forwarded_leaves(self): + leave_type = create_leave_type( + leave_type_name="_Test_CF_leave_expiry", + is_carry_forward=1) + leave_type.insert() + + # 30 leaves allocated for first half of the year + allocation1 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, + to_date=self.mid_year, leave_type=leave_type.name) + # 4 days leave application in the first allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application(self.employee_id, first_sunday, add_days(first_sunday, 3), leave_type.name) + leave_application.reload() + # 30 leaves allocated for second half of the year + carry forward leaves (26) from the previous allocation + allocation2 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.mid_year, 1), to_date=self.year_end, + carry_forward=True, leave_type=leave_type.name) + + # Case 1: carry forwarded leaves considered in opening balance for second alloc + filters = frappe._dict({ + 'from_date': add_days(self.mid_year, 1), + 'to_date': self.year_end, + 'employee': self.employee_id + }) + report = execute(filters) + # available leaves from old alloc + opening_balance = allocation1.new_leaves_allocated - leave_application.total_leave_days + self.assertEqual(report[1][0].opening_balance, opening_balance) + + # Case 2: opening balance one day after alloc boundary = carry forwarded leaves + new leaves alloc + filters = frappe._dict({ + 'from_date': add_days(self.mid_year, 2), + 'to_date': self.year_end, + 'employee': self.employee_id + }) + report = execute(filters) + # available leaves from old alloc + opening_balance = allocation2.new_leaves_allocated + (allocation1.new_leaves_allocated - leave_application.total_leave_days) + self.assertEqual(report[1][0].opening_balance, opening_balance) diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index 14ce9ed22a66..c97d08af5c06 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -13,6 +13,7 @@ def execute(filters=None): + filters = frappe._dict(filters or {}) leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc") columns = get_columns(leave_types) diff --git a/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py new file mode 100644 index 000000000000..9b953de0dc27 --- /dev/null +++ b/erpnext/hr/report/employee_leave_balance_summary/test_employee_leave_balance_summary.py @@ -0,0 +1,117 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + + +import unittest + +import frappe +from frappe.utils import add_days, flt, get_year_ending, get_year_start, getdate + +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list +from erpnext.hr.doctype.leave_application.test_leave_application import ( + get_first_sunday, + make_allocation_record, +) +from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation +from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary import execute +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) + +test_records = frappe.get_test_records('Leave Type') + +class TestEmployeeLeaveBalance(unittest.TestCase): + def setUp(self): + for dt in ['Leave Application', 'Leave Allocation', 'Salary Slip', 'Leave Ledger Entry', 'Leave Type']: + frappe.db.delete(dt) + + frappe.set_user('Administrator') + + self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') + self.employee_id = make_employee('test_emp_leave_balance@example.com', company='_Test Company') + + self.date = getdate() + self.year_start = getdate(get_year_start(self.date)) + self.year_end = getdate(get_year_ending(self.date)) + + self.holiday_list = make_holiday_list('_Test Emp Balance Holiday List', self.year_start, self.year_end) + + def tearDown(self): + frappe.db.rollback() + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_employee_leave_balance_summary(self): + frappe.get_doc(test_records[0]).insert() + + # 5 leaves + allocation1 = make_allocation_record(employee=self.employee_id, from_date=add_days(self.year_start, -11), + to_date=add_days(self.year_start, -1), leaves=5) + # 30 leaves + allocation2 = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + + # 2 days leave within the first allocation + leave_application1 = make_leave_application(self.employee_id, add_days(self.year_start, -11), add_days(self.year_start, -10), + '_Test Leave Type') + leave_application1.reload() + + # expires 3 leaves + process_expired_allocation() + + # 4 days leave within the second allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application2 = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application2.reload() + + filters = { + 'date': self.date, + 'company': '_Test Company', + 'employee': self.employee_id + } + + report = execute(filters) + + expected_data = [[ + self.employee_id, + 'test_emp_leave_balance@example.com', + frappe.db.get_value('Employee', self.employee_id, 'department'), + flt( + allocation1.new_leaves_allocated # allocated = 5 + + allocation2.new_leaves_allocated # allocated = 30 + - leave_application1.total_leave_days # leaves taken in the 1st alloc = 2 + - (allocation1.new_leaves_allocated - leave_application1.total_leave_days) # leaves expired from 1st alloc = 3 + - leave_application2.total_leave_days # leaves taken in the 2nd alloc = 4 + ) + ]] + + self.assertEqual(report[1], expected_data) + + @set_holiday_list('_Test Emp Balance Holiday List', '_Test Company') + def test_get_leave_balance_near_alloc_expiry(self): + frappe.get_doc(test_records[0]).insert() + + # 30 leaves allocated + allocation = make_allocation_record(employee=self.employee_id, from_date=self.year_start, to_date=self.year_end) + # 4 days leave application in the first allocation + first_sunday = get_first_sunday(self.holiday_list, for_date=self.year_start) + leave_application = make_leave_application(self.employee_id, add_days(first_sunday, 1), add_days(first_sunday, 4), '_Test Leave Type') + leave_application.reload() + + # Leave balance should show actual balance, and not "consumption balance as per remaining days", near alloc end date + # eg: 3 days left for alloc to end, leave balance should still be 26 and not 3 + filters = { + 'date': add_days(self.year_end, -3), + 'company': '_Test Company', + 'employee': self.employee_id + } + report = execute(filters) + + expected_data = [[ + self.employee_id, + 'test_emp_leave_balance@example.com', + frappe.db.get_value('Employee', self.employee_id, 'department'), + flt(allocation.new_leaves_allocated - leave_application.total_leave_days) + ]] + + self.assertEqual(report[1], expected_data) diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json index 1db369d894fa..41297bdb3c6a 100644 --- a/erpnext/hr/workspace/hr/hr.json +++ b/erpnext/hr/workspace/hr/hr.json @@ -596,7 +596,7 @@ "type": "Link" }, { - "hidden": 0, + "hidden": 1, "is_query_report": 0, "label": "Loans", "onboard": 0, @@ -841,7 +841,7 @@ "type": "Link" } ], - "modified": "2022-08-09 15:41:19.850937", + "modified": "2023-07-31 12:53:15.884459", "modified_by": "Administrator", "module": "HR", "name": "HR", @@ -868,7 +868,7 @@ "link_to": "Job Opening", "type": "DocType" }, - { + { "format": "{} Open", "label": "Dashboard", "link_to": "Recursos Humanos", diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 623c1fd29f31..c9f0dfd57af3 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -360,7 +360,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:02:01.236364", + "modified": "2021-05-12 21:02:01.236364", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", @@ -386,7 +386,7 @@ "role": "Employee" } ], - "restrict_to_domain": "HR Reclutamiento, Capacitacion y Gastos", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "search_fields": "posting_date", "sort_field": "creation", "sort_order": "DESC", diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.json b/erpnext/loan_management/doctype/loan_application/loan_application.json index 661cc4ea1dcb..6054aa88f2a1 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.json +++ b/erpnext/loan_management/doctype/loan_application/loan_application.json @@ -215,7 +215,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:02:00.885610", + "modified": "2021-05-12 21:02:00.885610", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Application", @@ -273,7 +273,7 @@ "share": 1 } ], - "restrict_to_domain": "HR Reclutamiento, Capacitacion y Gastos", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "search_fields": "applicant_type, applicant, loan_type, loan_amount", "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index c0ded9eb9b30..7790928f18a9 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -154,7 +154,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-12 20:02:01.645477", + "modified": "2021-05-12 21:02:01.645477", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", @@ -180,7 +180,7 @@ "role": "Employee" } ], - "restrict_to_domain": "HR Reclutamiento, Capacitacion y Gastos", + "restrict_to_domain": "HR Asistencia, Vacaciones y Rendimiento de Gastos", "sort_field": "modified", "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3c409ede9c37..d37d3c3fc4df 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -362,4 +362,5 @@ execute:frappe.delete_doc('Page', "welcome-to-erpnext", force=True) execute:frappe.delete_doc('Page', "team-updates", force=True) erpnext.patches.v13_0.usuarios_reducidos_fix erpnext.patches.v13_0.release_1_7 -erpnext.patches.v13_0.remove_auditor_role_from_users \ No newline at end of file +erpnext.patches.v13_0.remove_auditor_role_from_users +erpnext.patches.v13_0.remove_old_hr_domains \ No newline at end of file diff --git a/erpnext/patches/v13_0/remove_old_hr_domains.py b/erpnext/patches/v13_0/remove_old_hr_domains.py new file mode 100644 index 000000000000..3ed6b5bfeec3 --- /dev/null +++ b/erpnext/patches/v13_0/remove_old_hr_domains.py @@ -0,0 +1,28 @@ +import frappe +import erpnext.hooks as hooks + +def execute(): + if not frappe.db.exists("Domain", "HR Asistencia, Vacaciones y Rendimiento de Gastos"): + d = frappe.new_doc('Domain') + d.name = 'HR Asistencia, Vacaciones y Rendimiento de Gastos' + d.domain = 'HR Asistencia, Vacaciones y Rendimiento de Gastos' + d.save() + frappe.db.commit() + + hooks.after_migrate.append("erpnext.patches.v13_0.remove_old_hr_domains.remove_old_hr_domains") + + +def remove_old_hr_domains(): + frappe.flags.in_patch = True + frappe.db.delete('Has Domain', {'domain': 'HR Asistencia y Vacaciones'}) + frappe.db.delete('Has Domain', {'domain': 'HR Reclutamiento, Capacitacion y Gastos'}) + frappe.delete_doc('Domain', 'HR Asistencia y Vacaciones', ignore_missing=True) + frappe.delete_doc('Domain', 'HR Reclutamiento, Capacitacion y Gastos', ignore_missing=True) + + ds = frappe.get_doc('Domain Settings') + for ad in ds.active_domains: + if ad.domain in ['HR Asistencia y Vacaciones', 'HR Reclutamiento, Capacitacion y Gastos']: + ad.delete() + + ds.save() + frappe.db.commit() \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 888150f0ae3b..eca93ad1fea2 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1333,7 +1333,7 @@ def add_leave_balances(self): 'total_allocated_leaves': flt(leave_values.get('total_leaves')), 'expired_leaves': flt(leave_values.get('expired_leaves')), 'used_leaves': flt(leave_values.get('leaves_taken')), - 'pending_leaves': flt(leave_values.get('pending_leaves')), + 'pending_leaves': flt(leave_values.get('leaves_pending_approval')), 'available_leaves': flt(leave_values.get('remaining_leaves')) }) diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 81582cecae3b..9f0502c2446b 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -881,7 +881,7 @@ def create_additional_salary(employee, payroll_period, amount): }).submit() return salary_date -def make_leave_application(employee, from_date, to_date, leave_type, company=None): +def make_leave_application(employee, from_date, to_date, leave_type, company=None, submit=True): leave_application = frappe.get_doc(dict( doctype = 'Leave Application', employee = employee, @@ -889,11 +889,12 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non from_date = from_date, to_date = to_date, company = company or erpnext.get_default_company() or "_Test Company", - docstatus = 1, status = "Approved", leave_approver = 'test@example.com' - )) - leave_application.submit() + )).insert() + + if submit: + leave_application.submit() def setup_test(): make_earning_salary_component(setup=True, company_list=["_Test Company"]) @@ -909,15 +910,21 @@ def setup_test(): frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None) -def make_holiday_list(): +def make_holiday_list(list_name=None, from_date=None, to_date=None): fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) +<<<<<<< HEAD holiday_list = frappe.db.exists("Holiday List", "Salary Slip Test Holiday List") if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"): +======= + name = list_name or "Salary Slip Test Holiday List" + holiday_list = frappe.db.exists("Holiday List", name) + if not holiday_list: +>>>>>>> c050ce49c2 (test: employee leave balance report) holiday_list = frappe.get_doc({ "doctype": "Holiday List", - "holiday_list_name": "Salary Slip Test Holiday List", - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], + "holiday_list_name": name, + "from_date": from_date or fiscal_year[1], + "to_date": to_date or fiscal_year[2], "weekly_off": "Sunday" }).insert() holiday_list.get_weekly_off_dates() diff --git a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json index c510459e7999..44bcc980bbf1 100644 --- a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json +++ b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json @@ -26,7 +26,7 @@ "fieldname": "total_allocated_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Total Allocated Leave", + "label": "Total Allocated Leave(s)", "no_copy": 1, "read_only": 1 }, @@ -34,7 +34,7 @@ "fieldname": "expired_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Expired Leave", + "label": "Expired Leave(s)", "no_copy": 1, "read_only": 1 }, @@ -42,7 +42,7 @@ "fieldname": "used_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Used Leave", + "label": "Used Leave(s)", "no_copy": 1, "read_only": 1 }, @@ -50,7 +50,7 @@ "fieldname": "pending_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Pending Leave", + "label": "Leave(s) Pending Approval", "no_copy": 1, "read_only": 1 }, @@ -58,7 +58,7 @@ "fieldname": "available_leaves", "fieldtype": "Float", "in_list_view": 1, - "label": "Available Leave", + "label": "Available Leave(s)", "no_copy": 1, "read_only": 1 } @@ -66,7 +66,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-12 20:00:09.655979", + "modified": "2022-02-28 14:01:32.327204", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip Leave", @@ -75,5 +75,6 @@ "restrict_to_domain": "Payroll", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index a873318d6518..806b33ca934a 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -71,8 +71,7 @@ def install(country=None): {'doctype': 'Domain', 'domain': 'RMA'}, {'doctype': 'Domain', 'domain': 'Retail Avanzado'}, {'doctype': 'Domain', 'domain': 'Servicios Google'}, - {'doctype': 'Domain', 'domain': 'HR Asistencia y Vacaciones'}, - {'doctype': 'Domain', 'domain': 'HR Reclutamiento, Capacitacion y Gastos'}, + {'doctype': 'Domain', 'domain': 'HR Asistencia, Vacaciones y Rendimiento de Gastos'}, {'doctype': 'Domain', 'domain': 'Build'}, {'doctype': 'Domain', 'domain': 'Pagos360'}, {'doctype': 'Domain', 'domain': 'Mercadolibre'}, diff --git a/erpnext/translations/es.csv b/erpnext/translations/es.csv index b3e6d1d3442c..3a6f8b5f3ae0 100644 --- a/erpnext/translations/es.csv +++ b/erpnext/translations/es.csv @@ -465,7 +465,7 @@ Add Timesheets,Añadir registros de horas, Add Timeslots,Añadir Intervalos de Tiempo, Add Topic to Courses,Agregar tema a los cursos, Add Users to Marketplace,Agregar usuarios al mercado, -Add Weekly Holidays,Añadir Vacaciones Semanales, +Add Weekly Holidays,Añadir días no laborables semanales, Add a new address,Añadir una nueva dirección, Add items in the Item Locations table,Agregar elementos en la tabla Ubicaciones de elementos, Add letterhead,Agregar membrete, @@ -477,7 +477,7 @@ Add to Cart,Añadir al carrito, Add to Courses,Agregar a cursos, Add to Details,Añadir a Detalles, Add to Featured Item,Agregar al artículo destacado, -Add to Holidays,Agregar a Vacaciones, +Add to Holidays,Agregar a días no laborables, Add to Programs,Agregar a programas, Add to Quote,Add to Quote, Add to Topics,Agregar a temas, @@ -551,8 +551,8 @@ Admitted,Aceptado, Admitted Datetime,Fecha de Entrada Admitida, Adults' pulse rate is anywhere between 50 and 80 beats per minute.,La frecuencia de pulso de los adultos está entre 50 y 80 latidos por minuto., Advance,Avanzar, -Advance Account,Cuenta anticipada, -Advance Amount,Importe Anticipado, +Advance Account,Cuenta contable para el anticipo, +Advance Amount,Importe de Anticipo, Advance Paid,Pago Anticipado, Advance Payments,Pagos adelantados, Advance TDS account is mandatory for advance TDS deduction,Advance TDS account is mandatory for advance TDS deduction, @@ -708,12 +708,12 @@ Allow User,Permitir al usuario, Allow User to Edit Discount,Permitir al usuario editar el descuento, Allow User to Edit Price List Rate in Transactions,Permitir al usuario editar el precio de lista de precios en las transacciones, Allow User to Edit Rate,Permitir al usuairo editar el Precio, -Allow Users,Permitir que los usuarios, +Allow Users,Permitir usuarios, Allow Zero Rate,Permitir precio cero, Allow Zero Valuation Rate,Permitir tasa de valoración cero, Allow check-out after shift end time (in minutes),Permitir la salida después de la hora de finalización del turno (en minutos), Allow material consumptions without immediately manufacturing finished goods against a Work Order,Allow material consumptions without immediately manufacturing finished goods against a Work Order, -Allow the following users to approve Leave Applications for block days.,Permitir a los usuarios siguientes aprobar solicitudes de ausencia en días bloqueados., +Allow the following users to approve Leave Applications for block days.,Permitir a los siguientes usuarios aprobar solicitudes de licencia en días bloqueados., Allow transferring raw materials even after the Required Quantity is fulfilled,Allow transferring raw materials even after the Required Quantity is fulfilled, Allowed Dimension,Dimensiones permitidas, Allowed To Transact With,Permitido para realizar Transacciones con, @@ -784,7 +784,7 @@ Anytime,En cualquier momento, App Type,Tipo de Aplicación, Apparel & Accessories,Indumentaria y accesorios, Appearance,Apariencia, -Applicable After (Working Days),Aplicable Después (Días Laborables), +Applicable After (Working Days),Min de días trabajados, Applicable Charges,Cargos Aplicables, Applicable Dimension,Dimensión aplicable, Applicable Earnings Component,Applicable Earnings Component, @@ -2472,7 +2472,7 @@ Default Donor Type,Tipo de donante predeterminado, Default Duration (In Minutes),Duración predeterminada (en minutos), Default Employee Advance Account,Cuenta Predeterminada de Anticipo de Empleado, Default Expense Account,Cuenta de gastos predeterminada, -Default Expense Claim Payable Account,Cuenta de pago por reclamación de gastos predeterminada, +Default Expense Claim Payable Account,Cuenta de pago por rendición de gastos predeterminada, Default Finance Book,Libro de Finanzas Predeterminado, Default Finished Goods Warehouse,Almacén predeterminado de productos terminados, Default Grading Scale,Escala de Calificación por defecto, @@ -3056,7 +3056,7 @@ Employee relieved on {0} must be set as 'Left',"Empleado relevado en {0} debe de Employee {0} already has Active Shift {1}: {2},El empleado {0} ya tiene turno activo {1}: {2}, Employee {0} already submited an apllication {1} for the payroll period {2},El Empleado {0} ya envió una Aplicación {1} para el período de nómina {2}, Employee {0} does not belongs to the company {1},El empleado {0} no pertenece a la empresa {1}, -Employee {0} has already applied for {1} between {2} and {3} : {4},Employee {0} has already applied for {1} between {2} and {3} : {4}, +Employee {0} has already applied for {1} between {2} and {3} : {4},El Empleado {0} ya solicitó {1} entre las fechas {2} y {3} : {4}, Employee {0} has no maximum benefit amount,El Empleado {0} no tiene una cantidad de beneficio máximo, Employee {0} is not active or does not exist,El empleado {0} no está activo o no existe, Employee {0} is on Leave on {1},El Empleado {0} está en de Licencia el {1}, @@ -3280,27 +3280,27 @@ Expense,Gastos, Expense / Difference account ({0}) must be a 'Profit or Loss' account,"La cuenta de Gastos/Diferencia ({0}) debe ser una cuenta de 'utilidad o pérdida """, Expense Account,Cuenta de gastos, Expense Account Missing,Falta la cuenta de gastos, -Expense Approver,Supervisor de gastos, -Expense Approver Mandatory In Expense Claim,Aprobador de Gastos obligatorio en la Reclamación de Gastos, -Expense Claim,Reembolso de gastos, +Expense Approver,Aprobador de Gastos, +Expense Approver Mandatory In Expense Claim,Aprobador de Gastos obligatorio en la Rendición de Gastos, +Expense Claim,Rendición de Gastos, Expense Claim Account,Cuenta de Gastos, -Expense Claim Advance,Anticipo de reembolso de Gastos, -Expense Claim Detail,Detalle de reembolso de gastos, +Expense Claim Advance,Anticipo de Rendición de Gastos, +Expense Claim Detail,Detalle de Rendición de Gastos, Expense Claim Type,Tipo de gasto, -Expense Claim for Vehicle Log {0},Reclamación de gastos para el registro de vehículos {0}, -Expense Claim {0} already exists for the Vehicle Log,Reclamación de Gastos {0} ya existe para el registro de vehículos, +Expense Claim for Vehicle Log {0},Rendición de Gastos para el registro de vehículos {0}, +Expense Claim {0} already exists for the Vehicle Log,Rendición de Gastos {0} ya existe para el registro de vehículos, Expense Date,Fecha de gasto, Expense Head,Cuenta de gastos, Expense Head Changed,Cuenta de gastos cambiada, Expense Proof,Prueba de Gastos, -Expense Taxes and Charges,Gastos Impuestos y Cargos, +Expense Taxes and Charges,Impuestos y Cargos, Expense Type,Tipo de Gasto, Expense account is mandatory for item {0},La cuenta de gastos es obligatoria para el producto {0}, Expenses,Gastos, Expenses Included In Asset Valuation,Gastos incluidos en la valoración de activos, Expenses Included In Valuation,Gastos de valoración, Expire Allocation,Caducar la asignación, -Expire Carry Forwarded Leaves (Days),Caducar Llevar hojas reenviadas (días), +Expire Carry Forwarded Leaves (Days),Cantidad de días después de los cuales caducan las licencias, Expired,Expirado, Expired Batches,Lotes Vencidos, Expired Leave,Permiso expirado, @@ -3947,12 +3947,12 @@ History In Company,Historia en la Compañia, Hold,Mantener, Hold Invoice,Retener la Factura, Hold Type,Tipo de Pausa, -Holiday,Vacaciones, -Holiday List,Lista de festividades, -Holiday List Name,Nombre de festividad, +Holiday,Día no laborable, +Holiday List,Lista de días no laborables, +Holiday List Name,Nombre, Holiday List for Optional Leave,Lista de vacaciones para la licencia opcional, -Holiday Management,Gestión de Vacaciones, -Holidays,Vacaciones, +Holiday Management,Gestión de Días no laborables, +Holidays,Días no laborables, Holidays this Month.,Feriados este mes., Holidays this Week.,Feriados esta semana., Home,Inicio, @@ -4190,7 +4190,7 @@ Include Sub-assembly Raw Materials,Incluir materias primas de subensamblaje, Include Subcontracted Items,Incluir Artículos Subcontratados, Include UOM,Incluir UOM, Include holidays in Total no. of Working Days,Incluir vacaciones con el numero total de días laborables, -Include holidays within leaves as leaves,Incluir las vacaciones y ausencias únicamente como ausencias, +Include holidays within leaves as leaves,Considerar feriados dentro de la licencia como ausencias, Include in gross,Incluir en bruto, Included in Gross Profit,Incluido en el beneficio bruto, Including items for sub assemblies,Incluir productos para subconjuntos, @@ -4436,7 +4436,7 @@ Is Advance,Es un anticipo, Is Applicable for Referral Bonus,Es aplicable a la bonificación por recomendación, Is Billable,Es facturable, Is Cancelled,Cancelado, -Is Carry Forward,Es un traslado, +Is Carry Forward,Es trasladable, Is Company,Es la Compañia, Is Company Account,Es la Cuenta de la Empresa, Is Compensatory,Es Compensatorio, @@ -4806,8 +4806,8 @@ Learning Management System Title,Título del sistema de gestión de aprendizaje, Leave,Salir, Leave From and To 0 for no upper and lower limit.,Leave From and To 0 for no upper and lower limit., Leave Allocated,Leave Allocated, -Leave Allocation,Asignación de vacaciones, -Leave Allocations,Dejar asignaciones, +Leave Allocation,Asignación de licencias, +Leave Allocations,Asignación de licencias, Leave Application,Solicitud de Licencia, Leave Approval Notification,Notificación de Autorización de Vacaciones, Leave Approval Notification Template,Plantilla de Notificación de Autorización de Vacaciones, @@ -4816,12 +4816,12 @@ Leave Approver Mandatory In Leave Application,Aprobador de Autorización de Vaca Leave Approver Name,Nombre del supervisor de ausencias, Leave Balance,Balance de Licencia, Leave Balance Before Application,Ausencias disponibles antes de la solicitud, -Leave Block List,Dejar lista de bloqueo, +Leave Block List,Lista de bloqueo de Licencias, Leave Block List Allow,Permitir Lista de Bloqueo de Vacaciones, Leave Block List Allowed,Lista de 'bloqueo de vacaciones / permisos' permitida, Leave Block List Date,Fecha de Lista de Bloqueo de Vacaciones, -Leave Block List Dates,Fechas de Lista de Bloqueo de Vacaciones, -Leave Block List Name,Nombre de la Lista de Bloqueo de Vacaciones, +Leave Block List Dates,Fechas de Lista de Bloqueo de licencias, +Leave Block List Name,Nombre de la lista, Leave Blocked,Vacaciones Bloqueadas, Leave Control Panel,Panel de control de ausencias, Leave Details,Leave Details, @@ -4833,8 +4833,8 @@ Leave Ledger Entry,Dejar entrada de libro mayor, Leave Period,Período de Licencia, Leave Policy,Política de Licencia, Leave Policy Assignment,Asignación de Política de Licencia, -Leave Policy Detail,Dejar detalles de la política, -Leave Policy Details,Dejar detalles de la política, +Leave Policy Detail,Detalle de la asignación de licencias, +Leave Policy Details,Detalles de la asignación de licencias, Leave Policy: {0} already assigned for Employee {1} for period {2} to {3},Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}, Leave Reason,Deja la Razón, Leave Settings,Configuración de Vacaciones, @@ -4842,7 +4842,7 @@ Leave Status Notification,Estado de Notificación de Vacaciones, Leave Status Notification Template,Plantilla de Estado de Notificación de Vacaciones, Leave Taken,Leave Taken, Leave Type,Tipo de Licencia, -Leave Type Name,Nombre del tipo de ausencia, +Leave Type Name,Nombre del tipo de licencia, Leave Type can be either without pay or partial pay,Leave Type can be either without pay or partial pay, Leave Type is madatory,Tipo de Licencia es obligatorio, Leave Type {0} cannot be allocated since it is leave without pay,No se puede asignar el tipo de vacaciones {0} ya que se trata de vacaciones sin sueldo., @@ -5220,7 +5220,7 @@ Max Discount (%),Descuento máximo (%), Max Exemption Amount,Cantidad de exención máxima, Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1},El monto máximo de exención no puede ser mayor que el monto máximo de exención {0} de la categoría de exención fiscal {1}, Max Grade,Grado máximo, -Max Leaves Allowed,Max Licencias Permitidas, +Max Leaves Allowed,Max licencias anuales permitidas, Max Qty,Cantidad máxima, Max Retry Limit,Límite máximo de reintento, Max Sample Quantity,Cantidad máxima de muestras, @@ -5236,7 +5236,7 @@ Max: {0},Máximo: {0}, Maximum Age,Edad Máxima, Maximum Assessment Score,Puntuación máxima de Evaluación, Maximum Capacity:,Maximum Capacity:, -Maximum Carry Forwarded Leaves,Máximo transporte de hojas enviadas, +Maximum Carry Forwarded Leaves,Cantidad máxima de licencias transportables, Maximum Continuous Days Applicable,Máximo de días continuos aplicables, Maximum Exempted Amount,Monto máximo exento, Maximum Exemption Amount,Monto Máximo de Exención, @@ -6313,7 +6313,7 @@ Penalty against loan:,Penalty against loan:, Pending,Pendiente, Pending Activities,Actividades pendientes, Pending Amount,Monto pendiente, -Pending Leave,Pending Leave, +Pending Leave,Licencias pendientes, Pending Principal Amount,Monto principal pendiente, Pending Qty,Cantidad pendiente, Pending Quantity,Cantidad pendiente, @@ -6730,7 +6730,7 @@ Please set valid GSTIN No. in Company Address {} for company {},Please set valid Please set {0},Please set {0},customer "Please set {0} for Batched Item {1}, which is used to set {2} on Submit.","Configure {0} para el artículo por lotes {1}, que se utiliza para configurar {2} en Enviar.", Please set {0} for address {1},Establezca {0} para la dirección {1}, -Please set {0} for the Employee: {1},Please set {0} for the Employee: {1}, +Please set {0} for the Employee: {1},Por favor establezca {0} para el empleado: {1}, Please setup Employee Naming System in Human Resource > HR Settings,Configure el Sistema de nombres de empleados en Recursos humanos> Configuración de recursos humanos, Please setup Instructor Naming System in Education > Education Settings,Configure el Sistema de nombres de instructores en Educación> Configuración de educación, Please setup Razorpay Plan ID,Configure el ID del plan de Razorpay, @@ -7554,7 +7554,7 @@ Requesting Practitioner,Practicante solicitante, Requesting Site,Sitio solicitante, Requesting payment against {0} {1} for amount {2},Solicitando el pago contra {0} {1} para la cantidad {2}, Requestor,Solicitante, -Require Full Funding,Requerir Fondos Completos, +Require Full Funding,Requerir Anticipo de Gastos, Require Result Value,Requerir Valor de Resultado, Required By,Solicitado para, Required Date,Fecha de solicitud, @@ -7779,7 +7779,7 @@ Row #{0}: Please specify Serial No for Item {1},"Fila #{0}: Por favor, especifiq Row #{0}: Qty increased by 1,Fila # {0}: la cantidad aumentó en 1, Row #{0}: Rate must be same as {1}: {2} ({3} / {4}),Fila #{0}: El valor debe ser el mismo que {1}: {2} ({3} / {4}), Row #{0}: Reference Document Type must be Donation,Row #{0}: Reference Document Type must be Donation, -Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry,Fila #{0}: El tipo de documento de referencia debe ser uno de Reembolso de Gastos o Asiento Contable, +Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry,Fila #{0}: El tipo de documento de referencia debe ser uno de Rendición de Gastos o Asiento Contable, "Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry","Fila #{0}: Tipo de documento de referencia debe ser uno de la orden de compra, factura de compra o de asiento contable", "Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning","Fila # {0}: el tipo de documento de referencia debe ser pedido de cliente, factura de venta, asiento de diario o reclamación.", Row #{0}: Rejected Qty can not be entered in Purchase Return,Fila #{0}: La cantidad rechazada no se puede introducir en el campo 'retorno de compras', @@ -8053,7 +8053,7 @@ Sample Size,Tamaño de muestra, Sample UOM,UOM de muestra, Sample quantity {0} cannot be more than received quantity {1},La Cantidad de Muestra {0} no puede ser más que la Cantidad Recibida {1}, Sanctioned,Sancionada, -Sanctioned Amount,Monto sancionado, +Sanctioned Amount,Monto Aprobado, Sanctioned Amount Limit,Límite de cantidad sancionada, Sanctioned Amount cannot be greater than Claim Amount in Row {0}.,Importe sancionado no puede ser mayor que el importe del reclamo en la línea {0}., Sanctioned Amount limit crossed for {0} {1},Límite de cantidad sancionado cruzado por {0} {1}, @@ -9528,7 +9528,7 @@ Total Advance,Total anticipo, Total Advance Amount,Monto Total Anticipado, Total Allocated Amount,Monto Total Asignado, Total Allocated Amount (Company Currency),Monto Total asignado (Divisa de la Compañia), -Total Allocated Leave,Total Allocated Leave, +Total Allocated Leave,Licencias asignadas totales, Total Amount,Importe total, Total Amount Credited,Monto Total Acreditado, Total Amount Currency,Monto total de divisas, @@ -9579,15 +9579,15 @@ Total Estimated Cost,Costo Total Estimado, Total Estimated Distance,Distancia Total Estimada, Total Exemption Amount,Importe Total de Exención, Total Expense,Gasto total, -Total Expense Claim (via Expense Claim),Total reembolso (Vía reembolso de gastos), -Total Expense Claim (via Expense Claims),Total reembolso (a través de Reembolso de gastos), +Total Expense Claim (via Expense Claim),Total reembolso (Vía Rendición de Gastos), +Total Expense Claim (via Expense Claims),Total reembolso (a través de Rendición de Gastos), Total Expense This Year,Gastos totales este año, Total Experience,Experiencia total, Total Forecast (Future Data),Pronóstico total (datos futuros), Total Forecast (Past Data),Pronóstico total (datos anteriores), Total Gain/Loss,Ganancia / Pérdida Total, Total Hold Time,Tiempo total de espera, -Total Holidays,Vacaciones Totales, +Total Holidays,Total de días no laborables, Total Hours (T),Horas totales (T), Total Income,Ingresos totales, Total Income This Year,Ingresos totales este año, @@ -9769,7 +9769,7 @@ Transporter ID,ID de Transportador, Transporter Info,Información de Transportista, Transporter Name,Nombre del Transportista, Travel,Viajes, -Travel Advance Required,Se Requiere Avance de Viaje, +Travel Advance Required,Se requiere Anticipo de Gastos, Travel Expenses,Gastos de Viaje, Travel From,Viajar Desde, Travel Funding,Financiación de Viajes, @@ -9872,7 +9872,7 @@ Unmarked Days,Días sin marcar, Unmarked Days is treated as {0}. You can can change this in {1},Unmarked Days is treated as {0}. You can can change this in {1},Payroll Settings Unmarked days,Días sin marcar, Unpaid,Impagado, -Unpaid Expense Claim,Reclamación de gastos no pagados, +Unpaid Expense Claim,Rendición de Gastos no pagados, Unpaid and Discounted,Sin pagar y con descuento, Unplanned machine maintenance,Mantenimiento no planificado de la máquina, Unpledge,Desatar, @@ -9954,7 +9954,7 @@ Use a name that is different from previous project name,Use un nombre que sea di Use for Shopping Cart,Utilizar para carrito de compras, Use this field to render any custom HTML in the section.,Use este campo para representar cualquier HTML personalizado en la sección., Used,Usado, -Used Leave,Used Leave, +Used Leave,Licencias utilizadas, Used for Production Plan,Se utiliza para el plan de producción, User,Usuario, User Details,Detalles de usuario, @@ -10210,7 +10210,7 @@ Weekday,Día laborable, Weekdays,Días de la Semana, Weekends,Fines de Semana, Weekly,Semanal, -Weekly Off,Semanal Desactivado, +Weekly Off,Día de la semana no laborable, Weight,Peso, Weight (In Kilogram),Peso (en kilogramo), Weight (kg),Weight (kg), @@ -10252,9 +10252,9 @@ Woocommerce Settings,Configuración de Woocommerce, Work Address,Dirección del trabajo, Work Anniversary Reminder,Work Anniversary Reminder, Work Done,Trabajo Realizado, -Work End Date,Fecha de Finalización del Trabajo, +Work End Date,Hasta la fecha, Work Experience Calculation method,Work Experience Calculation method, -Work From Date,Trabajar Desde la Fecha, +Work From Date,Desde la fecha, Work From Home,Trabajar Desde Casa, Work In Progress,Trabajo en Proceso, Work In Progress Warehouse,Almacén de trabajo en curso, @@ -10817,7 +10817,7 @@ Set Default Supplier,Establecer proveedor predeterminado, Outgoing Salary,Sueldo saliente, Employee Lifecycle,Ciclo de vida del empleado, Shift Management,Manejo de turnos, -Expense Claims,Reclamaciones de gastos, +Expense Claims,Rendiciónes de Gastos, Fleet Management,Gestión de la flota, Loans,Préstamos, Performance,Rendimiento, @@ -10920,4 +10920,8 @@ Avg Response Time,Tiempo medio de respuesta, Avg Hold Time,Tiempo medio de espera, Avg Resolution Time,Tiempo medio de resolución, Avg User Resolution Time,Tiempo medio de resolución del usuario, -Total Issues,Total de tickets, \ No newline at end of file +Total Issues,Total de tickets, +No Leave has been allocated.,No se ha asignado ningúna licencia., +Repay Unclaimed Amount from Salary,Pago anticipo de salario, +{0} Missing,Falta {0}, +or for Department: {0},o para el departamento: {0}, \ No newline at end of file