1003 lines
28 KiB
Python
1003 lines
28 KiB
Python
from odoo import api, fields, models, tools
|
|
|
|
|
|
class AttendanceAnalytics(models.Model):
|
|
_name = 'attendance.analytics'
|
|
_description = 'Attendance Analytics'
|
|
_auto = False
|
|
_rec_name = 'employee_id'
|
|
_order = 'date desc'
|
|
|
|
employee_id = fields.Many2one(
|
|
'hr.employee',
|
|
string='Employee'
|
|
)
|
|
|
|
department_id = fields.Many2one(
|
|
'hr.department',
|
|
string='Department'
|
|
)
|
|
|
|
date = fields.Date()
|
|
|
|
date_end = fields.Date(
|
|
string="End Date"
|
|
)
|
|
|
|
min_check_in = fields.Datetime()
|
|
|
|
max_check_out = fields.Datetime()
|
|
|
|
worked_hours = fields.Float()
|
|
|
|
out_time = fields.Float()
|
|
|
|
expected_check_in = fields.Float()
|
|
|
|
expected_check_out = fields.Float()
|
|
|
|
required_checkout_time = fields.Float()
|
|
|
|
compensated_time = fields.Float()
|
|
|
|
department_grace_period = fields.Integer()
|
|
|
|
late_minutes = fields.Float()
|
|
|
|
early_out_minutes = fields.Float()
|
|
|
|
is_late = fields.Boolean(
|
|
string="Late"
|
|
)
|
|
|
|
is_early_out = fields.Boolean(
|
|
string="Early Out"
|
|
)
|
|
|
|
is_compensation_pending = fields.Boolean(
|
|
string="Compensation Pending"
|
|
)
|
|
|
|
late_time = fields.Char(
|
|
string="Late Time",
|
|
compute="_compute_late_time"
|
|
)
|
|
|
|
early_out_time = fields.Char(
|
|
string="Early Out Time",
|
|
compute="_compute_early_out_time"
|
|
)
|
|
|
|
holiday_name = fields.Char()
|
|
|
|
is_holiday = fields.Boolean()
|
|
|
|
is_week_off = fields.Boolean()
|
|
|
|
status_message = fields.Char(
|
|
string="Status"
|
|
)
|
|
|
|
display_label = fields.Char(
|
|
compute="_compute_display_label",
|
|
store=False
|
|
)
|
|
|
|
color = fields.Integer(
|
|
compute="_compute_color"
|
|
)
|
|
|
|
status = fields.Selection([
|
|
('present', 'Present'),
|
|
('leave', 'Leave'),
|
|
('no_info', 'No Information')
|
|
])
|
|
|
|
late_request_id = fields.Many2one(
|
|
'late.coming.request',
|
|
string="Late Request"
|
|
)
|
|
|
|
late_approved = fields.Boolean(
|
|
string="Late Approved"
|
|
)
|
|
|
|
@api.depends('late_minutes')
|
|
def _compute_late_time(self):
|
|
|
|
for rec in self:
|
|
|
|
total_minutes = int(rec.late_minutes or 0)
|
|
|
|
hours = total_minutes // 60
|
|
minutes = total_minutes % 60
|
|
|
|
rec.late_time = "%02d:%02d" % (
|
|
hours,
|
|
minutes
|
|
)
|
|
|
|
@api.depends('early_out_minutes')
|
|
def _compute_early_out_time(self):
|
|
|
|
for rec in self:
|
|
|
|
total_minutes = int(
|
|
rec.early_out_minutes or 0
|
|
)
|
|
|
|
hours = total_minutes // 60
|
|
minutes = total_minutes % 60
|
|
|
|
rec.early_out_time = "%02d:%02d" % (
|
|
hours,
|
|
minutes
|
|
)
|
|
|
|
@api.depends(
|
|
'status_message',
|
|
'late_time',
|
|
'early_out_time'
|
|
)
|
|
def _compute_display_label(self):
|
|
|
|
for rec in self:
|
|
|
|
lines = []
|
|
|
|
if rec.employee_id:
|
|
lines.append(rec.employee_id.name)
|
|
|
|
if rec.status_message:
|
|
lines.append(rec.status_message)
|
|
|
|
if rec.late_minutes:
|
|
lines.append(
|
|
f"Late: {rec.late_time}"
|
|
)
|
|
|
|
if rec.early_out_minutes:
|
|
lines.append(
|
|
f"Early Out: {rec.early_out_time}"
|
|
)
|
|
|
|
rec.display_label = "\n".join(lines)
|
|
|
|
@api.depends(
|
|
'is_holiday',
|
|
'is_week_off',
|
|
'status',
|
|
'is_compensation_pending',
|
|
'is_late'
|
|
)
|
|
def _compute_color(self):
|
|
for rec in self:
|
|
|
|
if rec.is_holiday:
|
|
rec.color = 4
|
|
|
|
elif rec.is_week_off:
|
|
rec.color = 7
|
|
|
|
elif rec.status == 'leave':
|
|
rec.color = 3
|
|
|
|
elif rec.is_compensation_pending:
|
|
rec.color = 2
|
|
|
|
elif rec.is_late:
|
|
rec.color = 1
|
|
|
|
elif rec.status == 'present':
|
|
rec.color = 10
|
|
|
|
else:
|
|
rec.color = 8
|
|
|
|
def action_create_late_request(self):
|
|
|
|
self.ensure_one()
|
|
|
|
request = self.env['late.coming.request'].search([
|
|
('employee_id', '=', self.employee_id.id),
|
|
('attendance_date', '=', self.date)
|
|
], limit=1)
|
|
if not request:
|
|
request = self.env['late.coming.request'].create({
|
|
'employee_id': self.employee_id.id,
|
|
'attendance_date': self.date,
|
|
'check_in': self.min_check_in,
|
|
'late_minutes': self.late_minutes,
|
|
'department_grace_period':
|
|
self.department_grace_period,
|
|
'required_checkout_time':
|
|
self.required_checkout_time,
|
|
'compensation_pending':
|
|
self.is_compensation_pending,
|
|
'compensation_minutes':
|
|
self.early_out_minutes,
|
|
'status_message':
|
|
self.status_message,
|
|
})
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': 'Late Coming Request',
|
|
'res_model': 'late.coming.request',
|
|
'res_id': request.id,
|
|
'view_mode': 'form',
|
|
'target': 'current',
|
|
}
|
|
|
|
def init(self):
|
|
|
|
tools.drop_view_if_exists(
|
|
self.env.cr,
|
|
self._table
|
|
)
|
|
|
|
self.env.cr.execute("""
|
|
|
|
CREATE OR REPLACE VIEW attendance_analytics AS (
|
|
|
|
WITH employee_dates AS (
|
|
|
|
SELECT
|
|
|
|
emp.id AS employee_id,
|
|
|
|
emp.department_id,
|
|
|
|
emp.resource_calendar_id,
|
|
|
|
generate_series(
|
|
DATE(emp.create_date),
|
|
CURRENT_DATE,
|
|
interval '1 day'
|
|
)::date AS date
|
|
|
|
FROM hr_employee emp
|
|
|
|
WHERE emp.active = true
|
|
),
|
|
|
|
attendance_summary AS (
|
|
|
|
SELECT
|
|
|
|
att.employee_id,
|
|
|
|
DATE(att.check_in)
|
|
AS attendance_date,
|
|
|
|
MIN(att.check_in)
|
|
AS min_check_in,
|
|
|
|
MAX(att.check_out)
|
|
AS max_check_out,
|
|
|
|
SUM(att.worked_hours)
|
|
AS worked_hours
|
|
|
|
FROM hr_attendance att
|
|
|
|
GROUP BY
|
|
|
|
att.employee_id,
|
|
|
|
DATE(att.check_in)
|
|
|
|
),
|
|
|
|
department_grace AS (
|
|
|
|
SELECT
|
|
|
|
rcg.calendar_id,
|
|
|
|
rcg.department_id,
|
|
|
|
rcg.grace_period
|
|
|
|
FROM
|
|
resource_calendar_department_grace rcg
|
|
),
|
|
|
|
late_requests AS (
|
|
|
|
SELECT
|
|
|
|
id,
|
|
|
|
employee_id,
|
|
|
|
attendance_date,
|
|
|
|
state
|
|
|
|
FROM late_coming_request
|
|
|
|
WHERE state = 'approved'
|
|
),
|
|
|
|
holiday_data AS (
|
|
|
|
SELECT DISTINCT
|
|
|
|
rl.id,
|
|
|
|
rl.calendar_id,
|
|
|
|
rl.name AS holiday_name,
|
|
|
|
DATE(rl.date_from)
|
|
AS holiday_start,
|
|
|
|
DATE(rl.date_to)
|
|
AS holiday_end
|
|
|
|
FROM resource_calendar_leaves rl
|
|
|
|
WHERE rl.date_from IS NOT NULL
|
|
AND rl.date_to IS NOT NULL
|
|
AND rl.resource_id IS NULL
|
|
)
|
|
|
|
SELECT
|
|
|
|
row_number() OVER() AS id,
|
|
|
|
ed.employee_id,
|
|
|
|
ed.department_id,
|
|
|
|
ed.date,
|
|
|
|
ed.date AS date_end,
|
|
|
|
ats.min_check_in,
|
|
|
|
ats.max_check_out,
|
|
|
|
COALESCE(
|
|
ats.worked_hours,
|
|
0
|
|
) AS worked_hours,
|
|
CASE
|
|
WHEN ats.min_check_in IS NOT NULL
|
|
AND ats.max_check_out IS NOT NULL
|
|
THEN (
|
|
EXTRACT(
|
|
EPOCH FROM (
|
|
ats.max_check_out -
|
|
ats.min_check_in
|
|
)
|
|
) / 3600
|
|
) - ats.worked_hours
|
|
ELSE 0
|
|
END AS out_time,
|
|
|
|
CASE
|
|
WHEN ats.worked_hours > 9
|
|
THEN ats.worked_hours - 9
|
|
ELSE 0
|
|
END AS compensated_time,
|
|
|
|
lr.id AS late_request_id,
|
|
|
|
hd.holiday_name,
|
|
|
|
CASE
|
|
WHEN hd.id IS NOT NULL
|
|
THEN TRUE
|
|
ELSE FALSE
|
|
END AS is_holiday,
|
|
|
|
CASE
|
|
WHEN EXTRACT(
|
|
DOW FROM ed.date
|
|
) IN (0, 6)
|
|
|
|
THEN TRUE
|
|
ELSE FALSE
|
|
END AS is_week_off,
|
|
|
|
dg.grace_period
|
|
AS department_grace_period,
|
|
|
|
(
|
|
(
|
|
rc.shift_start_time * 60
|
|
)
|
|
+
|
|
COALESCE(
|
|
dg.grace_period,
|
|
rc.late_grace_period,
|
|
0
|
|
)
|
|
) AS expected_check_in,
|
|
|
|
(
|
|
rc.shift_end_time * 60
|
|
) AS expected_check_out,
|
|
|
|
CASE
|
|
|
|
WHEN ats.min_check_in IS NOT NULL
|
|
|
|
THEN GREATEST(
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR FROM ats.min_check_in
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.min_check_in
|
|
)
|
|
)
|
|
|
|
-
|
|
|
|
(
|
|
(
|
|
rc.shift_start_time * 60
|
|
)
|
|
+
|
|
COALESCE(
|
|
dg.grace_period,
|
|
rc.late_grace_period,
|
|
0
|
|
)
|
|
),
|
|
|
|
0
|
|
)
|
|
|
|
ELSE 0
|
|
|
|
END AS late_minutes,
|
|
|
|
CASE
|
|
|
|
WHEN ats.min_check_in IS NOT NULL
|
|
|
|
AND
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR FROM ats.min_check_in
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.min_check_in
|
|
)
|
|
)
|
|
|
|
>
|
|
|
|
(
|
|
(
|
|
rc.shift_start_time * 60
|
|
)
|
|
+
|
|
COALESCE(
|
|
dg.grace_period,
|
|
rc.late_grace_period,
|
|
0
|
|
)
|
|
)
|
|
|
|
THEN TRUE
|
|
|
|
ELSE FALSE
|
|
|
|
END AS is_late,
|
|
|
|
(
|
|
|
|
(
|
|
rc.shift_end_time * 60
|
|
)
|
|
|
|
+
|
|
|
|
CASE
|
|
|
|
WHEN ats.min_check_in
|
|
IS NOT NULL
|
|
|
|
THEN GREATEST(
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.min_check_in
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.min_check_in
|
|
)
|
|
)
|
|
|
|
-
|
|
|
|
(
|
|
(
|
|
rc.shift_start_time * 60
|
|
)
|
|
+
|
|
COALESCE(
|
|
dg.grace_period,
|
|
rc.late_grace_period,
|
|
0
|
|
)
|
|
),
|
|
|
|
0
|
|
)
|
|
|
|
ELSE 0
|
|
|
|
END
|
|
|
|
) AS required_checkout_time,
|
|
|
|
CASE
|
|
|
|
WHEN ats.max_check_out
|
|
IS NOT NULL
|
|
|
|
THEN GREATEST(
|
|
|
|
(
|
|
|
|
(
|
|
rc.shift_end_time * 60
|
|
)
|
|
|
|
+
|
|
|
|
CASE
|
|
|
|
WHEN ats.min_check_in
|
|
IS NOT NULL
|
|
|
|
THEN GREATEST(
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.min_check_in
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.min_check_in
|
|
)
|
|
)
|
|
|
|
-
|
|
|
|
(
|
|
(
|
|
rc.shift_start_time
|
|
* 60
|
|
)
|
|
+
|
|
COALESCE(
|
|
dg.grace_period,
|
|
rc.late_grace_period,
|
|
0
|
|
)
|
|
),
|
|
|
|
0
|
|
)
|
|
|
|
ELSE 0
|
|
|
|
END
|
|
|
|
)
|
|
|
|
-
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.max_check_out
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.max_check_out
|
|
)
|
|
),
|
|
|
|
0
|
|
)
|
|
|
|
ELSE 0
|
|
|
|
END AS early_out_minutes,
|
|
|
|
CASE
|
|
|
|
WHEN ats.max_check_out
|
|
IS NOT NULL
|
|
|
|
AND
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.max_check_out
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.max_check_out
|
|
)
|
|
)
|
|
|
|
<
|
|
|
|
(
|
|
|
|
(
|
|
rc.shift_end_time * 60
|
|
)
|
|
|
|
+
|
|
|
|
CASE
|
|
|
|
WHEN ats.min_check_in
|
|
IS NOT NULL
|
|
|
|
THEN GREATEST(
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.min_check_in
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.min_check_in
|
|
)
|
|
)
|
|
|
|
-
|
|
|
|
(
|
|
(
|
|
rc.shift_start_time
|
|
* 60
|
|
)
|
|
+
|
|
COALESCE(
|
|
dg.grace_period,
|
|
rc.late_grace_period,
|
|
0
|
|
)
|
|
),
|
|
|
|
0
|
|
)
|
|
|
|
ELSE 0
|
|
|
|
END
|
|
)
|
|
|
|
THEN TRUE
|
|
|
|
ELSE FALSE
|
|
|
|
END AS is_early_out,
|
|
|
|
CASE
|
|
|
|
WHEN
|
|
(
|
|
(
|
|
rc.shift_end_time * 60
|
|
)
|
|
|
|
+
|
|
|
|
CASE
|
|
|
|
WHEN ats.min_check_in
|
|
IS NOT NULL
|
|
|
|
THEN GREATEST(
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.min_check_in
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.min_check_in
|
|
)
|
|
)
|
|
|
|
-
|
|
|
|
(
|
|
(
|
|
rc.shift_start_time
|
|
* 60
|
|
)
|
|
+
|
|
COALESCE(
|
|
dg.grace_period,
|
|
rc.late_grace_period,
|
|
0
|
|
)
|
|
),
|
|
|
|
0
|
|
)
|
|
|
|
ELSE 0
|
|
|
|
END
|
|
)
|
|
|
|
>
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.max_check_out
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.max_check_out
|
|
)
|
|
)
|
|
|
|
THEN TRUE
|
|
|
|
ELSE FALSE
|
|
|
|
END AS is_compensation_pending,
|
|
|
|
CASE
|
|
WHEN lr.state = 'approved'
|
|
THEN TRUE
|
|
ELSE FALSE
|
|
END AS late_approved,
|
|
|
|
CASE
|
|
|
|
WHEN hd.id IS NOT NULL
|
|
THEN 'Holiday'
|
|
|
|
WHEN EXTRACT(
|
|
DOW FROM ed.date
|
|
) IN (0,6)
|
|
|
|
THEN 'Week Off'
|
|
|
|
WHEN leave.id IS NOT NULL
|
|
THEN 'Leave'
|
|
|
|
WHEN ats.min_check_in IS NULL
|
|
THEN 'Absent'
|
|
|
|
WHEN
|
|
(
|
|
(
|
|
|
|
(
|
|
rc.shift_end_time * 60
|
|
)
|
|
|
|
+
|
|
|
|
CASE
|
|
|
|
WHEN ats.min_check_in
|
|
IS NOT NULL
|
|
|
|
THEN GREATEST(
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.min_check_in
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.min_check_in
|
|
)
|
|
)
|
|
|
|
-
|
|
|
|
(
|
|
(
|
|
rc.shift_start_time
|
|
* 60
|
|
)
|
|
+
|
|
COALESCE(
|
|
dg.grace_period,
|
|
rc.late_grace_period,
|
|
0
|
|
)
|
|
),
|
|
|
|
0
|
|
)
|
|
|
|
ELSE 0
|
|
|
|
END
|
|
|
|
)
|
|
|
|
>
|
|
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.max_check_out
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.max_check_out
|
|
)
|
|
)
|
|
)
|
|
|
|
THEN 'Compensation Pending'
|
|
|
|
WHEN
|
|
(
|
|
(
|
|
EXTRACT(
|
|
HOUR
|
|
FROM ats.min_check_in
|
|
) * 60
|
|
)
|
|
+
|
|
EXTRACT(
|
|
MINUTE
|
|
FROM ats.min_check_in
|
|
)
|
|
)
|
|
|
|
>
|
|
|
|
(
|
|
(
|
|
rc.shift_start_time * 60
|
|
)
|
|
+
|
|
COALESCE(
|
|
dg.grace_period,
|
|
rc.late_grace_period,
|
|
0
|
|
)
|
|
)
|
|
|
|
THEN 'Late'
|
|
|
|
ELSE 'Present'
|
|
|
|
END AS status_message,
|
|
|
|
CASE
|
|
WHEN leave.id IS NOT NULL
|
|
THEN 'leave'
|
|
|
|
WHEN ats.min_check_in IS NOT NULL
|
|
THEN 'present'
|
|
|
|
ELSE 'no_info'
|
|
END AS status
|
|
|
|
FROM employee_dates ed
|
|
|
|
LEFT JOIN attendance_summary ats
|
|
ON ats.employee_id = ed.employee_id
|
|
AND ats.attendance_date = ed.date
|
|
|
|
LEFT JOIN hr_leave leave
|
|
ON leave.employee_id = ed.employee_id
|
|
AND ed.date BETWEEN
|
|
leave.request_date_from
|
|
AND leave.request_date_to
|
|
AND leave.state = 'validate'
|
|
|
|
LEFT JOIN late_requests lr
|
|
ON lr.employee_id = ed.employee_id
|
|
AND lr.attendance_date = ed.date
|
|
|
|
LEFT JOIN holiday_data hd
|
|
ON ed.date BETWEEN
|
|
hd.holiday_start
|
|
AND hd.holiday_end
|
|
AND (
|
|
hd.calendar_id
|
|
= ed.resource_calendar_id
|
|
OR hd.calendar_id IS NULL
|
|
)
|
|
|
|
LEFT JOIN resource_calendar rc
|
|
ON rc.id = ed.resource_calendar_id
|
|
|
|
LEFT JOIN department_grace dg
|
|
ON dg.calendar_id
|
|
= ed.resource_calendar_id
|
|
AND dg.department_id
|
|
= ed.department_id
|
|
|
|
)
|
|
|
|
""")
|
|
|
|
def action_send_absent_mail(self):
|
|
|
|
today = fields.Date.today()
|
|
|
|
records = self.search([
|
|
|
|
('date', '=', today),
|
|
|
|
('status', '=', 'no_info'),
|
|
|
|
('is_holiday', '=', False),
|
|
|
|
('is_week_off', '=', False),
|
|
|
|
])
|
|
|
|
template = self.env.ref(
|
|
'grace_period.email_template_absent_alert'
|
|
)
|
|
|
|
for rec in records:
|
|
|
|
if rec.employee_id.work_email:
|
|
template.send_mail(
|
|
rec.id,
|
|
force_send=True
|
|
)
|