diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index fd3c0efbc9b..a0a9584cc93 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -25,16 +25,13 @@ "out_time", "column_break_18", "standard_working_time", - "standard_working_time_delta", "working_time", - "working_timedelta", "late_entry", "early_exit", "overtime_details_section", "overtime_type", - "overtime_duration", "column_break_27", - "overtime_duration_words", + "overtime_duration", "amended_from" ], "fields": [ @@ -215,41 +212,23 @@ "description": "Shift duration for a day", "fetch_from": "shift.standard_working_time", "fieldname": "standard_working_time", - "fieldtype": "Data", + "fieldtype": "Duration", "label": " Standard Working Time", "read_only": 1 }, - { - "fetch_from": "shift.working_time_delta", - "fieldname": "standard_working_time_delta", - "fieldtype": "Time", - "hidden": 1, - "label": "Standard Working Time(Delta)" - }, { "depends_on": "working_time", "fieldname": "working_time", - "fieldtype": "Data", + "fieldtype": "Duration", "label": "Total Working Time", "precision": "1", "read_only": 1 }, { - "fieldname": "working_timedelta", - "fieldtype": "Time", - "hidden": 1, - "label": "Working Time(Delta)" - }, - { - "fieldname": "overtime_duration_words", - "fieldtype": "Data", - "label": "Overtime Duration(Words)", - "read_only": 1 - }, - { - "default": "00:00:00", + "default": "0000", "fieldname": "overtime_duration", - "fieldtype": "Time", + "fieldtype": "Duration", + "hide_days": 1, "label": "Overtime Duration" }, { @@ -267,7 +246,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-26 16:44:33.219313", + "modified": "2021-06-09 13:42:36.176547", "modified_by": "Administrator", "module": "HR", "name": "Attendance", diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index e5b3fdf656a..d96b8fd944b 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -22,6 +22,9 @@ class Attendance(Document): self.set_overtime_type() self.set_default_shift() + if not frappe.db.get_single_value('Payroll Settings', 'fetch_standard_working_hours_from_shift_type'): + self.standard_working_time = None + def validate_attendance_date(self): date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") @@ -54,6 +57,18 @@ class Attendance(Document): def set_overtime_type(self): self.overtime_type = get_overtime_type(self.employee) + if self.overtime_type: + if frappe.db.get_single_value("Payroll Settings", "overtime_based_on") != "Attendance": + frappe.msgprint(_('Set "Calculate Overtime Based On Attendance" to Attendance for Overtime Slip Creation')) + + maximum_overtime_hours_allowed = frappe.db.get_single_value("Payroll Settings", "maximum_overtime_hours_allowed") + + if maximum_overtime_hours_allowed and maximum_overtime_hours_allowed * 3600 < self.overtime_duration: + self.overtime_duration = maximum_overtime_hours_allowed * 3600 + frappe.msgprint(_("Overtime Duration can not be greater than {0} Hours. You can change this in Payroll settings").format( + str(maximum_overtime_hours_allowed) + )) + def check_leave_record(self): leave_record = frappe.db.sql(""" select leave_type, half_day, half_day_date @@ -91,11 +106,9 @@ class Attendance(Document): def calculate_overtime_duration(self): #this method is only for Calculation of overtime based on Attendance through Employee Checkins - overtime_duration = self.working_timedelta - self.standard_working_time_delta - self.overtime_duration = overtime_duration - overtime_duration = str(overtime_duration).split(':') - if int(overtime_duration[0]) or int(overtime_duration[1]): - self.overtime_duration_words = overtime_duration[0] + " Hours " + overtime_duration[1] + " Minutes" + self.overtime_duration = None + if int(self.working_time) > int(self.standard_working_time): + self.overtime_duration = int(self.working_time) - int(self.standard_working_time) @frappe.whitelist() def get_shift_type(employee, attendance_date): @@ -123,9 +136,6 @@ def get_shift_type(employee, attendance_date): @frappe.whitelist() def get_overtime_type(employee): - overtime_based_on = frappe.db.get_single_value("Payroll Settings", "overtime_based_on") - if overtime_based_on == "Attendance": - emp_department = frappe.db.get_value("Employee", employee, "department") if emp_department: overtime_type = frappe.get_list("Overtime Type", filters={"party_type": "Department", "party": emp_department}, fields=['name']) diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 791b32e9c0e..272ab6d36a8 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import now, cint, get_datetime +from frappe.utils import cint, get_datetime from frappe.model.document import Document from datetime import timedelta from math import modf @@ -42,8 +42,8 @@ class EmployeeCheckin(Document): self.shift_start = shift_actual_timings[2].start_datetime self.shift_end = shift_actual_timings[2].end_datetime elif frappe.db.get_value("Shift Type", shift_actual_timings[2].shift_type.name, "allow_overtime"): - # #because after Actual time it takes check-in/out invalid - # #if employee checkout late or check-in before before shift timing adding time buffer. + #because after Actual time it takes check-in/out invalid + #if employee checkout late or check-in before before shift timing adding time buffer. self.shift = shift_actual_timings[2].shift_type.name self.shift_start = shift_actual_timings[2].start_datetime self.shift_end = shift_actual_timings[2].end_datetime @@ -111,10 +111,6 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki from erpnext.hr.doctype.shift_type.shift_type import convert_time_into_duration working_time = convert_time_into_duration(working_timedelta) - print("working") - print(working_timedelta) - print(working_time) - doc_dict = { 'doctype': 'Attendance', 'employee': employee, diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 89ae4d535d4..d733d051963 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate +from frappe.utils import cint, cstr, getdate, now_datetime, nowdate from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from erpnext.hr.utils import validate_active_employee @@ -236,13 +236,15 @@ def get_shift_details(shift_type_name, for_date=nowdate()): end_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.end_time actual_start = start_datetime - timedelta(minutes=shift_type.begin_check_in_before_shift_start_time) actual_end = end_datetime + timedelta(minutes=shift_type.allow_check_out_after_shift_end_time) + allow_overtime = shift_type.allow_overtime return frappe._dict({ 'shift_type': shift_type, 'start_datetime': start_datetime, 'end_datetime': end_datetime, 'actual_start': actual_start, - 'actual_end': actual_end + 'actual_end': actual_end, + 'allow_overtime': allow_overtime }) @@ -254,22 +256,32 @@ def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_defa """ actual_shift_start = actual_shift_end = shift_details = None shift_timings_as_per_timestamp = get_employee_shift_timings(employee, for_datetime, consider_default_shift) - timestamp_list = [] - for shift in shift_timings_as_per_timestamp: - if shift: - timestamp_list.extend([shift.actual_start, shift.actual_end]) - else: - timestamp_list.extend([None, None]) - timestamp_index = None - for index, timestamp in enumerate(timestamp_list): - if timestamp and for_datetime <= timestamp: - timestamp_index = index - break - if timestamp_index and timestamp_index%2 == 1: - shift_details = shift_timings_as_per_timestamp[int((timestamp_index-1)/2)] - actual_shift_start = shift_details.actual_start - actual_shift_end = shift_details.actual_end - elif timestamp_index: - shift_details = shift_timings_as_per_timestamp[int(timestamp_index/2)] + + if not shift_timings_as_per_timestamp[0].allow_overtime: + # If Shift is not allowed for automatic calculation of overtime, then previous, current and next + # shift will also should be considered for valid and invalid checkins. + # if checkin time is not in current shift thenit will check prev and next shift for checkin validation. + timestamp_list = [] + for shift in shift_timings_as_per_timestamp: + if shift: + timestamp_list.extend([shift.actual_start, shift.actual_end]) + else: + timestamp_list.extend([None, None]) + + timestamp_index = None + for index, timestamp in enumerate(timestamp_list): + if timestamp and for_datetime <= timestamp: + timestamp_index = index + break + if timestamp_index and timestamp_index%2 == 1: + shift_details = shift_timings_as_per_timestamp[int((timestamp_index-1)/2)] + actual_shift_start = shift_details.actual_start + actual_shift_end = shift_details.actual_end + elif timestamp_index: + shift_details = shift_timings_as_per_timestamp[int(timestamp_index/2)] + else: + # for overtime calculation there is no valid and invalid checkins it should return the current shift and after that total working + # hours will be taken in consideration for overtime calculation. there will be no actual_shift_start/end. + shift_details = shift_timings_as_per_timestamp[1] return actual_shift_start, actual_shift_end, shift_details diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json index 4fda25d7327..7aaa508c692 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.json +++ b/erpnext/hr/doctype/shift_type/shift_type.json @@ -9,7 +9,6 @@ "start_time", "end_time", "standard_working_time", - "working_time_delta", "column_break_3", "holiday_list", "enable_auto_attendance", @@ -169,16 +168,10 @@ }, { "fieldname": "standard_working_time", - "fieldtype": "Data", + "fieldtype": "Duration", "label": "Standard Working Time", "read_only": 1 }, - { - "fieldname": "working_time_delta", - "fieldtype": "Time", - "hidden": 1, - "label": "Working time(delta)" - }, { "default": "0", "depends_on": "enable_auto_attendance", @@ -198,7 +191,7 @@ } ], "links": [], - "modified": "2021-05-26 14:10:09.574202", + "modified": "2021-06-09 13:38:25.697100", "modified_by": "Administrator", "module": "HR", "name": "Shift Type", diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index f3c6c055989..2b3af3d3b0b 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -36,8 +36,6 @@ class ShiftType(Document): time_difference = shift_start - shift_end self.standard_working_time = convert_time_into_duration(time_difference) - - def validate_overtime(self): if not frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type") and self.allow_overtime: frappe.throw(_('Please enable "Fetch Standard Working Hours from Shift Type" in payroll Settings for Overtime.')) @@ -47,6 +45,7 @@ class ShiftType(Document): @frappe.whitelist() def process_auto_attendance(self): + self.validate_overtime() if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin: return filters = { @@ -57,11 +56,8 @@ class ShiftType(Document): 'shift': self.name } logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time") - from pprint import pprint - pprint(logs) if self.allow_overtime == 1: - print("chumma") checkins_log = itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_start'])) else: checkins_log = itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])) @@ -69,7 +65,6 @@ class ShiftType(Document): for key, group in checkins_log: single_shift_logs = list(group) attendance_status, working_hours, late_entry, early_exit, in_time, out_time = self.get_attendance(single_shift_logs) - print(attendance_status, working_hours, late_entry, early_exit, in_time, out_time) mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, in_time, out_time, self.name) @@ -86,7 +81,6 @@ class ShiftType(Document): late_entry = early_exit = False total_working_hours, in_time, out_time = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on) - print(total_working_hours) if cint(self.enable_entry_grace_period) and in_time and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)): late_entry = True @@ -95,7 +89,6 @@ class ShiftType(Document): early_exit = True if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent: - print("------->>", 'Here', print(self.working_hours_threshold_for_absent)) return 'Absent', total_working_hours, late_entry, early_exit, in_time, out_time if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day: diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 7daea2da474..47aa67f3ef7 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -166,15 +166,15 @@ def set_mode_of_payment_account(): def create_account(): return frappe.get_doc({ - "doctype": "Account", - "company": "_Test Company", - "account_name": "Payment Account", - "root_type": "Asset", - "report_type": "Balance Sheet", - "currency": "INR", - "parent_account": "Bank Accounts - _TC", - "account_type": "Bank", - }).insert(ignore_permissions=True) + "doctype": "Account", + "company": "_Test Company", + "account_name": "Payment Account", + "root_type": "Asset", + "report_type": "Balance Sheet", + "currency": "INR", + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + }).insert(ignore_permissions=True) def create_employee_and_get_last_salary_slip(): employee = make_employee("test_employee@salary.com", company='_Test Company') diff --git a/erpnext/payroll/doctype/overtime_details/overtime_details.json b/erpnext/payroll/doctype/overtime_details/overtime_details.json index 0964e4d60db..fda72e5402c 100644 --- a/erpnext/payroll/doctype/overtime_details/overtime_details.json +++ b/erpnext/payroll/doctype/overtime_details/overtime_details.json @@ -7,15 +7,15 @@ "field_order": [ "reference_document_type", "reference_document", + "column_break_2", "date", - "start_time", - "end_time", + "start_date", + "end_date", + "section_break_5", "overtime_type", - "total_working_time", - "working_timedelta", "overtime_duration", - "overtime_durationtime", - "overtime_amount" + "column_break_10", + "standard_working_time" ], "fields": [ { @@ -28,17 +28,9 @@ { "fieldname": "date", "fieldtype": "Date", - "label": "Date" - }, - { - "fieldname": "start_time", - "fieldtype": "Datetime", - "label": "Start Time " - }, - { - "fieldname": "end_time", - "fieldtype": "Datetime", - "label": "End Time" + "in_list_view": 1, + "label": "Date", + "reqd": 1 }, { "fieldname": "overtime_type", @@ -48,49 +40,57 @@ "options": "Overtime Type", "reqd": 1 }, - { - "fieldname": "total_working_time", - "fieldtype": "Data", - "label": "Total Working Time" - }, - { - "default": "00:00:00", - "fieldname": "working_timedelta", - "fieldtype": "Time", - "label": "Working Time(Delta)" - }, { "fieldname": "overtime_duration", - "fieldtype": "Data", + "fieldtype": "Duration", + "hide_days": 1, "in_list_view": 1, "label": "Overtime Duration", "reqd": 1 }, - { - "default": "00:00:00", - "fieldname": "overtime_durationtime", - "fieldtype": "Time", - "hidden": 1, - "label": "Overtime Duration(Time)" - }, - { - "fieldname": "overtime_amount", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Overtime Amount", - "reqd": 1 - }, { "fieldname": "reference_document", "fieldtype": "Dynamic Link", + "in_list_view": 1, "label": "Reference Document", - "options": "reference_document_type" + "options": "reference_document_type", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "read_only": 1 + }, + { + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "read_only": 1 + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "standard_working_time", + "fieldtype": "Duration", + "label": "Standard Working Time", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-27 13:43:11.578682", + "modified": "2021-06-14 17:39:36.147530", "modified_by": "Administrator", "module": "Payroll", "name": "Overtime Details", diff --git a/erpnext/payroll/doctype/overtime_slip/overtime_slip.js b/erpnext/payroll/doctype/overtime_slip/overtime_slip.js index 642a9581dbc..df549f67858 100644 --- a/erpnext/payroll/doctype/overtime_slip/overtime_slip.js +++ b/erpnext/payroll/doctype/overtime_slip/overtime_slip.js @@ -2,53 +2,79 @@ // For license information, please see license.txt frappe.ui.form.on('Overtime Slip', { - onload: function() { - + onload: function (frm) { + frm.set_query("employee", () => { + return { + query: "erpnext.controllers.queries.employee_query" + }; + }); }, - employee: function(frm) { + + employee: function (frm) { if (frm.doc.employee) { - frm.events.set_frequency_and_dates(frm); - frm.events.get_emp_details_and_overtime_duration(frm); + frm.events.set_frequency_and_dates(frm).then(() => { + frm.events.get_emp_details_and_overtime_duration(frm); + }); } }, + from_date: function (frm) { - from_date: function(frm) { if (frm.doc.employee) { - frm.events.set_frequency_and_dates(frm); - frm.events.get_emp_details_and_overtime_duration(frm); + frm.events.set_frequency_and_dates(frm).then(() => { + frm.events.get_emp_details_and_overtime_duration(frm); + }); } }, - set_frequency_and_dates: function(frm) { - frappe.call({ + set_frequency_and_dates: function (frm) { + return frappe.call({ method: "erpnext.payroll.doctype.overtime_slip.overtime_slip.get_frequency_and_dates", args: { employee: frm.doc.employee, date: frm.doc.from_date || frm.doc.posting_date, }, - callback: function(r) { + callback: function (r) { frm.set_value("payroll_frequency", r.message[1]); - frm.doc.from_date = r.message[0].start_date; - frm.doc.to_date = r.message[0].end_date; - frm.refresh(); + if (r.message[0].start_date != frm.doc.from_date) { + frm.set_value("from_date", r.message[0].start_date); + } + frm.set_value("to_date", r.message[0].end_date); } }); }, - get_emp_details_and_overtime_duration: function(frm) { + get_emp_details_and_overtime_duration: function (frm) { if (frm.doc.employee) { return frappe.call({ method: 'get_emp_and_overtime_details', doc: frm.doc, - callback: function(r) { - + callback: function () { + frm.refresh(); } }); } }, +}); - reset_value: function(frm) { - +frappe.ui.form.on('Overtime Details', { + date: function (frm, cdt, cdn) { + let child = locals[cdt][cdn]; + if (child.date) { + frappe.call({ + method: "erpnext.payroll.doctype.overtime_slip.overtime_slip.get_standard_working_hours", + args: { + employee: frm.doc.employee, + date: child.date, + }, + callback: function (r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, 'standard_working_time', r.message); + } + } + }); + } else { + frappe.model.set_value(cdt, cdn, 'standard_working_time', 0); + } } }); diff --git a/erpnext/payroll/doctype/overtime_slip/overtime_slip.json b/erpnext/payroll/doctype/overtime_slip/overtime_slip.json index 228ca7da3e9..9f721d39e77 100644 --- a/erpnext/payroll/doctype/overtime_slip/overtime_slip.json +++ b/erpnext/payroll/doctype/overtime_slip/overtime_slip.json @@ -1,5 +1,6 @@ { "actions": [], + "autoname": "HR-OVR-SLIP-.#####", "creation": "2021-05-27 12:47:32.372698", "doctype": "DocType", "editable_grid": 1, @@ -21,10 +22,7 @@ "overtime_details", "section_break_13", "total_overtime_duration", - "total_overtime_durationtime", "column_break_17", - "amount", - "name1", "amended_from" ], "fields": [ @@ -119,19 +117,9 @@ }, { "fieldname": "total_overtime_duration", - "fieldtype": "Data", + "fieldtype": "Duration", "label": "Total Overtime Duration" }, - { - "fieldname": "total_overtime_durationtime", - "fieldtype": "Time", - "label": "Total Overtime Duration(Time)" - }, - { - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount" - }, { "fieldname": "section_break_12", "fieldtype": "Section Break" @@ -140,11 +128,6 @@ "fieldname": "column_break_17", "fieldtype": "Column Break" }, - { - "fieldname": "name1", - "fieldtype": "Duration", - "label": "name" - }, { "default": "Today", "fieldname": "posting_date", @@ -156,7 +139,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-31 15:07:39.485473", + "modified": "2021-06-10 13:35:57.511257", "modified_by": "Administrator", "module": "Payroll", "name": "Overtime Slip", diff --git a/erpnext/payroll/doctype/overtime_slip/overtime_slip.py b/erpnext/payroll/doctype/overtime_slip/overtime_slip.py index 0f29c23817d..3f80db0fbbf 100644 --- a/erpnext/payroll/doctype/overtime_slip/overtime_slip.py +++ b/erpnext/payroll/doctype/overtime_slip/overtime_slip.py @@ -1,15 +1,17 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from erpnext.hr.doctype.attendance.attendance import get_overtime_type import frappe from frappe import _ -from frappe.utils import get_datetime +from frappe.utils import get_datetime, getdate from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.payroll.doctype.gratuity.gratuity import get_salary_structure from frappe.model.document import Document -from pprint import pprint - class OvertimeSlip(Document): + def on_submit(self): + if self.status == "Pending": + frappe.throw(_("Overtime Slip with Status 'Approved' or 'Rejected' are allowed for Submission")) @frappe.whitelist() def get_emp_and_overtime_details(self): @@ -17,43 +19,138 @@ class OvertimeSlip(Document): records = [] if overtime_based_on == "Attendance": records = self.get_attendance_record() + if len(records): + self.create_overtime_details_row_for_attendance(records) elif overtime_based_on == "Timesheet": records = self.get_timesheet_record() + if len(records): + self.create_overtime_details_row_for_timesheet(records) else: frappe.throw(_('Select "Calculate Overtime Hours Based On" in Payroll Settings')) - if len(records): - self.create_overtime_details_row(records) - else: - frappe.throw(_("No {0} records found for Overtime").format(overtime_based_on)) + if len(self.overtime_details): + self.total_overtime_duration = sum([int(detail.overtime_duration) for detail in self.overtime_details]) - def create_overtime_details_row(self, records): - pprint(records) + if not len(records): + self.overtime_details = [] + frappe.msgprint(_("No {0} records found for Overtime").format(overtime_based_on)) + def create_overtime_details_row_for_attendance(self, records): + self.overtime_details = [] + for record in records: + if record.standard_working_time: + standard_working_time = record.standard_working_time + else: + standard_working_time = frappe.db.get_single_value("HR Settings", "standard_working_hours") * 3600 + if not standard_working_time: + frappe.throw(_('Please Set "Standard Working Hours" in HR settings')) + + if record.overtime_duration: + self.append("overtime_details", { + "reference_document_type": "Attendance", + "reference_document": record.name, + "date": record.attendance_date, + "overtime_type": record.overtime_type, + "overtime_duration": record.overtime_duration, + "standard_working_time": standard_working_time, + }) + + def create_overtime_details_row_for_timesheet(self, records): + self.overtime_details = [] + from math import modf + + standard_working_time = frappe.db.get_single_value("HR Settings", "standard_working_hours") * 3600 + if not standard_working_time: + frappe.throw(_('Please Set "Standard Working Hours" in HR settings')) + + + for record in records: + if record.overtime_hours: + overtime_hours = modf(record.overtime_hours) + record.overtime_hours = overtime_hours[1]*3600 + overtime_hours[0]*60 + self.append("overtime_details", { + "reference_document_type": "Timesheet", + "reference_document": record.name, + "date": record.overtime_on, + "start_date": record.start_date, + "end_date": record.end_date, + "overtime_type": record.overtime_type, + "overtime_duration": record.overtime_hours, + "standard_working_time": standard_working_time + }) def get_attendance_record(self): - records = frappe.db.sql("""SELECT overtime_duration, employee, name, attendance_date, overtime_type - FROM `TabAttendance` - WHERE - attendance_date >= %s AND attendance_date <= %s - AND employee = %s - AND docstatus = 1 AND status= 'Present' - AND ( - overtime_duration IS NOT NULL OR overtime_duration != '00:00:00.000000' - ) - """, (get_datetime(self.from_date), get_datetime(self.to_date), self.employee), as_dict=1) + if self.from_date and self.to_date: + records = frappe.db.sql("""SELECT overtime_duration, name, attendance_date, overtime_type, standard_working_time + FROM `TabAttendance` + WHERE + attendance_date >= %s AND attendance_date <= %s + AND employee = %s + AND docstatus = 1 AND status= 'Present' + AND ( + 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) + return records + return [] - return records + def get_timesheet_record(self): + 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 + FROM `TabTimesheet` AS ts + INNER JOIN `tabTimesheet Detail` As tsd ON tsd.parent = ts.name + WHERE + ts.docstatus = 1 + AND end_date > %(from_date)s AND end_date <= %(to_date)s + AND start_date >= %(from_date)s AND start_date < %(to_date)s + AND employee = %(employee)s + AND ( + 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) + return records + return [] + +@frappe.whitelist() +def get_standard_working_hours(employee, date): + shift_assignment = frappe.db.sql('''SELECT shift_type FROM `tabShift Assignment` + WHERE employee = %(employee)s + AND start_date < %(date)s + and (end_date > %(date)s or end_date is NULL or end_date = "") ''', { + "employee": employee, "date": get_datetime(date)} + , as_dict=1, debug=1) + + standard_working_time = 0 + + + 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: + standard_working_time = frappe.db.get_value("Shift Type", shift_assignment[0].shift_type, "standard_working_time") + elif not len(shift_assignment) and fetch_from_shift: + shift = frappe.db.get_value("Employee", employee, "default_shift") + if shift: + standard_working_time = frappe.db.get_value("Shift Type", shift, "standard_working_time") + else: + frappe.throw(_("Set Default Shift in Employee:{0}").format(employee)) + elif not fetch_from_shift: + standard_working_time = frappe.db.get_single_value("HR Settings", "standard_working_hours") * 3600 + if not standard_working_time: + frappe.throw(_('Please Set "Standard Working Hours" in HR settings')) + + return standard_working_time @frappe.whitelist() def get_frequency_and_dates(employee, date): - print(date) salary_structure = get_salary_structure(employee) if salary_structure: payroll_frequency = frappe.db.get_value('Salary Structure', salary_structure, 'payroll_frequency') date_details = get_start_end_dates(payroll_frequency, date, frappe.db.get_value('Employee', employee, 'company')) - print(date_details) return [date_details, payroll_frequency] else: frappe.throw(_("No Salary Structure Assignment found for Employee: {0}").format(employee)) diff --git a/erpnext/payroll/doctype/overtime_type/overtime_type.json b/erpnext/payroll/doctype/overtime_type/overtime_type.json index c403b00efb7..b74c9f53f3f 100644 --- a/erpnext/payroll/doctype/overtime_type/overtime_type.json +++ b/erpnext/payroll/doctype/overtime_type/overtime_type.json @@ -16,7 +16,7 @@ "weekend_multiplier", "column_break_9", "applicable_for_public_holiday", - "public_holiday_multipliers" + "public_holiday_multiplier" ], "fields": [ { @@ -83,15 +83,15 @@ }, { "depends_on": "eval: doc.applicable_for_public_holiday == 1", - "fieldname": "public_holiday_multipliers", + "fieldname": "public_holiday_multiplier", "fieldtype": "Float", - "label": "Public Holiday Multipliers", + "label": "Public Holiday Multiplier", "mandatory_depends_on": "eval: doc.applicable_for_public_holiday == 1" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-05-25 13:21:11.318945", + "modified": "2021-06-09 15:43:43.891270", "modified_by": "Administrator", "module": "Payroll", "name": "Overtime Type", diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index 393f647cc88..4d74e576f42 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -11,6 +11,7 @@ "amount", "year_to_date", "section_break_5", + "overtime_slips", "additional_salary", "statistical_component", "depends_on_payment_days", @@ -235,11 +236,25 @@ "label": "Year To Date", "options": "currency", "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='earnings' && doc.additional_salary", + "fieldname": "is_recurring_additional_salary", + "fieldtype": "Check", + "label": "Is Recurring Additional Salary", + "read_only": 1 + }, + { + "fieldname": "overtime_slips", + "fieldtype": "Small Text", + "label": "Overtime Slip(s)", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-01-14 13:39:15.847158", + "modified": "2021-08-09 17:00:13.386980", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 7e1fb0616d0..66074507d4b 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -336,9 +336,9 @@ class SalarySlip(TransactionBase): return payment_days - def get_holidays_for_employee(self, start_date, end_date): + def get_holidays_for_employee(self, start_date, end_date, as_dict = 0): holiday_list = get_holiday_list_for_employee(self.employee) - holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` + holidays = frappe.db.sql('''select holiday_date, weekly_off from `tabHoliday` where parent=%(holiday_list)s and holiday_date >= %(start_date)s @@ -346,11 +346,12 @@ class SalarySlip(TransactionBase): "holiday_list": holiday_list, "start_date": start_date, "end_date": end_date - }) - - holidays = [cstr(i) for i in holidays] - - return holidays + }, as_dict=1) + if as_dict: + return holidays + else: + holidays = [cstr(data.holiday_date)for data in holidays] + return holidays def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): lwp = 0 @@ -496,6 +497,7 @@ class SalarySlip(TransactionBase): payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) self.add_structure_components(component_type) + self.process_overtime_slips() self.add_additional_salary_components(component_type) if component_type == "earnings": self.add_employee_benefits(payroll_period) @@ -509,6 +511,105 @@ class SalarySlip(TransactionBase): if amount and struct_row.statistical_component == 0: self.update_component_row(struct_row, amount, component_type) + def process_overtime_slips(self): + overtime_slips = self.get_overtime_slips() + amounts, processed_overtime_slips = self.get_overtime_amount(overtime_slips) + self.add_overtime_component(amounts, processed_overtime_slips) + + def get_overtime_slips(self): + return frappe.get_all("Overtime Slip", filters = { + 'employee': self.employee, + 'posting_date': (">=", self.start_date), + 'posting_date': ("<=", self.end_date), + 'docstatus': 1 + }, fields = ["name", "from_date", 'to_date']) + + def get_overtime_amount(self, overtime_slips): + standard_duration_amount = 0; weekends_duration_amount= 0; public_holidays_duration_amount = 0 + calculated_amount = 0 + processed_overtime_slips = [] + overtime_types_details = {} + for slip in overtime_slips: + holiday_date = self.get_holidays_for_employee(slip.from_date, slip.to_date, as_dict=1) + + holiday_date_map = {} + for date in holiday_date: + holiday_date_map[cstr(date.holiday_date)] = date + + details = self.get_overtime_details(slip.name) + + for detail in details: + overtime_hours = detail.overtime_duration / 3600 + + if not detail.overtime_type in overtime_types_details: + details, applicable_components = self.get_overtime_type_detail(detail.overtime_type) + overtime_types_details[detail.overtime_type] = details + if len(applicable_components): + overtime_types_details[detail.overtime_type]["components"] = applicable_components + else: + frappe.throw(_("Select applicable components in Overtime Type: {0}").format( + frappe.bold(detail.overtime_type))) + + 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 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 + + standard_working_hours = detail.standard_working_time/3600 + applicable_hourly_wages = overtime_types_details[detail.overtime_type]["applicable_daily_amount"]/standard_working_hours + + overtime_date = cstr(detail.date) + if overtime_date in holiday_date_map.keys(): + if holiday_date_map[overtime_date].weekly_off == 1: + calculated_amount = overtime_hours * applicable_hourly_wages *\ + overtime_types_details[detail.overtime_type]['weekend_multiplier'] + weekends_duration_amount += calculated_amount + elif holiday_date_map[overtime_date].weekly_off == 0: + calculated_amount = overtime_hours * applicable_hourly_wages *\ + overtime_types_details[detail.overtime_type]['public_holiday_multiplier'] + public_holidays_duration_amount += calculated_amount + else: + calculated_amount = overtime_hours * applicable_hourly_wages *\ + overtime_types_details[detail.overtime_type]['standard_multiplier'] + standard_duration_amount += calculated_amount + + processed_overtime_slips.append(slip.name) + + return [weekends_duration_amount, public_holidays_duration_amount, standard_duration_amount] , processed_overtime_slips + def add_overtime_component(self, amounts, processed_overtime_slips): + if len(amounts): + overtime_salary_component = frappe.db.get_single_value("Payroll Settings", "overtime_salary_component") + + if not overtime_salary_component: + frappe.throw(_('Select {0} in {1}').format( + frappe.bold("Overtime Salary Component"), frappe.bold("Payroll Settings") + )) + else: + self.update_component_row( + get_salary_component_data(overtime_salary_component), + sum(amounts), + 'earnings', + processed_overtime_slips = processed_overtime_slips + ) + + def get_overtime_details(self, parent): + return frappe.get_all( + "Overtime Details", + filters = {"parent": parent}, + fields = ["date", "overtime_type", "overtime_duration", "standard_working_time"] + ) + + def get_overtime_type_detail(self, name): + detail = frappe.get_all("Overtime Type", filters = {"name": name}, fields = ["name", "standard_multiplier", "weekend_multiplier", "public_holiday_multiplier"])[0] + components = frappe.get_all("Overtime Salary Component", + filters = {"parent": name}, fields = ["salary_component"]) + + components = [data. salary_component for data in components] + + return detail, components + def get_data_for_eval(self): '''Returns data for evaluating formula''' data = frappe._dict() @@ -639,7 +740,7 @@ class SalarySlip(TransactionBase): tax_row = get_salary_component_data(d) self.update_component_row(tax_row, tax_amount, "deductions") - def update_component_row(self, component_data, amount, component_type, additional_salary=None): + def update_component_row(self, component_data, amount, component_type, additional_salary=None, processed_overtime_slips =[]): component_row = None for d in self.get(component_type): if d.salary_component != component_data.salary_component: @@ -679,6 +780,10 @@ class SalarySlip(TransactionBase): abbr = component_data.get('abbr') or component_data.get('salary_component_abbr') component_row.set('abbr', abbr) + processed_overtime_slips = ", ".join(processed_overtime_slips) + if processed_overtime_slips: + component_row.overtime_slips = processed_overtime_slips + if additional_salary: component_row.default_amount = 0 component_row.additional_amount = amount diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 84c7b8118b8..74984740cbf 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -2,20 +2,20 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Timesheet", { - setup: function(frm) { + setup: function (frm) { frappe.require("/assets/erpnext/js/projects/timer.js"); frm.add_fetch('employee', 'employee_name', 'employee_name'); - frm.fields_dict.employee.get_query = function() { + frm.fields_dict.employee.get_query = function () { return { - filters:{ + filters: { 'status': 'Active' } }; }; - frm.fields_dict['time_logs'].grid.get_field('task').get_query = function(frm, cdt, cdn) { + frm.fields_dict['time_logs'].grid.get_field('task').get_query = function (frm, cdt, cdn) { var child = locals[cdt][cdn]; - return{ + return { filters: { 'project': child.project, 'status': ["!=", "Cancelled"] @@ -23,8 +23,8 @@ frappe.ui.form.on("Timesheet", { }; }; - frm.fields_dict['time_logs'].grid.get_field('project').get_query = function() { - return{ + frm.fields_dict['time_logs'].grid.get_field('project').get_query = function () { + return { filters: { 'company': frm.doc.company } @@ -32,7 +32,7 @@ frappe.ui.form.on("Timesheet", { }; }, - onload: function(frm){ + onload: function (frm) { if (frm.doc.__islocal && frm.doc.time_logs) { calculate_time_and_amount(frm); } @@ -42,33 +42,37 @@ frappe.ui.form.on("Timesheet", { } }, - refresh: function(frm) { - if(frm.doc.docstatus==1) { - if(frm.doc.per_billed < 100 && frm.doc.total_billable_hours && frm.doc.total_billable_hours > frm.doc.total_billed_hours){ - frm.add_custom_button(__('Create Sales Invoice'), function() { frm.trigger("make_invoice") }, - "fa fa-file-text"); + refresh: function (frm) { + if (frm.doc.docstatus == 1) { + if (frm.doc.per_billed < 100 && frm.doc.total_billable_hours && frm.doc.total_billable_hours > frm.doc.total_billed_hours) { + frm.add_custom_button(__('Create Sales Invoice'), function () { + frm.trigger("make_invoice"); + }, + "fa fa-file-text"); } - if(!frm.doc.salary_slip && frm.doc.employee){ - frm.add_custom_button(__('Create Salary Slip'), function() { frm.trigger("make_salary_slip") }, - "fa fa-file-text"); + if (!frm.doc.salary_slip && frm.doc.employee) { + frm.add_custom_button(__('Create Salary Slip'), function () { + frm.trigger("make_salary_slip"); + }, + "fa fa-file-text"); } } if (frm.doc.docstatus < 1) { let button = 'Start Timer'; - $.each(frm.doc.time_logs || [], function(i, row) { + $.each(frm.doc.time_logs || [], function (i, row) { if ((row.from_time <= frappe.datetime.now_datetime()) && !row.completed) { button = 'Resume Timer'; } }); - frm.add_custom_button(__(button), function() { + frm.add_custom_button(__(button), function () { var flag = true; - $.each(frm.doc.time_logs || [], function(i, row) { + $.each(frm.doc.time_logs || [], function (i, row) { // Fetch the row for which from_time is not present - if (flag && row.activity_type && !row.from_time){ + if (flag && row.activity_type && !row.from_time) { erpnext.timesheet.timer(frm, row); row.from_time = frappe.datetime.now_datetime(); frm.refresh_fields("time_logs"); @@ -77,7 +81,7 @@ frappe.ui.form.on("Timesheet", { } // Fetch the row for timer where activity is not completed and from_time is before now_time if (flag && row.from_time <= frappe.datetime.now_datetime() && !row.completed) { - let timestamp = moment(frappe.datetime.now_datetime()).diff(moment(row.from_time),"seconds"); + let timestamp = moment(frappe.datetime.now_datetime()).diff(moment(row.from_time), "seconds"); erpnext.timesheet.timer(frm, row, timestamp); flag = false; } @@ -88,7 +92,7 @@ frappe.ui.form.on("Timesheet", { } }).addClass("btn-primary"); } - if(frm.doc.per_billed > 0) { + if (frm.doc.per_billed > 0) { frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false); frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false); } @@ -96,15 +100,15 @@ frappe.ui.form.on("Timesheet", { frm.trigger('set_dynamic_field_label'); }, - customer: function(frm) { - frm.set_query('parent_project', function(doc) { + customer: function (frm) { + frm.set_query('parent_project', function (doc) { return { filters: { "customer": doc.customer } }; }); - frm.set_query('project', 'time_logs', function(doc) { + frm.set_query('project', 'time_logs', function (doc) { return { filters: { "customer": doc.customer @@ -114,7 +118,7 @@ frappe.ui.form.on("Timesheet", { frm.refresh(); }, - currency: function(frm) { + currency: function (frm) { let base_currency = frappe.defaults.get_global_default('currency'); if (base_currency != frm.doc.currency) { frappe.call({ @@ -123,7 +127,7 @@ frappe.ui.form.on("Timesheet", { from_currency: frm.doc.currency, to_currency: base_currency }, - callback: function(r) { + callback: function (r) { if (r.message) { frm.set_value('exchange_rate', flt(r.message)); frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + " = [?] " + base_currency); @@ -134,14 +138,14 @@ frappe.ui.form.on("Timesheet", { frm.trigger('set_dynamic_field_label'); }, - exchange_rate: function(frm) { - $.each(frm.doc.time_logs, function(i, d) { + exchange_rate: function (frm) { + $.each(frm.doc.time_logs, function (i, d) { calculate_billing_costing_amount(frm, d.doctype, d.name); }); calculate_time_and_amount(frm); }, - set_dynamic_field_label: function(frm) { + set_dynamic_field_label: function (frm) { let base_currency = frappe.defaults.get_global_default('currency'); frm.set_currency_labels(["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"], base_currency); frm.set_currency_labels(["total_costing_amount", "total_billable_amount", "total_billed_amount"], frm.doc.currency); @@ -154,7 +158,7 @@ frappe.ui.form.on("Timesheet", { frm.set_currency_labels(["billing_rate", "billing_amount", "costing_rate", "costing_amount"], frm.doc.currency, "time_logs"); let time_logs_grid = frm.fields_dict.time_logs.grid; - $.each(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], function(i, d) { + $.each(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], function (i, d) { if (frappe.meta.get_docfield(time_logs_grid.doctype, d)) time_logs_grid.set_column_disp(d, frm.doc.currency != base_currency); }); @@ -162,7 +166,7 @@ frappe.ui.form.on("Timesheet", { frm.refresh_fields(); }, - make_invoice: function(frm) { + make_invoice: function (frm) { let fields = [{ "fieldtype": "Link", "label": __("Item Code"), @@ -187,7 +191,7 @@ frappe.ui.form.on("Timesheet", { dialog.set_primary_action(__('Create Sales Invoice'), () => { var args = dialog.get_values(); - if(!args) return; + if (!args) return; dialog.hide(); return frappe.call({ type: "GET", @@ -199,8 +203,8 @@ frappe.ui.form.on("Timesheet", { "currency": frm.doc.currency }, freeze: true, - callback: function(r) { - if(!r.exc) { + callback: function (r) { + if (!r.exc) { frappe.model.sync(r.message); frappe.set_route("Form", r.message.doctype, r.message.name); } @@ -210,20 +214,20 @@ frappe.ui.form.on("Timesheet", { dialog.show(); }, - make_salary_slip: function(frm) { + make_salary_slip: function (frm) { frappe.model.open_mapped_doc({ method: "erpnext.projects.doctype.timesheet.timesheet.make_salary_slip", frm: frm }); }, - parent_project: function(frm) { + parent_project: function (frm) { set_project_in_timelog(frm); } }); frappe.ui.form.on("Timesheet Detail", { - time_logs_remove: function(frm) { + time_logs_remove: function (frm) { calculate_time_and_amount(frm); }, @@ -236,54 +240,61 @@ frappe.ui.form.on("Timesheet Detail", { } }, - from_time: function(frm, cdt, cdn) { + from_time: function (frm, cdt, cdn) { calculate_end_time(frm, cdt, cdn); }, - to_time: function(frm, cdt, cdn) { + to_time: function (frm, cdt, cdn) { var child = locals[cdt][cdn]; - if(frm._setting_hours) return; + if (frm._setting_hours) return; var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600; frappe.model.set_value(cdt, cdn, "hours", hours); }, - time_logs_add: function(frm, cdt, cdn) { - if(frm.doc.parent_project) { + time_logs_add: function (frm, cdt, cdn) { + if (frm.doc.parent_project) { frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project); } }, - hours: function(frm, cdt, cdn) { + hours: function (frm, cdt, cdn) { calculate_end_time(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, - billing_hours: function(frm, cdt, cdn) { + billing_hours: function (frm, cdt, cdn) { calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, - billing_rate: function(frm, cdt, cdn) { + billing_rate: function (frm, cdt, cdn) { calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, - costing_rate: function(frm, cdt, cdn) { + costing_rate: function (frm, cdt, cdn) { calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, - is_billable: function(frm, cdt, cdn) { + is_billable: function (frm, cdt, cdn) { update_billing_hours(frm, cdt, cdn); update_time_rates(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn); calculate_time_and_amount(frm); }, - activity_type: function(frm, cdt, cdn) { + is_overtime: function(frm, cdt, cdn) { + let child = locals[cdt][cdn]; + if (child.is_overtime) { + get_overtime_type(frm, cdt, cdn); + } + }, + + activity_type: function (frm, cdt, cdn) { frappe.call({ method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost", args: { @@ -291,8 +302,8 @@ frappe.ui.form.on("Timesheet Detail", { activity_type: frm.selected_doc.activity_type, currency: frm.doc.currency }, - callback: function(r){ - if(r.message){ + callback: function (r) { + if (r.message) { frappe.model.set_value(cdt, cdn, 'billing_rate', r.message['billing_rate']); frappe.model.set_value(cdt, cdn, 'costing_rate', r.message['costing_rate']); calculate_billing_costing_amount(frm, cdt, cdn); @@ -302,16 +313,38 @@ frappe.ui.form.on("Timesheet Detail", { } }); -var calculate_end_time = function(frm, cdt, cdn) { +var get_overtime_type = function(frm, cdt, cdn) { + if (frm.doc.employee) { + frappe.call({ + method: "erpnext.hr.doctype.attendance.attendance.get_overtime_type", + args: { + employee: frm.doc.employee + }, + callback: function (r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, 'overtime_type', r.message); + } else { + frappe.model.set_value(cdt, cdn, 'is_overtime', 0); + frappe.throw(__("Define Overtime Type for Employee "+frm.doc.employee+" ")); + } + } + }); + } else { + frappe.model.set_value(cdt, cdn, 'is_overtime', 0); + frappe.throw({message: __("Select Employee if applicable for overtime"), title: "Employee Missing"}); + } +}; + +var calculate_end_time = function (frm, cdt, cdn) { let child = locals[cdt][cdn]; - if(!child.from_time) { + if (!child.from_time) { // if from_time value is not available then set the current datetime frappe.model.set_value(cdt, cdn, "from_time", frappe.datetime.get_datetime_as_string()); } let d = moment(child.from_time); - if(child.hours) { + if (child.hours) { d.add(child.hours, "hours"); frm._setting_hours = true; frappe.model.set_value(cdt, cdn, "to_time", @@ -321,7 +354,7 @@ var calculate_end_time = function(frm, cdt, cdn) { } }; -var update_billing_hours = function(frm, cdt, cdn) { +var update_billing_hours = function (frm, cdt, cdn) { let child = frappe.get_doc(cdt, cdn); if (!child.is_billable) { frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0); @@ -331,14 +364,14 @@ var update_billing_hours = function(frm, cdt, cdn) { } }; -var update_time_rates = function(frm, cdt, cdn) { +var update_time_rates = function (frm, cdt, cdn) { let child = frappe.get_doc(cdt, cdn); if (!child.is_billable) { frappe.model.set_value(cdt, cdn, 'billing_rate', 0.0); } }; -var calculate_billing_costing_amount = function(frm, cdt, cdn) { +var calculate_billing_costing_amount = function (frm, cdt, cdn) { let row = frappe.get_doc(cdt, cdn); let billing_amount = 0.0; let base_billing_amount = 0.0; @@ -356,13 +389,13 @@ var calculate_billing_costing_amount = function(frm, cdt, cdn) { frappe.model.set_value(cdt, cdn, 'costing_amount', flt(row.costing_rate) * flt(row.hours)); }; -var calculate_time_and_amount = function(frm) { +var calculate_time_and_amount = function (frm) { let tl = frm.doc.time_logs || []; let total_working_hr = 0; let total_billing_hr = 0; let total_billable_amount = 0; let total_costing_amount = 0; - for(var i=0; i { + frappe.db.get_value('Employee', options, fields).then(({ + message + }) => { if (message) { // there is an employee with the currently logged in user_id frm.set_value("employee", message.name); @@ -394,9 +431,9 @@ const set_employee_and_company = function(frm) { }; function set_project_in_timelog(frm) { - if(frm.doc.parent_project) { - $.each(frm.doc.time_logs || [], function(i, item) { + if (frm.doc.parent_project) { + $.each(frm.doc.time_logs || [], function (i, item) { frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project); }); } -} \ No newline at end of file +} diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index 75f7478ed18..23de2f07fab 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -14,11 +14,11 @@ "customer", "currency", "exchange_rate", - "sales_invoice", "column_break_3", - "salary_slip", "status", "parent_project", + "salary_slip", + "sales_invoice", "employee_detail", "employee", "employee_name", @@ -29,7 +29,10 @@ "end_date", "section_break_5", "time_logs", - "working_hours", + "overtime_details_section", + "overtime_type", + "total_overtime_hours", + "column_break_26", "total_hours", "billing_details", "total_billable_hours", @@ -173,10 +176,6 @@ "options": "Timesheet Detail", "reqd": 1 }, - { - "fieldname": "working_hours", - "fieldtype": "Section Break" - }, { "allow_on_submit": 1, "default": "0", @@ -313,13 +312,36 @@ "fieldname": "exchange_rate", "fieldtype": "Float", "label": "Exchange Rate" + }, + { + "depends_on": "eval: doc.total_overtime_hours", + "fieldname": "overtime_details_section", + "fieldtype": "Section Break", + "label": "Overtime Details" + }, + { + "fieldname": "overtime_type", + "fieldtype": "Link", + "label": "Overtime Type", + "options": "Overtime Type", + "read_only": 1 + }, + { + "fieldname": "total_overtime_hours", + "fieldtype": "Float", + "label": "Total Overtime Hours", + "read_only": 1 + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" } ], "icon": "fa fa-clock-o", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-18 16:10:08.249619", + "modified": "2021-06-14 17:10:31.434084", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet", diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index ae38d4ca192..38dab84b0e9 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -4,16 +4,12 @@ from __future__ import unicode_literals import frappe -from frappe import _ - import json +from frappe import _ from datetime import timedelta from erpnext.controllers.queries import get_match_cond -from frappe.utils import flt, time_diff_in_hours, get_datetime, getdate, cint, date_diff, add_to_date +from frappe.utils import flt, time_diff_in_hours, getdate from frappe.model.document import Document -from erpnext.manufacturing.doctype.workstation.workstation import (check_if_within_operating_hours, - WorkstationHolidayError) -from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.setup.utils import get_exchange_rate from erpnext.hr.utils import validate_active_employee @@ -31,6 +27,7 @@ class Timesheet(Document): self.update_cost() self.calculate_total_amounts() self.calculate_percentage_billed() + self.validate_overtime() self.set_dates() def set_employee_name(self): @@ -65,6 +62,45 @@ class Timesheet(Document): if self.total_billed_amount > 0 and self.total_billable_amount > 0: self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount + def validate_overtime(self): + total_overtime_hours= 0 + overtime_type = None + for data in self.time_logs: + overtime_type = data.overtime_type + if data.is_overtime: + if frappe.db.get_single_value("Payroll Settings", "overtime_based_on") == "Timesheet": + if not self.employee: + frappe.throw("Select Employee, if applicable for overtime") + + if not data.overtime_type: + frappe.throw(_("Define Overtime Type for Employee {0}").format(self.employee)) + + if data.overtime_on: + if data.overtime_on <= data.from_time or data.overtime_on >= data.to_time: + frappe.throw(_("Row {0}: {3} should be within {1} and {2}").format( + str(data.idx), + data.from_time, + data.to_time, + frappe.bold("Overtime On")) + ) + maximum_overtime_hours_allowed = frappe.db.get_single_value("Payroll Settings", "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"). + format( + str(data.idx), + frappe.bold(str(maximum_overtime_hours_allowed)) + )) + else: + frappe.throw(_('Please Set "Calculate Overtime Based On" to TimeSheet In Payroll Settings')) + + + if total_overtime_hours: + self.total_overtime_hours = total_overtime_hours + self.overtime_type =overtime_type + def update_billing_hours(self, args): if args.is_billable: if flt(args.billing_hours) == 0.0: diff --git a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json index ee04c612c9a..cdb8bbe8c39 100644 --- a/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json +++ b/erpnext/projects/doctype/timesheet_detail/timesheet_detail.json @@ -14,10 +14,16 @@ "to_time", "hours", "completed", + "section_break_9", + "is_overtime", + "overtime_type", + "column_break_12", + "overtime_on", + "overtime_hours", "section_break_7", "completed_qty", "workstation", - "column_break_12", + "column_break_18", "operation", "operation_id", "project_details", @@ -70,7 +76,7 @@ "fieldname": "hours", "fieldtype": "Float", "in_list_view": 1, - "label": "Hrs" + "label": "Working Hours" }, { "fieldname": "to_time", @@ -262,12 +268,47 @@ "label": "Costing Amount", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "is_overtime", + "fieldtype": "Check", + "label": "Is Applicable For Overtime" + }, + { + "depends_on": "eval: doc.is_overtime", + "fieldname": "overtime_on", + "fieldtype": "Date", + "label": "Overtime On", + "mandatory_depends_on": "eval: doc.is_overtime" + }, + { + "depends_on": "eval: doc.is_overtime", + "fieldname": "overtime_hours", + "fieldtype": "Float", + "label": "Overtime Hours", + "mandatory_depends_on": "eval: doc.is_overtime" + }, + { + "depends_on": "eval: doc.is_overtime", + "fieldname": "overtime_type", + "fieldtype": "Link", + "label": "Overtime Type", + "options": "Overtime Type" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-05-18 12:19:33.205940", + "modified": "2021-06-10 15:17:20.846091", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet Detail",