mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-02 19:59:12 +00:00
feat: Mark Unmarked Attendance (#20062)
* feat: Mark Unmarked Attendance * Update shift_type.py * Update attendance_list.js * Update attendance.py * Update attendance.py Co-authored-by: Nabin Hait <nabinhait@gmail.com>
This commit is contained in:
committed by
Nabin Hait
parent
b73b347697
commit
bd6e8b9cec
@@ -7,7 +7,8 @@ import frappe
|
|||||||
from frappe.utils import getdate, nowdate
|
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
|
from frappe.utils import cstr, get_datetime, get_datetime_str
|
||||||
|
from frappe.utils import update_progress_bar
|
||||||
|
|
||||||
class Attendance(Document):
|
class Attendance(Document):
|
||||||
def validate_duplicate_record(self):
|
def validate_duplicate_record(self):
|
||||||
@@ -89,17 +90,85 @@ def add_attendance(events, start, end, conditions=None):
|
|||||||
if e not in events:
|
if e not in events:
|
||||||
events.append(e)
|
events.append(e)
|
||||||
|
|
||||||
def mark_absent(employee, attendance_date, shift=None):
|
def mark_attendance(employee, attendance_date, status, shift=None):
|
||||||
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')}):
|
||||||
doc_dict = {
|
doc_dict = {
|
||||||
'doctype': 'Attendance',
|
'doctype': 'Attendance',
|
||||||
'employee': employee,
|
'employee': employee,
|
||||||
'attendance_date': attendance_date,
|
'attendance_date': attendance_date,
|
||||||
'status': 'Absent',
|
'status': status,
|
||||||
'company': employee_doc.company,
|
'company': employee_doc.company,
|
||||||
'shift': shift
|
'shift': shift
|
||||||
}
|
}
|
||||||
attendance = frappe.get_doc(doc_dict).insert()
|
attendance = frappe.get_doc(doc_dict).insert()
|
||||||
attendance.submit()
|
attendance.submit()
|
||||||
return attendance.name
|
return attendance.name
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def mark_bulk_attendance(data):
|
||||||
|
import json
|
||||||
|
from pprint import pprint
|
||||||
|
if isinstance(data, frappe.string_types):
|
||||||
|
data = json.loads(data)
|
||||||
|
data = frappe._dict(data)
|
||||||
|
company = frappe.get_value('Employee', data.employee, 'company')
|
||||||
|
for date in data.unmarked_days:
|
||||||
|
doc_dict = {
|
||||||
|
'doctype': 'Attendance',
|
||||||
|
'employee': data.employee,
|
||||||
|
'attendance_date': get_datetime(date),
|
||||||
|
'status': data.status,
|
||||||
|
'company': company,
|
||||||
|
}
|
||||||
|
attendance = frappe.get_doc(doc_dict).insert()
|
||||||
|
attendance.submit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_month_map():
|
||||||
|
return frappe._dict({
|
||||||
|
"January": 1,
|
||||||
|
"February": 2,
|
||||||
|
"March": 3,
|
||||||
|
"April": 4,
|
||||||
|
"May": 5,
|
||||||
|
"June": 6,
|
||||||
|
"July": 7,
|
||||||
|
"August": 8,
|
||||||
|
"September": 9,
|
||||||
|
"October": 10,
|
||||||
|
"November": 11,
|
||||||
|
"December": 12
|
||||||
|
})
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_unmarked_days(employee, month):
|
||||||
|
import calendar
|
||||||
|
month_map = get_month_map()
|
||||||
|
|
||||||
|
today = get_datetime()
|
||||||
|
|
||||||
|
dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)]
|
||||||
|
|
||||||
|
length = len(dates_of_month)
|
||||||
|
month_start, month_end = dates_of_month[0], dates_of_month[length-1]
|
||||||
|
|
||||||
|
|
||||||
|
records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [
|
||||||
|
["attendance_date", ">", month_start],
|
||||||
|
["attendance_date", "<", month_end],
|
||||||
|
["employee", "=", employee],
|
||||||
|
["docstatus", "!=", 2]
|
||||||
|
])
|
||||||
|
|
||||||
|
marked_days = [get_datetime(record.attendance_date) for record in records]
|
||||||
|
unmarked_days = []
|
||||||
|
|
||||||
|
for date in dates_of_month:
|
||||||
|
date_time = get_datetime(date)
|
||||||
|
if today.day == date_time.day and today.month == date_time.month:
|
||||||
|
break
|
||||||
|
if date_time not in marked_days:
|
||||||
|
unmarked_days.append(date)
|
||||||
|
|
||||||
|
return unmarked_days
|
||||||
|
|||||||
@@ -2,5 +2,105 @@ frappe.listview_settings['Attendance'] = {
|
|||||||
add_fields: ["status", "attendance_date"],
|
add_fields: ["status", "attendance_date"],
|
||||||
get_indicator: function(doc) {
|
get_indicator: function(doc) {
|
||||||
return [__(doc.status), doc.status=="Present" ? "green" : "darkgrey", "status,=," + doc.status];
|
return [__(doc.status), doc.status=="Present" ? "green" : "darkgrey", "status,=," + doc.status];
|
||||||
|
},
|
||||||
|
onload: function(list_view) {
|
||||||
|
let me = this;
|
||||||
|
const months = moment.months()
|
||||||
|
list_view.page.add_inner_button( __("Mark Attendance"), function(){
|
||||||
|
let dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Mark Attendance"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: 'employee',
|
||||||
|
label: __('For Employee'),
|
||||||
|
fieldtype: 'Link',
|
||||||
|
options: 'Employee',
|
||||||
|
reqd: 1,
|
||||||
|
onchange: function(){
|
||||||
|
dialog.set_df_property("unmarked_days", "hidden", 1);
|
||||||
|
dialog.set_df_property("status", "hidden", 1);
|
||||||
|
dialog.set_df_property("month", "value", '');
|
||||||
|
dialog.set_df_property("unmarked_days", "options", []);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("For Month"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
fieldname: "month",
|
||||||
|
options: months,
|
||||||
|
reqd: 1,
|
||||||
|
onchange: function(){
|
||||||
|
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
|
||||||
|
dialog.set_df_property("status", "hidden", 0);
|
||||||
|
dialog.set_df_property("unmarked_days", "options", []);
|
||||||
|
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
|
||||||
|
dialog.set_df_property("unmarked_days", "hidden", 0);
|
||||||
|
dialog.set_df_property("unmarked_days", "options", options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Status"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
fieldname: "status",
|
||||||
|
options: ["Present", "Absent", "Half Day"],
|
||||||
|
hidden:1,
|
||||||
|
reqd: 1,
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Unmarked Attendance for days"),
|
||||||
|
fieldname: "unmarked_days",
|
||||||
|
fieldtype: "MultiCheck",
|
||||||
|
options: [],
|
||||||
|
columns: 2,
|
||||||
|
hidden: 1
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_action(data){
|
||||||
|
frappe.confirm(__('Mark attendance as <b>' + data.status + '</b> for <b>' + data.month +'</b>' + ' on selected dates?'), () => {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
|
||||||
|
args: {
|
||||||
|
data : data
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if(r.message === 1) {
|
||||||
|
frappe.show_alert({message:__("Attendance Marked"), indicator:'blue'});
|
||||||
|
cur_dialog.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
dialog.hide();
|
||||||
|
list_view.refresh();
|
||||||
|
},
|
||||||
|
primary_action_label: __('Mark Attendance')
|
||||||
|
|
||||||
|
});
|
||||||
|
dialog.show();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get_multi_select_options: function(employee, month){
|
||||||
|
return new Promise(resolve => {
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days',
|
||||||
|
async: false,
|
||||||
|
args:{
|
||||||
|
employee: employee,
|
||||||
|
month: month,
|
||||||
|
}
|
||||||
|
}).then(r => {
|
||||||
|
var options = [];
|
||||||
|
for(var d in r.message){
|
||||||
|
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
|
||||||
|
var date = momentObj.format('DD-MM-YYYY');
|
||||||
|
options.push({ "label":date, "value": r.message[d] , "checked": 1});
|
||||||
|
}
|
||||||
|
resolve(options);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class TestAttendance(unittest.TestCase):
|
|||||||
employee = make_employee("test_mark_absent@example.com")
|
employee = make_employee("test_mark_absent@example.com")
|
||||||
date = nowdate()
|
date = nowdate()
|
||||||
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
|
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
|
||||||
from erpnext.hr.doctype.attendance.attendance import mark_absent
|
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||||
attendance = mark_absent(employee, date)
|
attendance = mark_attendance(employee, date, 'Absent')
|
||||||
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
|
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
|
||||||
self.assertEqual(attendance, fetch_attendance)
|
self.assertEqual(attendance, fetch_attendance)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ 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_absent
|
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
|
||||||
|
|
||||||
class ShiftType(Document):
|
class ShiftType(Document):
|
||||||
@@ -35,7 +35,7 @@ class ShiftType(Document):
|
|||||||
|
|
||||||
def get_attendance(self, logs):
|
def get_attendance(self, logs):
|
||||||
"""Return attendance_status, working_hours for a set of logs belonging to a single shift.
|
"""Return attendance_status, working_hours for a set of logs belonging to a single shift.
|
||||||
Assumtion:
|
Assumtion:
|
||||||
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
|
||||||
"""
|
"""
|
||||||
@@ -43,10 +43,10 @@ class ShiftType(Document):
|
|||||||
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
|
||||||
|
|
||||||
if cint(self.enable_exit_grace_period) and out_time and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)):
|
if cint(self.enable_exit_grace_period) and out_time and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)):
|
||||||
early_exit = True
|
early_exit = True
|
||||||
|
|
||||||
if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent:
|
if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent:
|
||||||
return 'Absent', total_working_hours, late_entry, early_exit
|
return 'Absent', total_working_hours, late_entry, early_exit
|
||||||
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:
|
||||||
@@ -75,7 +75,7 @@ class ShiftType(Document):
|
|||||||
for date in dates:
|
for date in dates:
|
||||||
shift_details = get_employee_shift(employee, date, True)
|
shift_details = get_employee_shift(employee, date, True)
|
||||||
if shift_details and shift_details.shift_type.name == self.name:
|
if shift_details and shift_details.shift_type.name == self.name:
|
||||||
mark_absent(employee, date, self.name)
|
mark_attendance(employee, date, self.name, 'Absent')
|
||||||
|
|
||||||
def get_assigned_employee(self, from_date=None, consider_default_shift=False):
|
def get_assigned_employee(self, from_date=None, consider_default_shift=False):
|
||||||
filters = {'date':('>=', from_date), 'shift_type': self.name, 'docstatus': '1'}
|
filters = {'date':('>=', from_date), 'shift_type': self.name, 'docstatus': '1'}
|
||||||
@@ -107,15 +107,15 @@ def get_filtered_date_list(employee, start_date, end_date, filter_attendance=Tru
|
|||||||
condition_query = ''
|
condition_query = ''
|
||||||
if filter_attendance:
|
if filter_attendance:
|
||||||
condition_query += """ and a.selected_date not in (
|
condition_query += """ and a.selected_date not in (
|
||||||
select attendance_date from `tabAttendance`
|
select attendance_date from `tabAttendance`
|
||||||
where docstatus = '1' and employee = %(employee)s
|
where docstatus = 1 and employee = %(employee)s
|
||||||
and attendance_date between %(start_date)s and %(end_date)s)"""
|
and attendance_date between %(start_date)s and %(end_date)s)"""
|
||||||
if holiday_list:
|
if holiday_list:
|
||||||
condition_query += """ and a.selected_date not in (
|
condition_query += """ and a.selected_date not in (
|
||||||
select holiday_date from `tabHoliday` where parenttype = 'Holiday List' and
|
select holiday_date from `tabHoliday` where parenttype = 'Holiday List' and
|
||||||
parentfield = 'holidays' and parent = %(holiday_list)s
|
parentfield = 'holidays' and parent = %(holiday_list)s
|
||||||
and holiday_date between %(start_date)s and %(end_date)s)"""
|
and holiday_date between %(start_date)s and %(end_date)s)"""
|
||||||
|
|
||||||
dates = frappe.db.sql("""select * from
|
dates = frappe.db.sql("""select * from
|
||||||
({base_dates_query}) as a
|
({base_dates_query}) as a
|
||||||
where a.selected_date <= %(end_date)s {condition_query}
|
where a.selected_date <= %(end_date)s {condition_query}
|
||||||
|
|||||||
Reference in New Issue
Block a user