Merge pull request #33510 from ruchamahabal/mark-attendance-period

This commit is contained in:
Rucha Mahabal
2023-01-03 16:47:31 +05:30
committed by GitHub
4 changed files with 206 additions and 215 deletions

View File

@@ -5,7 +5,16 @@
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, formatdate, get_datetime, getdate, nowdate from frappe.utils import (
add_days,
cint,
cstr,
formatdate,
get_datetime,
get_link_to_form,
getdate,
nowdate,
)
from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee
@@ -106,8 +115,6 @@ class Attendance(Document):
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 unlink_attendance_from_checkins(self): def unlink_attendance_from_checkins(self):
from frappe.utils import get_link_to_form
EmployeeCheckin = frappe.qb.DocType("Employee Checkin") EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
linked_logs = ( linked_logs = (
frappe.qb.from_(EmployeeCheckin) frappe.qb.from_(EmployeeCheckin)
@@ -221,75 +228,39 @@ def mark_bulk_attendance(data):
attendance.submit() 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() @frappe.whitelist()
def get_unmarked_days(employee, month, exclude_holidays=0): def get_unmarked_days(employee, from_date, to_date, exclude_holidays=0):
import calendar
month_map = get_month_map()
today = get_datetime()
joining_date, relieving_date = frappe.get_cached_value( joining_date, relieving_date = frappe.get_cached_value(
"Employee", employee, ["date_of_joining", "relieving_date"] "Employee", employee, ["date_of_joining", "relieving_date"]
) )
start_day = 1
end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
if joining_date and joining_date.year == today.year and joining_date.month == month_map[month]: from_date = max(getdate(from_date), joining_date or getdate(from_date))
start_day = joining_date.day to_date = min(getdate(to_date), relieving_date or getdate(to_date))
if (
relieving_date and relieving_date.year == today.year and relieving_date.month == month_map[month]
):
end_day = relieving_date.day + 1
dates_of_month = [
"{}-{}-{}".format(today.year, month_map[month], r) for r in range(start_day, end_day)
]
month_start, month_end = dates_of_month[0], dates_of_month[-1]
records = frappe.get_all( records = frappe.get_all(
"Attendance", "Attendance",
fields=["attendance_date", "employee"], fields=["attendance_date", "employee"],
filters=[ filters=[
["attendance_date", ">=", month_start], ["attendance_date", ">=", from_date],
["attendance_date", "<=", month_end], ["attendance_date", "<=", to_date],
["employee", "=", employee], ["employee", "=", employee],
["docstatus", "!=", 2], ["docstatus", "!=", 2],
], ],
) )
marked_days = [get_datetime(record.attendance_date) for record in records] marked_days = [getdate(record.attendance_date) for record in records]
if cint(exclude_holidays): if cint(exclude_holidays):
holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end) holiday_dates = get_holiday_dates_for_employee(employee, from_date, to_date)
holidays = [get_datetime(record) for record in holiday_dates] holidays = [getdate(record) for record in holiday_dates]
marked_days.extend(holidays) marked_days.extend(holidays)
unmarked_days = [] unmarked_days = []
for date in dates_of_month: while from_date <= to_date:
date_time = get_datetime(date) if from_date not in marked_days:
if today.day <= date_time.day and today.month <= date_time.month: unmarked_days.append(from_date)
break
if date_time not in marked_days: from_date = add_days(from_date, 1)
unmarked_days.append(date)
return unmarked_days return unmarked_days

View File

@@ -1,5 +1,6 @@
frappe.listview_settings['Attendance'] = { frappe.listview_settings["Attendance"] = {
add_fields: ["status", "attendance_date"], add_fields: ["status", "attendance_date"],
get_indicator: function (doc) { get_indicator: function (doc) {
if (["Present", "Work From Home"].includes(doc.status)) { if (["Present", "Work From Home"].includes(doc.status)) {
return [__(doc.status), "green", "status,=," + doc.status]; return [__(doc.status), "green", "status,=," + doc.status];
@@ -10,157 +11,185 @@ frappe.listview_settings['Attendance'] = {
} }
}, },
onload: function(list_view) { onload: function (list_view) {
let me = this; let me = this;
const months = moment.months();
const curMonth = moment().format("MMMM"); list_view.page.add_inner_button(__("Mark Attendance"), function () {
months.splice(months.indexOf(curMonth) + 1); let first_day_of_month = moment().startOf('month');
list_view.page.add_inner_button(__("Mark Attendance"), function() {
if (moment().toDate().getDate() === 1) {
first_day_of_month = first_day_of_month.subtract(1, "month");
}
let dialog = new frappe.ui.Dialog({ let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"), title: __("Mark Attendance"),
fields: [{ fields: [
fieldname: 'employee', {
label: __('For Employee'), fieldname: "employee",
fieldtype: 'Link', label: __("For Employee"),
options: 'Employee', fieldtype: "Link",
get_query: () => { options: "Employee",
return {query: "erpnext.controllers.queries.employee_query"}; get_query: () => {
return {
query: "erpnext.controllers.queries.employee_query",
};
},
reqd: 1,
onchange: () => me.reset_dialog(dialog),
}, },
reqd: 1, {
onchange: function() { fieldtype: "Section Break",
dialog.set_df_property("unmarked_days", "hidden", 1); fieldname: "time_period_section",
dialog.set_df_property("status", "hidden", 1); hidden: 1,
dialog.set_df_property("exclude_holidays", "hidden", 1); },
dialog.set_df_property("month", "value", ''); {
dialog.set_df_property("unmarked_days", "options", []); label: __("Start"),
dialog.no_unmarked_days_left = false; fieldtype: "Date",
} fieldname: "from_date",
}, reqd: 1,
{ default: first_day_of_month.toDate(),
label: __("For Month"), onchange: () => me.get_unmarked_days(dialog),
fieldtype: "Select", },
fieldname: "month", {
options: months, fieldtype: "Column Break",
reqd: 1, fieldname: "time_period_column",
onchange: function() { },
if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { {
dialog.set_df_property("status", "hidden", 0); label: __("End"),
dialog.set_df_property("exclude_holidays", "hidden", 0); fieldtype: "Date",
dialog.set_df_property("unmarked_days", "options", []); fieldname: "to_date",
dialog.no_unmarked_days_left = false; reqd: 1,
me.get_multi_select_options( default: moment().subtract(1, 'days').toDate(),
dialog.fields_dict.employee.value, onchange: () => me.get_unmarked_days(dialog),
dialog.fields_dict.month.value, },
dialog.fields_dict.exclude_holidays.get_value() {
).then(options => { fieldtype: "Section Break",
if (options.length > 0) { fieldname: "days_section",
dialog.set_df_property("unmarked_days", "hidden", 0); hidden: 1,
dialog.set_df_property("unmarked_days", "options", options); },
} else { {
dialog.no_unmarked_days_left = true; label: __("Status"),
} fieldtype: "Select",
}); fieldname: "status",
} options: ["Present", "Absent", "Half Day", "Work From Home"],
} reqd: 1,
}, },
{ {
label: __("Status"), label: __("Exclude Holidays"),
fieldtype: "Select", fieldtype: "Check",
fieldname: "status", fieldname: "exclude_holidays",
options: ["Present", "Absent", "Half Day", "Work From Home"], onchange: () => me.get_unmarked_days(dialog),
hidden: 1, },
reqd: 1, {
label: __("Unmarked Attendance for days"),
}, fieldname: "unmarked_days",
{ fieldtype: "MultiCheck",
label: __("Exclude Holidays"), options: [],
fieldtype: "Check", columns: 2,
fieldname: "exclude_holidays", },
hidden: 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", []);
dialog.no_unmarked_days_left = false;
me.get_multi_select_options(
dialog.fields_dict.employee.value,
dialog.fields_dict.month.value,
dialog.fields_dict.exclude_holidays.get_value()
).then(options => {
if (options.length > 0) {
dialog.set_df_property("unmarked_days", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", options);
} else {
dialog.no_unmarked_days_left = true;
}
});
}
}
},
{
label: __("Unmarked Attendance for days"),
fieldname: "unmarked_days",
fieldtype: "MultiCheck",
options: [],
columns: 2,
hidden: 1
}],
primary_action(data) { primary_action(data) {
if (cur_dialog.no_unmarked_days_left) { if (cur_dialog.no_unmarked_days_left) {
frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}", frappe.msgprint(
[dialog.fields_dict.month.value, dialog.fields_dict.employee.value])); __(
"Attendance from {0} to {1} has already been marked for the Employee {2}",
[data.from_date, data.to_date, data.employee]
)
);
} else { } else {
frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => { frappe.confirm(
frappe.call({ __("Mark attendance as {0} for {1} on selected dates?", [
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance", data.status,
args: { data.employee,
data: data ]),
}, () => {
callback: function (r) { frappe.call({
if (r.message === 1) { method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
frappe.show_alert({ args: {
message: __("Attendance Marked"), data: data,
indicator: 'blue' },
}); callback: function (r) {
cur_dialog.hide(); if (r.message === 1) {
} frappe.show_alert({
} message: __("Attendance Marked"),
}); indicator: "blue",
}); });
cur_dialog.hide();
}
},
});
}
);
} }
dialog.hide(); dialog.hide();
list_view.refresh(); list_view.refresh();
}, },
primary_action_label: __('Mark Attendance') primary_action_label: __("Mark Attendance"),
}); });
dialog.show(); dialog.show();
}); });
}, },
get_multi_select_options: function(employee, month, exclude_holidays) { reset_dialog: function (dialog) {
return new Promise(resolve => { let fields = dialog.fields_dict;
frappe.call({
method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days', dialog.set_df_property(
async: false, "time_period_section",
args: { "hidden",
employee: employee, fields.employee.value ? 0 : 1
month: month, );
exclude_holidays: exclude_holidays
} dialog.set_df_property("days_section", "hidden", 1);
}).then(r => { dialog.set_df_property("unmarked_days", "options", []);
var options = []; dialog.no_unmarked_days_left = false;
for (var d in r.message) { fields.exclude_holidays.value = false;
var momentObj = moment(r.message[d], 'YYYY-MM-DD');
var date = momentObj.format('DD-MM-YYYY'); fields.to_date.datepicker.update({
options.push({ maxDate: moment().subtract(1, 'days').toDate()
"label": date,
"value": r.message[d],
"checked": 1
});
}
resolve(options);
});
}); });
}
this.get_unmarked_days(dialog)
},
get_unmarked_days: function (dialog) {
let fields = dialog.fields_dict;
if (fields.employee.value && fields.from_date.value && fields.to_date.value) {
dialog.set_df_property("days_section", "hidden", 0);
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("exclude_holidays", "hidden", 0);
dialog.no_unmarked_days_left = false;
frappe
.call({
method: "erpnext.hr.doctype.attendance.attendance.get_unmarked_days",
async: false,
args: {
employee: fields.employee.value,
from_date: fields.from_date.value,
to_date: fields.to_date.value,
exclude_holidays: fields.exclude_holidays.value,
},
})
.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,
});
}
dialog.set_df_property(
"unmarked_days",
"options",
options.length > 0 ? options : []
);
dialog.no_unmarked_days_left = options.length === 0;
});
}
},
}; };

View File

@@ -6,6 +6,7 @@ from frappe.tests.utils import FrappeTestCase
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
add_months, add_months,
get_first_day,
get_last_day, get_last_day,
get_year_ending, get_year_ending,
get_year_start, get_year_start,
@@ -13,11 +14,7 @@ from frappe.utils import (
nowdate, nowdate,
) )
from erpnext.hr.doctype.attendance.attendance import ( from erpnext.hr.doctype.attendance.attendance import get_unmarked_days, mark_attendance
get_month_map,
get_unmarked_days,
mark_attendance,
)
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.tests.test_utils import get_first_sunday from erpnext.hr.tests.test_utils import get_first_sunday
@@ -28,7 +25,7 @@ class TestAttendance(FrappeTestCase):
def setUp(self): def setUp(self):
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
from_date = get_year_start(getdate()) from_date = get_year_start(add_months(getdate(), -1))
to_date = get_year_ending(getdate()) to_date = get_year_ending(getdate())
self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date) self.holiday_list = make_holiday_list(from_date=from_date, to_date=to_date)
@@ -55,9 +52,10 @@ class TestAttendance(FrappeTestCase):
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
mark_attendance(employee, attendance_date, "Present") mark_attendance(employee, attendance_date, "Present")
month_name = get_month_name(attendance_date)
unmarked_days = get_unmarked_days(employee, month_name) unmarked_days = get_unmarked_days(
employee, get_first_day(attendance_date), get_last_day(attendance_date)
)
unmarked_days = [getdate(date) for date in unmarked_days] unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day # attendance already marked for the day
@@ -81,9 +79,10 @@ class TestAttendance(FrappeTestCase):
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
mark_attendance(employee, attendance_date, "Present") mark_attendance(employee, attendance_date, "Present")
month_name = get_month_name(attendance_date)
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True) unmarked_days = unmarked_days = get_unmarked_days(
employee, get_first_day(attendance_date), get_last_day(attendance_date), exclude_holidays=True
)
unmarked_days = [getdate(date) for date in unmarked_days] unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day # attendance already marked for the day
@@ -110,9 +109,10 @@ class TestAttendance(FrappeTestCase):
attendance_date = add_days(date, 2) attendance_date = add_days(date, 2)
mark_attendance(employee, attendance_date, "Present") mark_attendance(employee, attendance_date, "Present")
month_name = get_month_name(attendance_date)
unmarked_days = get_unmarked_days(employee, month_name) unmarked_days = get_unmarked_days(
employee, get_first_day(attendance_date), get_last_day(attendance_date)
)
unmarked_days = [getdate(date) for date in unmarked_days] unmarked_days = [getdate(date) for date in unmarked_days]
# attendance already marked for the day # attendance already marked for the day
@@ -124,10 +124,3 @@ class TestAttendance(FrappeTestCase):
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
def get_month_name(date):
month_number = date.month
for month, number in get_month_map().items():
if number == month_number:
return month

View File

@@ -1,7 +1,7 @@
import frappe import frappe
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import now_datetime from frappe.utils import add_months, getdate
from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -14,9 +14,7 @@ class TestMonthlyAttendanceSheet(FrappeTestCase):
frappe.db.delete("Attendance", {"employee": self.employee}) frappe.db.delete("Attendance", {"employee": self.employee})
def test_monthly_attendance_sheet_report(self): def test_monthly_attendance_sheet_report(self):
now = now_datetime() previous_month_first = add_months(getdate(), -1).replace(day=1)
previous_month = now.month - 1
previous_month_first = now.replace(day=1).replace(month=previous_month).date()
company = frappe.db.get_value("Employee", self.employee, "company") company = frappe.db.get_value("Employee", self.employee, "company")
@@ -27,8 +25,8 @@ class TestMonthlyAttendanceSheet(FrappeTestCase):
filters = frappe._dict( filters = frappe._dict(
{ {
"month": previous_month, "month": previous_month_first.month,
"year": now.year, "year": previous_month_first.year,
"company": company, "company": company,
} }
) )