diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 0a81fe5eec6..2e734f2aa30 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -5,7 +5,16 @@ import frappe from frappe import _ 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 @@ -106,8 +115,6 @@ class Attendance(Document): frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee)) def unlink_attendance_from_checkins(self): - from frappe.utils import get_link_to_form - EmployeeCheckin = frappe.qb.DocType("Employee Checkin") linked_logs = ( frappe.qb.from_(EmployeeCheckin) @@ -221,75 +228,39 @@ def mark_bulk_attendance(data): 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, exclude_holidays=0): - import calendar - - month_map = get_month_map() - today = get_datetime() - +def get_unmarked_days(employee, from_date, to_date, exclude_holidays=0): joining_date, relieving_date = frappe.get_cached_value( "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]: - start_day = joining_date.day - - 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] + from_date = max(getdate(from_date), joining_date or getdate(from_date)) + to_date = min(getdate(to_date), relieving_date or getdate(to_date)) records = frappe.get_all( "Attendance", fields=["attendance_date", "employee"], filters=[ - ["attendance_date", ">=", month_start], - ["attendance_date", "<=", month_end], + ["attendance_date", ">=", from_date], + ["attendance_date", "<=", to_date], ["employee", "=", employee], ["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): - holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end) - holidays = [get_datetime(record) for record in holiday_dates] + holiday_dates = get_holiday_dates_for_employee(employee, from_date, to_date) + holidays = [getdate(record) for record in holiday_dates] marked_days.extend(holidays) 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) + while from_date <= to_date: + if from_date not in marked_days: + unmarked_days.append(from_date) + + from_date = add_days(from_date, 1) return unmarked_days diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 7d69a83e35b..bf9bc690b1b 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -1,5 +1,6 @@ -frappe.listview_settings['Attendance'] = { +frappe.listview_settings["Attendance"] = { add_fields: ["status", "attendance_date"], + get_indicator: function (doc) { if (["Present", "Work From Home"].includes(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; - const months = moment.months(); - const curMonth = moment().format("MMMM"); - months.splice(months.indexOf(curMonth) + 1); - list_view.page.add_inner_button(__("Mark Attendance"), function() { + + list_view.page.add_inner_button(__("Mark Attendance"), function () { + let first_day_of_month = moment().startOf('month'); + + if (moment().toDate().getDate() === 1) { + first_day_of_month = first_day_of_month.subtract(1, "month"); + } + let dialog = new frappe.ui.Dialog({ title: __("Mark Attendance"), - fields: [{ - fieldname: 'employee', - label: __('For Employee'), - fieldtype: 'Link', - options: 'Employee', - get_query: () => { - return {query: "erpnext.controllers.queries.employee_query"}; + fields: [ + { + fieldname: "employee", + label: __("For Employee"), + fieldtype: "Link", + options: "Employee", + get_query: () => { + return { + query: "erpnext.controllers.queries.employee_query", + }; + }, + reqd: 1, + onchange: () => me.reset_dialog(dialog), }, - reqd: 1, - onchange: function() { - dialog.set_df_property("unmarked_days", "hidden", 1); - dialog.set_df_property("status", "hidden", 1); - dialog.set_df_property("exclude_holidays", "hidden", 1); - dialog.set_df_property("month", "value", ''); - dialog.set_df_property("unmarked_days", "options", []); - dialog.no_unmarked_days_left = false; - } - }, - { - 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("exclude_holidays", "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: __("Status"), - fieldtype: "Select", - fieldname: "status", - options: ["Present", "Absent", "Half Day", "Work From Home"], - hidden: 1, - reqd: 1, - - }, - { - label: __("Exclude Holidays"), - fieldtype: "Check", - 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 - }], + { + fieldtype: "Section Break", + fieldname: "time_period_section", + hidden: 1, + }, + { + label: __("Start"), + fieldtype: "Date", + fieldname: "from_date", + reqd: 1, + default: first_day_of_month.toDate(), + onchange: () => me.get_unmarked_days(dialog), + }, + { + fieldtype: "Column Break", + fieldname: "time_period_column", + }, + { + label: __("End"), + fieldtype: "Date", + fieldname: "to_date", + reqd: 1, + default: moment().subtract(1, 'days').toDate(), + onchange: () => me.get_unmarked_days(dialog), + }, + { + fieldtype: "Section Break", + fieldname: "days_section", + hidden: 1, + }, + { + label: __("Status"), + fieldtype: "Select", + fieldname: "status", + options: ["Present", "Absent", "Half Day", "Work From Home"], + reqd: 1, + }, + { + label: __("Exclude Holidays"), + fieldtype: "Check", + fieldname: "exclude_holidays", + onchange: () => me.get_unmarked_days(dialog), + }, + { + label: __("Unmarked Attendance for days"), + fieldname: "unmarked_days", + fieldtype: "MultiCheck", + options: [], + columns: 2, + }, + ], primary_action(data) { if (cur_dialog.no_unmarked_days_left) { - frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}", - [dialog.fields_dict.month.value, dialog.fields_dict.employee.value])); + frappe.msgprint( + __( + "Attendance from {0} to {1} has already been marked for the Employee {2}", + [data.from_date, data.to_date, data.employee] + ) + ); } else { - frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status, data.month]), () => { - 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(); - } - } - }); - }); + frappe.confirm( + __("Mark attendance as {0} for {1} on selected dates?", [ + data.status, + data.employee, + ]), + () => { + 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') - + primary_action_label: __("Mark Attendance"), }); dialog.show(); }); }, - get_multi_select_options: function(employee, month, exclude_holidays) { - return new Promise(resolve => { - frappe.call({ - method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days', - async: false, - args: { - employee: employee, - month: month, - exclude_holidays: exclude_holidays - } - }).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); - }); + reset_dialog: function (dialog) { + let fields = dialog.fields_dict; + + dialog.set_df_property( + "time_period_section", + "hidden", + fields.employee.value ? 0 : 1 + ); + + dialog.set_df_property("days_section", "hidden", 1); + dialog.set_df_property("unmarked_days", "options", []); + dialog.no_unmarked_days_left = false; + fields.exclude_holidays.value = false; + + fields.to_date.datepicker.update({ + maxDate: moment().subtract(1, 'days').toDate() }); - } + + 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; + }); + } + }, }; diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index b78f5c06206..88f4c387bde 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -6,6 +6,7 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import ( add_days, add_months, + get_first_day, get_last_day, get_year_ending, get_year_start, @@ -13,11 +14,7 @@ from frappe.utils import ( nowdate, ) -from erpnext.hr.doctype.attendance.attendance import ( - get_month_map, - get_unmarked_days, - mark_attendance, -) +from erpnext.hr.doctype.attendance.attendance import get_unmarked_days, mark_attendance from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.tests.test_utils import get_first_sunday @@ -28,7 +25,7 @@ class TestAttendance(FrappeTestCase): def setUp(self): 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()) 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) 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] # attendance already marked for the day @@ -81,9 +79,10 @@ class TestAttendance(FrappeTestCase): frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list) 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] # attendance already marked for the day @@ -110,9 +109,10 @@ class TestAttendance(FrappeTestCase): attendance_date = add_days(date, 2) 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] # attendance already marked for the day @@ -124,10 +124,3 @@ class TestAttendance(FrappeTestCase): def tearDown(self): 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 diff --git a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py index 91da08eee50..84c66a7bf3f 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/test_monthly_attendance_sheet.py @@ -1,7 +1,7 @@ import frappe from dateutil.relativedelta import relativedelta 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.employee.test_employee import make_employee @@ -14,9 +14,7 @@ class TestMonthlyAttendanceSheet(FrappeTestCase): frappe.db.delete("Attendance", {"employee": self.employee}) def test_monthly_attendance_sheet_report(self): - now = now_datetime() - previous_month = now.month - 1 - previous_month_first = now.replace(day=1).replace(month=previous_month).date() + previous_month_first = add_months(getdate(), -1).replace(day=1) company = frappe.db.get_value("Employee", self.employee, "company") @@ -27,8 +25,8 @@ class TestMonthlyAttendanceSheet(FrappeTestCase): filters = frappe._dict( { - "month": previous_month, - "year": now.year, + "month": previous_month_first.month, + "year": previous_month_first.year, "company": company, } )