Updated grace period, recruitment auto doc, and shift swap
This commit is contained in:
parent
14427d20fe
commit
f698203a7f
|
|
@ -1,4 +1,5 @@
|
||||||
from odoo import api, fields, models, tools, _
|
from odoo import api, fields, models, tools, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
class AttendanceAnalytics(models.Model):
|
class AttendanceAnalytics(models.Model):
|
||||||
|
|
@ -64,6 +65,8 @@ class AttendanceAnalytics(models.Model):
|
||||||
|
|
||||||
('absent', 'Absent'),
|
('absent', 'Absent'),
|
||||||
|
|
||||||
|
('invalid_attendance', 'Invalid Attendance'),
|
||||||
|
|
||||||
('half_day', 'Half Day'),
|
('half_day', 'Half Day'),
|
||||||
|
|
||||||
('late_in', 'Late In'),
|
('late_in', 'Late In'),
|
||||||
|
|
@ -192,7 +195,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if rec.status == 'present':
|
if rec.status == 'present':
|
||||||
rec.color = 10
|
rec.color = 10
|
||||||
elif rec.status == 'absent':
|
elif rec.status in ('absent', 'invalid_attendance'):
|
||||||
rec.color = 1
|
rec.color = 1
|
||||||
elif rec.status == 'half_day':
|
elif rec.status == 'half_day':
|
||||||
rec.color = 2
|
rec.color = 2
|
||||||
|
|
@ -280,6 +283,11 @@ class AttendanceAnalytics(models.Model):
|
||||||
def action_create_shiftswap_request(self):
|
def action_create_shiftswap_request(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
|
if self.date and self.date < fields.Date.context_today(self):
|
||||||
|
raise UserError(
|
||||||
|
_("Shift Swap cannot be requested for past dates.")
|
||||||
|
)
|
||||||
|
|
||||||
request = self.env['shift.swap.request'].search([
|
request = self.env['shift.swap.request'].search([
|
||||||
('employee_id', '=', self.employee_id.id),
|
('employee_id', '=', self.employee_id.id),
|
||||||
('roster_date', '=', self.date),
|
('roster_date', '=', self.date),
|
||||||
|
|
@ -341,7 +349,11 @@ class AttendanceAnalytics(models.Model):
|
||||||
|
|
||||||
att.employee_id,
|
att.employee_id,
|
||||||
|
|
||||||
DATE(att.check_in)
|
DATE(
|
||||||
|
att.check_in
|
||||||
|
AT TIME ZONE 'UTC'
|
||||||
|
AT TIME ZONE 'Asia/Kolkata'
|
||||||
|
)
|
||||||
AS attendance_date,
|
AS attendance_date,
|
||||||
|
|
||||||
MIN(att.check_in)
|
MIN(att.check_in)
|
||||||
|
|
@ -350,6 +362,18 @@ class AttendanceAnalytics(models.Model):
|
||||||
MAX(att.check_out)
|
MAX(att.check_out)
|
||||||
AS max_check_out,
|
AS max_check_out,
|
||||||
|
|
||||||
|
MIN(
|
||||||
|
att.check_in
|
||||||
|
AT TIME ZONE 'UTC'
|
||||||
|
AT TIME ZONE 'Asia/Kolkata'
|
||||||
|
) AS min_check_in_local,
|
||||||
|
|
||||||
|
MAX(
|
||||||
|
att.check_out
|
||||||
|
AT TIME ZONE 'UTC'
|
||||||
|
AT TIME ZONE 'Asia/Kolkata'
|
||||||
|
) AS max_check_out_local,
|
||||||
|
|
||||||
SUM(att.worked_hours)
|
SUM(att.worked_hours)
|
||||||
AS worked_hours
|
AS worked_hours
|
||||||
|
|
||||||
|
|
@ -359,7 +383,11 @@ class AttendanceAnalytics(models.Model):
|
||||||
|
|
||||||
att.employee_id,
|
att.employee_id,
|
||||||
|
|
||||||
DATE(att.check_in)
|
DATE(
|
||||||
|
att.check_in
|
||||||
|
AT TIME ZONE 'UTC'
|
||||||
|
AT TIME ZONE 'Asia/Kolkata'
|
||||||
|
)
|
||||||
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -432,6 +460,32 @@ class AttendanceAnalytics(models.Model):
|
||||||
WHERE rl.date_from IS NOT NULL
|
WHERE rl.date_from IS NOT NULL
|
||||||
AND rl.date_to IS NOT NULL
|
AND rl.date_to IS NOT NULL
|
||||||
AND rl.resource_id IS NULL
|
AND rl.resource_id IS NULL
|
||||||
|
),
|
||||||
|
|
||||||
|
calendar_day_schedule AS (
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
|
||||||
|
rca.calendar_id,
|
||||||
|
|
||||||
|
rca.dayofweek,
|
||||||
|
|
||||||
|
MIN(rca.hour_from) * 60
|
||||||
|
AS shift_start_minutes,
|
||||||
|
|
||||||
|
MAX(rca.hour_to) * 60
|
||||||
|
AS shift_end_minutes,
|
||||||
|
|
||||||
|
SUM(rca.hour_to - rca.hour_from)
|
||||||
|
AS hours_per_day
|
||||||
|
|
||||||
|
FROM resource_calendar_attendance rca
|
||||||
|
|
||||||
|
GROUP BY
|
||||||
|
|
||||||
|
rca.calendar_id,
|
||||||
|
|
||||||
|
rca.dayofweek
|
||||||
)
|
)
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -450,20 +504,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
ats.worked_hours,
|
ats.worked_hours,
|
||||||
0
|
0
|
||||||
) AS worked_hours,
|
) AS worked_hours,
|
||||||
rc.hours_per_day AS hours_per_day,
|
es.hours_per_day AS hours_per_day,
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.hours_per_day
|
es.hours_per_day
|
||||||
+
|
+
|
||||||
COALESCE(rc.over_time_hrs, 0)
|
COALESCE(rc.over_time_hrs, 0)
|
||||||
) AS allowed_ot_limit,
|
) AS allowed_ot_limit,
|
||||||
|
|
||||||
CASE
|
CASE
|
||||||
|
|
||||||
WHEN ats.worked_hours > rc.hours_per_day
|
WHEN ats.worked_hours > es.hours_per_day
|
||||||
|
|
||||||
THEN
|
THEN
|
||||||
ats.worked_hours - rc.hours_per_day
|
ats.worked_hours - es.hours_per_day
|
||||||
|
|
||||||
ELSE 0
|
ELSE 0
|
||||||
|
|
||||||
|
|
@ -474,7 +528,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
WHEN ats.worked_hours >
|
WHEN ats.worked_hours >
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.hours_per_day
|
es.hours_per_day
|
||||||
+
|
+
|
||||||
COALESCE(rc.over_time_hrs, 0)
|
COALESCE(rc.over_time_hrs, 0)
|
||||||
)
|
)
|
||||||
|
|
@ -527,19 +581,11 @@ class AttendanceAnalytics(models.Model):
|
||||||
AS department_grace_period,
|
AS department_grace_period,
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
) AS expected_check_in,
|
) AS expected_check_in,
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
) AS expected_check_out,
|
) AS expected_check_out,
|
||||||
|
|
||||||
CASE
|
CASE
|
||||||
|
|
@ -551,28 +597,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR FROM ats.min_check_in
|
HOUR FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -591,28 +629,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR FROM ats.min_check_in
|
HOUR FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
THEN TRUE
|
THEN TRUE
|
||||||
|
|
@ -624,7 +654,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
+
|
+
|
||||||
|
|
@ -640,28 +670,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -671,7 +693,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
|
|
||||||
END
|
END
|
||||||
|
|
||||||
) AS required_checkout_time,
|
) / 60.0 AS required_checkout_time,
|
||||||
|
|
||||||
CASE
|
CASE
|
||||||
|
|
||||||
|
|
@ -683,7 +705,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
+
|
+
|
||||||
|
|
@ -699,29 +721,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time
|
|
||||||
* 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -739,13 +752,13 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -767,13 +780,13 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -782,7 +795,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
+
|
+
|
||||||
|
|
@ -798,29 +811,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time
|
|
||||||
* 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -842,7 +846,7 @@ class AttendanceAnalytics(models.Model):
|
||||||
WHEN
|
WHEN
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
+
|
+
|
||||||
|
|
@ -858,29 +862,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
-
|
-
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time
|
|
||||||
* 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
|
||||||
0
|
0
|
||||||
|
|
@ -897,13 +892,13 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -932,35 +927,30 @@ class AttendanceAnalytics(models.Model):
|
||||||
WHEN ats.min_check_in IS NULL
|
WHEN ats.min_check_in IS NULL
|
||||||
THEN 'absent'
|
THEN 'absent'
|
||||||
|
|
||||||
WHEN ats.worked_hours < (rc.hours_per_day / 2.0)
|
WHEN ats.worked_hours < 1.0
|
||||||
|
THEN 'invalid_attendance'
|
||||||
|
|
||||||
|
WHEN ats.worked_hours < (es.hours_per_day / 2.0)
|
||||||
THEN 'half_day'
|
THEN 'half_day'
|
||||||
|
|
||||||
WHEN
|
WHEN
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR FROM ats.min_check_in
|
HOUR FROM ats.min_check_in_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.min_check_in
|
FROM ats.min_check_in_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
(
|
(
|
||||||
(
|
es.shift_start_minutes
|
||||||
rc.shift_start_time * 60
|
|
||||||
)
|
|
||||||
+
|
|
||||||
COALESCE(
|
|
||||||
dg.grace_period,
|
|
||||||
rc.late_grace_period,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
THEN 'late_in'
|
THEN 'late_in'
|
||||||
|
|
@ -970,20 +960,20 @@ class AttendanceAnalytics(models.Model):
|
||||||
(
|
(
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
HOUR
|
HOUR
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
+
|
+
|
||||||
EXTRACT(
|
EXTRACT(
|
||||||
MINUTE
|
MINUTE
|
||||||
FROM ats.max_check_out
|
FROM ats.max_check_out_local
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
<
|
<
|
||||||
|
|
||||||
(
|
(
|
||||||
rc.shift_end_time * 60
|
es.shift_end_minutes
|
||||||
)
|
)
|
||||||
|
|
||||||
THEN 'early_out'
|
THEN 'early_out'
|
||||||
|
|
@ -1026,6 +1016,36 @@ class AttendanceAnalytics(models.Model):
|
||||||
LEFT JOIN resource_calendar rc
|
LEFT JOIN resource_calendar rc
|
||||||
ON rc.id = ed.resource_calendar_id
|
ON rc.id = ed.resource_calendar_id
|
||||||
|
|
||||||
|
LEFT JOIN calendar_day_schedule cds
|
||||||
|
ON cds.calendar_id = ed.resource_calendar_id
|
||||||
|
AND cds.dayofweek = (
|
||||||
|
(
|
||||||
|
(
|
||||||
|
EXTRACT(DOW FROM ed.date)::integer
|
||||||
|
+ 6
|
||||||
|
) % 7
|
||||||
|
)::text
|
||||||
|
)
|
||||||
|
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(rc.shift_start_time * 60, 0),
|
||||||
|
cds.shift_start_minutes,
|
||||||
|
0
|
||||||
|
) AS shift_start_minutes,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(rc.shift_end_time * 60, 0),
|
||||||
|
cds.shift_end_minutes,
|
||||||
|
0
|
||||||
|
) AS shift_end_minutes,
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(rc.hours_per_day, 0),
|
||||||
|
cds.hours_per_day,
|
||||||
|
0
|
||||||
|
) AS hours_per_day
|
||||||
|
) es ON TRUE
|
||||||
|
|
||||||
LEFT JOIN department_grace dg
|
LEFT JOIN department_grace dg
|
||||||
ON dg.calendar_id
|
ON dg.calendar_id
|
||||||
= ed.resource_calendar_id
|
= ed.resource_calendar_id
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
<field name="name">attendance.analytics.list</field>
|
<field name="name">attendance.analytics.list</field>
|
||||||
<field name="model">attendance.analytics</field>
|
<field name="model">attendance.analytics</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list string="Attendance Analytics">
|
<list string="Attendance Analytics"
|
||||||
|
decoration-danger="status == 'absent' or status == 'invalid_attendance'">
|
||||||
<field name="employee_id"/>
|
<field name="employee_id"/>
|
||||||
<field name="department_id"/>
|
<field name="department_id"/>
|
||||||
<field name="date"/>
|
<field name="date"/>
|
||||||
|
|
@ -28,7 +29,7 @@
|
||||||
<field name="status"
|
<field name="status"
|
||||||
widget="badge"
|
widget="badge"
|
||||||
decoration-success="status == 'present'"
|
decoration-success="status == 'present'"
|
||||||
decoration-danger="status == 'absent'"
|
decoration-danger="status == 'absent' or status == 'invalid_attendance'"
|
||||||
decoration-warning="status == 'late_in'"
|
decoration-warning="status == 'late_in'"
|
||||||
decoration-info="status == 'half_day'"
|
decoration-info="status == 'half_day'"
|
||||||
decoration-primary="status == 'holiday'"/>
|
decoration-primary="status == 'holiday'"/>
|
||||||
|
|
@ -146,4 +147,4 @@
|
||||||
action="action_attendance_analytics"
|
action="action_attendance_analytics"
|
||||||
sequence="-51"/>
|
sequence="-51"/>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
parsed_data = self._post_process_jd_data(parsed_data, parsed_payload["text"])
|
parsed_data = self._post_process_jd_data(parsed_data, parsed_payload["text"])
|
||||||
else:
|
else:
|
||||||
parsed_data = self._post_process_resume_data(parsed_data, parsed_payload["text"], line.file_name)
|
parsed_data = self._post_process_resume_data(parsed_data, parsed_payload["text"], line.file_name)
|
||||||
line.extracted_payload = json.dumps(parsed_data, indent=2, ensure_ascii=False)
|
line.write(dict(
|
||||||
|
line._prepare_editable_vals(parsed_data),
|
||||||
|
extracted_payload=json.dumps(parsed_data, indent=2, ensure_ascii=False),
|
||||||
|
))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
processed += 1
|
processed += 1
|
||||||
|
|
@ -255,6 +258,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parsed_data = json.loads(line.extracted_payload)
|
parsed_data = json.loads(line.extracted_payload)
|
||||||
|
parsed_data = line._get_edited_parsed_data(parsed_data)
|
||||||
|
|
||||||
with self.env.cr.savepoint():
|
with self.env.cr.savepoint():
|
||||||
|
|
||||||
|
|
@ -642,10 +646,13 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
data["email"] = email_match.group(1)
|
data["email"] = email_match.group(1)
|
||||||
|
|
||||||
phone_matches = re.findall(r"(\+?\d[\d\-\s()]{7,}\d)", extracted_text)
|
phone_matches = re.findall(r"(\+?\d[\d\-\s()]{7,}\d)", extracted_text)
|
||||||
if phone_matches and not data.get("phone"):
|
phone_values = []
|
||||||
data["phone"] = phone_matches[0].strip()
|
for phone_candidate in [data.get("phone"), data.get("alternate_phone")] + phone_matches:
|
||||||
if len(phone_matches) > 1 and not data.get("alternate_phone"):
|
phone_value = self._clean_resume_phone_value(phone_candidate)
|
||||||
data["alternate_phone"] = phone_matches[1].strip()
|
if phone_value and phone_value not in phone_values:
|
||||||
|
phone_values.append(phone_value)
|
||||||
|
data["phone"] = phone_values[0] if phone_values else False
|
||||||
|
data["alternate_phone"] = phone_values[1] if len(phone_values) > 1 else False
|
||||||
|
|
||||||
linkedin_match = re.search(r"(https?://(?:www\.)?linkedin\.com/[^\s]+)", extracted_text, re.I)
|
linkedin_match = re.search(r"(https?://(?:www\.)?linkedin\.com/[^\s]+)", extracted_text, re.I)
|
||||||
if linkedin_match:
|
if linkedin_match:
|
||||||
|
|
@ -654,11 +661,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
if not data.get("total_experience_years"):
|
if not data.get("total_experience_years"):
|
||||||
data["total_experience_years"] = self._guess_total_experience(extracted_text)
|
data["total_experience_years"] = self._guess_total_experience(extracted_text)
|
||||||
|
|
||||||
|
resume_skills = self._normalize_resume_skill_values(data.get("skills") or [], extracted_text)
|
||||||
data["skills"] = self.env[
|
data["skills"] = self.env[
|
||||||
"document.parser.service"
|
"document.parser.service"
|
||||||
].validate_explicit_skills(
|
].validate_explicit_skills(
|
||||||
extracted_text,
|
extracted_text,
|
||||||
data.get("skills") or []
|
resume_skills
|
||||||
)
|
)
|
||||||
data["education_history"] = self._normalize_resume_list(data.get("education_history"))
|
data["education_history"] = self._normalize_resume_list(data.get("education_history"))
|
||||||
data["employer_history"] = self._normalize_resume_list(data.get("employer_history"))
|
data["employer_history"] = self._normalize_resume_list(data.get("employer_history"))
|
||||||
|
|
@ -1431,8 +1439,57 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
normalized = re.sub(r"[^\d+]", "", value)
|
normalized = re.sub(r"[^\d+]", "", value)
|
||||||
return normalized or False
|
return normalized or False
|
||||||
|
|
||||||
|
def _clean_resume_phone_value(self, value):
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
raw_value = str(value).strip()
|
||||||
|
if re.fullmatch(r"(?:19|20)\d{2}\s*[-/]\s*(?:19|20)\d{2}", raw_value):
|
||||||
|
return False
|
||||||
|
normalized = self._normalize_phone(raw_value)
|
||||||
|
digits = re.sub(r"\D", "", normalized or "")
|
||||||
|
if len(digits) < 10 or len(digits) > 15:
|
||||||
|
return False
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
def _normalize_resume_skill_values(self, skills, extracted_text):
|
||||||
|
skill_parts = []
|
||||||
|
for skill in skills:
|
||||||
|
for skill_part in self._split_resume_skill_value(skill):
|
||||||
|
normalized = self._normalize_skill_name(skill_part)
|
||||||
|
if normalized and self._is_valid_resume_skill(normalized, extracted_text):
|
||||||
|
skill_parts.append(normalized)
|
||||||
|
normalized_skills = []
|
||||||
|
index = 0
|
||||||
|
while index < len(skill_parts):
|
||||||
|
current_skill = skill_parts[index]
|
||||||
|
next_skill = skill_parts[index + 1] if index + 1 < len(skill_parts) else False
|
||||||
|
combined_skill = "%s %s" % (current_skill, next_skill) if next_skill else False
|
||||||
|
if combined_skill and self._is_valid_resume_skill(combined_skill, extracted_text):
|
||||||
|
normalized_skills.append(combined_skill)
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
normalized_skills.append(current_skill)
|
||||||
|
index += 1
|
||||||
|
return self._deduplicate_skill_names(normalized_skills)
|
||||||
|
|
||||||
|
def _split_resume_skill_value(self, value):
|
||||||
|
value = re.sub(r"^[\s\-*\u2022•]+", "", str(value or "")).strip()
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
if ":" in value:
|
||||||
|
label, value = [item.strip() for item in value.split(":", 1)]
|
||||||
|
if not re.search(r"(programming|frameworks?|databases?|analytics?|cloud|devops|tools?|technologies|skills?)", label, re.I):
|
||||||
|
value = "%s %s" % (label, value)
|
||||||
|
value = re.sub(r"\((?:advanced|intermediate|beginner|expert)\)", "", value, flags=re.I)
|
||||||
|
return [item for item in re.split(r"[,;|/]", value) if item.strip()]
|
||||||
|
|
||||||
def _normalize_skill_name(self, value):
|
def _normalize_skill_name(self, value):
|
||||||
value = re.sub(r"\s+", " ", (value or "")).strip(" -,:;")
|
value = re.sub(r"^[\s\-*\u2022•]+", "", value or "")
|
||||||
|
value = re.sub(r"\s+", " ", value).strip(" -,:;()")
|
||||||
|
if ":" in value:
|
||||||
|
label, skill_value = [item.strip() for item in value.split(":", 1)]
|
||||||
|
if re.search(r"(programming|frameworks?|databases?|analytics?|cloud|devops|tools?|technologies|skills?)", label, re.I):
|
||||||
|
value = skill_value
|
||||||
value = re.sub(r"^[0-9.)\-(\s]+", "", value).strip()
|
value = re.sub(r"^[0-9.)\-(\s]+", "", value).strip()
|
||||||
if not value:
|
if not value:
|
||||||
return False
|
return False
|
||||||
|
|
@ -2034,13 +2091,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"<ul>",
|
"<ul>",
|
||||||
]
|
]
|
||||||
for row in rows:
|
for row in rows:
|
||||||
html_parts.append(
|
html_parts.append("<li><strong>%s</strong>: <span class='text-%s'>%s</span>" % (
|
||||||
"<li><strong>%s</strong>: <span class='text-%s'>%s</span></li>" % (
|
escape(row["filename"]),
|
||||||
escape(row["filename"]),
|
row["level"],
|
||||||
row["level"],
|
escape(row["message"]),
|
||||||
escape(row["message"]),
|
))
|
||||||
)
|
html_parts.append("</li>")
|
||||||
)
|
|
||||||
html_parts.extend(["</ul>", "</div>"])
|
html_parts.extend(["</ul>", "</div>"])
|
||||||
return "".join(html_parts)
|
return "".join(html_parts)
|
||||||
|
|
||||||
|
|
@ -2072,3 +2128,128 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
|
||||||
extracted_payload = fields.Text(readonly=True)
|
extracted_payload = fields.Text(readonly=True)
|
||||||
candidate_id = fields.Many2one("hr.candidate", readonly=True)
|
candidate_id = fields.Many2one("hr.candidate", readonly=True)
|
||||||
applicant_id = fields.Many2one("hr.applicant", readonly=True)
|
applicant_id = fields.Many2one("hr.applicant", readonly=True)
|
||||||
|
target_model = fields.Selection(related="wizard_id.target_model", readonly=True)
|
||||||
|
|
||||||
|
full_name = fields.Char()
|
||||||
|
first_name = fields.Char()
|
||||||
|
last_name = fields.Char()
|
||||||
|
email = fields.Char()
|
||||||
|
phone = fields.Char()
|
||||||
|
alternate_phone = fields.Char()
|
||||||
|
linkedin_profile = fields.Char()
|
||||||
|
current_location = fields.Char()
|
||||||
|
current_organization = fields.Char()
|
||||||
|
total_experience_years = fields.Float()
|
||||||
|
relevant_experience_years = fields.Float()
|
||||||
|
notice_period = fields.Char()
|
||||||
|
degree = fields.Char()
|
||||||
|
skills_text = fields.Text(string="Skills")
|
||||||
|
summary = fields.Text()
|
||||||
|
|
||||||
|
request_id = fields.Char(string="Request ID")
|
||||||
|
start_date = fields.Char()
|
||||||
|
end_date = fields.Char()
|
||||||
|
site_location = fields.Char()
|
||||||
|
job_title = fields.Char()
|
||||||
|
job_summary = fields.Text()
|
||||||
|
requirements = fields.Text()
|
||||||
|
primary_skills_text = fields.Text(string="Primary Skills")
|
||||||
|
secondary_skills_text = fields.Text(string="Secondary Skills")
|
||||||
|
budget = fields.Char()
|
||||||
|
experience_years = fields.Float()
|
||||||
|
job_category = fields.Char()
|
||||||
|
|
||||||
|
def action_save_line_changes(self):
|
||||||
|
self.ensure_one()
|
||||||
|
parsed_data = self._get_edited_parsed_data()
|
||||||
|
self.extracted_payload = json.dumps(parsed_data, indent=2, ensure_ascii=False)
|
||||||
|
return {"type": "ir.actions.act_window_close"}
|
||||||
|
|
||||||
|
def _prepare_editable_vals(self, parsed_data):
|
||||||
|
parsed_data = parsed_data or {}
|
||||||
|
return {
|
||||||
|
"full_name": parsed_data.get("full_name"),
|
||||||
|
"first_name": parsed_data.get("first_name"),
|
||||||
|
"last_name": parsed_data.get("last_name"),
|
||||||
|
"email": parsed_data.get("email"),
|
||||||
|
"phone": parsed_data.get("phone"),
|
||||||
|
"alternate_phone": parsed_data.get("alternate_phone"),
|
||||||
|
"linkedin_profile": parsed_data.get("linkedin_profile"),
|
||||||
|
"current_location": parsed_data.get("current_location"),
|
||||||
|
"current_organization": parsed_data.get("current_organization"),
|
||||||
|
"total_experience_years": self._float_or_zero(parsed_data.get("total_experience_years")),
|
||||||
|
"relevant_experience_years": self._float_or_zero(parsed_data.get("relevant_experience_years")),
|
||||||
|
"notice_period": parsed_data.get("notice_period"),
|
||||||
|
"degree": parsed_data.get("degree"),
|
||||||
|
"skills_text": self._list_to_text(parsed_data.get("skills")),
|
||||||
|
"summary": parsed_data.get("summary"),
|
||||||
|
"request_id": parsed_data.get("request_id"),
|
||||||
|
"start_date": parsed_data.get("start_date"),
|
||||||
|
"end_date": parsed_data.get("end_date"),
|
||||||
|
"site_location": parsed_data.get("site_location"),
|
||||||
|
"job_title": parsed_data.get("job_title"),
|
||||||
|
"job_summary": parsed_data.get("job_summary"),
|
||||||
|
"requirements": parsed_data.get("requirements"),
|
||||||
|
"primary_skills_text": self._list_to_text(parsed_data.get("primary_skills")),
|
||||||
|
"secondary_skills_text": self._list_to_text(parsed_data.get("secondary_skills")),
|
||||||
|
"budget": parsed_data.get("budget"),
|
||||||
|
"experience_years": self._float_or_zero(parsed_data.get("experience_years")),
|
||||||
|
"job_category": parsed_data.get("job_category"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_edited_parsed_data(self, base_data=None):
|
||||||
|
data = dict(base_data or {})
|
||||||
|
if self.target_model == "job_recruitment":
|
||||||
|
data.update({
|
||||||
|
"request_id": self.request_id,
|
||||||
|
"start_date": self.start_date,
|
||||||
|
"end_date": self.end_date,
|
||||||
|
"site_location": self.site_location,
|
||||||
|
"job_title": self.job_title,
|
||||||
|
"job_summary": self.job_summary,
|
||||||
|
"requirements": self.requirements,
|
||||||
|
"primary_skills": self._text_to_list(self.primary_skills_text),
|
||||||
|
"secondary_skills": self._text_to_list(self.secondary_skills_text),
|
||||||
|
"budget": self.budget,
|
||||||
|
"experience_years": self.experience_years,
|
||||||
|
"job_category": self.job_category,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
data.update({
|
||||||
|
"full_name": self.full_name,
|
||||||
|
"first_name": self.first_name,
|
||||||
|
"last_name": self.last_name,
|
||||||
|
"email": self.email,
|
||||||
|
"phone": self.phone,
|
||||||
|
"alternate_phone": self.alternate_phone,
|
||||||
|
"linkedin_profile": self.linkedin_profile,
|
||||||
|
"current_location": self.current_location,
|
||||||
|
"current_organization": self.current_organization,
|
||||||
|
"total_experience_years": self.total_experience_years,
|
||||||
|
"relevant_experience_years": self.relevant_experience_years,
|
||||||
|
"notice_period": self.notice_period,
|
||||||
|
"degree": self.degree,
|
||||||
|
"skills": self._text_to_list(self.skills_text),
|
||||||
|
"summary": self.summary,
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _list_to_text(self, value):
|
||||||
|
if not value:
|
||||||
|
return False
|
||||||
|
if isinstance(value, list):
|
||||||
|
return "\n".join(str(item) for item in value if item not in (False, None, ""))
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def _text_to_list(self, value):
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
return [item.strip(" -,\t") for item in str(value).splitlines() if item.strip(" -,\t")]
|
||||||
|
|
||||||
|
def _float_or_zero(self, value):
|
||||||
|
if value in (False, None, ""):
|
||||||
|
return 0.0
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,89 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<group string="Uploaded Files" invisible="not line_ids">
|
<group string="Uploaded Files" invisible="not line_ids">
|
||||||
<field name="line_ids" nolabel="1" readonly="1">
|
<field name="line_ids" nolabel="1" create="0" delete="0">
|
||||||
|
<form string="Parsed Document" create="0" delete="0">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="file_name" readonly="1"/>
|
||||||
|
<field name="state" readonly="1"/>
|
||||||
|
<field name="message" readonly="1"/>
|
||||||
|
<field name="target_model" invisible="1"/>
|
||||||
|
<field name="extracted_payload" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="candidate_id" readonly="1" invisible="target_model == 'job_recruitment'"/>
|
||||||
|
<field name="applicant_id" readonly="1" invisible="target_model != 'applicant'"/>
|
||||||
|
<field name="attachment_id" readonly="1"/>
|
||||||
|
<field name="file" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook invisible="target_model == 'job_recruitment'">
|
||||||
|
<page string="Candidate Details">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="full_name"/>
|
||||||
|
<field name="first_name"/>
|
||||||
|
<field name="last_name"/>
|
||||||
|
<field name="email"/>
|
||||||
|
<field name="phone"/>
|
||||||
|
<field name="alternate_phone"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="linkedin_profile"/>
|
||||||
|
<field name="current_location"/>
|
||||||
|
<field name="current_organization"/>
|
||||||
|
<field name="total_experience_years"/>
|
||||||
|
<field name="relevant_experience_years"/>
|
||||||
|
<field name="notice_period"/>
|
||||||
|
<field name="degree"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="summary" nolabel="1" placeholder="Summary"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Skills">
|
||||||
|
<field name="skills_text" nolabel="1" placeholder="Enter one skill per line"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
|
||||||
|
<notebook invisible="target_model != 'job_recruitment'">
|
||||||
|
<page string="Job Details">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="request_id"/>
|
||||||
|
<field name="job_title"/>
|
||||||
|
<field name="site_location"/>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="budget"/>
|
||||||
|
<field name="experience_years"/>
|
||||||
|
<field name="job_category"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="job_summary" nolabel="1" placeholder="Job Summary"/>
|
||||||
|
<field name="requirements" nolabel="1" placeholder="Requirements"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Skills">
|
||||||
|
<group>
|
||||||
|
<field name="primary_skills_text" nolabel="1" placeholder="Enter one primary skill per line"/>
|
||||||
|
<field name="secondary_skills_text" nolabel="1" placeholder="Enter one secondary skill per line"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<footer>
|
||||||
|
<button name="action_save_line_changes" string="Save & Close" type="object" class="btn-primary"/>
|
||||||
|
<button string="Close" special="cancel" class="btn-secondary"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
<kanban class="o_kanban_small_column">
|
<kanban class="o_kanban_small_column">
|
||||||
<templates>
|
<templates>
|
||||||
<t t-name="kanban-box">
|
<t t-name="kanban-box">
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,24 @@ class ShiftSwapRequest(models.Model):
|
||||||
store=True
|
store=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _validate_roster_date_not_past(self):
|
||||||
|
today = fields.Date.context_today(self)
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
if rec.roster_date and rec.roster_date < today:
|
||||||
|
raise UserError(
|
||||||
|
_("Shift Swap cannot be requested for past dates.")
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.constrains('roster_date')
|
||||||
|
def _check_roster_date_not_past(self):
|
||||||
|
self._validate_roster_date_not_past()
|
||||||
|
|
||||||
def action_submit(self):
|
def action_submit(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
||||||
|
rec._validate_roster_date_not_past()
|
||||||
|
|
||||||
if not rec.reason:
|
if not rec.reason:
|
||||||
raise UserError(
|
raise UserError(
|
||||||
_("Please enter reason.")
|
_("Please enter reason.")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue