Calculation of Late Entries and Deduction of Salary

Hi Everyone,
How we can calculate the total no of late entries for each employee when creating Attendance? Basically I want to add a field in Attendance Doctype where when the user tick the checkbox of Late Entry that field show the user how many times this employee got late in the month, and with the use of this field I want to deduct some amount from the employee salary by applying condition in salary component like:
IF employee’s late_entry >= 2
Total salary = Basic - 100
How can I do this? Need help

@Past_Papers This is merely a suggestion.

Do you use the Shift Type doctype?

If yes, then it has a start_time field and based on that, you can check if the attendance is late or not by comparing the current time with the start_time using a Client Script on the attendance form that changes the value of the hidden late check field accordingly.

You will also have to modify the Attendance doctype and make the shift field required.

Also you have to modify the Salary Slip doctype and add a custom read_only field called late_days and with another Client Script on the salary slip form, you can count the late attendance based on the start and end date of the salary slip.

Finally you can use the custom field late_days from the salary slip in the formula field of the Salary Component.

Thanks for the reply…
Yes I have already tried shift type but not used any client script…

Client Script on the attendance form that changes the value of the hidden late check field accordingly.
How can I do this? can you please share code…

@Past_Papers Assuming that the fieldname of the hidden late field is is_late and it is of type Check, then the code will be:

frappe.ui.form.on('Attendance', {
    shift: function(frm) {
        if (frm.doc.shift) {
            frappe.db.get_value('Shift Type', frm.doc.shift, 'start_time')
            .then(r => {
                let start_time = r.message.start_time,
                start_dt = new Date(frappe.datetime.now_date() + ' ' + start_time),
                now_dt = new Date(),
                allowed_late_minutes = 10;
                if (allowed_late_minutes) {
                    start_dt.setMinutes(start_dt.getMinutes() + allowed_late_minutes);
                }
                frm.set_value('is_late', now_dt > start_dt ? 1 : 0);
            });
        }
    }
});

The code will immediately get triggered when the employee selects a shift.

For the allowed_late_minutes variable in the code, put the number of minutes an employee is allowed to be late (I set it for 10 minutes). This way if the time of the attendance is greater than the start_time of the shift + 10 minutes allowed, then the employee will be considered late.

Remember, this will only work if the shift field in the Attendance doctype is set as mandatory.

@Past_Papers Instead of creating a new is_late field in the Attendance doctype, you can modify the doctype and set the fields:

  1. Late Entry/late_entry as read only
  2. Shift/shift as mandatory

For the allowed_late_minutes, you should use the Late Entry Grace Period/late_entry_grace_period from the Shift Type doctype

Then use the following code instead:

frappe.ui.form.on('Attendance', {
    shift: function(frm) {
        if (frm.doc.shift) {
            frappe.db.get_value('Shift Type', frm.doc.shift, ['start_time', 'enable_entry_grace_period', 'late_entry_grace_period'])
            .then(r => {
                let data = r.message,
                start_dt = new Date(frappe.datetime.now_date() + ' ' + data.start_time),
                now_dt = new Date();
                if (data.enable_entry_grace_period && data.late_entry_grace_period) {
                    start_dt.setMinutes(start_dt.getMinutes() + data.late_entry_grace_period);
                }
                frm.set_value('late_entry', now_dt > start_dt ? 1 : 0);
            });
        } else {
            frm.set_value('late_entry', 0);
        }
    }
});

After running this code there is an issue system set late entry automatically for each day in a shift for the selected employee.

@Past_Papers That is weird. Well try the following code, it will show messages in the console of your browser. Post a screenshot of the console so I can see what is wrong.

frappe.ui.form.on('Attendance', {
    shift: function(frm) {
        if (frm.doc.shift) {
            frappe.db.get_value('Shift Type', frm.doc.shift, ['start_time', 'enable_entry_grace_period', 'late_entry_grace_period'])
            .then(r => {
                let data = r.message,
                start_dt = new Date(frappe.datetime.now_date() + ' ' + data.start_time),
                now_dt = new Date();
                console.log('Shift start time: ' + data.start_time);
                console.log('Shift start date & time: ' + moment(start_dt).format(frappe.defaultDatetimeFormat));
                console.log('Date & time now: ' + moment(now_dt).format(frappe.defaultDatetimeFormat));
                if (data.enable_entry_grace_period && data.late_entry_grace_period) {
                    start_dt.setMinutes(start_dt.getMinutes() + data.late_entry_grace_period);
                    console.log('Shift\'s allowed late period: ' + data.late_entry_grace_period);
                    console.log('Late shift date & time: ' +  moment(start_dt).format(frappe.defaultDatetimeFormat));
                }
                frm.set_value('late_entry', now_dt > start_dt ? 1 : 0);
            });
        } else {
            frm.set_value('late_entry', 0);
        }
    }
});

