diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index eaca9f6ebe..906f6f77f2 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -87,11 +87,12 @@ "search_index": 1 }, { - "depends_on": "eval:doc.status==\"On Leave\"", + "depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)", "fieldname": "leave_type", "fieldtype": "Link", "in_standard_filter": 1, "label": "Leave Type", + "mandatory_depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)", "oldfieldname": "leave_type", "oldfieldtype": "Link", "options": "Leave Type" @@ -100,6 +101,7 @@ "fieldname": "leave_application", "fieldtype": "Link", "label": "Leave Application", + "no_copy": 1, "options": "Leave Application", "read_only": 1 }, @@ -175,7 +177,8 @@ "icon": "fa fa-ok", "idx": 1, "is_submittable": 1, - "modified": "2020-02-19 14:25:32.945842", + "links": [], + "modified": "2020-04-11 11:40:14.319496", "modified_by": "Administrator", "module": "HR", "name": "Attendance", diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 9e965dbc39..7355a56128 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -7,33 +7,15 @@ import frappe from frappe.utils import getdate, nowdate from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, get_datetime, get_datetime_str -from frappe.utils import update_progress_bar +from frappe.utils import cstr, get_datetime, format_date class Attendance(Document): - def validate_duplicate_record(self): - res = frappe.db.sql("""select name from `tabAttendance` where employee = %s and attendance_date = %s - and name != %s and docstatus != 2""", - (self.employee, getdate(self.attendance_date), self.name)) - if res: - frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee)) - - def check_leave_record(self): - leave_record = frappe.db.sql("""select leave_type, half_day, half_day_date from `tabLeave Application` - where employee = %s and %s between from_date and to_date and status = 'Approved' - and docstatus = 1""", (self.employee, self.attendance_date), as_dict=True) - if leave_record: - for d in leave_record: - if d.half_day_date == getdate(self.attendance_date): - self.status = 'Half Day' - frappe.msgprint(_("Employee {0} on Half day on {1}").format(self.employee, self.attendance_date)) - else: - self.status = 'On Leave' - self.leave_type = d.leave_type - frappe.msgprint(_("Employee {0} is on Leave on {1}").format(self.employee, self.attendance_date)) - - if self.status == "On Leave" and not leave_record: - frappe.throw(_("No leave record found for employee {0} for {1}").format(self.employee, self.attendance_date)) + def validate(self): + from erpnext.controllers.status_updater import validate_status + validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) + self.validate_attendance_date() + self.validate_duplicate_record() + self.check_leave_record() def validate_attendance_date(self): date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") @@ -44,19 +26,52 @@ class Attendance(Document): elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining): frappe.throw(_("Attendance date can not be less than employee's joining date")) + def validate_duplicate_record(self): + res = frappe.db.sql(""" + select name from `tabAttendance` + where employee = %s + and attendance_date = %s + and name != %s + and docstatus != 2 + """, (self.employee, getdate(self.attendance_date), self.name)) + if res: + frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee)) + + def check_leave_record(self): + leave_record = frappe.db.sql(""" + select leave_type, half_day, half_day_date + from `tabLeave Application` + where employee = %s + and %s between from_date and to_date + and status = 'Approved' + and docstatus = 1 + """, (self.employee, self.attendance_date), as_dict=True) + if leave_record: + for d in leave_record: + self.leave_type = d.leave_type + if d.half_day_date == getdate(self.attendance_date): + self.status = 'Half Day' + frappe.msgprint(_("Employee {0} on Half day on {1}") + .format(self.employee, format_date(self.attendance_date))) + else: + self.status = 'On Leave' + frappe.msgprint(_("Employee {0} is on Leave on {1}") + .format(self.employee, format_date(self.attendance_date))) + + if self.status in ("On Leave", "Half Day"): + if not leave_record: + frappe.msgprint(_("No leave record found for employee {0} on {1}") + .format(self.employee, format_date(self.attendance_date)), alert=1) + elif self.leave_type: + self.leave_type = None + self.leave_application = None + def validate_employee(self): emp = frappe.db.sql("select name from `tabEmployee` where name = %s and status = 'Active'", self.employee) if not emp: frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee)) - def validate(self): - from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) - self.validate_attendance_date() - self.validate_duplicate_record() - self.check_leave_record() - @frappe.whitelist() def get_events(start, end, filters=None): events = [] @@ -90,18 +105,20 @@ def add_attendance(events, start, end, conditions=None): if e not in events: events.append(e) -def mark_attendance(employee, attendance_date, status, shift=None): - employee_doc = frappe.get_doc('Employee', employee) +def mark_attendance(employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False): if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}): - doc_dict = { + company = frappe.db.get_value('Employee', employee, 'company') + attendance = frappe.get_doc({ 'doctype': 'Attendance', 'employee': employee, 'attendance_date': attendance_date, 'status': status, - 'company': employee_doc.company, - 'shift': shift - } - attendance = frappe.get_doc(doc_dict).insert() + 'company': company, + 'shift': shift, + 'leave_type': leave_type + }) + attendance.flags.ignore_validate = ignore_validate + attendance.insert() attendance.submit() return attendance.name diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 90f49886f8..9161ed822a 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -13,10 +13,12 @@ "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", "payroll_settings", + "payroll_based_on", + "max_working_hours_against_timesheet", "include_holidays_in_total_working_days", "disable_rounded_total", - "max_working_hours_against_timesheet", "column_break_11", + "daily_wages_fraction_for_half_day", "email_salary_slip_to_employee", "encrypt_salary_slips_in_emails", "password_policy", @@ -184,13 +186,27 @@ "fieldtype": "Link", "label": "Role Allowed to Create Backdated Leave Application", "options": "Role" + }, + { + "default": "Leave", + "fieldname": "payroll_based_on", + "fieldtype": "Select", + "label": "Calculate Working Days in Payroll based on", + "options": "Leave\nAttendance" + }, + { + "default": "0.5", + "description": "The fraction of daily wages to be paid for half-day attendance", + "fieldname": "daily_wages_fraction_for_half_day", + "fieldtype": "Float", + "label": "Daily Wages Fraction for Half Day" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2020-01-06 18:46:30.189815", + "modified": "2020-04-13 21:20:59.382394", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index bf919067ca..5ed4c87c62 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -15,6 +15,9 @@ class HRSettings(Document): self.set_naming_series() self.validate_password_policy() + if not self.daily_wages_fraction_for_half_day: + self.daily_wages_fraction_for_half_day = 0.5 + def set_naming_series(self): from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series set_by_naming_series("Employee", "employee_number", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.js b/erpnext/hr/doctype/salary_slip/salary_slip.js index f430eeed4e..1c4d4e34c5 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.js +++ b/erpnext/hr/doctype/salary_slip/salary_slip.js @@ -51,7 +51,7 @@ frappe.ui.form.on("Salary Slip", { }, end_date: function(frm) { - frm.events.get_emp_and_leave_details(frm); + frm.events.get_emp_and_working_day_details(frm); }, set_end_date: function(frm){ @@ -86,7 +86,7 @@ frappe.ui.form.on("Salary Slip", { salary_slip_based_on_timesheet: function(frm) { frm.trigger("toggle_fields"); - frm.events.get_emp_and_leave_details(frm); + frm.events.get_emp_and_working_day_details(frm); }, payroll_frequency: function(frm) { @@ -95,15 +95,14 @@ frappe.ui.form.on("Salary Slip", { }, employee: function(frm) { - frm.events.get_emp_and_leave_details(frm); + frm.events.get_emp_and_working_day_details(frm); }, leave_without_pay: function(frm){ if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) { return frappe.call({ - method: 'process_salary_based_on_leave', + method: 'process_salary_based_on_working_days', doc: frm.doc, - args: {"lwp": frm.doc.leave_without_pay}, callback: function(r, rt) { frm.refresh(); } @@ -115,12 +114,12 @@ frappe.ui.form.on("Salary Slip", { frm.toggle_display(['hourly_wages', 'timesheets'], cint(frm.doc.salary_slip_based_on_timesheet)===1); frm.toggle_display(['payment_days', 'total_working_days', 'leave_without_pay'], - frm.doc.payroll_frequency!=""); + frm.doc.payroll_frequency != ""); }, - get_emp_and_leave_details: function(frm) { + get_emp_and_working_day_details: function(frm) { return frappe.call({ - method: 'get_emp_and_leave_details', + method: 'get_emp_and_working_day_details', doc: frm.doc, callback: function(r, rt) { frm.refresh(); diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.json b/erpnext/hr/doctype/salary_slip/salary_slip.json index 097d3a096b..54a8164587 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.json +++ b/erpnext/hr/doctype/salary_slip/salary_slip.json @@ -11,20 +11,20 @@ "employee_name", "department", "designation", + "branch", "column_break1", - "company", + "status", "journal_entry", "payroll_entry", + "company", "letter_head", - "branch", - "status", "section_break_10", "salary_slip_based_on_timesheet", - "payroll_frequency", "start_date", "end_date", "column_break_15", "salary_structure", + "payroll_frequency", "total_working_days", "leave_without_pay", "payment_days", @@ -309,6 +309,7 @@ { "fieldname": "earning", "fieldtype": "Column Break", + "label": "Earning", "oldfieldtype": "Column Break", "width": "50%" }, @@ -323,6 +324,7 @@ { "fieldname": "deduction", "fieldtype": "Column Break", + "label": "Deduction", "oldfieldtype": "Column Break", "width": "50%" }, @@ -463,7 +465,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-04-09 20:02:53.159827", + "modified": "2020-04-14 20:02:53.159827", "modified_by": "Administrator", "module": "HR", "name": "Salary Slip", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 40fe572d75..916b64a83d 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext import datetime, math -from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words +from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, format_date from frappe.model.naming import make_autoname from frappe import msgprint, _ @@ -44,9 +44,9 @@ class SalarySlip(TransactionBase): if not (len(self.get("earnings")) or len(self.get("deductions"))): # get details from salary structure - self.get_emp_and_leave_details() + self.get_emp_and_working_day_details() else: - self.get_leave_details(lwp = self.leave_without_pay) + self.get_working_days_details(lwp = self.leave_without_pay) self.calculate_net_pay() @@ -117,7 +117,7 @@ class SalarySlip(TransactionBase): self.start_date = date_details.start_date self.end_date = date_details.end_date - def get_emp_and_leave_details(self): + def get_emp_and_working_day_details(self): '''First time, load all the components from salary structure''' if self.employee: self.set("earnings", []) @@ -129,7 +129,8 @@ class SalarySlip(TransactionBase): joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) - self.get_leave_details(joining_date, relieving_date) + #getin leave details + self.get_working_days_details(joining_date, relieving_date) struct = self.check_sal_struct(joining_date, relieving_date) if struct: @@ -188,10 +189,9 @@ class SalarySlip(TransactionBase): make_salary_slip(self._salary_structure_doc.name, self) - def get_leave_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0): - if not joining_date: - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + def get_working_days_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0): + payroll_based_on = frappe.db.get_value("HR Settings", None, "payroll_based_on") + include_holidays_in_total_working_days = frappe.db.get_single_value("HR Settings", "include_holidays_in_total_working_days") working_days = date_diff(self.end_date, self.start_date) + 1 if for_preview: @@ -200,24 +200,42 @@ class SalarySlip(TransactionBase): return holidays = self.get_holidays_for_employee(self.start_date, self.end_date) - actual_lwp = self.calculate_lwp(holidays, working_days) - if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")): + + if not cint(include_holidays_in_total_working_days): working_days -= len(holidays) if working_days < 0: frappe.throw(_("There are more holidays than working days this month.")) + if not payroll_based_on: + frappe.throw(_("Please set Payroll based on in HR settings")) + + if payroll_based_on == "Attendance": + actual_lwp = self.calculate_lwp_based_on_attendance(holidays) + else: + actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days) + if not lwp: lwp = actual_lwp elif lwp != actual_lwp: - frappe.msgprint(_("Leave Without Pay does not match with approved Leave Application records")) + frappe.msgprint(_("Leave Without Pay does not match with approved {} records") + .format(payroll_based_on)) - self.total_working_days = working_days self.leave_without_pay = lwp + self.total_working_days = working_days - payment_days = flt(self.get_payment_days(joining_date, relieving_date)) - flt(lwp) - self.payment_days = payment_days > 0 and payment_days or 0 + payment_days = self.get_payment_days(joining_date, + relieving_date, include_holidays_in_total_working_days) + + if flt(payment_days) > flt(lwp): + self.payment_days = flt(payment_days) - flt(lwp) + else: + self.payment_days = 0 + + def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days): + if not joining_date: + joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, + ["date_of_joining", "relieving_date"]) - def get_payment_days(self, joining_date, relieving_date): start_date = getdate(self.start_date) if joining_date: if getdate(self.start_date) <= joining_date <= getdate(self.end_date): @@ -235,9 +253,10 @@ class SalarySlip(TransactionBase): payment_days = date_diff(end_date, start_date) + 1 - if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")): + if not cint(include_holidays_in_total_working_days): holidays = self.get_holidays_for_employee(start_date, end_date) payment_days -= len(holidays) + return payment_days def get_holidays_for_employee(self, start_date, end_date): @@ -256,27 +275,67 @@ class SalarySlip(TransactionBase): return holidays - def calculate_lwp(self, holidays, working_days): + def calculate_lwp_based_on_leave_application(self, holidays, working_days): lwp = 0 holidays = "','".join(holidays) + daily_wages_fraction_for_half_day = \ + flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + for d in range(working_days): dt = add_days(cstr(getdate(self.start_date)), d) leave = frappe.db.sql(""" SELECT t1.name, - CASE WHEN t1.half_day_date = %(dt)s or t1.to_date = t1.from_date + CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date) THEN t1.half_day else 0 END FROM `tabLeave Application` t1, `tabLeave Type` t2 WHERE t2.name = t1.leave_type AND t2.is_lwp = 1 AND t1.docstatus = 1 AND t1.employee = %(employee)s - AND CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date and ifnull(t1.salary_slip, '') = '' - WHEN t2.include_holiday THEN %(dt)s between from_date and to_date and ifnull(t1.salary_slip, '') = '' - END + AND ifnull(t1.salary_slip, '') = '' + AND CASE + WHEN t2.include_holiday != 1 + THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date + WHEN t2.include_holiday + THEN %(dt)s between from_date and to_date + END """.format(holidays), {"employee": self.employee, "dt": dt}) if leave: - lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1) + is_half_day_leave = cint(leave[0][1]) + lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + + return lwp + + def calculate_lwp_based_on_attendance(self, holidays): + lwp = 0 + + daily_wages_fraction_for_half_day = \ + flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + + lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1)) + + attendances = frappe.db.sql(''' + SELECT attendance_date, status, leave_type + FROM `tabAttendance` + WHERE + status in ("Absent", "Half Day", "On leave") + AND employee = %s + AND docstatus = 1 + AND attendance_date between %s and %s + ''', values=(self.employee, self.start_date, self.end_date), as_dict=1) + + for d in attendances: + if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types: + continue + + if format_date(d.attendance_date, "yyyy-mm-dd") in holidays: + if d.status == "Absent" or \ + (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]): + continue + + lwp += (1 - daily_wages_fraction_for_half_day) if d.status == "Half Day" else 1 + return lwp def add_earning_for_hourly_wages(self, doc, salary_component, amount): @@ -945,7 +1004,7 @@ class SalarySlip(TransactionBase): if not self.salary_slip_based_on_timesheet: self.get_date_details() self.pull_emp_details() - self.get_leave_details(for_preview=for_preview) + self.get_working_days_details(for_preview=for_preview) self.calculate_net_pay() def pull_emp_details(self): @@ -954,8 +1013,8 @@ class SalarySlip(TransactionBase): self.bank_name = emp.bank_name self.bank_account_no = emp.bank_ac_no - def process_salary_based_on_leave(self, lwp=0): - self.get_leave_details(lwp=lwp) + def process_salary_based_on_working_days(self): + self.get_working_days_details(lwp=self.leave_without_pay) self.calculate_net_pay() def unlink_ref_doc_from_salary_slip(ref_no): diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index 73bb19e9ee..fc687a355c 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -21,18 +21,105 @@ class TestSalarySlip(unittest.TestCase): make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, company_list=["_Test Company"]) - for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]: + for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance"]: frappe.db.sql("delete from `tab%s`" % dt) self.make_holiday_list() frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0) - + 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 tearDown(self): frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") + def test_payment_days_based_on_attendance(self): + from erpnext.hr.doctype.attendance.attendance import mark_attendance + no_of_days = self.get_no_of_days() + + # Payroll based on attendance + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Attendance") + frappe.db.set_value("HR Settings", None, "daily_wages_fraction_for_half_day", 0.75) + + emp_id = make_employee("test_for_attendance@salary.com") + frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) + + frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) + + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + mark_attendance(emp_id, first_sunday, 'Absent', ignore_validate=True) # invalid lwp + mark_attendance(emp_id, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # valid lwp + mark_attendance(emp_id, add_days(first_sunday, 2), 'Half Day', leave_type='Leave Without Pay', ignore_validate=True) # valid 0.75 lwp + mark_attendance(emp_id, add_days(first_sunday, 3), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # valid lwp + mark_attendance(emp_id, add_days(first_sunday, 4), 'On Leave', leave_type='Casual Leave', ignore_validate=True) # invalid lwp + mark_attendance(emp_id, add_days(first_sunday, 7), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # invalid lwp + + ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + + self.assertEqual(ss.leave_without_pay, 2.25) + + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 2.25) + + #Gross pay calculation based on attendances + gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) + + self.assertEqual(ss.gross_pay, gross_pay) + + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + + def test_payment_days_based_on_leave_application(self): + no_of_days = self.get_no_of_days() + + # Payroll based on attendance + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + + emp_id = make_employee("test_for_attendance@salary.com") + frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) + + frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) + + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay") + + ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + + self.assertEqual(ss.leave_without_pay, 3) + + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 3) + + #Gross pay calculation based on attendances + gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) + + self.assertEqual(ss.gross_pay, gross_pay) + + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1) @@ -315,7 +402,6 @@ class TestSalarySlip(unittest.TestCase): return [no_of_days_in_month[1], no_of_holidays_in_month] - def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure if not salary_structure: @@ -603,3 +689,17 @@ def create_additional_salary(employee, payroll_period, amount): "type": "Earning" }).submit() return salary_date + +def make_leave_application(employee, from_date, to_date, leave_type, company=None): + leave_application = frappe.get_doc(dict( + doctype = 'Leave Application', + employee = employee, + leave_type = leave_type, + 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() \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 801d583ab1..0ea83fd7bc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -669,6 +669,7 @@ erpnext.patches.v12_0.update_healthcare_refactored_changes erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list +erpnext.patches.v12_0.set_default_payroll_based_on erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 diff --git a/erpnext/patches/v12_0/set_default_payroll_based_on.py b/erpnext/patches/v12_0/set_default_payroll_based_on.py new file mode 100644 index 0000000000..04b54a6cf6 --- /dev/null +++ b/erpnext/patches/v12_0/set_default_payroll_based_on.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("hr", "doctype", "hr_settings") + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") \ No newline at end of file