feat: Overtime based on Attendance and Timesheet

This commit is contained in:
Anurag Mishra
2021-06-14 18:32:32 +05:30
parent 3851d15360
commit da2e95dbcc
18 changed files with 634 additions and 289 deletions

View File

@@ -25,16 +25,13 @@
"out_time", "out_time",
"column_break_18", "column_break_18",
"standard_working_time", "standard_working_time",
"standard_working_time_delta",
"working_time", "working_time",
"working_timedelta",
"late_entry", "late_entry",
"early_exit", "early_exit",
"overtime_details_section", "overtime_details_section",
"overtime_type", "overtime_type",
"overtime_duration",
"column_break_27", "column_break_27",
"overtime_duration_words", "overtime_duration",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@@ -215,41 +212,23 @@
"description": "Shift duration for a day", "description": "Shift duration for a day",
"fetch_from": "shift.standard_working_time", "fetch_from": "shift.standard_working_time",
"fieldname": "standard_working_time", "fieldname": "standard_working_time",
"fieldtype": "Data", "fieldtype": "Duration",
"label": " Standard Working Time", "label": " Standard Working Time",
"read_only": 1 "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", "depends_on": "working_time",
"fieldname": "working_time", "fieldname": "working_time",
"fieldtype": "Data", "fieldtype": "Duration",
"label": "Total Working Time", "label": "Total Working Time",
"precision": "1", "precision": "1",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "working_timedelta", "default": "0000",
"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",
"fieldname": "overtime_duration", "fieldname": "overtime_duration",
"fieldtype": "Time", "fieldtype": "Duration",
"hide_days": 1,
"label": "Overtime Duration" "label": "Overtime Duration"
}, },
{ {
@@ -267,7 +246,7 @@
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-26 16:44:33.219313", "modified": "2021-06-09 13:42:36.176547",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Attendance", "name": "Attendance",

View File

@@ -22,6 +22,9 @@ class Attendance(Document):
self.set_overtime_type() self.set_overtime_type()
self.set_default_shift() 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): def validate_attendance_date(self):
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") 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): def set_overtime_type(self):
self.overtime_type = get_overtime_type(self.employee) 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): def check_leave_record(self):
leave_record = frappe.db.sql(""" leave_record = frappe.db.sql("""
select leave_type, half_day, half_day_date select leave_type, half_day, half_day_date
@@ -91,11 +106,9 @@ class Attendance(Document):
def calculate_overtime_duration(self): def calculate_overtime_duration(self):
#this method is only for Calculation of overtime based on Attendance through Employee Checkins #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 = None
self.overtime_duration = overtime_duration if int(self.working_time) > int(self.standard_working_time):
overtime_duration = str(overtime_duration).split(':') self.overtime_duration = int(self.working_time) - int(self.standard_working_time)
if int(overtime_duration[0]) or int(overtime_duration[1]):
self.overtime_duration_words = overtime_duration[0] + " Hours " + overtime_duration[1] + " Minutes"
@frappe.whitelist() @frappe.whitelist()
def get_shift_type(employee, attendance_date): def get_shift_type(employee, attendance_date):
@@ -123,9 +136,6 @@ def get_shift_type(employee, attendance_date):
@frappe.whitelist() @frappe.whitelist()
def get_overtime_type(employee): 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") emp_department = frappe.db.get_value("Employee", employee, "department")
if emp_department: if emp_department:
overtime_type = frappe.get_list("Overtime Type", filters={"party_type": "Department", "party": emp_department}, fields=['name']) overtime_type = frappe.get_list("Overtime Type", filters={"party_type": "Department", "party": emp_department}, fields=['name'])

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import now, cint, get_datetime from frappe.utils import cint, get_datetime
from frappe.model.document import Document from frappe.model.document import Document
from datetime import timedelta from datetime import timedelta
from math import modf from math import modf
@@ -42,8 +42,8 @@ class EmployeeCheckin(Document):
self.shift_start = shift_actual_timings[2].start_datetime self.shift_start = shift_actual_timings[2].start_datetime
self.shift_end = shift_actual_timings[2].end_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"): 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 #because after Actual time it takes check-in/out invalid
# #if employee checkout late or check-in before before shift timing adding time buffer. #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 = shift_actual_timings[2].shift_type.name
self.shift_start = shift_actual_timings[2].start_datetime self.shift_start = shift_actual_timings[2].start_datetime
self.shift_end = shift_actual_timings[2].end_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 from erpnext.hr.doctype.shift_type.shift_type import convert_time_into_duration
working_time = convert_time_into_duration(working_timedelta) working_time = convert_time_into_duration(working_timedelta)
print("working")
print(working_timedelta)
print(working_time)
doc_dict = { doc_dict = {
'doctype': 'Attendance', 'doctype': 'Attendance',
'employee': employee, 'employee': employee,

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document 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.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from erpnext.hr.utils import validate_active_employee 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 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_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) actual_end = end_datetime + timedelta(minutes=shift_type.allow_check_out_after_shift_end_time)
allow_overtime = shift_type.allow_overtime
return frappe._dict({ return frappe._dict({
'shift_type': shift_type, 'shift_type': shift_type,
'start_datetime': start_datetime, 'start_datetime': start_datetime,
'end_datetime': end_datetime, 'end_datetime': end_datetime,
'actual_start': actual_start, '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 actual_shift_start = actual_shift_end = shift_details = None
shift_timings_as_per_timestamp = get_employee_shift_timings(employee, for_datetime, consider_default_shift) 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 not shift_timings_as_per_timestamp[0].allow_overtime:
if shift: # If Shift is not allowed for automatic calculation of overtime, then previous, current and next
timestamp_list.extend([shift.actual_start, shift.actual_end]) # shift will also should be considered for valid and invalid checkins.
else: # if checkin time is not in current shift thenit will check prev and next shift for checkin validation.
timestamp_list.extend([None, None]) timestamp_list = []
timestamp_index = None for shift in shift_timings_as_per_timestamp:
for index, timestamp in enumerate(timestamp_list): if shift:
if timestamp and for_datetime <= timestamp: timestamp_list.extend([shift.actual_start, shift.actual_end])
timestamp_index = index else:
break timestamp_list.extend([None, None])
if timestamp_index and timestamp_index%2 == 1:
shift_details = shift_timings_as_per_timestamp[int((timestamp_index-1)/2)] timestamp_index = None
actual_shift_start = shift_details.actual_start for index, timestamp in enumerate(timestamp_list):
actual_shift_end = shift_details.actual_end if timestamp and for_datetime <= timestamp:
elif timestamp_index: timestamp_index = index
shift_details = shift_timings_as_per_timestamp[int(timestamp_index/2)] 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 return actual_shift_start, actual_shift_end, shift_details

View File

@@ -9,7 +9,6 @@
"start_time", "start_time",
"end_time", "end_time",
"standard_working_time", "standard_working_time",
"working_time_delta",
"column_break_3", "column_break_3",
"holiday_list", "holiday_list",
"enable_auto_attendance", "enable_auto_attendance",
@@ -169,16 +168,10 @@
}, },
{ {
"fieldname": "standard_working_time", "fieldname": "standard_working_time",
"fieldtype": "Data", "fieldtype": "Duration",
"label": "Standard Working Time", "label": "Standard Working Time",
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "working_time_delta",
"fieldtype": "Time",
"hidden": 1,
"label": "Working time(delta)"
},
{ {
"default": "0", "default": "0",
"depends_on": "enable_auto_attendance", "depends_on": "enable_auto_attendance",
@@ -198,7 +191,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-05-26 14:10:09.574202", "modified": "2021-06-09 13:38:25.697100",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Shift Type", "name": "Shift Type",

View File

@@ -36,8 +36,6 @@ class ShiftType(Document):
time_difference = shift_start - shift_end time_difference = shift_start - shift_end
self.standard_working_time = convert_time_into_duration(time_difference) self.standard_working_time = convert_time_into_duration(time_difference)
def validate_overtime(self): def validate_overtime(self):
if not frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type") and self.allow_overtime: 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.')) 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() @frappe.whitelist()
def process_auto_attendance(self): 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: if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin:
return return
filters = { filters = {
@@ -57,11 +56,8 @@ class ShiftType(Document):
'shift': self.name 'shift': self.name
} }
logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time") 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: if self.allow_overtime == 1:
print("chumma")
checkins_log = itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_start'])) checkins_log = itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_start']))
else: else:
checkins_log = itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])) 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: for key, group in checkins_log:
single_shift_logs = list(group) single_shift_logs = list(group)
attendance_status, working_hours, late_entry, early_exit, in_time, out_time = self.get_attendance(single_shift_logs) 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) 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 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) 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)): 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 late_entry = True
@@ -95,7 +89,6 @@ class ShiftType(Document):
early_exit = True early_exit = True
if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent: 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 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: if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day:

View File

@@ -166,15 +166,15 @@ def set_mode_of_payment_account():
def create_account(): def create_account():
return frappe.get_doc({ return frappe.get_doc({
"doctype": "Account", "doctype": "Account",
"company": "_Test Company", "company": "_Test Company",
"account_name": "Payment Account", "account_name": "Payment Account",
"root_type": "Asset", "root_type": "Asset",
"report_type": "Balance Sheet", "report_type": "Balance Sheet",
"currency": "INR", "currency": "INR",
"parent_account": "Bank Accounts - _TC", "parent_account": "Bank Accounts - _TC",
"account_type": "Bank", "account_type": "Bank",
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)
def create_employee_and_get_last_salary_slip(): def create_employee_and_get_last_salary_slip():
employee = make_employee("test_employee@salary.com", company='_Test Company') employee = make_employee("test_employee@salary.com", company='_Test Company')

View File

@@ -7,15 +7,15 @@
"field_order": [ "field_order": [
"reference_document_type", "reference_document_type",
"reference_document", "reference_document",
"column_break_2",
"date", "date",
"start_time", "start_date",
"end_time", "end_date",
"section_break_5",
"overtime_type", "overtime_type",
"total_working_time",
"working_timedelta",
"overtime_duration", "overtime_duration",
"overtime_durationtime", "column_break_10",
"overtime_amount" "standard_working_time"
], ],
"fields": [ "fields": [
{ {
@@ -28,17 +28,9 @@
{ {
"fieldname": "date", "fieldname": "date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Date" "in_list_view": 1,
}, "label": "Date",
{ "reqd": 1
"fieldname": "start_time",
"fieldtype": "Datetime",
"label": "Start Time "
},
{
"fieldname": "end_time",
"fieldtype": "Datetime",
"label": "End Time"
}, },
{ {
"fieldname": "overtime_type", "fieldname": "overtime_type",
@@ -48,49 +40,57 @@
"options": "Overtime Type", "options": "Overtime Type",
"reqd": 1 "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", "fieldname": "overtime_duration",
"fieldtype": "Data", "fieldtype": "Duration",
"hide_days": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Overtime Duration", "label": "Overtime Duration",
"reqd": 1 "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", "fieldname": "reference_document",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Document", "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, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-05-27 13:43:11.578682", "modified": "2021-06-14 17:39:36.147530",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Overtime Details", "name": "Overtime Details",

View File

@@ -2,53 +2,79 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Overtime Slip', { 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) { if (frm.doc.employee) {
frm.events.set_frequency_and_dates(frm); frm.events.set_frequency_and_dates(frm).then(() => {
frm.events.get_emp_details_and_overtime_duration(frm); frm.events.get_emp_details_and_overtime_duration(frm);
});
} }
}, },
from_date: function (frm) {
from_date: function(frm) {
if (frm.doc.employee) { if (frm.doc.employee) {
frm.events.set_frequency_and_dates(frm); frm.events.set_frequency_and_dates(frm).then(() => {
frm.events.get_emp_details_and_overtime_duration(frm); frm.events.get_emp_details_and_overtime_duration(frm);
});
} }
}, },
set_frequency_and_dates: function(frm) { set_frequency_and_dates: function (frm) {
frappe.call({ return frappe.call({
method: "erpnext.payroll.doctype.overtime_slip.overtime_slip.get_frequency_and_dates", method: "erpnext.payroll.doctype.overtime_slip.overtime_slip.get_frequency_and_dates",
args: { args: {
employee: frm.doc.employee, employee: frm.doc.employee,
date: frm.doc.from_date || frm.doc.posting_date, date: frm.doc.from_date || frm.doc.posting_date,
}, },
callback: function(r) { callback: function (r) {
frm.set_value("payroll_frequency", r.message[1]); frm.set_value("payroll_frequency", r.message[1]);
frm.doc.from_date = r.message[0].start_date; if (r.message[0].start_date != frm.doc.from_date) {
frm.doc.to_date = r.message[0].end_date; frm.set_value("from_date", r.message[0].start_date);
frm.refresh(); }
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) { if (frm.doc.employee) {
return frappe.call({ return frappe.call({
method: 'get_emp_and_overtime_details', method: 'get_emp_and_overtime_details',
doc: frm.doc, 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);
}
} }
}); });

View File

@@ -1,5 +1,6 @@
{ {
"actions": [], "actions": [],
"autoname": "HR-OVR-SLIP-.#####",
"creation": "2021-05-27 12:47:32.372698", "creation": "2021-05-27 12:47:32.372698",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -21,10 +22,7 @@
"overtime_details", "overtime_details",
"section_break_13", "section_break_13",
"total_overtime_duration", "total_overtime_duration",
"total_overtime_durationtime",
"column_break_17", "column_break_17",
"amount",
"name1",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@@ -119,19 +117,9 @@
}, },
{ {
"fieldname": "total_overtime_duration", "fieldname": "total_overtime_duration",
"fieldtype": "Data", "fieldtype": "Duration",
"label": "Total Overtime 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", "fieldname": "section_break_12",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -140,11 +128,6 @@
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "name1",
"fieldtype": "Duration",
"label": "name"
},
{ {
"default": "Today", "default": "Today",
"fieldname": "posting_date", "fieldname": "posting_date",
@@ -156,7 +139,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-31 15:07:39.485473", "modified": "2021-06-10 13:35:57.511257",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Overtime Slip", "name": "Overtime Slip",

View File

@@ -1,15 +1,17 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from erpnext.hr.doctype.attendance.attendance import get_overtime_type
import frappe import frappe
from frappe import _ 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.payroll_entry.payroll_entry import get_start_end_dates
from erpnext.payroll.doctype.gratuity.gratuity import get_salary_structure from erpnext.payroll.doctype.gratuity.gratuity import get_salary_structure
from frappe.model.document import Document from frappe.model.document import Document
from pprint import pprint
class OvertimeSlip(Document): 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() @frappe.whitelist()
def get_emp_and_overtime_details(self): def get_emp_and_overtime_details(self):
@@ -17,43 +19,138 @@ class OvertimeSlip(Document):
records = [] records = []
if overtime_based_on == "Attendance": if overtime_based_on == "Attendance":
records = self.get_attendance_record() records = self.get_attendance_record()
if len(records):
self.create_overtime_details_row_for_attendance(records)
elif overtime_based_on == "Timesheet": elif overtime_based_on == "Timesheet":
records = self.get_timesheet_record() records = self.get_timesheet_record()
if len(records):
self.create_overtime_details_row_for_timesheet(records)
else: else:
frappe.throw(_('Select "Calculate Overtime Hours Based On" in Payroll Settings')) frappe.throw(_('Select "Calculate Overtime Hours Based On" in Payroll Settings'))
if len(records): if len(self.overtime_details):
self.create_overtime_details_row(records) self.total_overtime_duration = sum([int(detail.overtime_duration) for detail in self.overtime_details])
else:
frappe.throw(_("No {0} records found for Overtime").format(overtime_based_on))
def create_overtime_details_row(self, records): if not len(records):
pprint(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): def get_attendance_record(self):
records = frappe.db.sql("""SELECT overtime_duration, employee, name, attendance_date, overtime_type if self.from_date and self.to_date:
FROM `TabAttendance` records = frappe.db.sql("""SELECT overtime_duration, name, attendance_date, overtime_type, standard_working_time
WHERE FROM `TabAttendance`
attendance_date >= %s AND attendance_date <= %s WHERE
AND employee = %s attendance_date >= %s AND attendance_date <= %s
AND docstatus = 1 AND status= 'Present' AND employee = %s
AND ( AND docstatus = 1 AND status= 'Present'
overtime_duration IS NOT NULL OR overtime_duration != '00:00:00.000000' 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) )
""", (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() @frappe.whitelist()
def get_frequency_and_dates(employee, date): def get_frequency_and_dates(employee, date):
print(date)
salary_structure = get_salary_structure(employee) salary_structure = get_salary_structure(employee)
if salary_structure: if salary_structure:
payroll_frequency = frappe.db.get_value('Salary Structure', salary_structure, 'payroll_frequency') 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')) date_details = get_start_end_dates(payroll_frequency, date, frappe.db.get_value('Employee', employee, 'company'))
print(date_details)
return [date_details, payroll_frequency] return [date_details, payroll_frequency]
else: else:
frappe.throw(_("No Salary Structure Assignment found for Employee: {0}").format(employee)) frappe.throw(_("No Salary Structure Assignment found for Employee: {0}").format(employee))

View File

@@ -16,7 +16,7 @@
"weekend_multiplier", "weekend_multiplier",
"column_break_9", "column_break_9",
"applicable_for_public_holiday", "applicable_for_public_holiday",
"public_holiday_multipliers" "public_holiday_multiplier"
], ],
"fields": [ "fields": [
{ {
@@ -83,15 +83,15 @@
}, },
{ {
"depends_on": "eval: doc.applicable_for_public_holiday == 1", "depends_on": "eval: doc.applicable_for_public_holiday == 1",
"fieldname": "public_holiday_multipliers", "fieldname": "public_holiday_multiplier",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Public Holiday Multipliers", "label": "Public Holiday Multiplier",
"mandatory_depends_on": "eval: doc.applicable_for_public_holiday == 1" "mandatory_depends_on": "eval: doc.applicable_for_public_holiday == 1"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-05-25 13:21:11.318945", "modified": "2021-06-09 15:43:43.891270",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Overtime Type", "name": "Overtime Type",

View File

@@ -11,6 +11,7 @@
"amount", "amount",
"year_to_date", "year_to_date",
"section_break_5", "section_break_5",
"overtime_slips",
"additional_salary", "additional_salary",
"statistical_component", "statistical_component",
"depends_on_payment_days", "depends_on_payment_days",
@@ -235,11 +236,25 @@
"label": "Year To Date", "label": "Year To Date",
"options": "currency", "options": "currency",
"read_only": 1 "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, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-14 13:39:15.847158", "modified": "2021-08-09 17:00:13.386980",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Detail", "name": "Salary Detail",

View File

@@ -336,9 +336,9 @@ class SalarySlip(TransactionBase):
return payment_days 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) 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 where
parent=%(holiday_list)s parent=%(holiday_list)s
and holiday_date >= %(start_date)s and holiday_date >= %(start_date)s
@@ -346,11 +346,12 @@ class SalarySlip(TransactionBase):
"holiday_list": holiday_list, "holiday_list": holiday_list,
"start_date": start_date, "start_date": start_date,
"end_date": end_date "end_date": end_date
}) }, as_dict=1)
if as_dict:
holidays = [cstr(i) for i in holidays] return holidays
else:
return holidays holidays = [cstr(data.holiday_date)for data in holidays]
return holidays
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
lwp = 0 lwp = 0
@@ -496,6 +497,7 @@ class SalarySlip(TransactionBase):
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company) payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
self.add_structure_components(component_type) self.add_structure_components(component_type)
self.process_overtime_slips()
self.add_additional_salary_components(component_type) self.add_additional_salary_components(component_type)
if component_type == "earnings": if component_type == "earnings":
self.add_employee_benefits(payroll_period) self.add_employee_benefits(payroll_period)
@@ -509,6 +511,105 @@ class SalarySlip(TransactionBase):
if amount and struct_row.statistical_component == 0: if amount and struct_row.statistical_component == 0:
self.update_component_row(struct_row, amount, component_type) 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): def get_data_for_eval(self):
'''Returns data for evaluating formula''' '''Returns data for evaluating formula'''
data = frappe._dict() data = frappe._dict()
@@ -639,7 +740,7 @@ class SalarySlip(TransactionBase):
tax_row = get_salary_component_data(d) tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions") 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 component_row = None
for d in self.get(component_type): for d in self.get(component_type):
if d.salary_component != component_data.salary_component: 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') abbr = component_data.get('abbr') or component_data.get('salary_component_abbr')
component_row.set('abbr', 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: if additional_salary:
component_row.default_amount = 0 component_row.default_amount = 0
component_row.additional_amount = amount component_row.additional_amount = amount

View File

@@ -2,20 +2,20 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Timesheet", { frappe.ui.form.on("Timesheet", {
setup: function(frm) { setup: function (frm) {
frappe.require("/assets/erpnext/js/projects/timer.js"); frappe.require("/assets/erpnext/js/projects/timer.js");
frm.add_fetch('employee', 'employee_name', 'employee_name'); frm.add_fetch('employee', 'employee_name', 'employee_name');
frm.fields_dict.employee.get_query = function() { frm.fields_dict.employee.get_query = function () {
return { return {
filters:{ filters: {
'status': 'Active' '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]; var child = locals[cdt][cdn];
return{ return {
filters: { filters: {
'project': child.project, 'project': child.project,
'status': ["!=", "Cancelled"] 'status': ["!=", "Cancelled"]
@@ -23,8 +23,8 @@ frappe.ui.form.on("Timesheet", {
}; };
}; };
frm.fields_dict['time_logs'].grid.get_field('project').get_query = function() { frm.fields_dict['time_logs'].grid.get_field('project').get_query = function () {
return{ return {
filters: { filters: {
'company': frm.doc.company '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) { if (frm.doc.__islocal && frm.doc.time_logs) {
calculate_time_and_amount(frm); calculate_time_and_amount(frm);
} }
@@ -42,33 +42,37 @@ frappe.ui.form.on("Timesheet", {
} }
}, },
refresh: function(frm) { refresh: function (frm) {
if(frm.doc.docstatus==1) { 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){ 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") }, frm.add_custom_button(__('Create Sales Invoice'), function () {
"fa fa-file-text"); frm.trigger("make_invoice");
},
"fa fa-file-text");
} }
if(!frm.doc.salary_slip && frm.doc.employee){ if (!frm.doc.salary_slip && frm.doc.employee) {
frm.add_custom_button(__('Create Salary Slip'), function() { frm.trigger("make_salary_slip") }, frm.add_custom_button(__('Create Salary Slip'), function () {
"fa fa-file-text"); frm.trigger("make_salary_slip");
},
"fa fa-file-text");
} }
} }
if (frm.doc.docstatus < 1) { if (frm.doc.docstatus < 1) {
let button = 'Start Timer'; 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) { if ((row.from_time <= frappe.datetime.now_datetime()) && !row.completed) {
button = 'Resume Timer'; button = 'Resume Timer';
} }
}); });
frm.add_custom_button(__(button), function() { frm.add_custom_button(__(button), function () {
var flag = true; 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 // 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); erpnext.timesheet.timer(frm, row);
row.from_time = frappe.datetime.now_datetime(); row.from_time = frappe.datetime.now_datetime();
frm.refresh_fields("time_logs"); 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 // 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) { 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); erpnext.timesheet.timer(frm, row, timestamp);
flag = false; flag = false;
} }
@@ -88,7 +92,7 @@ frappe.ui.form.on("Timesheet", {
} }
}).addClass("btn-primary"); }).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("billing_hours", false);
frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", 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'); frm.trigger('set_dynamic_field_label');
}, },
customer: function(frm) { customer: function (frm) {
frm.set_query('parent_project', function(doc) { frm.set_query('parent_project', function (doc) {
return { return {
filters: { filters: {
"customer": doc.customer "customer": doc.customer
} }
}; };
}); });
frm.set_query('project', 'time_logs', function(doc) { frm.set_query('project', 'time_logs', function (doc) {
return { return {
filters: { filters: {
"customer": doc.customer "customer": doc.customer
@@ -114,7 +118,7 @@ frappe.ui.form.on("Timesheet", {
frm.refresh(); frm.refresh();
}, },
currency: function(frm) { currency: function (frm) {
let base_currency = frappe.defaults.get_global_default('currency'); let base_currency = frappe.defaults.get_global_default('currency');
if (base_currency != frm.doc.currency) { if (base_currency != frm.doc.currency) {
frappe.call({ frappe.call({
@@ -123,7 +127,7 @@ frappe.ui.form.on("Timesheet", {
from_currency: frm.doc.currency, from_currency: frm.doc.currency,
to_currency: base_currency to_currency: base_currency
}, },
callback: function(r) { callback: function (r) {
if (r.message) { if (r.message) {
frm.set_value('exchange_rate', flt(r.message)); frm.set_value('exchange_rate', flt(r.message));
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + " = [?] " + base_currency); 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'); frm.trigger('set_dynamic_field_label');
}, },
exchange_rate: function(frm) { exchange_rate: function (frm) {
$.each(frm.doc.time_logs, function(i, d) { $.each(frm.doc.time_logs, function (i, d) {
calculate_billing_costing_amount(frm, d.doctype, d.name); calculate_billing_costing_amount(frm, d.doctype, d.name);
}); });
calculate_time_and_amount(frm); 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'); 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(["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); 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"); 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; 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)) if (frappe.meta.get_docfield(time_logs_grid.doctype, d))
time_logs_grid.set_column_disp(d, frm.doc.currency != base_currency); time_logs_grid.set_column_disp(d, frm.doc.currency != base_currency);
}); });
@@ -162,7 +166,7 @@ frappe.ui.form.on("Timesheet", {
frm.refresh_fields(); frm.refresh_fields();
}, },
make_invoice: function(frm) { make_invoice: function (frm) {
let fields = [{ let fields = [{
"fieldtype": "Link", "fieldtype": "Link",
"label": __("Item Code"), "label": __("Item Code"),
@@ -187,7 +191,7 @@ frappe.ui.form.on("Timesheet", {
dialog.set_primary_action(__('Create Sales Invoice'), () => { dialog.set_primary_action(__('Create Sales Invoice'), () => {
var args = dialog.get_values(); var args = dialog.get_values();
if(!args) return; if (!args) return;
dialog.hide(); dialog.hide();
return frappe.call({ return frappe.call({
type: "GET", type: "GET",
@@ -199,8 +203,8 @@ frappe.ui.form.on("Timesheet", {
"currency": frm.doc.currency "currency": frm.doc.currency
}, },
freeze: true, freeze: true,
callback: function(r) { callback: function (r) {
if(!r.exc) { if (!r.exc) {
frappe.model.sync(r.message); frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name); frappe.set_route("Form", r.message.doctype, r.message.name);
} }
@@ -210,20 +214,20 @@ frappe.ui.form.on("Timesheet", {
dialog.show(); dialog.show();
}, },
make_salary_slip: function(frm) { make_salary_slip: function (frm) {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.projects.doctype.timesheet.timesheet.make_salary_slip", method: "erpnext.projects.doctype.timesheet.timesheet.make_salary_slip",
frm: frm frm: frm
}); });
}, },
parent_project: function(frm) { parent_project: function (frm) {
set_project_in_timelog(frm); set_project_in_timelog(frm);
} }
}); });
frappe.ui.form.on("Timesheet Detail", { frappe.ui.form.on("Timesheet Detail", {
time_logs_remove: function(frm) { time_logs_remove: function (frm) {
calculate_time_and_amount(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); calculate_end_time(frm, cdt, cdn);
}, },
to_time: function(frm, cdt, cdn) { to_time: function (frm, cdt, cdn) {
var child = locals[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; var hours = moment(child.to_time).diff(moment(child.from_time), "seconds") / 3600;
frappe.model.set_value(cdt, cdn, "hours", hours); frappe.model.set_value(cdt, cdn, "hours", hours);
}, },
time_logs_add: function(frm, cdt, cdn) { time_logs_add: function (frm, cdt, cdn) {
if(frm.doc.parent_project) { if (frm.doc.parent_project) {
frappe.model.set_value(cdt, cdn, 'project', 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_end_time(frm, cdt, cdn);
calculate_billing_costing_amount(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm); 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_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm); 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_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm); 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_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm); calculate_time_and_amount(frm);
}, },
is_billable: function(frm, cdt, cdn) { is_billable: function (frm, cdt, cdn) {
update_billing_hours(frm, cdt, cdn); update_billing_hours(frm, cdt, cdn);
update_time_rates(frm, cdt, cdn); update_time_rates(frm, cdt, cdn);
calculate_billing_costing_amount(frm, cdt, cdn); calculate_billing_costing_amount(frm, cdt, cdn);
calculate_time_and_amount(frm); 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({ frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost", method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
args: { args: {
@@ -291,8 +302,8 @@ frappe.ui.form.on("Timesheet Detail", {
activity_type: frm.selected_doc.activity_type, activity_type: frm.selected_doc.activity_type,
currency: frm.doc.currency currency: frm.doc.currency
}, },
callback: function(r){ callback: function (r) {
if(r.message){ if (r.message) {
frappe.model.set_value(cdt, cdn, 'billing_rate', r.message['billing_rate']); frappe.model.set_value(cdt, cdn, 'billing_rate', r.message['billing_rate']);
frappe.model.set_value(cdt, cdn, 'costing_rate', r.message['costing_rate']); frappe.model.set_value(cdt, cdn, 'costing_rate', r.message['costing_rate']);
calculate_billing_costing_amount(frm, cdt, cdn); 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]; 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 // 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()); frappe.model.set_value(cdt, cdn, "from_time", frappe.datetime.get_datetime_as_string());
} }
let d = moment(child.from_time); let d = moment(child.from_time);
if(child.hours) { if (child.hours) {
d.add(child.hours, "hours"); d.add(child.hours, "hours");
frm._setting_hours = true; frm._setting_hours = true;
frappe.model.set_value(cdt, cdn, "to_time", 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); let child = frappe.get_doc(cdt, cdn);
if (!child.is_billable) { if (!child.is_billable) {
frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0); 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); let child = frappe.get_doc(cdt, cdn);
if (!child.is_billable) { if (!child.is_billable) {
frappe.model.set_value(cdt, cdn, 'billing_rate', 0.0); 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 row = frappe.get_doc(cdt, cdn);
let billing_amount = 0.0; let billing_amount = 0.0;
let base_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)); 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 tl = frm.doc.time_logs || [];
let total_working_hr = 0; let total_working_hr = 0;
let total_billing_hr = 0; let total_billing_hr = 0;
let total_billable_amount = 0; let total_billable_amount = 0;
let total_costing_amount = 0; let total_costing_amount = 0;
for(var i=0; i<tl.length; i++) { for (var i = 0; i < tl.length; i++) {
if (tl[i].hours) { if (tl[i].hours) {
total_working_hr += tl[i].hours; total_working_hr += tl[i].hours;
total_billable_amount += tl[i].billing_amount; total_billable_amount += tl[i].billing_amount;
@@ -381,10 +414,14 @@ var calculate_time_and_amount = function(frm) {
}; };
// set employee (and company) to the one that's currently logged in // set employee (and company) to the one that's currently logged in
const set_employee_and_company = function(frm) { const set_employee_and_company = function (frm) {
const options = { user_id: frappe.session.user }; const options = {
user_id: frappe.session.user
};
const fields = ['name', 'company']; const fields = ['name', 'company'];
frappe.db.get_value('Employee', options, fields).then(({ message }) => { frappe.db.get_value('Employee', options, fields).then(({
message
}) => {
if (message) { if (message) {
// there is an employee with the currently logged in user_id // there is an employee with the currently logged in user_id
frm.set_value("employee", message.name); frm.set_value("employee", message.name);
@@ -394,9 +431,9 @@ const set_employee_and_company = function(frm) {
}; };
function set_project_in_timelog(frm) { function set_project_in_timelog(frm) {
if(frm.doc.parent_project) { if (frm.doc.parent_project) {
$.each(frm.doc.time_logs || [], function(i, item) { $.each(frm.doc.time_logs || [], function (i, item) {
frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project); frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project);
}); });
} }
} }

View File

@@ -14,11 +14,11 @@
"customer", "customer",
"currency", "currency",
"exchange_rate", "exchange_rate",
"sales_invoice",
"column_break_3", "column_break_3",
"salary_slip",
"status", "status",
"parent_project", "parent_project",
"salary_slip",
"sales_invoice",
"employee_detail", "employee_detail",
"employee", "employee",
"employee_name", "employee_name",
@@ -29,7 +29,10 @@
"end_date", "end_date",
"section_break_5", "section_break_5",
"time_logs", "time_logs",
"working_hours", "overtime_details_section",
"overtime_type",
"total_overtime_hours",
"column_break_26",
"total_hours", "total_hours",
"billing_details", "billing_details",
"total_billable_hours", "total_billable_hours",
@@ -173,10 +176,6 @@
"options": "Timesheet Detail", "options": "Timesheet Detail",
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "working_hours",
"fieldtype": "Section Break"
},
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"default": "0", "default": "0",
@@ -313,13 +312,36 @@
"fieldname": "exchange_rate", "fieldname": "exchange_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Exchange Rate" "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", "icon": "fa fa-clock-o",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-18 16:10:08.249619", "modified": "2021-06-14 17:10:31.434084",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Timesheet", "name": "Timesheet",

View File

@@ -4,16 +4,12 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _
import json import json
from frappe import _
from datetime import timedelta from datetime import timedelta
from erpnext.controllers.queries import get_match_cond 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 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.setup.utils import get_exchange_rate
from erpnext.hr.utils import validate_active_employee from erpnext.hr.utils import validate_active_employee
@@ -31,6 +27,7 @@ class Timesheet(Document):
self.update_cost() self.update_cost()
self.calculate_total_amounts() self.calculate_total_amounts()
self.calculate_percentage_billed() self.calculate_percentage_billed()
self.validate_overtime()
self.set_dates() self.set_dates()
def set_employee_name(self): def set_employee_name(self):
@@ -65,6 +62,45 @@ class Timesheet(Document):
if self.total_billed_amount > 0 and self.total_billable_amount > 0: if self.total_billed_amount > 0 and self.total_billable_amount > 0:
self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount 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): def update_billing_hours(self, args):
if args.is_billable: if args.is_billable:
if flt(args.billing_hours) == 0.0: if flt(args.billing_hours) == 0.0:

View File

@@ -14,10 +14,16 @@
"to_time", "to_time",
"hours", "hours",
"completed", "completed",
"section_break_9",
"is_overtime",
"overtime_type",
"column_break_12",
"overtime_on",
"overtime_hours",
"section_break_7", "section_break_7",
"completed_qty", "completed_qty",
"workstation", "workstation",
"column_break_12", "column_break_18",
"operation", "operation",
"operation_id", "operation_id",
"project_details", "project_details",
@@ -70,7 +76,7 @@
"fieldname": "hours", "fieldname": "hours",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Hrs" "label": "Working Hours"
}, },
{ {
"fieldname": "to_time", "fieldname": "to_time",
@@ -262,12 +268,47 @@
"label": "Costing Amount", "label": "Costing Amount",
"print_hide": 1, "print_hide": 1,
"read_only": 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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-05-18 12:19:33.205940", "modified": "2021-06-10 15:17:20.846091",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Timesheet Detail", "name": "Timesheet Detail",