From ea6b13308e79a107be20fcf44621d6388a01099e Mon Sep 17 00:00:00 2001 From: waleedsaeed-bp Date: Tue, 12 Nov 2024 16:49:09 +0100 Subject: [PATCH 1/2] Add bp_emp_attendacne v2 module --- addons_bp/bp_emp_attendance/__init__.py | 6 + addons_bp/bp_emp_attendance/__manifest__.py | 35 ++ .../data/attendance_data.xml | 203 +++++++++++ .../post-update-attendance-overtime.py | 10 + .../bp_emp_attendance/models/__init__.py | 7 + .../models/attendance_overtime.py | 117 ++++++ .../models/employee_attendance.py | 335 ++++++++++++++++++ .../bp_emp_attendance/models/hr_employee.py | 189 ++++++++++ .../bp_emp_attendance/models/hr_leave.py | 31 ++ .../bp_emp_attendance/report/__init__.py | 4 + .../report/hr_custom_report_attendance.py | 70 ++++ .../hr_custom_report_attendance_views.xml | 110 ++++++ .../security/attendance_security.xml | 16 + .../security/ir.model.access.csv | 3 + .../static/description/icon.png | Bin 0 -> 5444 bytes addons_bp/bp_emp_attendance/tests/__init__.py | 4 + .../tests/test_hr_attendance_break_time.py | 162 +++++++++ .../views/hr_attendance_overtime_views.xml | 22 ++ .../views/hr_attendance_view.xml | 30 ++ .../bp_emp_attendance/views/hr_employee.xml | 72 ++++ .../views/hr_leave_views.xml | 19 + .../bp_emp_attendance/wizard/__init__.py | 4 + .../wizard/hr_attendance_extra_hours.py | 59 +++ .../hr_attendance_extra_hours_wizard.xml | 34 ++ 24 files changed, 1542 insertions(+) create mode 100644 addons_bp/bp_emp_attendance/__init__.py create mode 100644 addons_bp/bp_emp_attendance/__manifest__.py create mode 100644 addons_bp/bp_emp_attendance/data/attendance_data.xml create mode 100644 addons_bp/bp_emp_attendance/migrations/16.0.0.0.2/post-update-attendance-overtime.py create mode 100644 addons_bp/bp_emp_attendance/models/__init__.py create mode 100644 addons_bp/bp_emp_attendance/models/attendance_overtime.py create mode 100644 addons_bp/bp_emp_attendance/models/employee_attendance.py create mode 100644 addons_bp/bp_emp_attendance/models/hr_employee.py create mode 100644 addons_bp/bp_emp_attendance/models/hr_leave.py create mode 100644 addons_bp/bp_emp_attendance/report/__init__.py create mode 100644 addons_bp/bp_emp_attendance/report/hr_custom_report_attendance.py create mode 100644 addons_bp/bp_emp_attendance/report/hr_custom_report_attendance_views.xml create mode 100644 addons_bp/bp_emp_attendance/security/attendance_security.xml create mode 100644 addons_bp/bp_emp_attendance/security/ir.model.access.csv create mode 100644 addons_bp/bp_emp_attendance/static/description/icon.png create mode 100644 addons_bp/bp_emp_attendance/tests/__init__.py create mode 100644 addons_bp/bp_emp_attendance/tests/test_hr_attendance_break_time.py create mode 100644 addons_bp/bp_emp_attendance/views/hr_attendance_overtime_views.xml create mode 100644 addons_bp/bp_emp_attendance/views/hr_attendance_view.xml create mode 100644 addons_bp/bp_emp_attendance/views/hr_employee.xml create mode 100644 addons_bp/bp_emp_attendance/views/hr_leave_views.xml create mode 100644 addons_bp/bp_emp_attendance/wizard/__init__.py create mode 100644 addons_bp/bp_emp_attendance/wizard/hr_attendance_extra_hours.py create mode 100644 addons_bp/bp_emp_attendance/wizard/hr_attendance_extra_hours_wizard.xml diff --git a/addons_bp/bp_emp_attendance/__init__.py b/addons_bp/bp_emp_attendance/__init__.py new file mode 100644 index 0000000..57568e3 --- /dev/null +++ b/addons_bp/bp_emp_attendance/__init__.py @@ -0,0 +1,6 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizard +from . import report diff --git a/addons_bp/bp_emp_attendance/__manifest__.py b/addons_bp/bp_emp_attendance/__manifest__.py new file mode 100644 index 0000000..6d1037a --- /dev/null +++ b/addons_bp/bp_emp_attendance/__manifest__.py @@ -0,0 +1,35 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Employee Attendance & Extra Hours", + "sequence": 20, + "version": "1.0.0", + "category": "Human Resources/Attendances", + "author": "bloopark systems GmbH & Co. KG", + "website": "http://www.bloopark.de", + "license": "LGPL-3", + "depends": [ + "hr_attendance", + "hr_holidays_attendance", + ], + "installable": True, + "auto_install": False, + "application": False, + "data": [ + # security files + "security/ir.model.access.csv", + "security/attendance_security.xml", + # Wizard files + "wizard/hr_attendance_extra_hours_wizard.xml", + # Views files + "views/hr_attendance_view.xml", + "views/hr_employee.xml", + "views/hr_leave_views.xml", + "views/hr_attendance_overtime_views.xml", + # Data files + "data/attendance_data.xml", + # Report files + "report/hr_custom_report_attendance_views.xml", + ], + "demo": [], +} diff --git a/addons_bp/bp_emp_attendance/data/attendance_data.xml b/addons_bp/bp_emp_attendance/data/attendance_data.xml new file mode 100644 index 0000000..7aa175c --- /dev/null +++ b/addons_bp/bp_emp_attendance/data/attendance_data.xml @@ -0,0 +1,203 @@ + + + + + Attendance: notify employee/manager missing day + + code + model.cron_missing_attendance() + + 1 + days + -1 + + + + + Manager attendance: Daily attendance check + + Reminder to log employee manager timesheet + {{ (object.user_id.company_id.partner_id.email_formatted or user.email_formatted) }} + {{ object.parent_id.work_email }} + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + +
+ Manager timesheet reminder + + +
+
+
+
+ + + + +
+
+ Hi + + , +
+
+ I'd like to inform you that" + + + + ", +
+ who's working in( + + + + ) department, didn't log her/his attendance for today. +
+
+ Thanks. +
+
+
+
+ +
+ +
+
+
+ +
+ + + Employee attendance: Daily attendance check + + Reminder to log employee timesheet + {{ (object.user_id.company_id.partner_id.email_formatted or user.email_formatted) }} + {{ object.work_email }} + + + + + +
+ + + + + + + + + + + + + + + +
+ + + + + + + + +
+ Employee timesheet reminder + + +
+
+
+
+ + + + +
+
+ Hi + + , +
+
+ I'd like to inform you that, you didn't log your attendance for today. +
+ Please contact + ! +
+
+ Thanks. +
+
+
+
+ +
+ +
+
+
+ +
+ + + Update Previous Attendance + + + code + action = records._update_previous_attendance_action() + + +
+
diff --git a/addons_bp/bp_emp_attendance/migrations/16.0.0.0.2/post-update-attendance-overtime.py b/addons_bp/bp_emp_attendance/migrations/16.0.0.0.2/post-update-attendance-overtime.py new file mode 100644 index 0000000..0186bf1 --- /dev/null +++ b/addons_bp/bp_emp_attendance/migrations/16.0.0.0.2/post-update-attendance-overtime.py @@ -0,0 +1,10 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import SUPERUSER_ID, api + + +def migrate(cr, version): + env = api.Environment(cr, SUPERUSER_ID, {}) + overtimes = env["hr.attendance.overtime"].search([]) + overtimes._compute_overtime_data() + overtimes.action_update_day_overtime() diff --git a/addons_bp/bp_emp_attendance/models/__init__.py b/addons_bp/bp_emp_attendance/models/__init__.py new file mode 100644 index 0000000..257bfe9 --- /dev/null +++ b/addons_bp/bp_emp_attendance/models/__init__.py @@ -0,0 +1,7 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import employee_attendance +from . import hr_employee +from . import attendance_overtime +from . import hr_leave diff --git a/addons_bp/bp_emp_attendance/models/attendance_overtime.py b/addons_bp/bp_emp_attendance/models/attendance_overtime.py new file mode 100644 index 0000000..2c3baee --- /dev/null +++ b/addons_bp/bp_emp_attendance/models/attendance_overtime.py @@ -0,0 +1,117 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import datetime + +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models + + +class HrAttendanceOvertime(models.Model): + _inherit = "hr.attendance.overtime" + + request_date = fields.Date( + compute="_compute_overtime_data", + store=True, + string='Day (Request)', + ) + leave_ids = fields.One2many( + "hr.leave", + "overtime_id", + readonly=True, + string="Leaves", + ) + worked_hours = fields.Float( + compute="_compute_overtime_data", + store=True, + ) + date = fields.Date(string='Day (Approved)') + + @api.depends("leave_ids", "adjustment", "date", "employee_id", "duration") + def _compute_overtime_data(self): + hr_att = self.env["hr.attendance"].sudo() + for record in self: + worked_hours = 0.0 + request_date = record.date or False + if record.adjustment and record.leave_ids: + request_date = record.leave_ids[0].request_date_from + date_end = request_date + relativedelta(days=1) + + # Day attendance hours + if not record.adjustment: + att_group = hr_att.read_group( + [ + ("employee_id", "=", record.employee_id.id), + ("check_in", ">=", request_date), + ("check_out", "<", date_end), + ], + groupby=["employee_id"], + fields=["employee_id", "worked_hours"], + lazy=False, + ) + day_attendance = sum(x.get("worked_hours") for x in att_group) + worked_hours = day_attendance + + record.request_date = request_date + record.worked_hours = worked_hours + + @api.model_create_multi + def create(self, vals): + res = super().create(vals) + for record in res: + record.action_update_day_overtime() + return res + + def write(self, vals): + res = super().write(vals) + my_list_set = { + "employee_id", + "date", + } + my_dict_set = set(vals) + if my_list_set.intersection(my_dict_set): + self.action_update_day_overtime() + return res + + def action_update_day_overtime(self): + hr_leaves = self.env["hr.leave"].sudo() + for record in self.sudo().filtered(lambda o: not o.adjustment): + date_from = record.date + employee = record.employee_id + resource_calendar = record.employee_id.resource_calendar_id + dt_from = datetime.combine(date_from, datetime.min.time()) + dt_to = datetime.combine(date_from, datetime.max.time()) + work_data = resource_calendar.get_work_duration_data( + from_datetime=dt_from, + to_datetime=dt_to, + compute_leaves=True, + ) + required_hours = work_data.get("hours") or 0.00 + day_attendance = ( + self.env["hr.attendance"] + .sudo() + .search( + [ + ("employee_id", "=", employee.id), + ("check_in", ">=", dt_from), + ("check_in", "<", dt_to), + ], + order="check_in asc", + ) + ) + logged_hours = sum(day_attendance.mapped("worked_hours")) + + # Check employee leaves ['Extra hours' or 'On Vacations'] + emp_leaves_ids = employee.check_employee_vacation( + date_from=dt_from, + date_to=dt_to, + ) + emp_leaves = hr_leaves.browse(emp_leaves_ids) + overtime_leaves = emp_leaves.filtered( + lambda l: l.holiday_status_id.overtime_deductible + ).mapped("overtime_id") + leave_hours_dur = sum(overtime_leaves.mapped("duration")) or 0.0 + if emp_leaves and not overtime_leaves: + leave_hours_dur = -1 * required_hours + + record.duration = logged_hours - required_hours - leave_hours_dur diff --git a/addons_bp/bp_emp_attendance/models/employee_attendance.py b/addons_bp/bp_emp_attendance/models/employee_attendance.py new file mode 100644 index 0000000..3742d7d --- /dev/null +++ b/addons_bp/bp_emp_attendance/models/employee_attendance.py @@ -0,0 +1,335 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import date, datetime, timedelta + +import pytz +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models +from odoo.tools import Markup + + + +class HrAttendance(models.Model): + _inherit = "hr.attendance" + + net_working_time = fields.Float(copy=False) + employee_break_time = fields.Float(copy=False, string='Deducted Time') + employee_extra_hours = fields.Float(copy=False) + worked_hours = fields.Float( + compute="_compute_de_worked_hours", + store=True, + ) + + def _update_overtime_hours(self, over_hours): + self.write({'worked_hours': over_hours}) + + @api.model_create_multi + def create(self, vals): + res = super().create(vals) + for record in res: + record.action_recompute_worked_hours() + record.action_update_overtime() + return res + + def write(self, vals): + for attendance in self: + if (attendance.check_in and vals.get("check_in")) or ( + attendance.check_out and vals.get("check_out") + ): + attendance.action_log_employee_message( + old_values=attendance.read()[0], + new_values=vals, + ) + + res = super().write(vals) + my_list_set = { + "employee_id", + "check_in", + "check_out", + "employee_extra_hours", + } + my_dict_set = set(vals) + if my_list_set.intersection(my_dict_set): + self.action_recompute_worked_hours() + self.action_update_overtime() + return res + + @api.depends( + "net_working_time", + "employee_break_time", + "employee_extra_hours", + ) + def _compute_de_worked_hours(self): + """This method implemented to replace odoo standard calculation. + + From (check out - check_in) + To → employee_extra_hours + (net_working_time - employee_break_time) + + and also to prevent override/edit odoo from another modules, + that's why we added new method name _compute_de_worked_hours + """ + for attendance in self: + net = attendance.net_working_time + break_time = attendance.employee_break_time + employee_extra_hours = attendance.employee_extra_hours + attendance.worked_hours = employee_extra_hours + (net - break_time) + + def action_recompute_worked_hours(self): + """Test break/net work time calculation is correctly computed If employee has + only 1 record check in/out in same day: case 1: working hours less than 6:00 h. + + → break_time should be (00:00) + case 2: working hours between than 6:00h ~ 6:30h + → break_time = (check_out - check_in) - 6 + case 3: working hours more than 6:30 h + → break_time = 00:30 hh:mm + + Else employee logged many record per day: + case 1: total day working hours less than 6:00 h + → break_time should be (00:00) + case 2: total day working hours between than 6:00h ~ 6:30h + → break_time = (sum_working_time - 6 - sum_break_time_before) + case 3: total day working hours more than 6:30 h + → break_time = 0.75 - sum_break_time_before + """ + for attendance in self: + check_in = attendance.check_in + check_out = attendance.check_out + employee_break_time = 0.0 + net_working_time = 0.0 + if (check_out and check_in) and (check_out > check_in): + tz = pytz.timezone(attendance.employee_id.tz or "UTC") + start_naive = check_in.astimezone(tz).replace( + hour=0, + minute=0, + second=0, + ) + end_naive = check_in.astimezone(tz).replace( + hour=23, + minute=59, + second=59, + ) + + # If employee logged his time in normal day + last_attendance = self.sudo().search( + [ + ("employee_id", "=", attendance.employee_id.id), + ("check_in", ">=", start_naive), + ("check_out", "<=", end_naive), + ("check_out", "<=", attendance.check_in), + ("id", "not in", attendance.ids), + ], + order="check_out desc", + limit=1, + ) + attendance_before = self.sudo().search( + [ + ("employee_id", "=", attendance.employee_id.id), + ("check_in", ">=", start_naive), + ("check_out", "<=", end_naive), + ("check_out", "<=", attendance.check_in), + ("id", "not in", attendance.ids), + ], + order="check_out desc", + ) + attendance_after = self.sudo().search( + [ + ("employee_id", "=", attendance.employee_id.id), + ("check_in", ">=", attendance.check_in), + ("check_out", "<=", end_naive), + ("id", "not in", attendance.ids), + ], + order="check_out desc", + limit=1, + ) + if attendance_after: + attendance_after.action_recompute_worked_hours() + + if not last_attendance: + net_working_time = self.get_diff_min( + check_in=check_in, + check_out=check_out, + ) + + if net_working_time > 9: + employee_break_time = 0.75 + + elif 6.5 < net_working_time <= 9: + employee_break_time = 0.50 + + elif 6 < net_working_time <= 6.5: + employee_break_time = net_working_time - 6 + + elif len(attendance_before) >= 1: + net_working_time = self.get_diff_min( + check_in=check_in, + check_out=check_out, + ) + sum_working_time = ( + sum(attendance_before.mapped("net_working_time")) + + net_working_time + ) + current = attendance + total_break_time = 0 + for record in attendance_before: + total_break_time += self.get_diff_min( + record.check_out, current.check_in + ) + current = record + + for record in attendance_before: + record.employee_break_time = 0 + + if sum_working_time > 9 and total_break_time < 0.75: + employee_break_time = 0.75 - total_break_time + elif 6.5 < sum_working_time <= 9.0 and total_break_time <= 0.5: + employee_break_time = 0.5 - total_break_time + + elif (6 <= sum_working_time <= 6.5) and ( + total_break_time < (sum_working_time - 6) + ): + employee_break_time = sum_working_time - 6 - total_break_time + elif sum_working_time > 9 and total_break_time > 0.75: + + net_working_time = net_working_time + employee_break_time = 0 + + attendance.net_working_time = net_working_time + attendance.employee_break_time = employee_break_time + + @api.model + def get_diff_min(self, check_in, check_out): + """This method implemented to calculate diff between and dates in minutes. + + :param check_in: check in datetime + :param check_out: check out datetime + :return: diff in minutes (check_out - check_in) + :rtype: float + """ + diff = check_out - check_in + diff_seconds = diff.total_seconds() + value = diff_seconds / 3600.0 + return value + + def action_update_overtime(self): + overtime = self.env["hr.attendance.overtime"].sudo() + for rec in self: + date_start = rec.check_in.date() + date_end = date_start + relativedelta(days=1) + day_overtime = overtime.search( + [ + ("employee_id", "=", rec.employee_id.id), + ("date", ">=", date_start), + ("date", "<", date_end), + ("adjustment", "=", False), + ] + ) + day_overtime.action_update_day_overtime() + if not day_overtime: + self.env["hr.attendance.overtime"].sudo().with_context( + { + 'worked_hours': rec.worked_hours, + } + ).create( + { + "employee_id": rec.employee_id.id, + "adjustment": False, + "date": date_start, + "duration": 0, + } + ) + + def action_log_employee_message(self, old_values, new_values): + employee_id = ( + new_values.get("employee_id") or old_values.get("employee_id")[0] + if old_values.get("employee_id") + else False + ) + + check_in = old_values.get("check_in") or new_values.get("check_in") + new_check_in = new_values.get("check_in") or False + + check_out = old_values.get("check_out") or new_values.get("check_out") + new_check_out = new_values.get("check_out") or False + + if employee_id and (new_check_in or new_check_out): + check_in = self.get_datetime_context_timestamp(check_in) + new_check_in = self.get_datetime_context_timestamp(new_check_in) + check_out = self.get_datetime_context_timestamp(check_out) + new_check_out = self.get_datetime_context_timestamp(new_check_out) + + employee = self.env["hr.employee"].sudo().browse(employee_id) + body = Markup (f"{self.env.user.name} changed Attendance Data:
") + if new_check_in != check_in and new_check_in: + body += Markup(f"
  • Check in : {check_in} → {new_check_in}
  • ") + if new_check_out != check_out and new_check_out: + body += Markup( + f"
  • Check out : {check_out} → {new_check_out}
  • " + ) + employee.message_post(body=body) + + def get_datetime_context_timestamp(self, data): + """ + :param data: datetime or sting datetime + :return: datetime with time stamp + """ + if not data: + return "" + datetime_data = fields.Datetime.from_string(str(data)) + return fields.Datetime.to_string( + fields.Datetime.context_timestamp(self, datetime_data) + ) + + @api.model + def action_create_missing_attendance(self, year=False): + if not year: + year = int(date.today().year) + date_from = date(year=int(year), month=1, day=1) + date_to = date.today() + range_date = range(int((date_to - date_from).days)) + dates_list = [date_from + timedelta(n) for n in range_date] + attendance_list = [] + + for ydate in dates_list: + employees = ( + self.env["hr.employee"] + .sudo() + .search( + [ + ("check_daily_attendance", "=", True), + ("first_contract_date", "<=", ydate), + ] + ) + ) + check_in = datetime.combine(ydate, datetime.min.time()) + check_out = datetime.combine(ydate, datetime.max.time()) + for emp in employees: + work_data = emp.resource_calendar_id.get_work_duration_data( + from_datetime=check_in, + to_datetime=check_out, + compute_leaves=True, + ) + leaves = emp.check_employee_vacation( + date_from=check_in, + date_to=check_out, + ) + + if work_data.get("hours") and not leaves: + attendance_day = self.sudo().search( + [ + ("employee_id", "=", emp.id), + ("check_in", ">=", check_in), + ("check_out", "<=", check_out), + ], + order="check_out desc", + ) + if not attendance_day: + attendance_list.append( + { + "employee_id": emp.id, + "check_in": check_in.replace(hour=4), + "check_out": check_in.replace(hour=4), + } + ) + self.sudo().create(attendance_list) diff --git a/addons_bp/bp_emp_attendance/models/hr_employee.py b/addons_bp/bp_emp_attendance/models/hr_employee.py new file mode 100644 index 0000000..f35af74 --- /dev/null +++ b/addons_bp/bp_emp_attendance/models/hr_employee.py @@ -0,0 +1,189 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from collections import namedtuple +from datetime import datetime, timedelta + +from odoo import _, api, fields, models + + +class HrEmployeePublic(models.Model): + _inherit = "hr.employee.public" + + is_employee_officer = fields.Boolean( + compute="_compute_is_employee_officer", + ) + check_daily_attendance = fields.Boolean( + related='employee_id.check_daily_attendance' + ) + send_missing_attendance_mail = fields.Boolean( + related='employee_id.send_missing_attendance_mail' + ) + + def hr_attendance_report_action(self): + return self.employee_id.hr_attendance_report_action() + + def _compute_is_employee_officer(self): + login_user = self.env.user + for rec in self.sudo(): + is_employee_officer = self.env.user.has_group( + "hr_attendance.group_hr_attendance_user" + ) + employee_user = rec.employee_id.user_id + + if not is_employee_officer and (login_user.id == employee_user.id): + is_employee_officer = True + + rec.is_employee_officer = is_employee_officer + + +class HrEmployee(models.Model): + _inherit = "hr.employee" + + check_daily_attendance = fields.Boolean() + send_missing_attendance_mail = fields.Boolean( + string="Send Daily Mail For Missing Attendance", + default=False, + ) + + def hr_attendance_report_action(self): + self.ensure_one() + return self.env["ir.actions.actions"]._for_xml_id( + "bp_emp_attendance.hr_custom_own_report_attendance_action" + ) + + def action_open_extra_hours_wizard(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Add/Deduct Employee Working Hours"), + "res_model": "hr.attendance.extra.hours", + "views": [(False, "form")], + "view_type": "form", + "view_mode": "form", + "target": "new", + "context": { + "default_employee_id": self.id, + }, + } + + @api.model + def cron_missing_attendance(self): + date_now = datetime.now() + dt_from = datetime.combine(date_now, datetime.min.time()) + dt_to = datetime.combine(date_now, datetime.max.time()) + attendance = self.env["hr.attendance"].sudo() + employees = self.sudo().search( + [ + ("parent_id", "!=", False), + ("check_daily_attendance", "=", True), + ] + ) + employee_attendance = attendance.search( + [ + ("check_in", ">=", dt_from), + ("check_out", "<=", dt_to), + ], + order="check_out desc", + ).mapped("employee_id") + employees_to_log = list(set(employees) ^ set(employee_attendance)) + attendance_list = [] + check_dt = date_now.replace( + hour=6, + minute=0, + second=0, + microsecond=0, + ) + manager_template = self.env.ref( + "bp_emp_attendance.email_template_manager_daily_attendance", + raise_if_not_found=False, + ) + employee_template = self.env.ref( + "bp_emp_attendance.email_template_employee_daily_attendance", + raise_if_not_found=False, + ) + + for employee in employees_to_log: + work_data = employee.resource_calendar_id.get_work_duration_data( + from_datetime=dt_from, + to_datetime=dt_to, + compute_leaves=True, + ) + leaves = employee.check_employee_vacation( + date_from=dt_from, + date_to=dt_to, + ) + + if work_data.get("hours") and not leaves: + attendance_list.append( + { + "employee_id": employee.id, + "check_in": check_dt, + "check_out": check_dt, + } + ) + if employee.send_missing_attendance_mail: + manager_template.sudo().send_mail( + employee.id, + force_send=True, + ) + employee_template.sudo().send_mail( + employee.id, + force_send=True, + ) + attendance.create(attendance_list) + + def check_employee_vacation(self, date_from, date_to): + self.ensure_one() + hr_leaves = self.env["hr.leave"].sudo() + holidays = hr_leaves.search( + [ + ("employee_id", "=", self.id), + ("state", "=", "validate"), + ] + ) + + overlap_list = [] + Range = namedtuple("Range", ["start", "end"]) + r2 = Range(start=date_from, end=date_to) + for line in holidays: + r1 = Range(start=line.date_from, end=line.date_to) + latest_start = max(r1.start, r2.start) + earliest_end = min(r1.end, r2.end) + delta = (earliest_end - latest_start).days + 1 + overlap = max(0, delta) + if overlap and (line not in overlap_list): + overlap_list.append(line.id) + return overlap_list + + def _update_previous_attendance_action(self): + """Function for server action to update previous attendances for the + employee.""" + dates = [] + date_now = datetime.now() + # Starting date to update attendance record + date_start = datetime(2023, 8, 15) + attendance = self.env["hr.attendance"].sudo() + + while date_start: + if date_start != datetime.combine(date_now, datetime.min.time()): + dates.append(date_start) + date_start = date_start + timedelta(days=1) + else: + date_start = False + + for each_date in dates: + dt_from = datetime.combine(each_date, datetime.min.time()) + dt_to = datetime.combine(each_date, datetime.max.time()) + + for employee in self: + employee_attendance = attendance.search( + [ + ("check_in", ">=", dt_from), + ("check_out", "<=", dt_to), + ("employee_id", "=", employee.id), + ], + order="check_out desc", + limit=1, + ) + employee_attendance.action_recompute_worked_hours() diff --git a/addons_bp/bp_emp_attendance/models/hr_leave.py b/addons_bp/bp_emp_attendance/models/hr_leave.py new file mode 100644 index 0000000..2335d4a --- /dev/null +++ b/addons_bp/bp_emp_attendance/models/hr_leave.py @@ -0,0 +1,31 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo import api, fields, models + + +class HrLeave(models.Model): + _inherit = "hr.leave" + + employee_overtime = fields.Float(related="employee_id.total_overtime") + + def action_refuse(self): + if self.env.user.has_group('bp_emp_attendance.group_cancel_extra_hours'): + self.mapped("overtime_id").unlink() + super().action_refuse() + + @api.depends('holiday_status_id') + def _compute_overtime_deductible(self): + has_group = self.env.user.has_group( + 'bp_emp_attendance.group_cancel_extra_hours' + ) + return ( + self.write( + { + "overtime_deductible": False, + } + ) + if has_group + else super()._compute_overtime_deductible() + ) diff --git a/addons_bp/bp_emp_attendance/report/__init__.py b/addons_bp/bp_emp_attendance/report/__init__.py new file mode 100644 index 0000000..abac9d6 --- /dev/null +++ b/addons_bp/bp_emp_attendance/report/__init__.py @@ -0,0 +1,4 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import hr_custom_report_attendance diff --git a/addons_bp/bp_emp_attendance/report/hr_custom_report_attendance.py b/addons_bp/bp_emp_attendance/report/hr_custom_report_attendance.py new file mode 100644 index 0000000..bb21ccf --- /dev/null +++ b/addons_bp/bp_emp_attendance/report/hr_custom_report_attendance.py @@ -0,0 +1,70 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, tools + + +class HRAttendanceReport(models.Model): + _name = "custom.hr.attendance.report" + _description = "Attendance Report Custom" + _auto = False + + employee_id = fields.Many2one("hr.employee", readonly=True) + check_in = fields.Date("Check In", readonly=True) + worked_hours = fields.Float("Hours Worked", readonly=True) + overtime_hours = fields.Float("Extra Hours", readonly=True) + department_id = fields.Many2one("hr.department", readonly=True) + + @api.model + def _select(self): + return """ + SELECT + ovr.id, + ovr.employee_id, + ovr.check_in, + worked_hours, + department_id, + ovr.overtime_hours + """ + + @api.model + def _from(self): + return """ + FROM( + SELECT + id, + row_number() over (partition by employee_id, CAST(date AS DATE)) as ot_check, + employee_id, + worked_hours, + CAST(request_date as DATE) as check_in, + duration as overtime_hours + FROM + hr_attendance_overtime + + ) as ovr + """ + + def _join(self): + return """ + LEFT JOIN hr_employee ON hr_employee.id = ovr.employee_id + """ + + def init(self): + tools.drop_view_if_exists(self.env.cr, self._table) + self.env.cr.execute( + """ + CREATE OR REPLACE VIEW %s AS ( + %s + %s + %s + ) + """ + % ( + self._table, + self._select(), + self._from(), + self._join(), + ) + ) diff --git a/addons_bp/bp_emp_attendance/report/hr_custom_report_attendance_views.xml b/addons_bp/bp_emp_attendance/report/hr_custom_report_attendance_views.xml new file mode 100644 index 0000000..b24c181 --- /dev/null +++ b/addons_bp/bp_emp_attendance/report/hr_custom_report_attendance_views.xml @@ -0,0 +1,110 @@ + + + + + + custom.hr.attendance.report.tree + custom.hr.attendance.report + + + + + + + + + + + + + custom.hr.attendance.report.form + custom.hr.attendance.report + +
    + + + + + + + + + + +
    +
    +
    + + + custom.hr.attendance.report.pivot + custom.hr.attendance.report + + + + + + + + + + + + + + custom.hr.attendance.report.search + custom.hr.attendance.report + + + + + + + + + + + + + + + + + + Employees Report + ir.actions.act_window + custom.hr.attendance.report + pivot,tree + + + + Employee Attendance Report + ir.actions.act_window + custom.hr.attendance.report + pivot,tree + [('employee_id.user_id','=',uid)] + + + + + + + + Attendance Analysis + 1 + + + +
    + +
    diff --git a/addons_bp/bp_emp_attendance/security/attendance_security.xml b/addons_bp/bp_emp_attendance/security/attendance_security.xml new file mode 100644 index 0000000..6978975 --- /dev/null +++ b/addons_bp/bp_emp_attendance/security/attendance_security.xml @@ -0,0 +1,16 @@ + + + + + Can edit TimeSheet + + + + + + Can refuse approved extra hours requests + + + + + diff --git a/addons_bp/bp_emp_attendance/security/ir.model.access.csv b/addons_bp/bp_emp_attendance/security/ir.model.access.csv new file mode 100644 index 0000000..2a169d7 --- /dev/null +++ b/addons_bp/bp_emp_attendance/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +bp_emp_attendance.access_hr_attendance_extra_hours,access_hr_attendance_extra_hours,bp_emp_attendance.model_hr_attendance_extra_hours,base.group_user,1,1,1,1 +bp_emp_attendance.access_custom_hr_attendance_report,access_custom_hr_attendance_report,bp_emp_attendance.model_custom_hr_attendance_report,base.group_user,1,1,1,1 diff --git a/addons_bp/bp_emp_attendance/static/description/icon.png b/addons_bp/bp_emp_attendance/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2a7dfd16eb9c5b86ee6b218657b6c75af535fe65 GIT binary patch literal 5444 zcmZ`-2UHVVx28r)sDcpmO6W}p5I~4@qO?$@iS!bP^bjEQj!2QFkf0P15JW+wC`~{* zgmUQ!Qlu!o_x9qw_uc=!x7M4rX6DR3-)`TYGqY!&=N7edgrqz^jGp4-r?By5#slgP98{fxukP$3+)6sOa_Xl|!i{D3oJ+Q*3Outh<|vJe z2M;?(F|3;>kw6j>c`S@LbaTYmg0XI{?%pu00{34C7;*et4B-a<1;Mx|aGUAqgVj9H zj$mmqDKT+wMH(;|ERS|T!VKZ}{!u4hDR4VuFrF|7#Lv%9%nvH&fp&sO$jQk;#3dn; zlA=U}sJFj6#uh8;?#=VJk^i;>cl5SLqdYMv4|niyyS8>7J{SdV?%$67-TsafgF^nt zle_mnX%Q2I{GNeGh>1h~wM|r&|1E{7dANC^9lgDY_7x@M|3dyR*?;`}jn_k?9Ep+s zj^|(e|CIe-yuPys#)HTj8fCBPj&Vd2o&BRN`H#2%AMuYZdC2d?{|6C&v;3EoNV6i1 zJmlY!DbkRQhf$J{FjH&7RgAEt>si$GIwNcy8C+7%Qbx}P;Ev9!-7KjZ(OP(Brci?= zu5ABnciSBV)ifd{9bIQ<;8%4nbe`pDq6YS?XLdiG=luLqcUW^cb-K|oeG(MZ=iR3m zXnwfXP&o5V?!q9IH(w#}pmSWp-r?fx*lP2I6pH)MXPNuWOSBgA)1^?$z7$u&(R`AU z!@;`;k|Af<@=D@z>D92I_z8+yY&s84=9tkcmJJDGkNrg|rXe z59_gB%VT9ZMkQ7Xt~KyL91CImV!K09AOFGjJivO_wBV8Pvft&o;m%q`a6^W1^wmsI zVCb1ayV6lEAye-D#mQ<}?2@2qsl{NPqxLeUUjcnaEdV!CueZ|DK#Fg>6726KrEfM~ zDkj&+F6XD^Ymb$pSKoi40cyduWRb2xi>x47&)oXUi&F`F*>1nm%%q;MrPp1=N>brq zlx;hkbr6@@V_?c+Plh`5=1;$}h6D5Ek-ASWz1l&2o2|7r=}`NbFTs+iL0-I!&)c<` zMy1hth5QS7|6Q#YGH&_7rw3?)M8}PtTOt=}js)c~i;oUZ64(_jz8o*Tc*lO)EJMpG z%&QQ%pL*T>vkH2l(`zfF{<4!9={jBi06@# zK?M5c;=~0cXBQ<;WU`z?54=B(8PXYSP-hUb`0NSZuQ6d}L!MnsdPoMJVDPeDd1pkf zN`rd_(dwMRm6%@xD(0WuMg4adNg4Z&Yk3;R5kQ1|5F)lLQPM)cfxo&V+Wn zn}1;7)%Q|v(L{R0&*1)fmq5-9EE}0Upm1HT!l_3VF&TIWnEn&~R{V0iL;Gi6R>6Y` zWO(KZxovxGm5bxRZ1Yae`s7tqpWSo@GoN;V$=utKx0Hh>MuhRwG$lCxg9+Y?_rA~m zi#_A}!B3d^@-ky$apk5NudXoQsxQxW`-@=7!Vsd}N$AK-WU*C7xke`&bUc~umKpf9 zdLpyTiYqJ#Fg{-AyUPD*3LXDp?v%?3>1xQtI&>hz`k2=wL`sNwnoL-Bn=V1j`iA2HR>hldBS$TzH~}+a7)B z(M2Nd<`J^nI~$N}Ib;jfzxWZ~aGviv@$s?zE-u{r%Fw}&ws;1A+Sv^th}@pWz6Kgh zX45`s1j0*&?iY4W)p$qe&bgvrgrhmAJ}nf#aabc~knOO3fVa)$e-yav-S6MY^>wvE zXb+gHlQbFzB-@YG-&5-&5Nb}x_^hEib3k52v*qV}H+Ac7wr=PG6X`HE zpNZ6MS4z=!XQ9hbisYrb5kWjHbbrLOC+@jO!KVnlu**VNxfAop?wjW2hJ34@e!=_C ztALbbwgA@fI``Mpw?+}3-YM_!!)cvx3XPIGo6mgWY8%cECf}1@`TXv(;8cUS%(G%x z*`Wv*2SN5i5?q0HMmMqO?(PETtcYXucb?6$Bf*D)^eG1n34e%4`?O(o8*iG@yaW}_ z4YFB)X|u7a{DQz-jTG)B-$6@iMBv_^>J6cM(h&pq`ipvMVcTZA^$gB;hCEr`rrRu1XzGJlwLY5bKJ zPgswtVfKuPk}~44cb)K8Ug91!DKpWzCThXn$^N4oI5Zy*sexyQEP+4;wt1Aa*Q7pG ztkJ+wdF`>iV*?|D9MsOQjY~{4-)BlIpm-L({SzRXHMwNYT0?U)Q@2rXYLiWK?5;=xC6g>bCP`WHI;kJlLJc(l3K<3}$*4vN z*nO~S2&m5E$`JrU5u3uMrMLP;RMW9}cpvHYDs zqaNR64(VN9ZoR6vC;m;xy{;F93Y<^8?VxO)S-;zNXSsy>_;$}9*B%<0fl0qnZ3%UW zj;-$e($Z`L0^0H`-6l;msv`zF%Q)6<;MV#Mr$uhL$((4N)3zA(gqX1Ev8gV!>qfUa z@ie`;WtktiPphZ{L*lxWVH|Ske5tWS#hjCz{?P)$y+)KOh})9i<)g$!^`A9AvsXp^ zG_W#cL2+Y{%=$RZ9w1bX%?kVK7`irc|0`zwVy6evl0MI6&6X)g&&#ISN1pRGS0y%7 z>e(LQ1KmY~-^~7i8S%B9`M&7(wvKUr+tlXAsdaE!o+K8(a#uyaTQbsOd3Gd6{|Xy( zT4HJaj+Td}D!L-YdP^uGI({b;O#r_lAodA!s2)ML=*$JXwmAP{{^Vg-iu*Z(f+{=0 zwPAV^E#w1NiWt6)+!Ja8%E$fU$_Y6scALs(9yh33y0h`QrOOVXc(>`zL#f8RL@EpC z;(fcQR_muB3#vlR2}vU%QHVy{7QiYlaXC8-%4QvRMbWs}Z9DJoXsr;VaB+Mug}tyY zHFCDztLu5l6IfMj6lPnJ86mC~xyz5hf!CO1XfifB9sBbxp4r4CGL1hCC`Q$NIVrYu zj{f#F#yOco_Re+xCr$J(Cz^Ly;+_^kc!nuFA!Wjm>))!f?4+Gw5QJbd^kgr&@`5S zm@$jIVUg<2<)5Myf8WJ*;r84XarPtpuE{_<%uGhy}y@VXZo-&GQu?#`p6xkByyqF9|tVWDW> zY1CIsoTsAMD~Aqr%F;OQ(ReOnVXz6p$$z2vLdBLcoHXj;B42>tUMVRH;7@qJ+2Hiz zNyLiKkJeh!=JP^*H>Q@4vnit1GGJ#Q*HYfU%u_Y<(Gq9SR`~lzdaLTSJSIKP=VE~jtZ!N~ z{?zaqg9nVqKIw4MHT2IQXDQdA8{PXM!YV$m8s)=04H7aG;ls^+rBYIlXI|VEtXp6^HOhTN5D;qr)yr)x5oj@ zt3O@SZ_fl(IK4qP-y?}x&~Ey)u8Q5+FpYn&eUGX#gqu^Pr#9L9Kv%KLox^_RQLHyg zzDTmIAJ$Vsqb)j4{4BRCmgREGEW8$KHfK)^Iwh05LT;`=Ms>AAQCjPJm6&;BaX%ik zM7i}HDEqCqdzh`Y>LWnbJ_k)%%=j}(pnow+ii`$Uycf-qju(KOv!6MiFVD<-%n-y` z;Uew$fQZjOn!J9OgNqI-Pda?VcQMtP_I=LTHeBqU785iQa9%6Tv+hv2=`WDKJn^IT zYb~x?WUgzdY=&?5hV~RoG?-k?_s^4-L_YNiNyiJX=91_V-i8&rw*BUQ47tsWqPZC;D#azTNuE@dr|tEZyK-6 z#_*zErGIU~HOKR3(w`z26AulB_14+RHZ*r>G}?%i>-x=Udn&{R7et84#f!&szIi|c z?Ex^Qf@0Ew(s{^W3sL>d)Ra>C0}ju=cUQQdvkU{26;>#=So(kEgGH zAc#lOqFW?wSY+PicD4h5yo~ij%P|2vQnnY`9_D)4#o(nPb*NOIkr(>B5H=Pwe~(hC za6uAg-^nA`S@(~euQLs_qi>t=^?>2Q6)fMr>0C~+L9tAH9d{&kcZ1+13t`N5%Vg3l zbdC;ySr){kLzNe`^KM!uw}lc@JT&0=>+}92!fhymvglH6fcDG`j-$FMb1xHG;c+x1ROgn*&%U#>h|nVAI~=RF!lu5HL?J` zOECgC32f$;k^i)*N8F94w3ec>z(ZtyMukK7H4>1udtETyOL_TH4;H(zqQ)hGE8*d! z?B^@y@fCtdM`u=(+T2qEls(>Tbriy&L>Q5{7|+7wKWx%tU?;uwHY?($TiO4MDe}#jF&S+$f2yn|N~@&cwotdZ|I*^vBbHx2a1J6;%6*b4O8I zp9>4Aj33wh?C5ct-u6zUCiYsRE}4(QlEiEem$LLtgIE_Nc%5S7XCh!{MCeFq&Et$D zM8C0p5VtA<-)v)6itNu`kG@??Vo0Efs@?Hn%rLSx(46apXz*3ko&_9BmUQfg5b(Az zG9#DDZ%Iofkpkwu*Q{@!XtvjLr9>DZID4Pd;hkGb-`ZZP#Is2OHU1XS|$ZmiKnZRY-5RFw%jtTKS~! ze)j$FaBi_=K6Z@O)HJ{UV*&`n?%lmdJ|`*Gbf|F!w4re8^ZH0CtxQW>4_j%ebB?1B z6!BA&S_KSx^?aLG$?6MsPlO9+3s@0&5a%zDDq^L0K-sAN>F!{9jvsRz2y-JXT4JOw zpJ@kb)<|U7=Zb;M91-4jQ}q`g7w$ATd%%uZl=>hy`q_Pp`kmx zeD!==em?QWf2RK==se&UVfh53qlLs#O9_=8E14J*mHlrsm0eQWxz9g24W5zv{clh6 Mt}eV()h6P<0Kow(V*mgE literal 0 HcmV?d00001 diff --git a/addons_bp/bp_emp_attendance/tests/__init__.py b/addons_bp/bp_emp_attendance/tests/__init__.py new file mode 100644 index 0000000..7a16486 --- /dev/null +++ b/addons_bp/bp_emp_attendance/tests/__init__.py @@ -0,0 +1,4 @@ +# © 2023 bloopark systems () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import test_hr_attendance_break_time diff --git a/addons_bp/bp_emp_attendance/tests/test_hr_attendance_break_time.py b/addons_bp/bp_emp_attendance/tests/test_hr_attendance_break_time.py new file mode 100644 index 0000000..a889959 --- /dev/null +++ b/addons_bp/bp_emp_attendance/tests/test_hr_attendance_break_time.py @@ -0,0 +1,162 @@ +from datetime import datetime + +import pytz +from odoo.tests import new_test_user +from odoo.tests.common import TransactionCase + + +def tz_datetime(year, month, day, hour, minute): + tz = pytz.timezone("Europe/Brussels") + return ( + tz.localize(datetime(year, month, day, hour, minute)) + .astimezone(pytz.utc) + .replace(tzinfo=None) + ) + + +class TestHrAttendance(TransactionCase): + """Test for break time calculation.""" + + def setUp(self): + super().setUp() + self.employee_user = new_test_user( + self.env, + login="bp", + groups="base.group_user", + ) + self.employee_login = self.env["hr.employee"].create( + { + "name": "Martin Holz", + "pin": "4477", + "user_id": self.employee_user.id, + } + ) + + def get_attendance_data(self, attendance_id): + attendance = self.env["hr.attendance"].browse(int(attendance_id)) + return { + "net_working_time": round(attendance.net_working_time, 2), + "employee_break_time": round(attendance.employee_break_time, 2), + "worked_hours": round(attendance.worked_hours, 2), + } + + def test_employee_checkout(self): + # Make sure the attendance of the employee already checked out + assert self.employee_login.attendance_state == "checked_out" + + def test_attendance_case_1(self): + """Test break/net work time calculation is correctly computed if employee has + only one attendance log record.""" + + # Case 1: Employee total working hours less than 6 + attendance = self.env["hr.attendance"].create( + { + "employee_id": self.employee_login.id, + "check_in": tz_datetime(2022, 4, 7, 8, 0), + "check_out": tz_datetime(2022, 4, 7, 12, 0), + } + ) + attendance_data = self.get_attendance_data(attendance_id=attendance.id) + net = attendance_data.get("net_working_time") + break_time = attendance_data.get("employee_break_time") + worked_hours = net - break_time + assert net == 4.0 + assert break_time == 0.0 + assert round(attendance.worked_hours, 2) == worked_hours + + # Case 2: Employee total working hours between 6:15 less than 6:30 + attendance.check_out = tz_datetime(2022, 4, 7, 14, 15) + attendance_data = self.get_attendance_data(attendance_id=attendance.id) + net = attendance_data.get("net_working_time") + break_time = attendance_data.get("employee_break_time") + worked_hours = net - break_time + + assert net == 6.25 + assert break_time == 0.25 + assert round(attendance.worked_hours, 2) == worked_hours + + # Case 4: Employee total working hours greater than 06:30 + attendance.check_out = tz_datetime(2022, 4, 7, 16, 15) + attendance_data = self.get_attendance_data(attendance_id=attendance.id) + net = attendance_data.get("net_working_time") + break_time = attendance_data.get("employee_break_time") + worked_hours = net - break_time + + assert net == 8.25 + assert break_time == 0.5 + assert round(attendance.worked_hours, 2) == worked_hours + + def test_attendance_case_2(self): + """Test break/net work time calculation is correctly computed if employee has + multi check in/out in same day.""" + # Case 1 + attendance_1 = self.env["hr.attendance"].create( + { + "employee_id": self.employee_login.id, + "check_in": tz_datetime(2022, 4, 7, 8, 0), + "check_out": tz_datetime(2022, 4, 7, 9, 0), + } + ) + attendance_data = self.get_attendance_data( + attendance_id=attendance_1.id, + ) + net = attendance_data.get("net_working_time") + break_time = attendance_data.get("employee_break_time") + worked_hours = net - break_time + + assert net == 1.00 + assert break_time == 0.0 + assert round(attendance_1.worked_hours, 2) == worked_hours + + # Case 2 + attendance_2 = self.env["hr.attendance"].create( + { + "employee_id": self.employee_login.id, + "check_in": tz_datetime(2022, 4, 7, 9, 00), + "check_out": tz_datetime(2022, 4, 7, 17, 00), + } + ) + attendance_data = self.get_attendance_data( + attendance_id=attendance_2.id, + ) + net = attendance_data.get("net_working_time") + break_time = attendance_data.get("employee_break_time") + worked_hours = net - break_time + assert net == 8.00 + assert break_time == 0.5 + assert round(attendance_2.worked_hours, 2) == worked_hours + + # Case 3: Employee total working hours above 9hrs + attendance_3 = self.env["hr.attendance"].create( + { + "employee_id": self.employee_login.id, + "check_in": tz_datetime(2022, 4, 8, 8, 00), + "check_out": tz_datetime(2022, 4, 8, 18, 00), + } + ) + attendance_data = self.get_attendance_data( + attendance_id=attendance_3.id, + ) + attendance_3.action_recompute_worked_hours() + net = attendance_data.get("net_working_time") + break_time = attendance_data.get("employee_break_time") + worked_hours = net - break_time + assert net == 10 + assert break_time == 0.75 # 45 min + assert round(attendance_3.worked_hours, 2) == worked_hours + + # Case 4: Employee total working hours exact 9hr + attendance_3.write( + { + "check_out": tz_datetime(2022, 4, 8, 17, 00), + } + ) + attendance_data = self.get_attendance_data( + attendance_id=attendance_3.id, + ) + net = attendance_data.get("net_working_time") + break_time = attendance_data.get("employee_break_time") + worked_hours = net - break_time + assert net == 9.0 + assert break_time == 0.5 # 30 min + assert round(attendance_3.worked_hours, 2) == worked_hours diff --git a/addons_bp/bp_emp_attendance/views/hr_attendance_overtime_views.xml b/addons_bp/bp_emp_attendance/views/hr_attendance_overtime_views.xml new file mode 100644 index 0000000..3720d1f --- /dev/null +++ b/addons_bp/bp_emp_attendance/views/hr_attendance_overtime_views.xml @@ -0,0 +1,22 @@ + + + + + hr.attendance.overtime.view.tree + hr.attendance.overtime + + + + + + + hide + + + Sum Duration + + + + + + diff --git a/addons_bp/bp_emp_attendance/views/hr_attendance_view.xml b/addons_bp/bp_emp_attendance/views/hr_attendance_view.xml new file mode 100644 index 0000000..4f275ba --- /dev/null +++ b/addons_bp/bp_emp_attendance/views/hr_attendance_view.xml @@ -0,0 +1,30 @@ + + + + + hr.attendance.tree + hr.attendance + + + + + + + + + + + hr.employee.view.form.hr + hr.employee + + + + + + + + + + + + + + + + + + + + + + + +