@Past_Papers In case you didn’t know how the code works, let me explain.

  1. The code gets executed when you select a Shift.
  2. From the selected shift, it gets Start Time, Enable Entry Grace Period and Late Entry Grace Period.
  3. It converts the shift start time to a date object by adding today’s date and shift start time.
  4. It gets the date object of the current time of the system (The date & time when the attendance is getting created)
  5. If the grace period is enabled, it adds the late entry grace period (as minutes) to the shift start date object
  6. It checks if the current date object with the shift start date object. If the current is greater than the shift, the employee is considered late.

Example

  • Shift start time: 08:00:00
  • Shift start date object: 2022-08-11 08:00:00
  • Shift late entry grace period: 10 (minutes)
  • Current date object: 2022-08-11 08:20:00
  • Shift start date object + late entry grace period: 2022-08-11 08:10:00
  • Comparison: 2022-08-11 08:20:00 > 2022-08-11 08:10:00 = true means the employee is late
  • If current date object is 2022-08-11 08:09:00, then 2022-08-11 08:09:00 > 2022-08-11 08:10:00 = false means the employee is not late

Thanks for your guidance… I got your point and the code works fine… So, now how can I get total number of late days? please help.

@Past_Papers Assuming that the fieldname of No of Late Days is number_of_late_days, then the overall code that you need is:

If number_of_late_days isn’t the fieldname, please change it in the code.

frappe.ui.form.on('Attendance', {
    refresh: function(frm) {
        frm.trigger('update_total_late_days');
    },
    employee: function(frm) {
        frm.trigger('update_total_late_days');
    },
    attendance_date: function(frm) {
        frm.trigger('update_total_late_days');
    },
    shift: function(frm) {
        if (frm.doc.shift) {
            frappe.db.get_value('Shift Type', frm.doc.shift, ['start_time', 'enable_entry_grace_period', 'late_entry_grace_period'])
            .then(r => {
                let data = r.message,
                start_dt = moment(data.start_time, frappe.defaultTimeFormat),
                now_dt = moment();
                if (data.enable_entry_grace_period && data.late_entry_grace_period) {
                    start_dt.add(cint(data.late_entry_grace_period), 'minutes');
                }
                frm.set_value('late_entry', now_dt.diff(start_dt, 'minutes') > 0 ? 1 : 0);
            });
        } else {
            frm.set_value('late_entry', 0);
        }
    },
    update_total_late_days: function(frm) {
        if (!frm.doc.employee || frm.doc.attendance_date) {
            frm.set_value('number_of_late_days', 0);
            return;
        }
        let date = moment(frm.doc.attendance_date, frappe.defaultDateFormat);
        frappe.db.count('Attendance', {
            employee: frm.doc.employee,
            late_entry: 1,
            attendance_date: ['between', [
                date.startOf("month").format(frappe.defaultDateFormat),
                date.endOf("month").format(frappe.defaultDateFormat)
            ]]
        }).then(count => {
            frm.set_value('number_of_late_days', cint(count));
        });
    }
});

How it works

No. of Late Days

  • Before selecting an employee or if the attendance date is empty, the no. of late days will be set to 0.
  • When selecting an employee or changing the attendance date, the no. of late days will be uploaded.

Late Entry

Date methods updated, so please check if it’s working again

**Date methods updated, so please check if it’s working again**

Thanks… Date methods are working fine no issue… but after selecting an employee or changing the attendance date number of days still shows 0.

See it didn’t update the number_of_late_days field

@Past_Papers Sorry, there was a missing !.
Here is the updated code.

frappe.ui.form.on('Attendance', {
    refresh: function(frm) {
        frm.trigger('update_total_late_days');
    },
    employee: function(frm) {
        frm.trigger('update_total_late_days');
    },
    attendance_date: function(frm) {
        frm.trigger('update_total_late_days');
    },
    shift: function(frm) {
        if (frm.doc.shift) {
            frappe.db.get_value('Shift Type', frm.doc.shift, ['start_time', 'enable_entry_grace_period', 'late_entry_grace_period'])
            .then(r => {
                let data = r.message,
                start_dt = moment(data.start_time, frappe.defaultTimeFormat),
                now_dt = moment();
                if (data.enable_entry_grace_period && data.late_entry_grace_period) {
                    start_dt.add(cint(data.late_entry_grace_period), 'minutes');
                }
                frm.set_value('late_entry', now_dt.diff(start_dt, 'minutes') > 0 ? 1 : 0);
            });
        } else {
            frm.set_value('late_entry', 0);
        }
    },
    update_total_late_days: function(frm) {
        frm.set_value('number_of_late_days', 0);
        if (!frm.doc.employee || !frm.doc.attendance_date) return;
        let date = moment(frm.doc.attendance_date, frappe.defaultDateFormat);
        frappe.db.count('Attendance', {
            employee: frm.doc.employee,
            late_entry: 1,
            attendance_date: ['between', [
                date.startOf("month").format(frappe.defaultDateFormat),
                date.endOf("month").format(frappe.defaultDateFormat)
            ]]
        }).then(count => {
            frm.set_value('number_of_late_days', cint(count));
        });
    }
});

Thanks… I tried this code but there are two issues…

  1. System count total number of late days but it is not it didn’t show the total number of late days for the selected employee in a selected month, it shows the whole total number of late days recorded in the system for all employees.
  2. After this code system not allow me to submit the attendance it only saves the document.

See in the picture below:

It shows 166 in number of late days which is not correct for this employee… and also not allow to submit

@Past_Papers The total late days error was because of the frappe.db.count, it doesn’t filter properly and for being unable to submit, it’s because the form was modified after save and not marked as dirty.

I didn’t think about the form being submitable, so the fault is mine I’m sorry.

The following code should be working without any problem.

frappe.ui.form.on('Attendance', {
    refresh: function(frm) {
        frm.trigger('update_total_late_days');
    },
    employee: function(frm) {
        frm.trigger('update_total_late_days');
    },
    attendance_date: function(frm) {
        frm.trigger('update_total_late_days');
    },
    shift: function(frm) {
        if (!frm.is_new() && !frm.is_dirty()) return;
        if (frm.doc.shift) {
            frappe.db.get_value('Shift Type', frm.doc.shift, ['start_time', 'enable_entry_grace_period', 'late_entry_grace_period'])
            .then(r => {
                let data = r.message,
                start_dt = moment(data.start_time, frappe.defaultTimeFormat),
                now_dt = moment();
                if (data.enable_entry_grace_period && data.late_entry_grace_period) {
                    start_dt.add(cint(data.late_entry_grace_period), 'minutes');
                }
                frm.set_value('late_entry', now_dt.diff(start_dt, 'minutes') > 0 ? 1 : 0);
            });
        } else {
            frm.set_value('late_entry', 0);
        }
    },
    update_total_late_days: function(frm) {
        if (!frm.is_new() && !frm.is_dirty()) return;
        if (!frm.doc.employee || !frm.doc.attendance_date) {
            if (cint(frm.doc.number_of_late_days)) {
                frm.set_value('number_of_late_days', 0);
                if (!frm.is_new()) {
                    frm.dirty();
                    frm.save();
                }
            }
            return;
        }
            
        let date = moment(frm.doc.attendance_date, frappe.defaultDateFormat);
        frappe.db.get_list('Attendance', {
            fields: ['name'],
            filters: {
                employee: frm.doc.employee,
                late_entry: 1,
                attendance_date: ['between', [
                    date.startOf("month").format(frappe.defaultDateFormat),
                    date.endOf("month").format(frappe.defaultDateFormat)
                ]]
            }
        }).then(ret => {
            let count = ret.length, late_days = cint(frm.doc.number_of_late_days);
            if (count === late_days) return;
            frm.set_value('number_of_late_days', count);
            if (!frm.is_new()) {
                frm.dirty();
                frm.save();
            }
        });
    }
});

Thank you very much… This code works great!
One more question… Now finally I want to deduct 100 Rupees from employee salary like this:
After 2 late entries every late entry deduct 100 rupees from the basic salary…
Could you please share guidance on how can I do this?

Also you have to modify the Salary Slipdoctype and add a customread_onlyfield calledlate_daysand with anotherClient Script on the salary slip form, you can count the late attendance based on the start and end date of the salary slip.

How can I count late attendance on salary slip form so I can deduct Amount from salary using formula in Salary Component

@Past_Papers It’s great to know that it worked great for you. Regarding the deduction, I will check something in the hrm and get back to you.

@Past_Papers Regarding the deduction for late attendance, I couldn’t find a direct way to do that.

The Salary Slip only check Attendance for leaves. The deduction can be dded manually using a Client Script and calling add_child method but I’m not sure if it will work.

The deduction can be added manually using a Client Script and calling add_child method

Can you please share the script ?