mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-09 00:01:18 +00:00
fix: some enhancemets and test cases
This commit is contained in:
@@ -135,26 +135,26 @@ def get_shift_type(employee, attendance_date):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_overtime_type(employee):
|
def get_overtime_type(employee):
|
||||||
|
overtime_type = None
|
||||||
emp_department = frappe.db.get_value("Employee", employee, "department")
|
emp_department = frappe.db.get_value("Employee", employee, "department")
|
||||||
if emp_department:
|
if emp_department:
|
||||||
overtime_type = frappe.get_list("Overtime Type", filters={
|
overtime_type_doc = frappe.get_list("Overtime Type", filters={
|
||||||
"applicable_for": "Department", "department": emp_department}, fields=['name'])
|
"applicable_for": "Department", "department": emp_department}, fields=['name'])
|
||||||
if len(overtime_type):
|
if len(overtime_type_doc):
|
||||||
overtime_type = overtime_type[0].name
|
overtime_type = overtime_type_doc[0].name
|
||||||
|
|
||||||
emp_grade = frappe.db.get_value("Employee", employee, "grade")
|
emp_grade = frappe.db.get_value("Employee", employee, "grade")
|
||||||
|
|
||||||
if emp_grade:
|
if emp_grade:
|
||||||
overtime_type = frappe.get_list("Overtime Type", filters={
|
overtime_type_doc = frappe.get_list("Overtime Type", filters={
|
||||||
"applicable_for": "Employee Grade", "employee_grade": emp_grade},
|
"applicable_for": "Employee Grade", "employee_grade": emp_grade},
|
||||||
fields=['name'])
|
fields=['name'])
|
||||||
if len(overtime_type):
|
if len(overtime_type_doc):
|
||||||
|
overtime_type = overtime_type_doc[0].name
|
||||||
|
|
||||||
overtime_type = overtime_type[0].name
|
overtime_type_doc = frappe.get_list("Overtime Type", filters={
|
||||||
|
|
||||||
overtime_type = frappe.get_list("Overtime Type", filters={
|
|
||||||
"applicable_for": "Employee", "employee": employee}, fields=['name'])
|
"applicable_for": "Employee", "employee": employee}, fields=['name'])
|
||||||
if len(overtime_type):
|
if len(overtime_type_doc):
|
||||||
overtime_type = overtime_type[0].name
|
overtime_type = overtime_type_doc[0].name
|
||||||
|
|
||||||
return overtime_type
|
return overtime_type
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,6 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
|
|||||||
employee_doc = frappe.get_doc('Employee', employee)
|
employee_doc = frappe.get_doc('Employee', employee)
|
||||||
if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
|
if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
|
||||||
|
|
||||||
print(working_hours)
|
|
||||||
working_timedelta = '00:00:00'
|
working_timedelta = '00:00:00'
|
||||||
working_time = None
|
working_time = None
|
||||||
working_time = modf(working_hours)
|
working_time = modf(working_hours)
|
||||||
@@ -153,8 +152,6 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
|
|||||||
"""
|
"""
|
||||||
total_hours = 0
|
total_hours = 0
|
||||||
in_time = out_time = None
|
in_time = out_time = None
|
||||||
print("Madar Chod")
|
|
||||||
print(logs)
|
|
||||||
if check_in_out_type == 'Alternating entries as IN and OUT during the same shift':
|
if check_in_out_type == 'Alternating entries as IN and OUT during the same shift':
|
||||||
in_time = logs[0].time
|
in_time = logs[0].time
|
||||||
if len(logs) >= 2:
|
if len(logs) >= 2:
|
||||||
|
|||||||
@@ -88,12 +88,12 @@ def make_n_checkins(employee, n, hours_to_reverse=1):
|
|||||||
return logs
|
return logs
|
||||||
|
|
||||||
|
|
||||||
def make_checkin(employee, time=now_datetime()):
|
def make_checkin(employee, time=now_datetime(), log_type = "IN"):
|
||||||
log = frappe.get_doc({
|
log = frappe.get_doc({
|
||||||
"doctype": "Employee Checkin",
|
"doctype": "Employee Checkin",
|
||||||
"employee" : employee,
|
"employee" : employee,
|
||||||
"time" : time,
|
"time" : time,
|
||||||
"device_id" : "device1",
|
"device_id" : "device1",
|
||||||
"log_type" : "IN"
|
"log_type" : log_type
|
||||||
}).insert()
|
}).insert()
|
||||||
return log
|
return log
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ShiftType(Document):
|
|||||||
start_time = self.start_time.split(":")
|
start_time = self.start_time.split(":")
|
||||||
|
|
||||||
shift_end = timedelta(hours = int(end_time[0]), minutes = int(end_time[1]), seconds = int(end_time[2]))
|
shift_end = timedelta(hours = int(end_time[0]), minutes = int(end_time[1]), seconds = int(end_time[2]))
|
||||||
shift_start = timedelta(hours =int(start_time[0]), minutes = int(start_time[1]), seconds = int(start_time[2]))
|
shift_start = timedelta(hours = int(start_time[0]), minutes = int(start_time[1]), seconds = int(start_time[2]))
|
||||||
|
|
||||||
if shift_end > shift_start:
|
if shift_end > shift_start:
|
||||||
time_difference = shift_end - shift_start
|
time_difference = shift_end - shift_start
|
||||||
|
|||||||
@@ -8,3 +8,13 @@ import unittest
|
|||||||
|
|
||||||
class TestShiftType(unittest.TestCase):
|
class TestShiftType(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def create_shift_type():
|
||||||
|
shift_type = frappe.new_doc("Shift Type")
|
||||||
|
shift_type.name = "test shift"
|
||||||
|
shift_type.start_time = "9:00:00"
|
||||||
|
shift_type.end_time = "18:00:00"
|
||||||
|
shift_type.enable_auto_attendance = 1
|
||||||
|
|
||||||
|
shift_type.save()
|
||||||
|
return shift_type
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-25 12:49:03.287694",
|
"modified": "2021-06-16 14:48:59.476787",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Overtime Salary Component",
|
"name": "Overtime Salary Component",
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-15 12:53:17.355755",
|
"modified": "2021-06-16 14:55:13.441709",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Overtime Slip",
|
"name": "Overtime Slip",
|
||||||
@@ -162,9 +162,32 @@
|
|||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "System Manager",
|
"role": "HR Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "HR User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Employee",
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -9,14 +9,18 @@ from erpnext.payroll.doctype.gratuity.gratuity import get_salary_structure
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
class OvertimeSlip(Document):
|
class OvertimeSlip(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
if not (self.from_date or self.to_date or self.payroll_frequency):
|
||||||
|
date = self.from_date or self.posting_date
|
||||||
|
data = get_frequency_and_dates(self.employee, date)
|
||||||
|
|
||||||
|
self.from_date = data[0].start_date
|
||||||
|
self.to_date = data[0].end_date
|
||||||
|
self.payroll_frequency = data[1]
|
||||||
|
|
||||||
self.validate_overlap()
|
self.validate_overlap()
|
||||||
if self.from_date >= self.to_date:
|
if self.from_date >= self.to_date:
|
||||||
frappe.throw(_("From date can not be greater than To date"))
|
frappe.throw(_("From date can not be greater than To date"))
|
||||||
|
|
||||||
if not (self.from_date or self.to_date or self.payroll_frequency):
|
|
||||||
date = self.from_date or self.posting_date
|
|
||||||
get_frequency_and_dates(self.employee, date)
|
|
||||||
|
|
||||||
if not len(self.overtime_details):
|
if not len(self.overtime_details):
|
||||||
self.get_emp_and_overtime_details()
|
self.get_emp_and_overtime_details()
|
||||||
|
|
||||||
@@ -29,7 +33,8 @@ class OvertimeSlip(Document):
|
|||||||
"docstatus": ("<", 2),
|
"docstatus": ("<", 2),
|
||||||
"employee": self.employee,
|
"employee": self.employee,
|
||||||
"to_date": (">=", self.from_date),
|
"to_date": (">=", self.from_date),
|
||||||
"from_date": ("<=", self.to_date)
|
"from_date": ("<=", self.to_date),
|
||||||
|
"name": ("!=", self.name)
|
||||||
})
|
})
|
||||||
if len(overtime_slips):
|
if len(overtime_slips):
|
||||||
form_link = get_link_to_form("Overtime Slip", overtime_slips[0].name)
|
form_link = get_link_to_form("Overtime Slip", overtime_slips[0].name)
|
||||||
@@ -119,17 +124,13 @@ class OvertimeSlip(Document):
|
|||||||
AND (
|
AND (
|
||||||
overtime_duration IS NOT NULL OR overtime_duration != '00:00:00.000000'
|
overtime_duration IS NOT NULL OR overtime_duration != '00:00:00.000000'
|
||||||
)
|
)
|
||||||
""", (getdate(self.from_date), getdate(self.to_date), self.employee), as_dict=1, debug = 1)
|
""", (getdate(self.from_date), getdate(self.to_date), self.employee), as_dict=1)
|
||||||
return records
|
return records
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_timesheet_record(self):
|
def get_timesheet_record(self):
|
||||||
if self.from_date and self.to_date:
|
if self.from_date and self.to_date:
|
||||||
|
|
||||||
"""SELECT Orders.OrderID, Customers.CustomerName, Orders.OrderDate
|
|
||||||
FROM Orders
|
|
||||||
INNER JOIN Customers ON Orders.CustomerID=Customers.CustomerID;"""
|
|
||||||
|
|
||||||
records = frappe.db.sql("""SELECT ts.name, ts.start_date, ts.end_date, tsd.overtime_on, tsd.overtime_type, tsd.overtime_hours
|
records = frappe.db.sql("""SELECT ts.name, ts.start_date, ts.end_date, tsd.overtime_on, tsd.overtime_type, tsd.overtime_hours
|
||||||
FROM `TabTimesheet` AS ts
|
FROM `TabTimesheet` AS ts
|
||||||
INNER JOIN `tabTimesheet Detail` As tsd ON tsd.parent = ts.name
|
INNER JOIN `tabTimesheet Detail` As tsd ON tsd.parent = ts.name
|
||||||
@@ -141,7 +142,7 @@ class OvertimeSlip(Document):
|
|||||||
AND (
|
AND (
|
||||||
total_overtime_hours IS NOT NULL OR total_overtime_hours != 0
|
total_overtime_hours IS NOT NULL OR total_overtime_hours != 0
|
||||||
)
|
)
|
||||||
""", {"from_date": get_datetime(self.from_date), "to_date": get_datetime(self.to_date),"employee": self.employee}, as_dict=1, debug = 1)
|
""", {"from_date": get_datetime(self.from_date), "to_date": get_datetime(self.to_date),"employee": self.employee}, as_dict=1)
|
||||||
return records
|
return records
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -156,7 +157,6 @@ def get_standard_working_hours(employee, date):
|
|||||||
|
|
||||||
standard_working_time = 0
|
standard_working_time = 0
|
||||||
|
|
||||||
|
|
||||||
fetch_from_shift = frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type")
|
fetch_from_shift = frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type")
|
||||||
|
|
||||||
if len(shift_assignment) and fetch_from_shift:
|
if len(shift_assignment) and fetch_from_shift:
|
||||||
|
|||||||
@@ -1,8 +1,154 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
import frappe
|
||||||
# import frappe
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
|
from erpnext.payroll.doctype.overtime_type.test_overtime_type import create_overtime_type
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
from erpnext.hr.doctype.shift_type.test_shift_type import create_shift_type
|
||||||
|
from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
|
||||||
|
from frappe.utils import today, add_days, get_datetime
|
||||||
|
from datetime import timedelta
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
class TestOvertimeSlip(unittest.TestCase):
|
class TestOvertimeSlip(unittest.TestCase):
|
||||||
pass
|
def tearDown(self):
|
||||||
|
for doctype in ["Overtime Type","Overtime Slip", "Attendance", "Employee Checkin", "Shift Type"]:
|
||||||
|
frappe.db.sql("DELETE FROM `tab{0}`".format(doctype))
|
||||||
|
|
||||||
|
frappe.db.sql("DELETE FROM `tabEmployee` WHERE user_id = 'test_employee@overtime.com'")
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
def test_overtime_based_on_attendance_without_shift_type(self):
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Attendance")
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "fetch_standard_working_hours_from_shift_type", 0)
|
||||||
|
frappe.db.set_value("HR Settings", None, "standard_working_hours", 7)
|
||||||
|
|
||||||
|
employee = make_employee("test_employee@overtime.com", company='_Test Company')
|
||||||
|
make_salary_structure("structure for Overtime", "Monthly", employee=employee)
|
||||||
|
overtime_type = create_overtime_type(employee=employee)
|
||||||
|
attendance_record = create_attendance_records_for_overtime(employee, overtime_type.name)
|
||||||
|
slip = create_overtime_slip(employee)
|
||||||
|
|
||||||
|
for detail in slip.overtime_details:
|
||||||
|
self.assertIn(detail.reference_document, attendance_record.keys())
|
||||||
|
if detail.reference_document in attendance_record.keys():
|
||||||
|
self.assertEqual(detail.overtime_duration, attendance_record[detail.reference_document]["overtime_duration"])
|
||||||
|
self.assertEqual(str(detail.date), attendance_record[detail.reference_document]["attendance_date"])
|
||||||
|
|
||||||
|
def test_overtime_based_on_attendance_with_shift_type_through_employee_checkins(self):
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Attendance")
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "fetch_standard_working_hours_from_shift_type", 1)
|
||||||
|
|
||||||
|
shift_type = create_shift_type()
|
||||||
|
shift_type.allow_overtime = 1
|
||||||
|
shift_type.process_attendance_after = add_days(today(), -1)
|
||||||
|
shift_type.last_sync_of_checkin = get_datetime(add_days(today(), 1))
|
||||||
|
shift_type.save()
|
||||||
|
|
||||||
|
employee = make_employee("test_employee@overtime.com", company='_Test Company')
|
||||||
|
make_salary_structure("structure for Overtime", "Monthly", employee=employee)
|
||||||
|
|
||||||
|
frappe.db.set_value("Employee", employee, "default_shift", shift_type.name)
|
||||||
|
|
||||||
|
checkin = make_checkin(employee, time = get_datetime(today()) + timedelta(hours=9))
|
||||||
|
checkout = make_checkin(employee, time = get_datetime(today()) + timedelta(hours=20), log_type="OUT")
|
||||||
|
|
||||||
|
self.assertEqual(checkin.shift, shift_type.name)
|
||||||
|
self.assertEqual(checkout.shift, shift_type.name)
|
||||||
|
|
||||||
|
create_overtime_type(employee=employee)
|
||||||
|
|
||||||
|
shift_type.process_auto_attendance()
|
||||||
|
checkin.reload()
|
||||||
|
|
||||||
|
attendance_records = frappe.get_all("Attendance", filters = {
|
||||||
|
'shift': shift_type.name, 'status': 'Present'
|
||||||
|
}, fields = ["name", "overtime_duration", "overtime_type", "attendance_date"])
|
||||||
|
|
||||||
|
records = {}
|
||||||
|
for record in attendance_records:
|
||||||
|
records[record.name] = {
|
||||||
|
"overtime_duration": record.overtime_duration,
|
||||||
|
"overtime_type": record.overtime_type,
|
||||||
|
"attendance_date": record.attendance_date
|
||||||
|
}
|
||||||
|
|
||||||
|
slip = create_overtime_slip(employee)
|
||||||
|
|
||||||
|
for detail in slip.overtime_details:
|
||||||
|
self.assertIn(detail.reference_document, records.keys())
|
||||||
|
if detail.reference_document in records.keys():
|
||||||
|
self.assertEqual(detail.overtime_duration, records[detail.reference_document]["overtime_duration"])
|
||||||
|
self.assertEqual(str(detail.date), str(records[detail.reference_document]["attendance_date"]))
|
||||||
|
|
||||||
|
def test_overtime_based_on_timesheet(self):
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Timesheet")
|
||||||
|
frappe.db.set_value("HR Settings", None, "standard_working_hours", 7)
|
||||||
|
|
||||||
|
employee = make_employee("test_employee@overtime.com", company='_Test Company')
|
||||||
|
make_salary_structure("structure for Overtime", "Monthly", employee=employee)
|
||||||
|
overtime_type = create_overtime_type(employee=employee)
|
||||||
|
time_log, timesheet = create_timesheet_record_for_overtime(employee, overtime_type.name)
|
||||||
|
slip = create_overtime_slip(employee)
|
||||||
|
|
||||||
|
for detail in slip.overtime_details:
|
||||||
|
self.assertEqual(time_log.overtime_hours * 3600, detail.overtime_duration)
|
||||||
|
self.assertEqual(time_log.overtime_on, get_datetime(detail.date))
|
||||||
|
self.assertEqual(time_log.overtime_type, detail.overtime_type)
|
||||||
|
self.assertEqual(timesheet, detail.reference_document)
|
||||||
|
|
||||||
|
def create_attendance_records_for_overtime(employee, overtime_type):
|
||||||
|
records = {}
|
||||||
|
for x in range(2):
|
||||||
|
attendance = frappe.new_doc('Attendance')
|
||||||
|
attendance.employee = employee
|
||||||
|
attendance.status = 'Present'
|
||||||
|
attendance.attendance_date = add_days(today(), -(x))
|
||||||
|
attendance.overtime_type = overtime_type
|
||||||
|
attendance.overtime_duration = 2 * 3600 #for convertion to duration
|
||||||
|
|
||||||
|
attendance.save()
|
||||||
|
attendance.submit()
|
||||||
|
|
||||||
|
records[attendance.name] = {
|
||||||
|
"overtime_duration": attendance.overtime_duration,
|
||||||
|
"overtime_type": attendance.overtime_type,
|
||||||
|
"attendance_date": attendance.attendance_date
|
||||||
|
}
|
||||||
|
|
||||||
|
return records
|
||||||
|
|
||||||
|
def create_timesheet_record_for_overtime(employee, overtime_type):
|
||||||
|
timesheet =frappe.new_doc("Timesheet")
|
||||||
|
timesheet.employee = employee
|
||||||
|
|
||||||
|
timesheet.time_logs = []
|
||||||
|
time_log = {
|
||||||
|
"activity_type": "Planning",
|
||||||
|
"from_time": get_datetime(add_days(today(), -1)),
|
||||||
|
"to_time": get_datetime(add_days(today(), 2)),
|
||||||
|
"expected_hours": 48,
|
||||||
|
"hours": 48,
|
||||||
|
"is_overtime": 1,
|
||||||
|
"overtime_type": overtime_type,
|
||||||
|
"overtime_on": get_datetime(today()),
|
||||||
|
"overtime_hours": 7
|
||||||
|
}
|
||||||
|
timesheet.append("time_logs", time_log)
|
||||||
|
|
||||||
|
timesheet.save()
|
||||||
|
timesheet.submit()
|
||||||
|
|
||||||
|
return frappe._dict(time_log), timesheet.name
|
||||||
|
|
||||||
|
|
||||||
|
def create_overtime_slip(employee):
|
||||||
|
slip = frappe.new_doc("Overtime Slip")
|
||||||
|
slip.employee = employee
|
||||||
|
|
||||||
|
slip.overtime_details = []
|
||||||
|
|
||||||
|
slip.save()
|
||||||
|
return slip
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,42 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
# import frappe
|
import frappe
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
class TestOvertimeType(unittest.TestCase):
|
class TestOvertimeType(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def create_overtime_type(**args):
|
||||||
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
overtime_type = frappe.new_doc("Overtime Type")
|
||||||
|
overtime_type.name = "Test Overtime"
|
||||||
|
overtime_type.applicable_for = args.applicable_for or "Employee"
|
||||||
|
if overtime_type.applicable_for == "Department":
|
||||||
|
overtime_type.department = args.department
|
||||||
|
elif overtime_type.applicable_for == "Employee Grade":
|
||||||
|
overtime_type.employee_grade = args.employee_grade
|
||||||
|
else:
|
||||||
|
overtime_type.employee = args.employee
|
||||||
|
|
||||||
|
overtime_type.standard_multiplier = 1.25
|
||||||
|
overtime_type.applicable_for_weekend = args.applicable_for_weekend or 0
|
||||||
|
overtime_type.applicable_for_public_holiday = args.applicable_for_public_holiday or 0
|
||||||
|
|
||||||
|
if args.applicable_for_weekend:
|
||||||
|
overtime_type.weekend_multiplier = 1.5
|
||||||
|
|
||||||
|
if args.applicable_for_public_holidays:
|
||||||
|
overtime_type.public_holiday_multiplier = 2
|
||||||
|
|
||||||
|
overtime_type.append("applicable_salary_component", {
|
||||||
|
"salary_component": "Basic Salary"
|
||||||
|
})
|
||||||
|
|
||||||
|
overtime_type.save()
|
||||||
|
|
||||||
|
return overtime_type
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -572,7 +572,6 @@ class SalarySlip(TransactionBase):
|
|||||||
if "applicable_amount" not in overtime_types_details[detail.overtime_type].keys():
|
if "applicable_amount" not in overtime_types_details[detail.overtime_type].keys():
|
||||||
component_amount = sum([data.default_amount for data in self.earnings if data.salary_component in \
|
component_amount = sum([data.default_amount for data in self.earnings if data.salary_component in \
|
||||||
overtime_types_details[detail.overtime_type]["components"] and not data.get('additional_salary', None)])
|
overtime_types_details[detail.overtime_type]["components"] and not data.get('additional_salary', None)])
|
||||||
|
|
||||||
overtime_types_details[detail.overtime_type]["applicable_daily_amount"] = component_amount/self.total_working_days
|
overtime_types_details[detail.overtime_type]["applicable_daily_amount"] = component_amount/self.total_working_days
|
||||||
|
|
||||||
standard_working_hours = detail.standard_working_time/3600
|
standard_working_hours = detail.standard_working_time/3600
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
||||||
|
frappe.db.sql("DELETE FROM `tabOvertime Type`")
|
||||||
|
frappe.db.sql("DELETE FROM `tabOvertime Slip`")
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
def test_payment_days_based_on_attendance(self):
|
def test_payment_days_based_on_attendance(self):
|
||||||
@@ -460,6 +462,63 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
# undelete fixture data
|
# undelete fixture data
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
def test_overtime_calculation(self):
|
||||||
|
from erpnext.payroll.doctype.overtime_type.test_overtime_type import create_overtime_type
|
||||||
|
from erpnext.payroll.doctype.overtime_slip.test_overtime_slip import create_overtime_slip
|
||||||
|
from erpnext.payroll.doctype.overtime_slip.test_overtime_slip import create_attendance_records_for_overtime
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
employee = make_employee("overtime_calc@salary.slip")
|
||||||
|
salary_structure = make_salary_structure("structure for Overtime 2", "Monthly", employee=employee)
|
||||||
|
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Attendance")
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "fetch_standard_working_hours_from_shift_type", 0)
|
||||||
|
self.get_salary_component_for_overtime()
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_salary_component", "Overtime Allowance")
|
||||||
|
|
||||||
|
overtime_type = create_overtime_type(employee = employee).name
|
||||||
|
create_attendance_records_for_overtime(employee, overtime_type=overtime_type)
|
||||||
|
|
||||||
|
slip = create_overtime_slip(employee)
|
||||||
|
slip.status = "Approved"
|
||||||
|
slip.submit()
|
||||||
|
|
||||||
|
salary_slip = make_salary_slip(salary_structure.name, employee = employee)
|
||||||
|
overtime_component_details = {}
|
||||||
|
applicable_amount = 0
|
||||||
|
|
||||||
|
for earning in salary_slip.earnings:
|
||||||
|
if earning.salary_component == "Overtime Allowance":
|
||||||
|
overtime_component_details = earning
|
||||||
|
|
||||||
|
if earning.salary_component == "Basic Salary":
|
||||||
|
applicable_amount = earning.default_amount
|
||||||
|
|
||||||
|
self.assertIn("Overtime Allowance", overtime_component_details.salary_component)
|
||||||
|
self.assertEqual(slip.name, overtime_component_details.overtime_slips)
|
||||||
|
|
||||||
|
daily_wages = applicable_amount/ salary_slip.total_working_days
|
||||||
|
hourly_wages = daily_wages/ frappe.db.get_single_value("Hr Settings", "standard_working_hours")
|
||||||
|
overtime_amount = hourly_wages * 4 * 1.25
|
||||||
|
#since multiplier is defined as 1.25 and
|
||||||
|
|
||||||
|
self.assertEquals(flt(overtime_amount, 2), flt(overtime_component_details.amount, 2) )
|
||||||
|
|
||||||
|
|
||||||
|
# formula = sum(applicable_component)/(working_days)/ daily_standard_working_time * overtime hours * multiplier
|
||||||
|
|
||||||
|
def get_salary_component_for_overtime(self):
|
||||||
|
component = [{
|
||||||
|
"salary_component": 'Overtime Allowance',
|
||||||
|
"abbr":'OA',
|
||||||
|
"type": "Earning",
|
||||||
|
"amount_based_on_formula": 0
|
||||||
|
}]
|
||||||
|
|
||||||
|
company = erpnext.get_default_company()
|
||||||
|
make_salary_component(component, test_tax = 0, company_list=[company])
|
||||||
|
|
||||||
def make_activity_for_employee(self):
|
def make_activity_for_employee(self):
|
||||||
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
|
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
|
||||||
activity_type.billing_rate = 50
|
activity_type.billing_rate = 50
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ frappe.ui.form.on("Timesheet", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.docstatus < 1) {
|
if (frm.doc.docstatus < 1 && !cur_frm.doc.__islocal) {
|
||||||
|
|
||||||
let button = 'Start Timer';
|
let button = 'Start Timer';
|
||||||
$.each(frm.doc.time_logs || [], function (i, row) {
|
$.each(frm.doc.time_logs || [], function (i, row) {
|
||||||
|
|||||||
@@ -119,7 +119,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Employee",
|
"label": "Employee",
|
||||||
"options": "Employee"
|
"options": "Employee",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "employee",
|
"depends_on": "employee",
|
||||||
@@ -341,7 +342,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-14 17:10:31.434084",
|
"modified": "2021-06-17 17:45:12.064677",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Timesheet",
|
"name": "Timesheet",
|
||||||
|
|||||||
@@ -84,19 +84,17 @@ class Timesheet(Document):
|
|||||||
frappe.bold("Overtime On"))
|
frappe.bold("Overtime On"))
|
||||||
)
|
)
|
||||||
maximum_overtime_hours_allowed = frappe.db.get_single_value("Payroll Settings", "maximum_overtime_hours_allowed")
|
maximum_overtime_hours_allowed = frappe.db.get_single_value("Payroll Settings", "maximum_overtime_hours_allowed")
|
||||||
|
if int(maximum_overtime_hours_allowed) and data.overtime_hours > maximum_overtime_hours_allowed:
|
||||||
if data.overtime_hours <= maximum_overtime_hours_allowed:
|
|
||||||
total_overtime_hours += data.overtime_hours
|
|
||||||
else:
|
|
||||||
frappe.throw(_("Row {0}: Overtime Hours can not be greater than {1} for a day. You can change this in Payroll Settings").
|
frappe.throw(_("Row {0}: Overtime Hours can not be greater than {1} for a day. You can change this in Payroll Settings").
|
||||||
format(
|
format(
|
||||||
str(data.idx),
|
str(data.idx),
|
||||||
frappe.bold(str(maximum_overtime_hours_allowed))
|
frappe.bold(str(maximum_overtime_hours_allowed))
|
||||||
))
|
))
|
||||||
|
else:
|
||||||
|
total_overtime_hours += data.overtime_hours
|
||||||
else:
|
else:
|
||||||
frappe.throw(_('Please Set "Calculate Overtime Based On" to TimeSheet In Payroll Settings'))
|
frappe.throw(_('Please Set "Calculate Overtime Based On" to TimeSheet In Payroll Settings'))
|
||||||
|
|
||||||
|
|
||||||
if total_overtime_hours:
|
if total_overtime_hours:
|
||||||
self.total_overtime_hours = total_overtime_hours
|
self.total_overtime_hours = total_overtime_hours
|
||||||
self.overtime_type =overtime_type
|
self.overtime_type =overtime_type
|
||||||
@@ -140,20 +138,8 @@ class Timesheet(Document):
|
|||||||
self.update_task_and_project()
|
self.update_task_and_project()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_mandatory_fields()
|
|
||||||
self.update_task_and_project()
|
self.update_task_and_project()
|
||||||
|
|
||||||
def validate_mandatory_fields(self):
|
|
||||||
for data in self.time_logs:
|
|
||||||
if not data.from_time and not data.to_time:
|
|
||||||
frappe.throw(_("Row {0}: From Time and To Time is mandatory.").format(data.idx))
|
|
||||||
|
|
||||||
if not data.activity_type and self.employee:
|
|
||||||
frappe.throw(_("Row {0}: Activity Type is mandatory.").format(data.idx))
|
|
||||||
|
|
||||||
if flt(data.hours) == 0.0:
|
|
||||||
frappe.throw(_("Row {0}: Hours value must be greater than zero.").format(data.idx))
|
|
||||||
|
|
||||||
def update_task_and_project(self):
|
def update_task_and_project(self):
|
||||||
tasks, projects = [], []
|
tasks, projects = [], []
|
||||||
|
|
||||||
|
|||||||
@@ -53,14 +53,16 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Activity Type",
|
"label": "Activity Type",
|
||||||
"options": "Activity Type"
|
"options": "Activity Type",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 2,
|
"columns": 2,
|
||||||
"fieldname": "from_time",
|
"fieldname": "from_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "From Time"
|
"label": "From Time",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_3",
|
"fieldname": "section_break_3",
|
||||||
@@ -76,12 +78,14 @@
|
|||||||
"fieldname": "hours",
|
"fieldname": "hours",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Working Hours"
|
"label": "Working Hours",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "to_time",
|
"fieldname": "to_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "To Time"
|
"label": "To Time",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -308,7 +312,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-10 15:17:20.846091",
|
"modified": "2021-06-17 17:44:09.306304",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Timesheet Detail",
|
"name": "Timesheet Detail",
|
||||||
|
|||||||
Reference in New Issue
Block a user