Compare commits
20 Commits
revert-328
...
overtime-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7539cfecd2 | ||
|
|
18f04111d6 | ||
|
|
b2ba9aacb2 | ||
|
|
4a6694e80a | ||
|
|
f3ae956eae | ||
|
|
fd325a123c | ||
|
|
8db493b2be | ||
|
|
21bc752da5 | ||
|
|
96db78f7bc | ||
|
|
32f3b24c3a | ||
|
|
10647bfcd7 | ||
|
|
7d272dc648 | ||
|
|
b96165883e | ||
|
|
5683857f9f | ||
|
|
73b3121127 | ||
|
|
769d774ccc | ||
|
|
da2e95dbcc | ||
|
|
3851d15360 | ||
|
|
e4fd6d7763 | ||
|
|
8f1850b21c |
@@ -58,8 +58,8 @@ class GLEntry(Document):
|
|||||||
if not self.get(k):
|
if not self.get(k):
|
||||||
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
|
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
|
||||||
|
|
||||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
|
||||||
if not (self.party_type and self.party):
|
if not (self.party_type and self.party):
|
||||||
|
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||||
if account_type == "Receivable":
|
if account_type == "Receivable":
|
||||||
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
|
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
|
||||||
.format(self.voucher_type, self.voucher_no, self.account))
|
.format(self.voucher_type, self.voucher_no, self.account))
|
||||||
@@ -73,15 +73,19 @@ class GLEntry(Document):
|
|||||||
.format(self.voucher_type, self.voucher_no, self.account))
|
.format(self.voucher_type, self.voucher_no, self.account))
|
||||||
|
|
||||||
def pl_must_have_cost_center(self):
|
def pl_must_have_cost_center(self):
|
||||||
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
|
"""Validate that profit and loss type account GL entries have a cost center."""
|
||||||
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
|
|
||||||
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
|
|
||||||
self.voucher_type, self.voucher_no, self.account)
|
|
||||||
msg += " "
|
|
||||||
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
|
|
||||||
self.voucher_type)
|
|
||||||
|
|
||||||
frappe.throw(msg, title=_("Missing Cost Center"))
|
if self.cost_center or self.voucher_type == 'Period Closing Voucher':
|
||||||
|
return
|
||||||
|
|
||||||
|
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
|
||||||
|
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
|
||||||
|
self.voucher_type, self.voucher_no, self.account)
|
||||||
|
msg += " "
|
||||||
|
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
|
||||||
|
self.voucher_type)
|
||||||
|
|
||||||
|
frappe.throw(msg, title=_("Missing Cost Center"))
|
||||||
|
|
||||||
def validate_dimensions_for_pl_and_bs(self):
|
def validate_dimensions_for_pl_and_bs(self):
|
||||||
account_type = frappe.db.get_value("Account", self.account, "report_type")
|
account_type = frappe.db.get_value("Account", self.account, "report_type")
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ def merge_similar_entries(gl_map, precision=None):
|
|||||||
return merged_gl_map
|
return merged_gl_map
|
||||||
|
|
||||||
def check_if_in_list(gle, gl_map, dimensions=None):
|
def check_if_in_list(gle, gl_map, dimensions=None):
|
||||||
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
|
account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher',
|
||||||
'cost_center', 'project', 'voucher_detail_no']
|
'cost_center', 'against_voucher_type', 'party_type', 'project']
|
||||||
|
|
||||||
if dimensions:
|
if dimensions:
|
||||||
account_head_fieldnames = account_head_fieldnames + dimensions
|
account_head_fieldnames = account_head_fieldnames + dimensions
|
||||||
@@ -110,10 +110,12 @@ def check_if_in_list(gle, gl_map, dimensions=None):
|
|||||||
same_head = True
|
same_head = True
|
||||||
if e.account != gle.account:
|
if e.account != gle.account:
|
||||||
same_head = False
|
same_head = False
|
||||||
|
continue
|
||||||
|
|
||||||
for fieldname in account_head_fieldnames:
|
for fieldname in account_head_fieldnames:
|
||||||
if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)):
|
if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)):
|
||||||
same_head = False
|
same_head = False
|
||||||
|
break
|
||||||
|
|
||||||
if same_head:
|
if same_head:
|
||||||
return e
|
return e
|
||||||
@@ -143,16 +145,19 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
|||||||
validate_expense_against_budget(args)
|
validate_expense_against_budget(args)
|
||||||
|
|
||||||
def validate_cwip_accounts(gl_map):
|
def validate_cwip_accounts(gl_map):
|
||||||
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting"))
|
"""Validate that CWIP account are not used in Journal Entry"""
|
||||||
|
if gl_map and gl_map[0].voucher_type != "Journal Entry":
|
||||||
|
return
|
||||||
|
|
||||||
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
|
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting"))
|
||||||
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
|
if cwip_enabled:
|
||||||
where account_type = 'Capital Work in Progress' and is_group=0""")]
|
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
|
||||||
|
where account_type = 'Capital Work in Progress' and is_group=0""")]
|
||||||
|
|
||||||
for entry in gl_map:
|
for entry in gl_map:
|
||||||
if entry.account in cwip_accounts:
|
if entry.account in cwip_accounts:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
|
_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
|
||||||
|
|
||||||
def round_off_debit_credit(gl_map):
|
def round_off_debit_credit(gl_map):
|
||||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
|
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
|
||||||
|
|||||||
@@ -920,7 +920,6 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa
|
|||||||
_delete_gl_entries(voucher_type, voucher_no)
|
_delete_gl_entries(voucher_type, voucher_no)
|
||||||
|
|
||||||
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
|
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
|
||||||
future_stock_vouchers = []
|
|
||||||
|
|
||||||
values = []
|
values = []
|
||||||
condition = ""
|
condition = ""
|
||||||
@@ -936,30 +935,46 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
|
|||||||
condition += " and company = %s"
|
condition += " and company = %s"
|
||||||
values.append(company)
|
values.append(company)
|
||||||
|
|
||||||
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
|
future_stock_vouchers = frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
|
||||||
from `tabStock Ledger Entry` sle
|
from `tabStock Ledger Entry` sle
|
||||||
where
|
where
|
||||||
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
|
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
|
||||||
and is_cancelled = 0
|
and is_cancelled = 0
|
||||||
{condition}
|
{condition}
|
||||||
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
|
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
|
||||||
tuple([posting_date, posting_time] + values), as_dict=True):
|
tuple([posting_date, posting_time] + values), as_dict=True)
|
||||||
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
|
|
||||||
|
|
||||||
return future_stock_vouchers
|
return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers]
|
||||||
|
|
||||||
def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
|
def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
|
||||||
|
""" Get voucherwise list of GL entries.
|
||||||
|
|
||||||
|
Only fetches GLE fields required for comparing with new GLE.
|
||||||
|
Check compare_existing_and_expected_gle function below.
|
||||||
|
"""
|
||||||
gl_entries = {}
|
gl_entries = {}
|
||||||
if future_stock_vouchers:
|
if not future_stock_vouchers:
|
||||||
for d in frappe.db.sql("""select * from `tabGL Entry`
|
return gl_entries
|
||||||
where posting_date >= %s and voucher_no in (%s)""" %
|
|
||||||
('%s', ', '.join(['%s']*len(future_stock_vouchers))),
|
voucher_nos = [d[1] for d in future_stock_vouchers]
|
||||||
tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1):
|
|
||||||
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
|
gles = frappe.db.sql("""
|
||||||
|
select name, account, credit, debit, cost_center, project
|
||||||
|
from `tabGL Entry`
|
||||||
|
where
|
||||||
|
posting_date >= %s and voucher_no in (%s)""" %
|
||||||
|
('%s', ', '.join(['%s'] * len(voucher_nos))),
|
||||||
|
tuple([posting_date] + voucher_nos), as_dict=1)
|
||||||
|
|
||||||
|
for d in gles:
|
||||||
|
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
|
||||||
|
|
||||||
return gl_entries
|
return gl_entries
|
||||||
|
|
||||||
def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
|
def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
|
||||||
|
if len(existing_gle) != len(expected_gle):
|
||||||
|
return False
|
||||||
|
|
||||||
matched = True
|
matched = True
|
||||||
for entry in expected_gle:
|
for entry in expected_gle:
|
||||||
account_existed = False
|
account_existed = False
|
||||||
|
|||||||
@@ -1,15 +1,73 @@
|
|||||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
cur_frm.add_fetch('employee', 'company', 'company');
|
frappe.ui.form.on('Attendance', {
|
||||||
cur_frm.add_fetch('employee', 'employee_name', 'employee_name');
|
onload: function(frm) {
|
||||||
|
|
||||||
cur_frm.cscript.onload = function(doc, cdt, cdn) {
|
frappe.db.get_single_value("Payroll Settings", "fetch_standard_working_hours_from_shift_type").then((r)=>{
|
||||||
if(doc.__islocal) cur_frm.set_value("attendance_date", frappe.datetime.get_today());
|
if (!r) {
|
||||||
}
|
// for not fetching from Shift Type
|
||||||
|
delete cur_frm.fetch_dict["shift"];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
|
if (frm.doc.__islocal) {
|
||||||
return{
|
frm.set_value("attendance_date", frappe.datetime.get_today());
|
||||||
query: "erpnext.controllers.queries.employee_query"
|
}
|
||||||
|
|
||||||
|
frm.set_query("employee", () => {
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.employee_query"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
employee: function(frm) {
|
||||||
|
if (frm.doc.employee) {
|
||||||
|
frm.events.set_shift(frm);
|
||||||
|
frm.events.set_overtime_type(frm);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set_shift: function(frm) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.hr.doctype.attendance.attendance.get_shift_type",
|
||||||
|
args: {
|
||||||
|
employee: frm.doc.employee,
|
||||||
|
attendance_date: frm.doc.attendance_date
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
frm.set_value("shift", r.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
set_overtime_type: function(frm) {
|
||||||
|
frappe.db.get_single_value("Payroll Settings", "overtime_based_on").then((r)=>{
|
||||||
|
if (r == "Attendance") {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.hr.doctype.attendance.attendance.get_overtime_type",
|
||||||
|
args: {
|
||||||
|
employee: frm.doc.employee,
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (r.message) {
|
||||||
|
frm.set_value("overtime_type", r.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
frm.set_value("overtime_type", '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
overtime_duration: function(frm) {
|
||||||
|
let duration = frm.doc.overtime_duration.split(":");
|
||||||
|
let overtime_duration_words = duration[0] + " Hours " + duration[1] + " Minutes";
|
||||||
|
frm.set_value("overtime_duration_words", overtime_duration_words);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
});
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
"naming_series",
|
"naming_series",
|
||||||
"employee",
|
"employee",
|
||||||
"employee_name",
|
"employee_name",
|
||||||
"working_hours",
|
|
||||||
"status",
|
"status",
|
||||||
"leave_type",
|
"leave_type",
|
||||||
"leave_application",
|
"leave_application",
|
||||||
@@ -20,13 +19,19 @@
|
|||||||
"company",
|
"company",
|
||||||
"department",
|
"department",
|
||||||
"attendance_request",
|
"attendance_request",
|
||||||
"details_section",
|
"shift_details_section",
|
||||||
"shift",
|
"shift",
|
||||||
"in_time",
|
"in_time",
|
||||||
"out_time",
|
"out_time",
|
||||||
"column_break_18",
|
"column_break_18",
|
||||||
|
"shift_duration",
|
||||||
|
"working_time",
|
||||||
"late_entry",
|
"late_entry",
|
||||||
"early_exit",
|
"early_exit",
|
||||||
|
"overtime_details_section",
|
||||||
|
"overtime_type",
|
||||||
|
"column_break_27",
|
||||||
|
"overtime_duration",
|
||||||
"amended_from"
|
"amended_from"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -69,14 +74,6 @@
|
|||||||
"oldfieldtype": "Data",
|
"oldfieldtype": "Data",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"depends_on": "working_hours",
|
|
||||||
"fieldname": "working_hours",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"label": "Working Hours",
|
|
||||||
"precision": "1",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "Present",
|
"default": "Present",
|
||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
@@ -125,6 +122,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "employee.company",
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
@@ -146,7 +144,8 @@
|
|||||||
"fieldname": "shift",
|
"fieldname": "shift",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Shift",
|
"label": "Shift",
|
||||||
"options": "Shift Type"
|
"options": "Shift Type",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "attendance_request",
|
"fieldname": "attendance_request",
|
||||||
@@ -177,11 +176,6 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Early Exit"
|
"label": "Early Exit"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "details_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Details"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "shift",
|
"depends_on": "shift",
|
||||||
"fieldname": "in_time",
|
"fieldname": "in_time",
|
||||||
@@ -199,13 +193,60 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_18",
|
"fieldname": "column_break_18",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "overtime_type",
|
||||||
|
"fieldname": "overtime_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Overtime Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "overtime_type",
|
||||||
|
"fieldname": "overtime_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Overtime Type",
|
||||||
|
"options": "Overtime Type",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "working_time",
|
||||||
|
"fieldname": "working_time",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"label": "Total Working Time",
|
||||||
|
"precision": "1",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0000",
|
||||||
|
"fieldname": "overtime_duration",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"hide_days": 1,
|
||||||
|
"label": "Overtime Duration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "shift",
|
||||||
|
"fieldname": "shift_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Shift Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_27",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Shift duration for a day",
|
||||||
|
"fetch_from": "shift.standard_working_time",
|
||||||
|
"fieldname": "shift_duration",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"label": "Shift Duration",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-ok",
|
"icon": "fa fa-ok",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-18 17:26:09.703215",
|
"modified": "2021-08-11 11:55:50.076043",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Attendance",
|
"name": "Attendance",
|
||||||
|
|||||||
@@ -4,11 +4,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from frappe.utils import getdate, nowdate
|
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr, get_datetime, formatdate
|
|
||||||
from erpnext.hr.utils import validate_active_employee
|
from erpnext.hr.utils import validate_active_employee
|
||||||
|
from frappe.utils import cstr, get_datetime, formatdate, getdate, nowdate
|
||||||
|
|
||||||
class Attendance(Document):
|
class Attendance(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@@ -19,12 +18,17 @@ class Attendance(Document):
|
|||||||
self.validate_duplicate_record()
|
self.validate_duplicate_record()
|
||||||
self.validate_employee_status()
|
self.validate_employee_status()
|
||||||
self.check_leave_record()
|
self.check_leave_record()
|
||||||
|
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.shift_duration = 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")
|
||||||
|
|
||||||
# leaves can be marked for future dates
|
# leaves can be marked for future dates
|
||||||
if self.status != 'On Leave' and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()):
|
if self.status != "On Leave" and not self.leave_application and getdate(self.attendance_date) > getdate(nowdate()):
|
||||||
frappe.throw(_("Attendance can not be marked for future dates"))
|
frappe.throw(_("Attendance can not be marked for future dates"))
|
||||||
elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining):
|
elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining):
|
||||||
frappe.throw(_("Attendance date can not be less than employee's joining date"))
|
frappe.throw(_("Attendance date can not be less than employee's joining date"))
|
||||||
@@ -45,6 +49,25 @@ class Attendance(Document):
|
|||||||
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
|
if frappe.db.get_value("Employee", self.employee, "status") == "Inactive":
|
||||||
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
|
frappe.throw(_("Cannot mark attendance for an Inactive employee {0}").format(self.employee))
|
||||||
|
|
||||||
|
def set_default_shift(self):
|
||||||
|
if not self.shift:
|
||||||
|
self.shift = get_shift_type(self.employee, self.attendance_date)
|
||||||
|
|
||||||
|
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):
|
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
|
||||||
@@ -58,11 +81,11 @@ class Attendance(Document):
|
|||||||
for d in leave_record:
|
for d in leave_record:
|
||||||
self.leave_type = d.leave_type
|
self.leave_type = d.leave_type
|
||||||
if d.half_day_date == getdate(self.attendance_date):
|
if d.half_day_date == getdate(self.attendance_date):
|
||||||
self.status = 'Half Day'
|
self.status = "Half Day"
|
||||||
frappe.msgprint(_("Employee {0} on Half day on {1}")
|
frappe.msgprint(_("Employee {0} on Half day on {1}")
|
||||||
.format(self.employee, formatdate(self.attendance_date)))
|
.format(self.employee, formatdate(self.attendance_date)))
|
||||||
else:
|
else:
|
||||||
self.status = 'On Leave'
|
self.status = "On Leave"
|
||||||
frappe.msgprint(_("Employee {0} is on Leave on {1}")
|
frappe.msgprint(_("Employee {0} is on Leave on {1}")
|
||||||
.format(self.employee, formatdate(self.attendance_date)))
|
.format(self.employee, formatdate(self.attendance_date)))
|
||||||
|
|
||||||
@@ -80,6 +103,66 @@ class Attendance(Document):
|
|||||||
if not emp:
|
if not emp:
|
||||||
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
||||||
|
|
||||||
|
def calculate_overtime_duration(self):
|
||||||
|
#this method is only for Calculation of overtime based on Attendance through Employee Checkins
|
||||||
|
self.overtime_duration = None
|
||||||
|
|
||||||
|
if not self.shift_duration and self.shift:
|
||||||
|
self.shift_duration = frappe.db.get_value("Shift Type", self.shift, "shift_duration")
|
||||||
|
|
||||||
|
if not self.overtime_type:
|
||||||
|
self.overtime_type = get_overtime_type(self.employee)
|
||||||
|
|
||||||
|
if int(self.working_time) > int(self.shift_duration):
|
||||||
|
self.overtime_duration = int(self.working_time) - int(self.shift_duration)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_shift_type(employee, attendance_date):
|
||||||
|
shift_assignment = frappe.db.sql('''SELECT name, shift_type
|
||||||
|
FROM
|
||||||
|
`tabShift Assignment`
|
||||||
|
WHERE
|
||||||
|
docstatus = 1
|
||||||
|
AND employee = %(employee)s AND start_date <= %(attendance_date)s
|
||||||
|
AND (end_date >= %(attendance_date)s OR end_date IS null)
|
||||||
|
AND status = "Active"
|
||||||
|
''', {
|
||||||
|
"employee": employee,
|
||||||
|
"attendance_date": attendance_date,
|
||||||
|
}, as_dict = 1)
|
||||||
|
|
||||||
|
if len(shift_assignment):
|
||||||
|
shift = shift_assignment[0].shift_type
|
||||||
|
else:
|
||||||
|
shift = frappe.db.get_value("Employee", employee, "default_shift")
|
||||||
|
return shift
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_overtime_type(employee):
|
||||||
|
overtime_type = None
|
||||||
|
emp_details = frappe.db.get_value("Employee", employee, ["department", "grade"], as_dict=1)
|
||||||
|
|
||||||
|
emp_department = emp_details.department
|
||||||
|
if emp_department:
|
||||||
|
overtime_type_doc = frappe.get_list("Overtime Type", filters={
|
||||||
|
"applicable_for": "Department", "department": emp_department}, fields=["name"])
|
||||||
|
if len(overtime_type_doc):
|
||||||
|
overtime_type = overtime_type_doc[0].name
|
||||||
|
|
||||||
|
emp_grade = emp_details.grade
|
||||||
|
if emp_grade:
|
||||||
|
overtime_type_doc = frappe.get_list("Overtime Type", filters={
|
||||||
|
"applicable_for": "Employee Grade", "employee_grade": emp_grade},
|
||||||
|
fields=["name"])
|
||||||
|
if len(overtime_type_doc):
|
||||||
|
overtime_type = overtime_type_doc[0].name
|
||||||
|
|
||||||
|
overtime_type_doc = frappe.get_list("Overtime Type", filters={
|
||||||
|
"applicable_for": "Employee", "employee": employee}, fields=["name"])
|
||||||
|
if len(overtime_type_doc):
|
||||||
|
overtime_type = overtime_type_doc[0].name
|
||||||
|
return overtime_type
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_events(start, end, filters=None):
|
def get_events(start, end, filters=None):
|
||||||
events = []
|
events = []
|
||||||
@@ -134,7 +217,6 @@ def mark_attendance(employee, attendance_date, status, shift=None, leave_type=No
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def mark_bulk_attendance(data):
|
def mark_bulk_attendance(data):
|
||||||
import json
|
import json
|
||||||
from pprint import pprint
|
|
||||||
if isinstance(data, frappe.string_types):
|
if isinstance(data, frappe.string_types):
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
data = frappe._dict(data)
|
data = frappe._dict(data)
|
||||||
@@ -184,7 +266,7 @@ def get_unmarked_days(employee, month):
|
|||||||
month_start, month_end = dates_of_month[0], dates_of_month[length-1]
|
month_start, month_end = dates_of_month[0], dates_of_month[length-1]
|
||||||
|
|
||||||
|
|
||||||
records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [
|
records = frappe.get_all("Attendance", fields = ["attendance_date", "employee"] , filters = [
|
||||||
["attendance_date", ">=", month_start],
|
["attendance_date", ">=", month_start],
|
||||||
["attendance_date", "<=", month_end],
|
["attendance_date", "<=", month_end],
|
||||||
["employee", "=", employee],
|
["employee", "=", employee],
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
|
|
||||||
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 math import modf
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift
|
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift
|
||||||
@@ -29,17 +31,25 @@ class EmployeeCheckin(Document):
|
|||||||
|
|
||||||
def fetch_shift(self):
|
def fetch_shift(self):
|
||||||
shift_actual_timings = get_actual_start_end_datetime_of_shift(self.employee, get_datetime(self.time), True)
|
shift_actual_timings = get_actual_start_end_datetime_of_shift(self.employee, get_datetime(self.time), True)
|
||||||
if shift_actual_timings[0] and shift_actual_timings[1]:
|
allow_overtime = False
|
||||||
if shift_actual_timings[2].shift_type.determine_check_in_and_check_out == 'Strictly based on Log Type in Employee Checkin' and not self.log_type and not self.skip_auto_attendance:
|
if shift_actual_timings[2]:
|
||||||
frappe.throw(_('Log Type is required for check-ins falling in the shift: {0}.').format(shift_actual_timings[2].shift_type.name))
|
allow_overtime = frappe.db.get_value("Shift Type", shift_actual_timings[2].shift_type.name, "allow_overtime")
|
||||||
if not self.attendance:
|
if not allow_overtime:
|
||||||
self.shift = shift_actual_timings[2].shift_type.name
|
if shift_actual_timings[0] and shift_actual_timings[1]:
|
||||||
self.shift_actual_start = shift_actual_timings[0]
|
if shift_actual_timings[2].shift_type.determine_check_in_and_check_out == 'Strictly based on Log Type in Employee Checkin' and not self.log_type and not self.skip_auto_attendance:
|
||||||
self.shift_actual_end = shift_actual_timings[1]
|
frappe.throw(_('Log Type is required for check-ins falling in the shift: {0}.').format(shift_actual_timings[2].shift_type.name))
|
||||||
self.shift_start = shift_actual_timings[2].start_datetime
|
if not self.attendance:
|
||||||
self.shift_end = shift_actual_timings[2].end_datetime
|
self.shift = shift_actual_timings[2].shift_type.name
|
||||||
else:
|
self.shift_actual_start = shift_actual_timings[0]
|
||||||
self.shift = None
|
self.shift_actual_end = shift_actual_timings[1]
|
||||||
|
self.shift_start = shift_actual_timings[2].start_datetime
|
||||||
|
self.shift_end = shift_actual_timings[2].end_datetime
|
||||||
|
elif 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.
|
||||||
|
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
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, skip_auto_attendance=0, employee_fieldname='attendance_device_id'):
|
def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, skip_auto_attendance=0, employee_fieldname='attendance_device_id'):
|
||||||
@@ -56,7 +66,8 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
|
|||||||
if not employee_field_value or not timestamp:
|
if not employee_field_value or not timestamp:
|
||||||
frappe.throw(_("'employee_field_value' and 'timestamp' are required."))
|
frappe.throw(_("'employee_field_value' and 'timestamp' are required."))
|
||||||
|
|
||||||
employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value}, ["name", "employee_name", employee_fieldname], as_dict=True)
|
employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value},
|
||||||
|
["name", "employee_name", employee_fieldname], as_dict=True)
|
||||||
if employee:
|
if employee:
|
||||||
employee = employee[0]
|
employee = employee[0]
|
||||||
else:
|
else:
|
||||||
@@ -73,7 +84,6 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
|
|||||||
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, in_time=None, out_time=None, shift=None):
|
def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, in_time=None, out_time=None, shift=None):
|
||||||
"""Creates an attendance and links the attendance to the Employee Checkin.
|
"""Creates an attendance and links the attendance to the Employee Checkin.
|
||||||
Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
|
Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
|
||||||
@@ -93,12 +103,21 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
|
|||||||
elif attendance_status in ('Present', 'Absent', 'Half Day'):
|
elif attendance_status in ('Present', 'Absent', 'Half Day'):
|
||||||
employee_doc = frappe.get_doc('Employee', employee)
|
employee_doc = frappe.get_doc('Employee', employee)
|
||||||
if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
|
if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}):
|
||||||
|
|
||||||
|
working_timedelta = '00:00:00'
|
||||||
|
working_time = None
|
||||||
|
working_time = modf(working_hours)
|
||||||
|
if working_time[1] or working_time[0]:
|
||||||
|
working_timedelta = timedelta(hours =int(working_time[1]), minutes = int(working_time[0] * 60))
|
||||||
|
from erpnext.hr.doctype.shift_type.shift_type import convert_time_into_duration
|
||||||
|
working_time = convert_time_into_duration(working_timedelta)
|
||||||
|
|
||||||
doc_dict = {
|
doc_dict = {
|
||||||
'doctype': 'Attendance',
|
'doctype': 'Attendance',
|
||||||
'employee': employee,
|
'employee': employee,
|
||||||
'attendance_date': attendance_date,
|
'attendance_date': attendance_date,
|
||||||
'status': attendance_status,
|
'status': attendance_status,
|
||||||
'working_hours': working_hours,
|
'working_time': working_time,
|
||||||
'company': employee_doc.company,
|
'company': employee_doc.company,
|
||||||
'shift': shift,
|
'shift': shift,
|
||||||
'late_entry': late_entry,
|
'late_entry': late_entry,
|
||||||
@@ -107,6 +126,9 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
|
|||||||
'out_time': out_time
|
'out_time': out_time
|
||||||
}
|
}
|
||||||
attendance = frappe.get_doc(doc_dict).insert()
|
attendance = frappe.get_doc(doc_dict).insert()
|
||||||
|
if frappe.db.get_value("Shift Type", shift, "allow_overtime"):
|
||||||
|
attendance.calculate_overtime_duration()
|
||||||
|
attendance.save()
|
||||||
attendance.submit()
|
attendance.submit()
|
||||||
frappe.db.sql("""update `tabEmployee Checkin`
|
frappe.db.sql("""update `tabEmployee Checkin`
|
||||||
set attendance = %s
|
set attendance = %s
|
||||||
@@ -121,10 +143,10 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
|
|||||||
frappe.throw(_('{} is an invalid Attendance Status.').format(attendance_status))
|
frappe.throw(_('{} is an invalid Attendance Status.').format(attendance_status))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
|
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
|
||||||
"""Given a set of logs in chronological order calculates the total working hours based on the parameters.
|
"""Given a set of logs in chronological order calculates the total working hours based on the parameters.
|
||||||
Zero is returned for all invalid cases.
|
Zero is returned for all invalid cases.
|
||||||
|
|
||||||
:param logs: The List of 'Employee Checkin'.
|
:param logs: The List of 'Employee Checkin'.
|
||||||
:param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin'
|
:param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin'
|
||||||
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'
|
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'
|
||||||
|
|||||||
@@ -40,13 +40,14 @@ class TestEmployeeCheckin(unittest.TestCase):
|
|||||||
log_names = [log.name for log in logs]
|
log_names = [log.name for log in logs]
|
||||||
logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'attendance':attendance.name})
|
logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'attendance':attendance.name})
|
||||||
self.assertEqual(logs_count, 4)
|
self.assertEqual(logs_count, 4)
|
||||||
attendance_count = frappe.db.count('Attendance', {'status':'Present', 'working_hours':8.2,
|
|
||||||
|
attendance_count = frappe.db.count('Attendance', {'status':'Present', 'working_time': 29460,
|
||||||
'employee':employee, 'attendance_date':now_date})
|
'employee':employee, 'attendance_date':now_date})
|
||||||
self.assertEqual(attendance_count, 1)
|
self.assertEqual(attendance_count, 1)
|
||||||
|
|
||||||
def test_calculate_working_hours(self):
|
def test_calculate_working_hours(self):
|
||||||
check_in_out_type = ['Alternating entries as IN and OUT during the same shift',
|
check_in_out_type = ['Alternating entries as IN and OUT during the same shift',
|
||||||
'Strictly based on Log Type in Employee Checkin']
|
'Strictly based on Log Type in Employee Checkin']
|
||||||
working_hours_calc_type = ['First Check-in and Last Check-out',
|
working_hours_calc_type = ['First Check-in and Last Check-out',
|
||||||
'Every Valid Check-in and Check-out']
|
'Every Valid Check-in and Check-out']
|
||||||
logs_type_1 = [
|
logs_type_1 = [
|
||||||
@@ -88,12 +89,12 @@ def make_n_checkins(employee, n, hours_to_reverse=1):
|
|||||||
return logs
|
return logs
|
||||||
|
|
||||||
|
|
||||||
def make_checkin(employee, time=now_datetime()):
|
def make_checkin(employee, time=now_datetime(), log_type = "IN"):
|
||||||
log = frappe.get_doc({
|
log = frappe.get_doc({
|
||||||
"doctype": "Employee Checkin",
|
"doctype": "Employee Checkin",
|
||||||
"employee" : employee,
|
"employee" : employee,
|
||||||
"time" : time,
|
"time" : time,
|
||||||
"device_id" : "device1",
|
"device_id" : "device1",
|
||||||
"log_type" : "IN"
|
"log_type" : log_type
|
||||||
}).insert()
|
}).insert()
|
||||||
return log
|
return log
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('HR Settings', {
|
frappe.ui.form.on('HR Settings', {
|
||||||
|
|
||||||
|
refresh: function(frm) {
|
||||||
|
frm.set_query('overtime_salary_component', function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
type: "Earning"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
restrict_backdated_leave_application: function(frm) {
|
restrict_backdated_leave_application: function(frm) {
|
||||||
frm.toggle_reqd("role_allowed_to_create_backdated_leave_application", frm.doc.restrict_backdated_leave_application);
|
frm.toggle_reqd("role_allowed_to_create_backdated_leave_application", frm.doc.restrict_backdated_leave_application);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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
|
||||||
@@ -61,12 +61,12 @@ class ShiftAssignment(Document):
|
|||||||
def throw_overlap_error(self, shift_details):
|
def throw_overlap_error(self, shift_details):
|
||||||
shift_details = frappe._dict(shift_details)
|
shift_details = frappe._dict(shift_details)
|
||||||
if shift_details.docstatus == 1 and shift_details.status == "Active":
|
if shift_details.docstatus == 1 and shift_details.status == "Active":
|
||||||
msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name))
|
msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name)) + " "
|
||||||
if shift_details.start_date:
|
if shift_details.start_date:
|
||||||
msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y"))
|
msg += _("from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y")) + " "
|
||||||
title = "Ongoing Shift"
|
title = "Ongoing Shift"
|
||||||
if shift_details.end_date:
|
if shift_details.end_date:
|
||||||
msg += _(" to {0}").format(getdate(self.end_date).strftime("%d-%m-%Y"))
|
msg += _("to {0}").format(getdate(self.end_date).strftime("%d-%m-%Y"))
|
||||||
title = "Active Shift"
|
title = "Active Shift"
|
||||||
if msg:
|
if msg:
|
||||||
frappe.throw(msg, title=title)
|
frappe.throw(msg, title=title)
|
||||||
@@ -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 shift_timings_as_per_timestamp[0] and 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
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"autoname": "prompt",
|
"autoname": "prompt",
|
||||||
"creation": "2018-04-13 16:22:52.954783",
|
"creation": "2018-04-13 16:22:52.954783",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@@ -7,20 +8,24 @@
|
|||||||
"field_order": [
|
"field_order": [
|
||||||
"start_time",
|
"start_time",
|
||||||
"end_time",
|
"end_time",
|
||||||
|
"standard_working_time",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"holiday_list",
|
"holiday_list",
|
||||||
"enable_auto_attendance",
|
"enable_auto_attendance",
|
||||||
|
"allow_overtime",
|
||||||
"auto_attendance_settings_section",
|
"auto_attendance_settings_section",
|
||||||
"determine_check_in_and_check_out",
|
"determine_check_in_and_check_out",
|
||||||
"working_hours_calculation_based_on",
|
"working_hours_calculation_based_on",
|
||||||
|
"column_break_10",
|
||||||
"begin_check_in_before_shift_start_time",
|
"begin_check_in_before_shift_start_time",
|
||||||
"allow_check_out_after_shift_end_time",
|
"allow_check_out_after_shift_end_time",
|
||||||
"column_break_10",
|
"section_break_15",
|
||||||
"working_hours_threshold_for_half_day",
|
"working_hours_threshold_for_half_day",
|
||||||
"working_hours_threshold_for_absent",
|
"working_hours_threshold_for_absent",
|
||||||
|
"column_break_19",
|
||||||
"process_attendance_after",
|
"process_attendance_after",
|
||||||
"last_sync_of_checkin",
|
"last_sync_of_checkin",
|
||||||
"grace_period_settings_auto_attendance_section",
|
"grace_period_settings_section",
|
||||||
"enable_entry_grace_period",
|
"enable_entry_grace_period",
|
||||||
"late_entry_grace_period",
|
"late_entry_grace_period",
|
||||||
"column_break_18",
|
"column_break_18",
|
||||||
@@ -29,6 +34,7 @@
|
|||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
|
"default": "00:00:00",
|
||||||
"fieldname": "start_time",
|
"fieldname": "start_time",
|
||||||
"fieldtype": "Time",
|
"fieldtype": "Time",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -36,6 +42,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "00:00:00",
|
||||||
"fieldname": "end_time",
|
"fieldname": "end_time",
|
||||||
"fieldtype": "Time",
|
"fieldtype": "Time",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -84,6 +91,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "60",
|
"default": "60",
|
||||||
|
"depends_on": "eval: doc.allow_overtime == 0",
|
||||||
"description": "The time before the shift start time during which Employee Check-in is considered for attendance.",
|
"description": "The time before the shift start time during which Employee Check-in is considered for attendance.",
|
||||||
"fieldname": "begin_check_in_before_shift_start_time",
|
"fieldname": "begin_check_in_before_shift_start_time",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
@@ -121,6 +129,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "60",
|
"default": "60",
|
||||||
|
"depends_on": "eval: doc.allow_overtime == 0",
|
||||||
"description": "Time after the end of shift during which check-out is considered for attendance.",
|
"description": "Time after the end of shift during which check-out is considered for attendance.",
|
||||||
"fieldname": "allow_check_out_after_shift_end_time",
|
"fieldname": "allow_check_out_after_shift_end_time",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
@@ -132,12 +141,6 @@
|
|||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Auto Attendance Settings"
|
"label": "Auto Attendance Settings"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"depends_on": "enable_auto_attendance",
|
|
||||||
"fieldname": "grace_period_settings_auto_attendance_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Grace Period Settings For Auto Attendance"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Mark attendance based on 'Employee Checkin' for Employees assigned to this shift.",
|
"description": "Mark attendance based on 'Employee Checkin' for Employees assigned to this shift.",
|
||||||
@@ -156,9 +159,60 @@
|
|||||||
"fieldname": "last_sync_of_checkin",
|
"fieldname": "last_sync_of_checkin",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "Last Sync of Checkin"
|
"label": "Last Sync of Checkin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "standard_working_time",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"label": "Standard Working Time",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "enable_auto_attendance",
|
||||||
|
"description": "Overtime will be calculated and Overtime Duration will reflect in attendance records. Check Payroll Settings for more options. ",
|
||||||
|
"fieldname": "allow_overtime",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow Overtime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable_auto_attendance",
|
||||||
|
"fieldname": "section_break_15",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_19",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable_auto_attendance",
|
||||||
|
"fieldname": "grace_period_settings_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Grace Period Settings"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2019-07-30 01:05:24.660666",
|
"links": [
|
||||||
|
{
|
||||||
|
"group": "Attendance and Checkin",
|
||||||
|
"link_doctype": "Attendance",
|
||||||
|
"link_fieldname": "shift"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Attendance and Checkin",
|
||||||
|
"link_doctype": "Employee Checkin",
|
||||||
|
"link_fieldname": "shift"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Request and Assignment",
|
||||||
|
"link_doctype": "Shift Request",
|
||||||
|
"link_fieldname": "shift_type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Request and Assignment",
|
||||||
|
"link_doctype": "Shift Assignment",
|
||||||
|
"link_fieldname": "shift_type"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2021-08-11 12:07:33.227032",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Shift Type",
|
"name": "Shift Type",
|
||||||
|
|||||||
@@ -4,19 +4,46 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import itertools
|
import itertools
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, getdate, get_datetime
|
from frappe.utils import cint, getdate, get_datetime
|
||||||
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift, get_employee_shift
|
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift, get_employee_shift
|
||||||
from erpnext.hr.doctype.employee_checkin.employee_checkin import mark_attendance_and_link_log, calculate_working_hours
|
from erpnext.hr.doctype.employee_checkin.employee_checkin import mark_attendance_and_link_log, calculate_working_hours
|
||||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||||
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 datetime import timedelta
|
||||||
|
|
||||||
class ShiftType(Document):
|
class ShiftType(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_overtime()
|
||||||
|
self.set_working_hours()
|
||||||
|
|
||||||
|
def set_working_hours(self):
|
||||||
|
end_time = self.end_time.split(':')
|
||||||
|
start_time = self.start_time.split(':')
|
||||||
|
|
||||||
|
shift_end = timedelta(hours = int(end_time[0]), minutes = int(end_time[1]), seconds = int(end_time[2]))
|
||||||
|
shift_start = timedelta(hours = int(start_time[0]), minutes = int(start_time[1]), seconds = int(start_time[2]))
|
||||||
|
|
||||||
|
if shift_end > shift_start:
|
||||||
|
time_difference = shift_end - shift_start
|
||||||
|
else:
|
||||||
|
# for night shift
|
||||||
|
time_difference = shift_start - shift_end
|
||||||
|
self.standard_working_time = convert_time_into_duration(time_difference)
|
||||||
|
|
||||||
|
def validate_overtime(self):
|
||||||
|
if self.allow_overtime:
|
||||||
|
if not frappe.db.get_single_value('Payroll Settings', 'fetch_standard_working_hours_from_shift_type'):
|
||||||
|
frappe.throw(_('Please enable "Fetch Standard Working Hours from Shift Type" in payroll Settings for Overtime.'))
|
||||||
|
|
||||||
|
if frappe.db.get_single_value("Payroll Settings", "overtime_based_on") != "Attendance":
|
||||||
|
frappe.throw(_('Please set Overtime based on "Attendance" in payroll Settings for Overtime.'))
|
||||||
|
|
||||||
@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 = {
|
||||||
@@ -27,10 +54,19 @@ 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")
|
||||||
for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
|
|
||||||
|
if self.allow_overtime == 1:
|
||||||
|
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']))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
for employee in self.get_assigned_employee(self.process_attendance_after, True):
|
for employee in self.get_assigned_employee(self.process_attendance_after, True):
|
||||||
self.mark_absent_for_dates_with_no_attendance(employee)
|
self.mark_absent_for_dates_with_no_attendance(employee)
|
||||||
|
|
||||||
@@ -41,8 +77,10 @@ class ShiftType(Document):
|
|||||||
1. These logs belongs to an single shift, single employee and is not in a holiday date.
|
1. These logs belongs to an single shift, single employee and is not in a holiday date.
|
||||||
2. Logs are in chronological order
|
2. Logs are in chronological order
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -51,8 +89,10 @@ class ShiftType(Document):
|
|||||||
|
|
||||||
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:
|
||||||
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:
|
||||||
return 'Half Day', total_working_hours, late_entry, early_exit, in_time, out_time
|
return 'Half Day', total_working_hours, late_entry, early_exit, in_time, out_time
|
||||||
|
|
||||||
return 'Present', total_working_hours, late_entry, early_exit, in_time, out_time
|
return 'Present', total_working_hours, late_entry, early_exit, in_time, out_time
|
||||||
|
|
||||||
def mark_absent_for_dates_with_no_attendance(self, employee):
|
def mark_absent_for_dates_with_no_attendance(self, employee):
|
||||||
@@ -126,3 +166,8 @@ def get_filtered_date_list(employee, start_date, end_date, filter_attendance=Tru
|
|||||||
{"employee":employee, "start_date":start_date, "end_date":end_date, "holiday_list":holiday_list}, as_list=True)
|
{"employee":employee, "start_date":start_date, "end_date":end_date, "holiday_list":holiday_list}, as_list=True)
|
||||||
|
|
||||||
return [getdate(date[0]) for date in dates]
|
return [getdate(date[0]) for date in dates]
|
||||||
|
|
||||||
|
|
||||||
|
def convert_time_into_duration(time_difference):
|
||||||
|
time_difference = str(time_difference).split(':')
|
||||||
|
return (int(time_difference[0]) * 3600) + (int(time_difference[1]) * 60) + int(time_difference[2])
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
from frappe import _
|
|
||||||
|
|
||||||
def get_data():
|
|
||||||
return {
|
|
||||||
'fieldname': 'shift',
|
|
||||||
'non_standard_fieldnames': {
|
|
||||||
'Shift Request': 'shift_type',
|
|
||||||
'Shift Assignment': 'shift_type'
|
|
||||||
},
|
|
||||||
'transactions': [
|
|
||||||
{
|
|
||||||
'items': ['Attendance', 'Employee Checkin', 'Shift Request', 'Shift Assignment']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -8,3 +8,13 @@ import unittest
|
|||||||
|
|
||||||
class TestShiftType(unittest.TestCase):
|
class TestShiftType(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def create_shift_type():
|
||||||
|
shift_type = frappe.new_doc("Shift Type")
|
||||||
|
shift_type.name = "test shift"
|
||||||
|
shift_type.start_time = "9:00:00"
|
||||||
|
shift_type.end_time = "18:00:00"
|
||||||
|
shift_type.enable_auto_attendance = 1
|
||||||
|
|
||||||
|
shift_type.save()
|
||||||
|
return shift_type
|
||||||
|
|||||||
@@ -235,11 +235,12 @@ def get_gratuity_rule_slabs(gratuity_rule):
|
|||||||
return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"], order_by="idx")
|
return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"], order_by="idx")
|
||||||
|
|
||||||
def get_salary_structure(employee):
|
def get_salary_structure(employee):
|
||||||
return frappe.get_list("Salary Structure Assignment", filters = {
|
salary_structure_assignment = frappe.get_list("Salary Structure Assignment", filters = {
|
||||||
"employee": employee, 'docstatus': 1
|
"employee": employee, 'docstatus': 1
|
||||||
},
|
},
|
||||||
fields=["from_date", "salary_structure"],
|
fields=["from_date", "salary_structure"],
|
||||||
order_by = "from_date desc")[0].salary_structure
|
order_by = "from_date desc")
|
||||||
|
return salary_structure_assignment[0].salary_structure if len(salary_structure_assignment) else None
|
||||||
|
|
||||||
def get_last_salary_slip(employee):
|
def get_last_salary_slip(employee):
|
||||||
return frappe.get_list("Salary Slip", filters = {
|
return frappe.get_list("Salary Slip", filters = {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Overtime Details', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
||||||
101
erpnext/payroll/doctype/overtime_details/overtime_details.json
Normal file
101
erpnext/payroll/doctype/overtime_details/overtime_details.json
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-27 13:39:56.788736",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"reference_document_type",
|
||||||
|
"reference_document",
|
||||||
|
"column_break_2",
|
||||||
|
"date",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"section_break_5",
|
||||||
|
"overtime_type",
|
||||||
|
"overtime_duration",
|
||||||
|
"column_break_10",
|
||||||
|
"standard_working_time"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "reference_document_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Reference Document Type ",
|
||||||
|
"options": "DocType",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "overtime_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Overtime Type ",
|
||||||
|
"options": "Overtime Type",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "overtime_duration",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"hide_days": 1,
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Overtime Duration",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reference_document",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Reference Document",
|
||||||
|
"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-06-14 17:39:36.147530",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Payroll",
|
||||||
|
"name": "Overtime Details",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class OvertimeDetails(Document):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestOvertimeDetails(unittest.TestCase):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-25 12:49:03.287694",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"salary_component"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "salary_component",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Salary Component ",
|
||||||
|
"options": "Salary Component",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-06-16 14:48:59.476787",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Payroll",
|
||||||
|
"name": "Overtime Salary Component",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class OvertimeSalaryComponent(Document):
|
||||||
|
pass
|
||||||
0
erpnext/payroll/doctype/overtime_slip/__init__.py
Normal file
0
erpnext/payroll/doctype/overtime_slip/__init__.py
Normal file
76
erpnext/payroll/doctype/overtime_slip/overtime_slip.js
Normal file
76
erpnext/payroll/doctype/overtime_slip/overtime_slip.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Overtime Slip', {
|
||||||
|
onload: function (frm) {
|
||||||
|
frm.set_query("employee", () => {
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.employee_query"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
employee: function (frm) {
|
||||||
|
if (frm.doc.employee) {
|
||||||
|
frm.events.set_frequency_and_dates(frm).then(() => {
|
||||||
|
frm.events.get_emp_details_and_overtime_duration(frm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
from_date: function (frm) {
|
||||||
|
|
||||||
|
if (frm.doc.employee) {
|
||||||
|
frm.events.set_frequency_and_dates(frm).then(() => {
|
||||||
|
frm.events.get_emp_details_and_overtime_duration(frm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set_frequency_and_dates: function (frm) {
|
||||||
|
|
||||||
|
if (frm.doc.employee) {
|
||||||
|
return frappe.call({
|
||||||
|
method: 'get_frequency_and_dates',
|
||||||
|
doc: frm.doc,
|
||||||
|
callback: function () {
|
||||||
|
frm.refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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 () {
|
||||||
|
frm.refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
204
erpnext/payroll/doctype/overtime_slip/overtime_slip.json
Normal file
204
erpnext/payroll/doctype/overtime_slip/overtime_slip.json
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "HR-OT-SLIP-.#####",
|
||||||
|
"creation": "2021-05-27 12:47:32.372698",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"employee_details",
|
||||||
|
"posting_date",
|
||||||
|
"employee",
|
||||||
|
"employee_name",
|
||||||
|
"column_break_4",
|
||||||
|
"status",
|
||||||
|
"company",
|
||||||
|
"department",
|
||||||
|
"section_break_7",
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"column_break_10",
|
||||||
|
"payroll_frequency",
|
||||||
|
"salary_slip",
|
||||||
|
"section_break_12",
|
||||||
|
"overtime_details",
|
||||||
|
"section_break_13",
|
||||||
|
"total_overtime_duration",
|
||||||
|
"column_break_17",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "employee",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Employee",
|
||||||
|
"options": "Employee",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "employee.employee_name",
|
||||||
|
"fieldname": "employee_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Employee Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "employee.department",
|
||||||
|
"fieldname": "department",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Department",
|
||||||
|
"options": "Department",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "employee.company",
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Pending",
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Pending\nApproved\nRejected",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Overtime Slip",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "overtime_details",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"options": "Overtime Details",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_4",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_7",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Payroll Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "From Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "to_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "To Date",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_10",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payroll_frequency",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Payroll Frequency",
|
||||||
|
"options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_13",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total_overtime_duration",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"label": "Total Overtime Duration"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_12",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Overtime Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_17",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Posting Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "salary_slip",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Salary Slip",
|
||||||
|
"options": "Salary Slip",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "employee_details",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Employee Details"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-08-11 12:30:27.536389",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Payroll",
|
||||||
|
"name": "Overtime Slip",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "HR Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "HR User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Employee",
|
||||||
|
"share": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"title_field": "employee_name",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
189
erpnext/payroll/doctype/overtime_slip/overtime_slip.py
Normal file
189
erpnext/payroll/doctype/overtime_slip/overtime_slip.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _, bold
|
||||||
|
from frappe.utils import get_datetime, getdate, get_link_to_form, formatdate
|
||||||
|
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
|
||||||
|
class OvertimeSlip(Document):
|
||||||
|
def validate(self):
|
||||||
|
if not (self.from_date or self.to_date or self.payroll_frequency):
|
||||||
|
self.get_frequency_and_dates()
|
||||||
|
|
||||||
|
self.validate_overlap()
|
||||||
|
if self.from_date >= self.to_date:
|
||||||
|
frappe.throw(_("From date can not be greater than To date"))
|
||||||
|
|
||||||
|
if not len(self.overtime_details):
|
||||||
|
self.get_emp_and_overtime_details()
|
||||||
|
|
||||||
|
def validate_overlap(self):
|
||||||
|
if not self.name:
|
||||||
|
# hack! if name is null, it could cause problems with !=
|
||||||
|
self.name = "new-overtime-slip-1"
|
||||||
|
|
||||||
|
overtime_slips = frappe.db.get_all("Overtime Slip", filters = {
|
||||||
|
"docstatus": ("<", 2),
|
||||||
|
"employee": self.employee,
|
||||||
|
"to_date": (">=", self.from_date),
|
||||||
|
"from_date": ("<=", self.to_date),
|
||||||
|
"name": ("!=", self.name)
|
||||||
|
})
|
||||||
|
if len(overtime_slips):
|
||||||
|
form_link = get_link_to_form("Overtime Slip", overtime_slips[0].name)
|
||||||
|
msg = _("Overtime Slip:{0} has been created between {1} and {1}").format(
|
||||||
|
bold(form_link),
|
||||||
|
bold(formatdate(self.from_date)), bold(formatdate(self.to_date)))
|
||||||
|
frappe.throw(msg)
|
||||||
|
|
||||||
|
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):
|
||||||
|
overtime_based_on = frappe.db.get_single_value("Payroll Settings", "overtime_based_on")
|
||||||
|
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:
|
||||||
|
link_to_settings = get_link_to_form('Payroll Settings', 'Payroll Settings', 'Payroll Settings')
|
||||||
|
frappe.throw(_('Select "Calculate Overtime Based On" in {0}').format(link_to_settings))
|
||||||
|
|
||||||
|
if len(self.overtime_details):
|
||||||
|
self.total_overtime_duration = sum([int(detail.overtime_duration) for detail in self.overtime_details])
|
||||||
|
|
||||||
|
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):
|
||||||
|
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)
|
||||||
|
return records
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_timesheet_record(self):
|
||||||
|
if self.from_date and self.to_date:
|
||||||
|
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)
|
||||||
|
return records
|
||||||
|
return []
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_frequency_and_dates(self):
|
||||||
|
|
||||||
|
date = self.from_date or self.posting_date
|
||||||
|
|
||||||
|
salary_structure = get_salary_structure(self.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', self.employee, "company"))
|
||||||
|
|
||||||
|
self.from_date = date_details.start_date
|
||||||
|
self.to_date = date_details.end_date
|
||||||
|
self.payroll_frequency = payroll_frequency
|
||||||
|
else:
|
||||||
|
frappe.throw(_("No Salary Structure Assignment found for Employee: {0}").format(self.employee))
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
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:
|
||||||
|
link_to_settings = get_link_to_form('HR Settings', 'HR Settings', 'HR Settings')
|
||||||
|
frappe.throw(_('Please Set "Standard Working Hours" in {0}').format(link_to_settings))
|
||||||
|
|
||||||
|
return standard_working_time
|
||||||
|
|
||||||
|
|
||||||
155
erpnext/payroll/doctype/overtime_slip/test_overtime_slip.py
Normal file
155
erpnext/payroll/doctype/overtime_slip/test_overtime_slip.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
import frappe
|
||||||
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
|
from erpnext.payroll.doctype.overtime_type.test_overtime_type import create_overtime_type
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
from erpnext.hr.doctype.shift_type.test_shift_type import create_shift_type
|
||||||
|
from erpnext.hr.doctype.employee_checkin.test_employee_checkin import make_checkin
|
||||||
|
from frappe.utils import today, add_days, get_datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class TestOvertimeSlip(unittest.TestCase):
|
||||||
|
def tearDown(self):
|
||||||
|
for doctype in ["Overtime Type","Overtime Slip", "Attendance", "Employee Checkin", "Shift Type"]:
|
||||||
|
frappe.db.sql("DELETE FROM `tab{0}`".format(doctype))
|
||||||
|
|
||||||
|
frappe.db.sql("DELETE FROM `tabEmployee` WHERE user_id = 'test_employee@overtime.com'")
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
def test_overtime_based_on_attendance_without_shift_type(self):
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Attendance")
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "fetch_standard_working_hours_from_shift_type", 0)
|
||||||
|
frappe.db.set_value("HR Settings", None, "standard_working_hours", 7)
|
||||||
|
|
||||||
|
employee = make_employee("test_employee@overtime.com", company="_Test Company")
|
||||||
|
make_salary_structure("structure for Overtime", "Monthly", employee=employee)
|
||||||
|
overtime_type = create_overtime_type(employee=employee)
|
||||||
|
attendance_record = create_attendance_records_for_overtime(employee, overtime_type.name)
|
||||||
|
slip = create_overtime_slip(employee)
|
||||||
|
|
||||||
|
for detail in slip.overtime_details:
|
||||||
|
self.assertIn(detail.reference_document, attendance_record.keys())
|
||||||
|
if detail.reference_document in attendance_record.keys():
|
||||||
|
self.assertEqual(detail.overtime_duration, attendance_record[detail.reference_document]["overtime_duration"])
|
||||||
|
self.assertEqual(str(detail.date), attendance_record[detail.reference_document]["attendance_date"])
|
||||||
|
|
||||||
|
def test_overtime_based_on_attendance_with_shift_type_through_employee_checkins(self):
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Attendance")
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "fetch_standard_working_hours_from_shift_type", 1)
|
||||||
|
|
||||||
|
shift_type = create_shift_type()
|
||||||
|
shift_type.allow_overtime = 1
|
||||||
|
shift_type.process_attendance_after = add_days(today(), -1)
|
||||||
|
shift_type.last_sync_of_checkin = get_datetime(add_days(today(), 1))
|
||||||
|
shift_type.save()
|
||||||
|
|
||||||
|
employee = make_employee("test_employee@overtime.com", company="_Test Company")
|
||||||
|
make_salary_structure("structure for Overtime", "Monthly", employee=employee)
|
||||||
|
|
||||||
|
frappe.db.set_value("Employee", employee, "default_shift", shift_type.name)
|
||||||
|
|
||||||
|
checkin = make_checkin(employee, time = get_datetime(today()) + timedelta(hours=9), log_type="IN")
|
||||||
|
checkout = make_checkin(employee, time = get_datetime(today()) + timedelta(hours=20), log_type="OUT")
|
||||||
|
|
||||||
|
self.assertEqual(checkin.shift, shift_type.name)
|
||||||
|
self.assertEqual(checkout.shift, shift_type.name)
|
||||||
|
|
||||||
|
create_overtime_type(employee=employee)
|
||||||
|
shift_type.reload()
|
||||||
|
shift_type.process_auto_attendance()
|
||||||
|
checkin.reload()
|
||||||
|
|
||||||
|
attendance_records = frappe.get_all("Attendance", filters = {
|
||||||
|
"shift": shift_type.name, "status": "Present"
|
||||||
|
}, fields = ["name", "overtime_duration", "overtime_type", "attendance_date"])
|
||||||
|
|
||||||
|
records = {}
|
||||||
|
for record in attendance_records:
|
||||||
|
records[record.name] = {
|
||||||
|
"overtime_duration": record.overtime_duration,
|
||||||
|
"overtime_type": record.overtime_type,
|
||||||
|
"attendance_date": record.attendance_date
|
||||||
|
}
|
||||||
|
|
||||||
|
slip = create_overtime_slip(employee)
|
||||||
|
|
||||||
|
for detail in slip.overtime_details:
|
||||||
|
self.assertIn(detail.reference_document, records.keys())
|
||||||
|
if detail.reference_document in records.keys():
|
||||||
|
self.assertEqual(detail.overtime_duration, records[detail.reference_document]["overtime_duration"])
|
||||||
|
self.assertEqual(str(detail.date), str(records[detail.reference_document]["attendance_date"]))
|
||||||
|
|
||||||
|
def test_overtime_based_on_timesheet(self):
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Timesheet")
|
||||||
|
frappe.db.set_value("HR Settings", None, "standard_working_hours", 7)
|
||||||
|
|
||||||
|
employee = make_employee("test_employee@overtime.com", company="_Test Company")
|
||||||
|
make_salary_structure("structure for Overtime", "Monthly", employee=employee)
|
||||||
|
overtime_type = create_overtime_type(employee=employee)
|
||||||
|
time_log, timesheet = create_timesheet_record_for_overtime(employee, overtime_type.name)
|
||||||
|
slip = create_overtime_slip(employee)
|
||||||
|
|
||||||
|
for detail in slip.overtime_details:
|
||||||
|
self.assertEqual(time_log.overtime_hours * 3600, detail.overtime_duration)
|
||||||
|
self.assertEqual(time_log.overtime_on, get_datetime(detail.date))
|
||||||
|
self.assertEqual(time_log.overtime_type, detail.overtime_type)
|
||||||
|
self.assertEqual(timesheet, detail.reference_document)
|
||||||
|
|
||||||
|
def create_attendance_records_for_overtime(employee, overtime_type):
|
||||||
|
records = {}
|
||||||
|
for x in range(2):
|
||||||
|
attendance = frappe.new_doc("Attendance")
|
||||||
|
attendance.employee = employee
|
||||||
|
attendance.status = "Present"
|
||||||
|
attendance.attendance_date = add_days(today(), -(x))
|
||||||
|
attendance.overtime_type = overtime_type
|
||||||
|
#to convert to duration
|
||||||
|
attendance.overtime_duration = 2 * 3600
|
||||||
|
|
||||||
|
attendance.save()
|
||||||
|
attendance.submit()
|
||||||
|
|
||||||
|
records[attendance.name] = {
|
||||||
|
"overtime_duration": attendance.overtime_duration,
|
||||||
|
"overtime_type": attendance.overtime_type,
|
||||||
|
"attendance_date": attendance.attendance_date
|
||||||
|
}
|
||||||
|
|
||||||
|
return records
|
||||||
|
|
||||||
|
def create_timesheet_record_for_overtime(employee, overtime_type):
|
||||||
|
timesheet =frappe.new_doc("Timesheet")
|
||||||
|
timesheet.employee = employee
|
||||||
|
|
||||||
|
timesheet.time_logs = []
|
||||||
|
time_log = {
|
||||||
|
"activity_type": "Planning",
|
||||||
|
"from_time": get_datetime(add_days(today(), -1)),
|
||||||
|
"to_time": get_datetime(add_days(today(), 2)),
|
||||||
|
"expected_hours": 48,
|
||||||
|
"hours": 48,
|
||||||
|
"is_overtime": 1,
|
||||||
|
"overtime_type": overtime_type,
|
||||||
|
"overtime_on": get_datetime(today()),
|
||||||
|
"overtime_hours": 7
|
||||||
|
}
|
||||||
|
timesheet.append("time_logs", time_log)
|
||||||
|
|
||||||
|
timesheet.save()
|
||||||
|
timesheet.submit()
|
||||||
|
|
||||||
|
return frappe._dict(time_log), timesheet.name
|
||||||
|
|
||||||
|
|
||||||
|
def create_overtime_slip(employee):
|
||||||
|
slip = frappe.new_doc("Overtime Slip")
|
||||||
|
slip.employee = employee
|
||||||
|
|
||||||
|
slip.overtime_details = []
|
||||||
|
|
||||||
|
slip.save()
|
||||||
|
return slip
|
||||||
|
|
||||||
0
erpnext/payroll/doctype/overtime_type/__init__.py
Normal file
0
erpnext/payroll/doctype/overtime_type/__init__.py
Normal file
15
erpnext/payroll/doctype/overtime_type/overtime_type.js
Normal file
15
erpnext/payroll/doctype/overtime_type/overtime_type.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Overtime Type', {
|
||||||
|
setup: function(frm) {
|
||||||
|
frm.set_query("applicable_for", () => {
|
||||||
|
let doctype_list = ["Employee", "Department", "Employee Grade"];
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
name: ["in", doctype_list]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
150
erpnext/payroll/doctype/overtime_type/overtime_type.json
Normal file
150
erpnext/payroll/doctype/overtime_type/overtime_type.json
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "Prompt",
|
||||||
|
"creation": "2021-05-25 12:49:09.178306",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"applicable_for",
|
||||||
|
"employee",
|
||||||
|
"department",
|
||||||
|
"employee_grade",
|
||||||
|
"column_break_3",
|
||||||
|
"applicable_salary_component",
|
||||||
|
"pay_rate_multipliers_section",
|
||||||
|
"standard_multiplier",
|
||||||
|
"applicable_for_weekend",
|
||||||
|
"weekend_multiplier",
|
||||||
|
"column_break_9",
|
||||||
|
"applicable_for_public_holiday",
|
||||||
|
"public_holiday_multiplier"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "applicable_salary_component",
|
||||||
|
"fieldtype": "Table MultiSelect",
|
||||||
|
"label": "Applicable Salary Component",
|
||||||
|
"options": "Overtime Salary Component",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Pay Rate Multipliers apply to the hourly wage for the position you\u2019re working during the overtime hours.",
|
||||||
|
"fieldname": "pay_rate_multipliers_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Pay Rate Multipliers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "standard_multiplier",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Standard Multiplier",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If unchecked, the standard multiplier will be taken as default for the weekend.\n",
|
||||||
|
"fieldname": "applicable_for_weekend",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Applicable for Weekend"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.applicable_for_weekend == 1",
|
||||||
|
"fieldname": "weekend_multiplier",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Weekend Multiplier",
|
||||||
|
"mandatory_depends_on": "eval: doc.applicable_for_weekend == 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_9",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "If unchecked, the standard multiplier will be taken as default for Public Holiday.",
|
||||||
|
"fieldname": "applicable_for_public_holiday",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Applicable for Public Holiday"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.applicable_for_public_holiday == 1",
|
||||||
|
"fieldname": "public_holiday_multiplier",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Public Holiday Multiplier",
|
||||||
|
"mandatory_depends_on": "eval: doc.applicable_for_public_holiday == 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "applicable_for",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Applicable For",
|
||||||
|
"options": "DocType",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.applicable_for == \"Employee\"",
|
||||||
|
"fieldname": "employee",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Employee",
|
||||||
|
"mandatory_depends_on": "eval: doc.applicable_for == \"Employee\"",
|
||||||
|
"options": "Employee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.applicable_for == \"Department\"",
|
||||||
|
"fieldname": "department",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Department",
|
||||||
|
"mandatory_depends_on": "eval: doc.applicable_for == \"Department\"",
|
||||||
|
"options": "Department"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.applicable_for == \"Employee Grade\"",
|
||||||
|
"fieldname": "employee_grade",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Employee Grade",
|
||||||
|
"mandatory_depends_on": "eval: doc.applicable_for == \"Employee Grade\"",
|
||||||
|
"options": "Employee Grade"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"link_doctype": "Attendance",
|
||||||
|
"link_fieldname": "overtime_type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link_doctype": "Timesheet",
|
||||||
|
"link_fieldname": "overtime_type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link_doctype": "Overtime Details",
|
||||||
|
"link_fieldname": "overtime_type"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2021-08-11 11:29:52.944112",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Payroll",
|
||||||
|
"name": "Overtime Type",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
8
erpnext/payroll/doctype/overtime_type/overtime_type.py
Normal file
8
erpnext/payroll/doctype/overtime_type/overtime_type.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class OvertimeType(Document):
|
||||||
|
pass
|
||||||
41
erpnext/payroll/doctype/overtime_type/test_overtime_type.py
Normal file
41
erpnext/payroll/doctype/overtime_type/test_overtime_type.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestOvertimeType(unittest.TestCase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_overtime_type(**args):
|
||||||
|
args = frappe._dict(args)
|
||||||
|
overtime_type = frappe.new_doc("Overtime Type")
|
||||||
|
overtime_type.name = "Test Overtime"
|
||||||
|
overtime_type.applicable_for = args.applicable_for or "Employee"
|
||||||
|
if overtime_type.applicable_for == "Department":
|
||||||
|
overtime_type.department = args.department
|
||||||
|
elif overtime_type.applicable_for == "Employee Grade":
|
||||||
|
overtime_type.employee_grade = args.employee_grade
|
||||||
|
else:
|
||||||
|
overtime_type.employee = args.employee
|
||||||
|
|
||||||
|
overtime_type.standard_multiplier = 1.25
|
||||||
|
overtime_type.applicable_for_weekend = args.applicable_for_weekend or 0
|
||||||
|
overtime_type.applicable_for_public_holiday = args.applicable_for_public_holiday or 0
|
||||||
|
|
||||||
|
if args.applicable_for_weekend:
|
||||||
|
overtime_type.weekend_multiplier = 1.5
|
||||||
|
|
||||||
|
if args.applicable_for_public_holidays:
|
||||||
|
overtime_type.public_holiday_multiplier = 2
|
||||||
|
|
||||||
|
overtime_type.append("applicable_salary_component", {
|
||||||
|
"salary_component": "Basic Salary"
|
||||||
|
})
|
||||||
|
|
||||||
|
overtime_type.save()
|
||||||
|
|
||||||
|
return overtime_type
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_comp
|
|||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure, create_salary_structure_assignment
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure, create_salary_structure_assignment
|
||||||
from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts
|
from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts
|
||||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
|
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
|
||||||
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_component_for_overtime
|
||||||
|
|
||||||
test_dependencies = ['Holiday List']
|
test_dependencies = ['Holiday List']
|
||||||
|
|
||||||
@@ -21,6 +22,9 @@ class TestPayrollEntry(unittest.TestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List')
|
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", '_Test Holiday List')
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_salary_component", "Overtime Allowance")
|
||||||
|
get_salary_component_for_overtime()
|
||||||
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for dt in ["Salary Slip", "Salary Component", "Salary Component Account",
|
for dt in ["Salary Slip", "Salary Component", "Salary Component Account",
|
||||||
|
|||||||
@@ -16,7 +16,13 @@
|
|||||||
"email_salary_slip_to_employee",
|
"email_salary_slip_to_employee",
|
||||||
"encrypt_salary_slips_in_emails",
|
"encrypt_salary_slips_in_emails",
|
||||||
"show_leave_balances_in_salary_slip",
|
"show_leave_balances_in_salary_slip",
|
||||||
"password_policy"
|
"password_policy",
|
||||||
|
"section_break_12",
|
||||||
|
"overtime_based_on",
|
||||||
|
"maximum_overtime_hours_allowed",
|
||||||
|
"column_break_14",
|
||||||
|
"overtime_salary_component",
|
||||||
|
"fetch_standard_working_hours_from_shift_type"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -91,13 +97,49 @@
|
|||||||
"fieldname": "show_leave_balances_in_salary_slip",
|
"fieldname": "show_leave_balances_in_salary_slip",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Leave Balances in Salary Slip"
|
"label": "Show Leave Balances in Salary Slip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "overtime_salary_component",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Overtime Salary Component",
|
||||||
|
"options": "Salary Component"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_12",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Overtime Calculation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval: doc.overtime_based_on == \"Attendance\"",
|
||||||
|
"description": "If unchecked, Standard Working Hours as defined in HR Settings will be taken into consideration.",
|
||||||
|
"fieldname": "fetch_standard_working_hours_from_shift_type",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Fetch Standard Working Hours from Shift Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_14",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Attendance",
|
||||||
|
"fieldname": "overtime_based_on",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Calculate Overtime Based On",
|
||||||
|
"options": "Attendance\nTimesheet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Overtime payment will not be given for more than the defined hours limit.",
|
||||||
|
"fieldname": "maximum_overtime_hours_allowed",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Maximum Overtime Hours Allowed For Payment "
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-03 17:49:59.579723",
|
"modified": "2021-08-11 11:45:10.568381",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Payroll Settings",
|
"name": "Payroll Settings",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -423,7 +423,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "net_pay_info",
|
"fieldname": "net_pay_info",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "net pay info"
|
"label": "Net Pay Info"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "net_pay",
|
"fieldname": "net_pay",
|
||||||
@@ -630,8 +630,13 @@
|
|||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 9,
|
"idx": 9,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2021-03-31 22:44:09.772331",
|
{
|
||||||
|
"link_doctype": "Overtime Slip",
|
||||||
|
"link_fieldname": "salary_slip"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2021-08-11 13:35:14.650988",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Salary Slip",
|
"name": "Salary Slip",
|
||||||
|
|||||||
@@ -82,8 +82,23 @@ class SalarySlip(TransactionBase):
|
|||||||
if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
|
if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
|
||||||
self.email_salary_slip()
|
self.email_salary_slip()
|
||||||
|
|
||||||
|
self.update_overtime_slip()
|
||||||
self.update_payment_status_for_gratuity()
|
self.update_payment_status_for_gratuity()
|
||||||
|
|
||||||
|
def update_overtime_slip(self):
|
||||||
|
overtime_slips = []
|
||||||
|
for data in self.earnings:
|
||||||
|
if data.overtime_slips:
|
||||||
|
overtime_slips.extend(data.overtime_slips.split(", "))
|
||||||
|
|
||||||
|
if self.docstatus == 1:
|
||||||
|
for slip in overtime_slips:
|
||||||
|
frappe.db.set_value("Overtime Slip", slip, "salary_slip", self.name)
|
||||||
|
|
||||||
|
if self.docstatus == 2:
|
||||||
|
for slip in overtime_slips:
|
||||||
|
frappe.db.set_value("Overtime Slip", slip, "salary_slip", None)
|
||||||
|
|
||||||
def update_payment_status_for_gratuity(self):
|
def update_payment_status_for_gratuity(self):
|
||||||
add_salary = frappe.db.get_all("Additional Salary",
|
add_salary = frappe.db.get_all("Additional Salary",
|
||||||
filters = {
|
filters = {
|
||||||
@@ -101,6 +116,7 @@ class SalarySlip(TransactionBase):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.update_status()
|
self.update_status()
|
||||||
|
self.update_overtime_slip
|
||||||
self.update_payment_status_for_gratuity()
|
self.update_payment_status_for_gratuity()
|
||||||
self.cancel_loan_repayment_entry()
|
self.cancel_loan_repayment_entry()
|
||||||
|
|
||||||
@@ -336,9 +352,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 +362,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 +513,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 +527,130 @@ 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_type_details_and_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': ("between", [self.start_date, self.end_date]),
|
||||||
|
'salary_slip': '',
|
||||||
|
'docstatus': 1
|
||||||
|
}, fields = ["name", "from_date", 'to_date'])
|
||||||
|
|
||||||
|
def get_overtime_type_details_and_amount(self, overtime_slips):
|
||||||
|
standard_duration_amount, weekends_duration_amount = 0, 0
|
||||||
|
public_holidays_duration_amount, calculated_amount = 0, 0
|
||||||
|
processed_overtime_slips = []
|
||||||
|
overtime_types_details = {}
|
||||||
|
for slip in overtime_slips:
|
||||||
|
holiday_date_map = self.get_holiday_map(slip.from_date, slip.to_date)
|
||||||
|
details = self.get_overtime_details(slip.name)
|
||||||
|
|
||||||
|
for detail in details:
|
||||||
|
overtime_hours = detail.overtime_duration / 3600
|
||||||
|
overtime_types_details = self.set_overtime_types_details(overtime_types_details, detail)
|
||||||
|
|
||||||
|
standard_working_hours = detail.standard_working_time/3600
|
||||||
|
applicable_hourly_wages = overtime_types_details[detail.overtime_type]["applicable_daily_amount"]/standard_working_hours
|
||||||
|
weekend_multiplier, public_holiday_multiplier = self.get_multipliers(overtime_types_details, detail)
|
||||||
|
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 * weekend_multiplier
|
||||||
|
weekends_duration_amount += calculated_amount
|
||||||
|
elif holiday_date_map[overtime_date].weekly_off == 0:
|
||||||
|
calculated_amount = overtime_hours * applicable_hourly_wages * 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 get_multipliers(self, overtime_types_details, detail):
|
||||||
|
weekend_multiplier = overtime_types_details[detail.overtime_type]['standard_multiplier']
|
||||||
|
public_holiday_multiplier = overtime_types_details[detail.overtime_type]['standard_multiplier']
|
||||||
|
|
||||||
|
if overtime_types_details[detail.overtime_type]['applicable_for_weekend']:
|
||||||
|
weekend_multiplier = overtime_types_details[detail.overtime_type]['weekend_multiplier']
|
||||||
|
if overtime_types_details[detail.overtime_type]['applicable_for_public_holiday']:
|
||||||
|
public_holiday_multiplier = overtime_types_details[detail.overtime_type]['public_holiday_multiplier']
|
||||||
|
|
||||||
|
return weekend_multiplier, public_holiday_multiplier
|
||||||
|
|
||||||
|
def get_holiday_map(self, from_date, to_date):
|
||||||
|
holiday_date = self.get_holidays_for_employee(from_date, to_date, as_dict=1)
|
||||||
|
|
||||||
|
holiday_date_map = {}
|
||||||
|
for date in holiday_date:
|
||||||
|
holiday_date_map[cstr(date.holiday_date)] = date
|
||||||
|
|
||||||
|
return holiday_date_map
|
||||||
|
|
||||||
|
def set_overtime_types_details(self, overtime_types_details, detail):
|
||||||
|
if detail.overtime_type not 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
|
||||||
|
|
||||||
|
return overtime_types_details
|
||||||
|
|
||||||
|
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:
|
||||||
|
component_data = frappe._dict(get_salary_component_data(overtime_salary_component) or {})
|
||||||
|
component_data.salary_component = overtime_salary_component
|
||||||
|
self.update_component_row(
|
||||||
|
component_data,
|
||||||
|
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",
|
||||||
|
"applicable_for_weekend", "applicable_for_public_holiday"]
|
||||||
|
)[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 +781,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 =[], is_recurring=0):
|
||||||
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 +821,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
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
||||||
|
frappe.db.sql("DELETE FROM `tabOvertime Type`")
|
||||||
|
frappe.db.sql("DELETE FROM `tabOvertime Slip`")
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
def test_payment_days_based_on_attendance(self):
|
def test_payment_days_based_on_attendance(self):
|
||||||
@@ -460,6 +462,49 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
# undelete fixture data
|
# undelete fixture data
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
|
||||||
|
def test_overtime_calculation(self):
|
||||||
|
from erpnext.payroll.doctype.overtime_type.test_overtime_type import create_overtime_type
|
||||||
|
from erpnext.payroll.doctype.overtime_slip.test_overtime_slip import create_overtime_slip
|
||||||
|
from erpnext.payroll.doctype.overtime_slip.test_overtime_slip import create_attendance_records_for_overtime
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
employee = make_employee("overtime_calc@salary.slip")
|
||||||
|
salary_structure = make_salary_structure("structure for Overtime 2", "Monthly", employee=employee)
|
||||||
|
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_based_on", "Attendance")
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "fetch_standard_working_hours_from_shift_type", 0)
|
||||||
|
get_salary_component_for_overtime()
|
||||||
|
frappe.db.set_value("Payroll Settings", None, "overtime_salary_component", "Overtime Allowance")
|
||||||
|
|
||||||
|
overtime_type = create_overtime_type(employee = employee).name
|
||||||
|
create_attendance_records_for_overtime(employee, overtime_type=overtime_type)
|
||||||
|
|
||||||
|
slip = create_overtime_slip(employee)
|
||||||
|
slip.status = "Approved"
|
||||||
|
slip.submit()
|
||||||
|
|
||||||
|
salary_slip = make_salary_slip(salary_structure.name, employee = employee)
|
||||||
|
overtime_component_details = {}
|
||||||
|
applicable_amount = 0
|
||||||
|
|
||||||
|
for earning in salary_slip.earnings:
|
||||||
|
if earning.salary_component == "Overtime Allowance":
|
||||||
|
overtime_component_details = earning
|
||||||
|
|
||||||
|
if earning.salary_component == "Basic Salary":
|
||||||
|
applicable_amount = earning.default_amount
|
||||||
|
|
||||||
|
self.assertIn("Overtime Allowance", overtime_component_details.salary_component)
|
||||||
|
self.assertEqual(slip.name, overtime_component_details.overtime_slips)
|
||||||
|
|
||||||
|
daily_wages = applicable_amount/ salary_slip.total_working_days
|
||||||
|
hourly_wages = daily_wages/ frappe.db.get_single_value("Hr Settings", "standard_working_hours")
|
||||||
|
overtime_amount = hourly_wages * 4 * 1.25
|
||||||
|
#since multiplier is defined as 1.25
|
||||||
|
# formula = sum(applicable_component)/(working_days)/ daily_standard_working_time * overtime hours * multiplier
|
||||||
|
self.assertEquals(flt(overtime_amount, 2), flt(overtime_component_details.amount, 2))
|
||||||
|
|
||||||
def make_activity_for_employee(self):
|
def make_activity_for_employee(self):
|
||||||
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
|
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
|
||||||
activity_type.billing_rate = 50
|
activity_type.billing_rate = 50
|
||||||
@@ -475,6 +520,17 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||||
|
|
||||||
|
def get_salary_component_for_overtime():
|
||||||
|
component = [{
|
||||||
|
"salary_component": 'Overtime Allowance',
|
||||||
|
"abbr":'OA',
|
||||||
|
"type": "Earning",
|
||||||
|
"amount_based_on_formula": 0
|
||||||
|
}]
|
||||||
|
|
||||||
|
company = erpnext.get_default_company()
|
||||||
|
make_salary_component(component, test_tax = 0, company_list=[company])
|
||||||
|
|
||||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
@@ -482,12 +538,8 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
|||||||
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
|
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
|
||||||
|
|
||||||
|
|
||||||
employee = frappe.db.get_value("Employee",
|
employee = frappe.db.get_value("Employee", {"user_id": user},
|
||||||
{
|
["name", "company", "employee_name"], as_dict=True)
|
||||||
"user_id": user
|
|
||||||
},
|
|
||||||
["name", "company", "employee_name"],
|
|
||||||
as_dict=True)
|
|
||||||
|
|
||||||
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company)
|
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee.name, company=employee.company)
|
||||||
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
|
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ class TestTimesheet(unittest.TestCase):
|
|||||||
"activity_type": "_Test Activity Type",
|
"activity_type": "_Test Activity Type",
|
||||||
"from_time": now_datetime(),
|
"from_time": now_datetime(),
|
||||||
"to_time": now_datetime() + datetime.timedelta(hours=3),
|
"to_time": now_datetime() + datetime.timedelta(hours=3),
|
||||||
"company": "_Test Company"
|
"company": "_Test Company",
|
||||||
|
"hours": 3
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
timesheet.append(
|
timesheet.append(
|
||||||
@@ -137,7 +138,8 @@ class TestTimesheet(unittest.TestCase):
|
|||||||
"activity_type": "_Test Activity Type",
|
"activity_type": "_Test Activity Type",
|
||||||
"from_time": now_datetime(),
|
"from_time": now_datetime(),
|
||||||
"to_time": now_datetime() + datetime.timedelta(hours=3),
|
"to_time": now_datetime() + datetime.timedelta(hours=3),
|
||||||
"company": "_Test Company"
|
"company": "_Test Company",
|
||||||
|
"hours": 3
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 && !cur_frm.doc.__islocal) {
|
||||||
|
|
||||||
let button = 'Start Timer';
|
let button = 'Start Timer';
|
||||||
$.each(frm.doc.time_logs || [], function(i, row) {
|
$.each(frm.doc.time_logs || [], function (i, row) {
|
||||||
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -116,7 +119,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Employee",
|
"label": "Employee",
|
||||||
"options": "Employee"
|
"options": "Employee",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "employee",
|
"depends_on": "employee",
|
||||||
@@ -173,10 +177,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",
|
||||||
@@ -314,13 +314,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-06-09 12:08:53.930200",
|
"modified": "2021-06-17 17:45:12.064677",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Timesheet",
|
"name": "Timesheet",
|
||||||
|
|||||||
@@ -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,43 @@ 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 int(maximum_overtime_hours_allowed) and data.overtime_hours > maximum_overtime_hours_allowed:
|
||||||
|
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:
|
||||||
|
total_overtime_hours += data.overtime_hours
|
||||||
|
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:
|
||||||
@@ -104,20 +138,8 @@ class Timesheet(Document):
|
|||||||
self.update_task_and_project()
|
self.update_task_and_project()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_mandatory_fields()
|
|
||||||
self.update_task_and_project()
|
self.update_task_and_project()
|
||||||
|
|
||||||
def validate_mandatory_fields(self):
|
|
||||||
for data in self.time_logs:
|
|
||||||
if not data.from_time and not data.to_time:
|
|
||||||
frappe.throw(_("Row {0}: From Time and To Time is mandatory.").format(data.idx))
|
|
||||||
|
|
||||||
if not data.activity_type and self.employee:
|
|
||||||
frappe.throw(_("Row {0}: Activity Type is mandatory.").format(data.idx))
|
|
||||||
|
|
||||||
if flt(data.hours) == 0.0:
|
|
||||||
frappe.throw(_("Row {0}: Hours value must be greater than zero.").format(data.idx))
|
|
||||||
|
|
||||||
def update_task_and_project(self):
|
def update_task_and_project(self):
|
||||||
tasks, projects = [], []
|
tasks, projects = [], []
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -47,14 +53,16 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Activity Type",
|
"label": "Activity Type",
|
||||||
"options": "Activity Type"
|
"options": "Activity Type",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": 2,
|
"columns": 2,
|
||||||
"fieldname": "from_time",
|
"fieldname": "from_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "From Time"
|
"label": "From Time",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_3",
|
"fieldname": "section_break_3",
|
||||||
@@ -70,12 +78,14 @@
|
|||||||
"fieldname": "hours",
|
"fieldname": "hours",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Hrs"
|
"label": "Working Hours",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "to_time",
|
"fieldname": "to_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "To Time"
|
"label": "To Time",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -262,12 +272,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-17 17:44:09.306304",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Timesheet Detail",
|
"name": "Timesheet Detail",
|
||||||
|
|||||||
@@ -188,6 +188,10 @@
|
|||||||
// horizontal hierarchy tree view
|
// horizontal hierarchy tree view
|
||||||
#hierarchy-chart-wrapper {
|
#hierarchy-chart-wrapper {
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
|
|
||||||
|
#arrows {
|
||||||
|
margin-top: -80px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hierarchy {
|
.hierarchy {
|
||||||
@@ -211,7 +215,6 @@
|
|||||||
#arrows {
|
#arrows {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
margin-top: -80px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-connector {
|
.active-connector {
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ def set_default_settings(args):
|
|||||||
hr_settings.leave_status_notification_template = _("Leave Status Notification")
|
hr_settings.leave_status_notification_template = _("Leave Status Notification")
|
||||||
hr_settings.save()
|
hr_settings.save()
|
||||||
|
|
||||||
|
|
||||||
|
payroll_settings = frappe.get_doc("Payroll Settings")
|
||||||
|
payroll_settings.overtime_based_on = "Attendance"
|
||||||
|
payroll_settings.overtime_salary_component = _("Overtime Allowance")
|
||||||
|
payroll_settings.save()
|
||||||
|
|
||||||
def set_no_copy_fields_in_variant_settings():
|
def set_no_copy_fields_in_variant_settings():
|
||||||
# set no copy fields of an item doctype to item variant settings
|
# set no copy fields of an item doctype to item variant settings
|
||||||
doc = frappe.get_doc('Item Variant Settings')
|
doc = frappe.get_doc('Item Variant Settings')
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ def install(country=None):
|
|||||||
{'doctype': 'Salary Component', 'salary_component': _('Income Tax'), 'description': _('Income Tax'), 'type': 'Deduction', 'is_income_tax_component': 1},
|
{'doctype': 'Salary Component', 'salary_component': _('Income Tax'), 'description': _('Income Tax'), 'type': 'Deduction', 'is_income_tax_component': 1},
|
||||||
{'doctype': 'Salary Component', 'salary_component': _('Basic'), 'description': _('Basic'), 'type': 'Earning'},
|
{'doctype': 'Salary Component', 'salary_component': _('Basic'), 'description': _('Basic'), 'type': 'Earning'},
|
||||||
{'doctype': 'Salary Component', 'salary_component': _('Arrear'), 'description': _('Arrear'), 'type': 'Earning'},
|
{'doctype': 'Salary Component', 'salary_component': _('Arrear'), 'description': _('Arrear'), 'type': 'Earning'},
|
||||||
|
{'doctype': 'Salary Component', 'salary_component': _('Overtime Allowance'), 'description': _('Overtime Allowance'), 'type': 'Earning'},
|
||||||
{'doctype': 'Salary Component', 'salary_component': _('Leave Encashment'), 'description': _('Leave Encashment'), 'type': 'Earning'},
|
{'doctype': 'Salary Component', 'salary_component': _('Leave Encashment'), 'description': _('Leave Encashment'), 'type': 'Earning'},
|
||||||
|
|
||||||
|
|
||||||
@@ -286,6 +287,7 @@ def set_more_defaults():
|
|||||||
update_selling_defaults()
|
update_selling_defaults()
|
||||||
update_buying_defaults()
|
update_buying_defaults()
|
||||||
update_hr_defaults()
|
update_hr_defaults()
|
||||||
|
update_payroll_defaults()
|
||||||
add_uom_data()
|
add_uom_data()
|
||||||
update_item_variant_settings()
|
update_item_variant_settings()
|
||||||
|
|
||||||
@@ -315,6 +317,13 @@ def update_hr_defaults():
|
|||||||
hr_settings.leave_status_notification_template = _("Leave Status Notification")
|
hr_settings.leave_status_notification_template = _("Leave Status Notification")
|
||||||
hr_settings.save()
|
hr_settings.save()
|
||||||
|
|
||||||
|
def update_payroll_defaults():
|
||||||
|
payroll_settings = frappe.get_doc("Payroll Settings")
|
||||||
|
payroll_settings.overtime_based_on = "Attendance"
|
||||||
|
payroll_settings.overtime_salary_component = _("Overtime Allowance")
|
||||||
|
payroll_settings.save()
|
||||||
|
|
||||||
|
|
||||||
def update_item_variant_settings():
|
def update_item_variant_settings():
|
||||||
# set no copy fields of an item doctype to item variant settings
|
# set no copy fields of an item doctype to item variant settings
|
||||||
doc = frappe.get_doc('Item Variant Settings')
|
doc = frappe.get_doc('Item Variant Settings')
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ class StockLedgerEntry(Document):
|
|||||||
"sum(actual_qty)") or 0
|
"sum(actual_qty)") or 0
|
||||||
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
|
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
|
||||||
|
|
||||||
#check for item quantity available in stock
|
|
||||||
def actual_amt_check(self):
|
def actual_amt_check(self):
|
||||||
|
"""Validate that qty at warehouse for selected batch is >=0"""
|
||||||
if self.batch_no and not self.get("allow_negative_stock"):
|
if self.batch_no and not self.get("allow_negative_stock"):
|
||||||
batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
|
batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
|
||||||
from `tabStock Ledger Entry`
|
from `tabStock Ledger Entry`
|
||||||
@@ -107,7 +107,7 @@ class StockLedgerEntry(Document):
|
|||||||
self.stock_uom = item_det.stock_uom
|
self.stock_uom = item_det.stock_uom
|
||||||
|
|
||||||
def check_stock_frozen_date(self):
|
def check_stock_frozen_date(self):
|
||||||
stock_settings = frappe.get_doc('Stock Settings', 'Stock Settings')
|
stock_settings = frappe.get_cached_doc('Stock Settings')
|
||||||
|
|
||||||
if stock_settings.stock_frozen_upto:
|
if stock_settings.stock_frozen_upto:
|
||||||
if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto)
|
if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto)
|
||||||
|
|||||||
@@ -271,15 +271,13 @@ class update_entries_after(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.data.setdefault(args.warehouse, frappe._dict())
|
|
||||||
warehouse_dict = self.data[args.warehouse]
|
|
||||||
previous_sle = get_previous_sle_of_current_voucher(args)
|
previous_sle = get_previous_sle_of_current_voucher(args)
|
||||||
warehouse_dict.previous_sle = previous_sle
|
|
||||||
|
|
||||||
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
|
self.data[args.warehouse] = frappe._dict({
|
||||||
setattr(warehouse_dict, key, flt(previous_sle.get(key)))
|
"previous_sle": previous_sle,
|
||||||
|
"qty_after_transaction": flt(previous_sle.qty_after_transaction),
|
||||||
warehouse_dict.update({
|
"valuation_rate": flt(previous_sle.valuation_rate),
|
||||||
|
"stock_value": flt(previous_sle.stock_value),
|
||||||
"prev_stock_value": previous_sle.stock_value or 0.0,
|
"prev_stock_value": previous_sle.stock_value or 0.0,
|
||||||
"stock_queue": json.loads(previous_sle.stock_queue or "[]"),
|
"stock_queue": json.loads(previous_sle.stock_queue or "[]"),
|
||||||
"stock_value_difference": 0.0
|
"stock_value_difference": 0.0
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ def get_avg_purchase_rate(serial_nos):
|
|||||||
|
|
||||||
def get_valuation_method(item_code):
|
def get_valuation_method(item_code):
|
||||||
"""get valuation method from item or default"""
|
"""get valuation method from item or default"""
|
||||||
val_method = frappe.db.get_value('Item', item_code, 'valuation_method')
|
val_method = frappe.db.get_value('Item', item_code, 'valuation_method', cache=True)
|
||||||
if not val_method:
|
if not val_method:
|
||||||
val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO"
|
val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO"
|
||||||
return val_method
|
return val_method
|
||||||
@@ -275,17 +275,17 @@ def get_valid_serial_nos(sr_nos, qty=0, item_code=''):
|
|||||||
return valid_serial_nos
|
return valid_serial_nos
|
||||||
|
|
||||||
def validate_warehouse_company(warehouse, company):
|
def validate_warehouse_company(warehouse, company):
|
||||||
warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company")
|
warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company", cache=True)
|
||||||
if warehouse_company and warehouse_company != company:
|
if warehouse_company and warehouse_company != company:
|
||||||
frappe.throw(_("Warehouse {0} does not belong to company {1}").format(warehouse, company),
|
frappe.throw(_("Warehouse {0} does not belong to company {1}").format(warehouse, company),
|
||||||
InvalidWarehouseCompany)
|
InvalidWarehouseCompany)
|
||||||
|
|
||||||
def is_group_warehouse(warehouse):
|
def is_group_warehouse(warehouse):
|
||||||
if frappe.db.get_value("Warehouse", warehouse, "is_group"):
|
if frappe.db.get_value("Warehouse", warehouse, "is_group", cache=True):
|
||||||
frappe.throw(_("Group node warehouse is not allowed to select for transactions"))
|
frappe.throw(_("Group node warehouse is not allowed to select for transactions"))
|
||||||
|
|
||||||
def validate_disabled_warehouse(warehouse):
|
def validate_disabled_warehouse(warehouse):
|
||||||
if frappe.db.get_value("Warehouse", warehouse, "disabled"):
|
if frappe.db.get_value("Warehouse", warehouse, "disabled", cache=True):
|
||||||
frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse)))
|
frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse)))
|
||||||
|
|
||||||
def update_included_uom_in_report(columns, result, include_uom, conversion_factors):
|
def update_included_uom_in_report(columns, result, include_uom, conversion_factors):
|
||||||
|
|||||||
Reference in New Issue
Block a user