mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-27 15:12:05 +02:00
19.0 vanilla
This commit is contained in:
parent
a1137a1456
commit
e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions
|
|
@ -0,0 +1 @@
|
|||
from . import hr_leave_attendance_report
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.sql import SQL, drop_view_if_exists
|
||||
|
||||
|
||||
class HrLeaveAttendanceReport(models.Model):
|
||||
_name = "hr.leave.attendance.report"
|
||||
_description = "Attendance and Leave Analysis Report"
|
||||
_auto = False
|
||||
|
||||
def _compute_display_name(self):
|
||||
for rec in self:
|
||||
rec.display_name = f"{rec.employee_id.display_name}, {rec.date}"
|
||||
|
||||
date = fields.Date("Date")
|
||||
employee_id = fields.Many2one("hr.employee", string="Employee")
|
||||
active = fields.Boolean(related="employee_id.active")
|
||||
department_id = fields.Many2one(related="employee_id.department_id", string="Department")
|
||||
job_id = fields.Many2one(related="employee_id.job_id", string="Job Position")
|
||||
schedule_id = fields.Many2one("resource.calendar", string="Working Schedule")
|
||||
expected_hours = fields.Float("Expected Hours")
|
||||
worked_hours = fields.Float("Worked Hours")
|
||||
leave_hours = fields.Float("Approved Time Off")
|
||||
difference_hours = fields.Float("Difference", help="Worked Hours - Expected Hours + Approved Time Off")
|
||||
|
||||
leave_type_names = fields.Char("Time Off Types", compute="_compute_leave_attendance_fields")
|
||||
leave_ids = fields.Many2many("hr.leave", string="Time Offs", compute="_compute_leave_attendance_fields")
|
||||
attendance_ids = fields.Many2many("hr.attendance", string="Attendances", compute="_compute_leave_attendance_fields")
|
||||
|
||||
@api.depends('employee_id', 'date')
|
||||
def _compute_leave_attendance_fields(self):
|
||||
today = fields.Date.today()
|
||||
min_date = today - relativedelta(years=1)
|
||||
max_date = today - relativedelta(days=1)
|
||||
|
||||
leaves_by_employees = dict(self.env['hr.leave']._read_group(
|
||||
domain=[
|
||||
('employee_id', 'in', self.employee_id.ids),
|
||||
('state', '=', 'validate'),
|
||||
('date_from', '<=', max_date),
|
||||
('date_to', '>=', min_date),
|
||||
],
|
||||
groupby=['employee_id'],
|
||||
aggregates=['id:recordset'],
|
||||
))
|
||||
attendances_by_employees = dict(self.env['hr.attendance']._read_group(
|
||||
domain=[
|
||||
('employee_id', 'in', self.employee_id.ids),
|
||||
('check_in', '>=', min_date),
|
||||
('check_in', '<=', max_date),
|
||||
],
|
||||
groupby=['employee_id'],
|
||||
aggregates=['id:recordset'],
|
||||
))
|
||||
|
||||
for rec in self:
|
||||
leaves = leaves_by_employees.get(rec.employee_id, self.env['hr.leave'])
|
||||
rec_date_leaves = leaves.filtered(
|
||||
lambda lv: self._timestamped(lv.date_from) <= rec.date <= self._timestamped(lv.date_to),
|
||||
)
|
||||
rec.leave_ids = rec_date_leaves.ids
|
||||
leave_type_ids = rec_date_leaves.mapped('holiday_status_id')
|
||||
rec.leave_type_names = ', '.join(leave_type_ids.mapped('name'))
|
||||
|
||||
attendances = attendances_by_employees.get(rec.employee_id, self.env['hr.attendance'])
|
||||
rec.attendance_ids = attendances.filtered(
|
||||
lambda att: self._timestamped(att.check_in) == rec.date,
|
||||
).ids
|
||||
|
||||
def _timestamped(self, date):
|
||||
return fields.Datetime.context_timestamp(self, date).date()
|
||||
|
||||
def _select(self):
|
||||
return """
|
||||
SELECT row_number() OVER (ORDER BY gs.day DESC, emp.id) AS id,
|
||||
gs.day::date AS date,
|
||||
emp.id AS employee_id,
|
||||
rc.id AS schedule_id,
|
||||
ROUND(COALESCE(att.worked_hours, 0.0)::numeric, 2) AS worked_hours,
|
||||
ROUND(COALESCE(rc.hours_per_day, 0.0)::numeric, 2) AS expected_hours,
|
||||
ROUND(COALESCE(dlh.leave_hours, 0.0)::numeric, 2) AS leave_hours,
|
||||
(
|
||||
ROUND(COALESCE(att.worked_hours, 0.0)::numeric, 2)
|
||||
- ROUND(COALESCE(rc.hours_per_day, 0.0)::numeric, 2)
|
||||
+ ROUND(COALESCE(dlh.leave_hours, 0.0)::numeric, 2)
|
||||
) AS difference_hours
|
||||
"""
|
||||
|
||||
def _from(self):
|
||||
return """
|
||||
FROM hr_employee AS emp
|
||||
CROSS JOIN LATERAL generate_series(
|
||||
(date_trunc('month', CURRENT_DATE) - INTERVAL '1 year')::date,
|
||||
(CURRENT_DATE - 1)::date,
|
||||
INTERVAL '1 day'
|
||||
) AS gs(day)
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT resource_calendar_id
|
||||
FROM hr_version AS v
|
||||
WHERE v.employee_id = emp.id
|
||||
AND v.contract_date_start IS NOT NULL
|
||||
AND v.contract_date_start <= gs.day
|
||||
AND (v.contract_date_end >= gs.day OR v.contract_date_end IS NULL)
|
||||
AND v.date_version <= gs.day
|
||||
ORDER BY v.date_version DESC
|
||||
LIMIT 1
|
||||
) AS ver
|
||||
ON TRUE
|
||||
"""
|
||||
|
||||
def _join_attendance(self):
|
||||
return """
|
||||
LEFT JOIN (
|
||||
SELECT employee_id,
|
||||
(check_in AT TIME ZONE 'UTC')::date AS check_date,
|
||||
SUM(worked_hours) AS worked_hours
|
||||
FROM hr_attendance
|
||||
GROUP BY employee_id, check_date
|
||||
) AS att
|
||||
ON att.employee_id = emp.id
|
||||
AND att.check_date = gs.day
|
||||
"""
|
||||
|
||||
def _join_calendar(self):
|
||||
return """
|
||||
JOIN resource_calendar AS rc
|
||||
ON ver.resource_calendar_id = rc.id
|
||||
"""
|
||||
|
||||
def _join_calendar_leaves(self):
|
||||
return """
|
||||
LEFT JOIN resource_calendar_leaves AS rcl
|
||||
ON (
|
||||
(rc.id = rcl.calendar_id OR rcl.calendar_id IS NULL)
|
||||
AND rcl.resource_id IS NULL
|
||||
AND rcl.company_id = emp.company_id
|
||||
AND gs.day
|
||||
BETWEEN (rcl.date_from AT TIME ZONE 'UTC')::date
|
||||
AND (rcl.date_to AT TIME ZONE 'UTC')::date
|
||||
)
|
||||
"""
|
||||
|
||||
def _join_resource_calendar_attendance(self):
|
||||
return """
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT ON (calendar_id, dayofweek) *
|
||||
FROM resource_calendar_attendance
|
||||
ORDER BY calendar_id, dayofweek
|
||||
) AS rca
|
||||
ON rc.id = rca.calendar_id
|
||||
AND CAST(rca.dayofweek AS INTEGER) = (
|
||||
CASE
|
||||
WHEN EXTRACT(DOW FROM gs.day) = 0 THEN 6
|
||||
ELSE EXTRACT(DOW FROM gs.day) - 1
|
||||
END
|
||||
) -- to map days between Odoo (Monday = 0, Tuesday = 1, ...) and Postgres (Sunday = 0, Monday = 1, ...)
|
||||
"""
|
||||
|
||||
def _join_daily_leave_hours(self):
|
||||
""" Generates a SQL join clause to calculate the total `leave_hours` taken for a specific day:
|
||||
- Sums up `number_of_hours` for all validated time offs within the day.
|
||||
- Excludes non-working days based on the employee's resource calendar and calendar leaves.
|
||||
- Handles leaves spanning multiple days, ensuring only working days are counted.
|
||||
- Converts all date fields to UTC to maintain consistency with Odoo's date storage.
|
||||
"""
|
||||
return """
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT SUM(lv.number_of_hours / NULLIF(wd.working_days,0)) AS leave_hours
|
||||
FROM hr_leave AS lv
|
||||
JOIN LATERAL (
|
||||
SELECT COUNT(*) AS working_days
|
||||
FROM generate_series(
|
||||
(lv.date_from AT TIME ZONE 'UTC')::date,
|
||||
(lv.date_to AT TIME ZONE 'UTC')::date,
|
||||
INTERVAL '1 day'
|
||||
)
|
||||
AS d(day)
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT ON (calendar_id, dayofweek) *
|
||||
FROM resource_calendar_attendance
|
||||
ORDER BY calendar_id, dayofweek
|
||||
) AS rca2
|
||||
ON rc.id = rca2.calendar_id
|
||||
AND CAST(rca2.dayofweek AS INTEGER) = (
|
||||
CASE
|
||||
WHEN EXTRACT(DOW FROM d.day) = 0 THEN 6
|
||||
ELSE EXTRACT(DOW FROM d.day) - 1
|
||||
END
|
||||
)
|
||||
LEFT JOIN resource_calendar_leaves rcl2
|
||||
ON (
|
||||
(rc.id = rcl2.calendar_id OR rcl2.calendar_id IS NULL)
|
||||
AND rcl2.resource_id IS NULL
|
||||
AND rcl2.company_id = emp.company_id
|
||||
AND d.day
|
||||
BETWEEN (rcl2.date_from AT TIME ZONE 'UTC')::date
|
||||
AND (rcl2.date_to AT TIME ZONE 'UTC')::date
|
||||
)
|
||||
WHERE rcl2.id IS NULL
|
||||
) AS wd ON TRUE
|
||||
WHERE lv.employee_id = emp.id
|
||||
AND gs.day
|
||||
BETWEEN (lv.date_from AT TIME ZONE 'UTC')::date
|
||||
AND (lv.date_to AT TIME ZONE 'UTC')::date
|
||||
AND lv.state = 'validate'
|
||||
) AS dlh
|
||||
ON TRUE
|
||||
"""
|
||||
|
||||
def _where(self):
|
||||
return """
|
||||
WHERE rca.id IS NOT NULL
|
||||
AND rcl.id IS NULL
|
||||
"""
|
||||
|
||||
def init(self):
|
||||
drop_view_if_exists(self.env.cr, self._table)
|
||||
self.env.cr.execute(SQL("""
|
||||
CREATE OR REPLACE VIEW %s AS (
|
||||
%s -- select
|
||||
%s -- from
|
||||
%s -- join_attendance
|
||||
%s -- join_calendar
|
||||
%s -- join_calendar_leaves
|
||||
%s -- join_resource_calendar_attendance
|
||||
%s -- join_daily_leave_hours
|
||||
%s -- where
|
||||
)""",
|
||||
SQL.identifier(self._table),
|
||||
SQL(self._select()),
|
||||
SQL(self._from()),
|
||||
SQL(self._join_attendance()),
|
||||
SQL(self._join_calendar()),
|
||||
SQL(self._join_calendar_leaves()),
|
||||
SQL(self._join_resource_calendar_attendance()),
|
||||
SQL(self._join_daily_leave_hours()),
|
||||
SQL(self._where()),
|
||||
))
|
||||
Loading…
Add table
Add a link
Reference in a new issue