feat : Leave type with partial payment (#23173)
* feat: Partially paid Leaves * feat: some importatnt validation * fix: requested changes * fix: requested changes * fix: travis, sider, codacy * fix: changes requested * test: Partially Paid Leaves
This commit is contained in:
parent
90e33e53fd
commit
f32cff1080
@ -672,10 +672,10 @@
|
||||
"oldfieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status == \"Left\"",
|
||||
"fieldname": "relieving_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Relieving Date",
|
||||
"mandatory_depends_on": "eval:doc.status == \"Left\"",
|
||||
"oldfieldname": "relieving_date",
|
||||
"oldfieldtype": "Date"
|
||||
},
|
||||
@ -822,7 +822,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2020-10-06 15:58:23.805489",
|
||||
"modified": "2020-10-16 14:41:10.580897",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee",
|
||||
|
@ -130,8 +130,7 @@ class LeaveApplication(Document):
|
||||
if self.status == "Approved":
|
||||
for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
|
||||
date = dt.strftime("%Y-%m-%d")
|
||||
status = "Half Day" if getdate(date) == getdate(self.half_day_date) else "On Leave"
|
||||
|
||||
status = "Half Day" if self.half_day_date and getdate(date) == getdate(self.half_day_date) else "On Leave"
|
||||
attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
|
||||
attendance_date = date, docstatus = ('!=', 2)))
|
||||
|
||||
@ -293,7 +292,8 @@ class LeaveApplication(Document):
|
||||
def set_half_day_date(self):
|
||||
if self.from_date == self.to_date and self.half_day == 1:
|
||||
self.half_day_date = self.from_date
|
||||
elif self.half_day == 0:
|
||||
|
||||
if self.half_day == 0:
|
||||
self.half_day_date = None
|
||||
|
||||
def notify_employee(self):
|
||||
|
@ -15,6 +15,8 @@
|
||||
"column_break_3",
|
||||
"is_carry_forward",
|
||||
"is_lwp",
|
||||
"is_ppl",
|
||||
"fraction_of_daily_salary_per_leave",
|
||||
"is_optional_leave",
|
||||
"allow_negative",
|
||||
"include_holiday",
|
||||
@ -77,6 +79,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_ppl == 0",
|
||||
"fieldname": "is_lwp",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Leave Without Pay"
|
||||
@ -183,12 +186,26 @@
|
||||
{
|
||||
"fieldname": "column_break_22",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_lwp == 0",
|
||||
"fieldname": "is_ppl",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Partially Paid Leave"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_ppl == 1",
|
||||
"fieldname": "fraction_of_daily_salary_per_leave",
|
||||
"fieldtype": "Float",
|
||||
"label": "Fraction of Daily Salary per Leave",
|
||||
"mandatory_depends_on": "eval:doc.is_ppl == 1"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-flag",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2019-12-12 12:48:37.780254",
|
||||
"modified": "2020-08-26 14:04:54.318687",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Type",
|
||||
|
@ -21,3 +21,9 @@ class LeaveType(Document):
|
||||
leave_allocation = [l['name'] for l in leave_allocation]
|
||||
if leave_allocation:
|
||||
frappe.throw(_('Leave application is linked with leave allocations {0}. Leave application cannot be set as leave without pay').format(", ".join(leave_allocation))) #nosec
|
||||
|
||||
if self.is_lwp and self.is_ppl:
|
||||
frappe.throw(_("Leave Type can be either without pay or partial pay"))
|
||||
|
||||
if self.is_ppl and (self.fraction_of_daily_salary_per_leave < 0 or self.fraction_of_daily_salary_per_leave > 1):
|
||||
frappe.throw(_("The fraction of Daily Salary per Leave should be between 0 and 1"))
|
||||
|
@ -18,9 +18,14 @@ def create_leave_type(**args):
|
||||
"allow_encashment": args.allow_encashment or 0,
|
||||
"is_earned_leave": args.is_earned_leave or 0,
|
||||
"is_lwp": args.is_lwp or 0,
|
||||
"is_ppl":args.is_ppl or 0,
|
||||
"is_carry_forward": args.is_carry_forward or 0,
|
||||
"expire_carry_forwarded_leaves_after_days": args.expire_carry_forwarded_leaves_after_days or 0,
|
||||
"encashment_threshold_days": args.encashment_threshold_days or 5,
|
||||
"earning_component": "Leave Encashment"
|
||||
})
|
||||
|
||||
if leave_type.is_ppl:
|
||||
leave_type.fraction_of_daily_salary_per_leave = args.fraction_of_daily_salary_per_leave or 0.5
|
||||
|
||||
return leave_type
|
@ -18,7 +18,7 @@ frappe.ui.form.on("Salary Slip", {
|
||||
filters: {
|
||||
employee: frm.doc.employee
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
frm.set_query("salary_component", "earnings", function() {
|
||||
@ -26,7 +26,7 @@ frappe.ui.form.on("Salary Slip", {
|
||||
filters: {
|
||||
type: "earning"
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("salary_component", "deductions", function() {
|
||||
@ -34,13 +34,13 @@ frappe.ui.form.on("Salary Slip", {
|
||||
filters: {
|
||||
type: "deduction"
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("employee", function() {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query"
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@ -66,7 +66,7 @@ frappe.ui.form.on("Salary Slip", {
|
||||
frm.set_value('end_date', r.message.end_date);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
company: function(frm) {
|
||||
@ -77,7 +77,7 @@ frappe.ui.form.on("Salary Slip", {
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.trigger("toggle_fields")
|
||||
frm.trigger("toggle_fields");
|
||||
|
||||
var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"];
|
||||
cur_frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false);
|
||||
@ -103,7 +103,7 @@ frappe.ui.form.on("Salary Slip", {
|
||||
return frappe.call({
|
||||
method: 'process_salary_based_on_working_days',
|
||||
doc: frm.doc,
|
||||
callback: function(r, rt) {
|
||||
callback: function() {
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
@ -121,10 +121,10 @@ frappe.ui.form.on("Salary Slip", {
|
||||
return frappe.call({
|
||||
method: 'get_emp_and_working_day_details',
|
||||
doc: frm.doc,
|
||||
callback: function(r, rt) {
|
||||
callback: function(r) {
|
||||
frm.refresh();
|
||||
if (r.message){
|
||||
frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true));
|
||||
if (r.message[1] !== "Leave" && r.message[0]) {
|
||||
frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as ")+ r.message[0] +__(". You can can change this in ") + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -141,7 +141,7 @@ frappe.ui.form.on('Salary Slip Timesheet', {
|
||||
});
|
||||
|
||||
// calculate total working hours, earnings based on hourly wages and totals
|
||||
var total_work_hours = function(frm, dt, dn) {
|
||||
var total_work_hours = function(frm) {
|
||||
var total_working_hours = 0.0;
|
||||
$.each(frm.doc["timesheets"] || [], function(i, timesheet) {
|
||||
total_working_hours += timesheet.working_hours;
|
||||
@ -165,4 +165,4 @@ var total_work_hours = function(frm, dt, dn) {
|
||||
frm.doc.rounded_total = Math.round(frm.doc.net_pay);
|
||||
refresh_many(['net_pay', 'rounded_total']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -136,8 +136,8 @@ class SalarySlip(TransactionBase):
|
||||
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
|
||||
self.set_time_sheet()
|
||||
self.pull_sal_struct()
|
||||
consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
|
||||
return consider_unmarked_attendance_as
|
||||
payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"])
|
||||
return [payroll_based_on, consider_unmarked_attendance_as]
|
||||
|
||||
def set_time_sheet(self):
|
||||
if self.salary_slip_based_on_timesheet:
|
||||
@ -210,10 +210,10 @@ class SalarySlip(TransactionBase):
|
||||
frappe.throw(_("Please set Payroll based on in Payroll settings"))
|
||||
|
||||
if payroll_based_on == "Attendance":
|
||||
actual_lwp, absent = self.calculate_lwp_and_absent_days_based_on_attendance(holidays)
|
||||
actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays)
|
||||
self.absent_days = absent
|
||||
else:
|
||||
actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days)
|
||||
actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days)
|
||||
|
||||
if not lwp:
|
||||
lwp = actual_lwp
|
||||
@ -300,7 +300,7 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
return holidays
|
||||
|
||||
def calculate_lwp_based_on_leave_application(self, holidays, working_days):
|
||||
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
|
||||
lwp = 0
|
||||
holidays = "','".join(holidays)
|
||||
daily_wages_fraction_for_half_day = \
|
||||
@ -311,10 +311,12 @@ class SalarySlip(TransactionBase):
|
||||
leave = frappe.db.sql("""
|
||||
SELECT t1.name,
|
||||
CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
|
||||
THEN t1.half_day else 0 END
|
||||
THEN t1.half_day else 0 END,
|
||||
t2.is_ppl,
|
||||
t2.fraction_of_daily_salary_per_leave
|
||||
FROM `tabLeave Application` t1, `tabLeave Type` t2
|
||||
WHERE t2.name = t1.leave_type
|
||||
AND t2.is_lwp = 1
|
||||
AND (t2.is_lwp = 1 or t2.is_ppl = 1)
|
||||
AND t1.docstatus = 1
|
||||
AND t1.employee = %(employee)s
|
||||
AND ifnull(t1.salary_slip, '') = ''
|
||||
@ -327,19 +329,35 @@ class SalarySlip(TransactionBase):
|
||||
""".format(holidays), {"employee": self.employee, "dt": dt})
|
||||
|
||||
if leave:
|
||||
equivalent_lwp_count = 0
|
||||
is_half_day_leave = cint(leave[0][1])
|
||||
lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
|
||||
is_partially_paid_leave = cint(leave[0][2])
|
||||
fraction_of_daily_salary_per_leave = flt(leave[0][3])
|
||||
|
||||
equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
|
||||
|
||||
if is_partially_paid_leave:
|
||||
equivalent_lwp_count *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
|
||||
|
||||
lwp += equivalent_lwp_count
|
||||
|
||||
return lwp
|
||||
|
||||
def calculate_lwp_and_absent_days_based_on_attendance(self, holidays):
|
||||
def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays):
|
||||
lwp = 0
|
||||
absent = 0
|
||||
|
||||
daily_wages_fraction_for_half_day = \
|
||||
flt(frappe.db.get_value("Payroll 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))
|
||||
leave_types = frappe.get_all("Leave Type",
|
||||
or_filters=[["is_ppl", "=", 1], ["is_lwp", "=", 1]],
|
||||
fields =["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"])
|
||||
|
||||
leave_type_map = {}
|
||||
for leave_type in leave_types:
|
||||
leave_type_map[leave_type.name] = leave_type
|
||||
|
||||
attendances = frappe.db.sql('''
|
||||
SELECT attendance_date, status, leave_type
|
||||
FROM `tabAttendance`
|
||||
@ -351,21 +369,30 @@ class SalarySlip(TransactionBase):
|
||||
''', 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:
|
||||
if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in leave_type_map.keys():
|
||||
continue
|
||||
|
||||
if formatdate(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]):
|
||||
(d.leave_type and d.leave_type in leave_type_map.keys() and not leave_type_map[d.leave_type]['include_holiday']):
|
||||
continue
|
||||
|
||||
if d.leave_type:
|
||||
fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type]["fraction_of_daily_salary_per_leave"]
|
||||
|
||||
if d.status == "Half Day":
|
||||
lwp += (1 - daily_wages_fraction_for_half_day)
|
||||
elif d.status == "On Leave" and d.leave_type in lwp_leave_types:
|
||||
lwp += 1
|
||||
equivalent_lwp = (1 - daily_wages_fraction_for_half_day)
|
||||
|
||||
if d.leave_type in leave_type_map.keys() and leave_type_map[d.leave_type]["is_ppl"]:
|
||||
equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
|
||||
lwp += equivalent_lwp
|
||||
elif d.status == "On Leave" and d.leave_type and d.leave_type in leave_type_map.keys():
|
||||
equivalent_lwp = 1
|
||||
if leave_type_map[d.leave_type]["is_ppl"]:
|
||||
equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
|
||||
lwp += equivalent_lwp
|
||||
elif d.status == "Absent":
|
||||
absent += 1
|
||||
|
||||
return lwp, absent
|
||||
|
||||
def add_earning_for_hourly_wages(self, doc, salary_component, amount):
|
||||
@ -949,9 +976,8 @@ class SalarySlip(TransactionBase):
|
||||
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
|
||||
total_amount = amounts['interest_amount'] + amounts['payable_principal_amount']
|
||||
if payment.total_payment > total_amount:
|
||||
frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2}
|
||||
against loan {3}""").format(payment.idx, frappe.bold(payment.total_payment),
|
||||
frappe.bold(total_amount), frappe.bold(payment.loan)))
|
||||
frappe.throw(_("Row {0}: Paid amount {1} is greater than pending accrued amount {2}against loan {3}").format(
|
||||
payment.idx, frappe.bold(payment.total_payment),frappe.bold(total_amount), frappe.bold(payment.loan)))
|
||||
|
||||
self.total_interest_amount += payment.interest_amount
|
||||
self.total_principal_amount += payment.principal_amount
|
||||
|
@ -13,6 +13,8 @@ from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_
|
||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration \
|
||||
import create_payroll_period, create_exemption_category
|
||||
|
||||
@ -93,14 +95,27 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay")
|
||||
|
||||
leave_type_ppl = create_leave_type(leave_type_name="Test Partially Paid Leave", is_ppl = 1)
|
||||
leave_type_ppl.save()
|
||||
|
||||
alloc = create_leave_allocation(
|
||||
employee = emp_id, from_date = add_days(first_sunday, 4),
|
||||
to_date = add_days(first_sunday, 10), new_leaves_allocated = 3,
|
||||
leave_type = "Test Partially Paid Leave")
|
||||
alloc.save()
|
||||
alloc.submit()
|
||||
|
||||
#two day leave ppl with fraction_of_daily_salary_per_leave = 0.5 equivalent to single day lwp
|
||||
make_leave_application(emp_id, add_days(first_sunday, 4), add_days(first_sunday, 5), "Test Partially Paid Leave")
|
||||
|
||||
ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly")
|
||||
|
||||
self.assertEqual(ss.leave_without_pay, 3)
|
||||
self.assertEqual(ss.leave_without_pay, 4)
|
||||
|
||||
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)
|
||||
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
|
||||
|
||||
#Gross pay calculation based on attendances
|
||||
gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay))
|
||||
|
Loading…
Reference in New Issue
Block a user