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.exceptions import UserError
|
||||
|
||||
|
||||
class AttendanceAnalytics(models.Model):
|
||||
|
|
@ -64,6 +65,8 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
('absent', 'Absent'),
|
||||
|
||||
('invalid_attendance', 'Invalid Attendance'),
|
||||
|
||||
('half_day', 'Half Day'),
|
||||
|
||||
('late_in', 'Late In'),
|
||||
|
|
@ -192,7 +195,7 @@ class AttendanceAnalytics(models.Model):
|
|||
for rec in self:
|
||||
if rec.status == 'present':
|
||||
rec.color = 10
|
||||
elif rec.status == 'absent':
|
||||
elif rec.status in ('absent', 'invalid_attendance'):
|
||||
rec.color = 1
|
||||
elif rec.status == 'half_day':
|
||||
rec.color = 2
|
||||
|
|
@ -280,6 +283,11 @@ class AttendanceAnalytics(models.Model):
|
|||
def action_create_shiftswap_request(self):
|
||||
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([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('roster_date', '=', self.date),
|
||||
|
|
@ -341,7 +349,11 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
att.employee_id,
|
||||
|
||||
DATE(att.check_in)
|
||||
DATE(
|
||||
att.check_in
|
||||
AT TIME ZONE 'UTC'
|
||||
AT TIME ZONE 'Asia/Kolkata'
|
||||
)
|
||||
AS attendance_date,
|
||||
|
||||
MIN(att.check_in)
|
||||
|
|
@ -350,6 +362,18 @@ class AttendanceAnalytics(models.Model):
|
|||
MAX(att.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)
|
||||
AS worked_hours
|
||||
|
||||
|
|
@ -359,7 +383,11 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
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
|
||||
AND rl.date_to IS NOT 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
|
||||
|
|
@ -450,20 +504,20 @@ class AttendanceAnalytics(models.Model):
|
|||
ats.worked_hours,
|
||||
0
|
||||
) 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)
|
||||
) AS allowed_ot_limit,
|
||||
|
||||
CASE
|
||||
|
||||
WHEN ats.worked_hours > rc.hours_per_day
|
||||
WHEN ats.worked_hours > es.hours_per_day
|
||||
|
||||
THEN
|
||||
ats.worked_hours - rc.hours_per_day
|
||||
ats.worked_hours - es.hours_per_day
|
||||
|
||||
ELSE 0
|
||||
|
||||
|
|
@ -474,7 +528,7 @@ class AttendanceAnalytics(models.Model):
|
|||
WHEN ats.worked_hours >
|
||||
|
||||
(
|
||||
rc.hours_per_day
|
||||
es.hours_per_day
|
||||
+
|
||||
COALESCE(rc.over_time_hrs, 0)
|
||||
)
|
||||
|
|
@ -527,19 +581,11 @@ class AttendanceAnalytics(models.Model):
|
|||
AS department_grace_period,
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
) AS expected_check_in,
|
||||
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
) AS expected_check_out,
|
||||
|
||||
CASE
|
||||
|
|
@ -551,28 +597,20 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR FROM ats.min_check_in
|
||||
HOUR FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -591,28 +629,20 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR FROM ats.min_check_in
|
||||
HOUR FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
>
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
)
|
||||
|
||||
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(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -671,7 +693,7 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
END
|
||||
|
||||
) AS required_checkout_time,
|
||||
) / 60.0 AS required_checkout_time,
|
||||
|
||||
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(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time
|
||||
* 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -739,13 +752,13 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
)
|
||||
),
|
||||
|
||||
|
|
@ -767,13 +780,13 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
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(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time
|
||||
* 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -842,7 +846,7 @@ class AttendanceAnalytics(models.Model):
|
|||
WHEN
|
||||
(
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
)
|
||||
|
||||
+
|
||||
|
|
@ -858,29 +862,20 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time
|
||||
* 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
),
|
||||
|
||||
0
|
||||
|
|
@ -897,13 +892,13 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
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
|
||||
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'
|
||||
|
||||
WHEN
|
||||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR FROM ats.min_check_in
|
||||
HOUR FROM ats.min_check_in_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
FROM ats.min_check_in_local
|
||||
)
|
||||
)
|
||||
|
||||
>
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
es.shift_start_minutes
|
||||
)
|
||||
|
||||
THEN 'late_in'
|
||||
|
|
@ -970,20 +960,20 @@ class AttendanceAnalytics(models.Model):
|
|||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.max_check_out
|
||||
FROM ats.max_check_out_local
|
||||
)
|
||||
)
|
||||
|
||||
<
|
||||
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
es.shift_end_minutes
|
||||
)
|
||||
|
||||
THEN 'early_out'
|
||||
|
|
@ -1026,6 +1016,36 @@ class AttendanceAnalytics(models.Model):
|
|||
LEFT JOIN resource_calendar rc
|
||||
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
|
||||
ON dg.calendar_id
|
||||
= ed.resource_calendar_id
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
<field name="name">attendance.analytics.list</field>
|
||||
<field name="model">attendance.analytics</field>
|
||||
<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="department_id"/>
|
||||
<field name="date"/>
|
||||
|
|
@ -28,7 +29,7 @@
|
|||
<field name="status"
|
||||
widget="badge"
|
||||
decoration-success="status == 'present'"
|
||||
decoration-danger="status == 'absent'"
|
||||
decoration-danger="status == 'absent' or status == 'invalid_attendance'"
|
||||
decoration-warning="status == 'late_in'"
|
||||
decoration-info="status == 'half_day'"
|
||||
decoration-primary="status == 'holiday'"/>
|
||||
|
|
@ -146,4 +147,4 @@
|
|||
action="action_attendance_analytics"
|
||||
sequence="-51"/>
|
||||
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -152,7 +152,10 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
parsed_data = self._post_process_jd_data(parsed_data, parsed_payload["text"])
|
||||
else:
|
||||
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:
|
||||
processed += 1
|
||||
|
|
@ -255,6 +258,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
|
||||
try:
|
||||
parsed_data = json.loads(line.extracted_payload)
|
||||
parsed_data = line._get_edited_parsed_data(parsed_data)
|
||||
|
||||
with self.env.cr.savepoint():
|
||||
|
||||
|
|
@ -642,10 +646,13 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
data["email"] = email_match.group(1)
|
||||
|
||||
phone_matches = re.findall(r"(\+?\d[\d\-\s()]{7,}\d)", extracted_text)
|
||||
if phone_matches and not data.get("phone"):
|
||||
data["phone"] = phone_matches[0].strip()
|
||||
if len(phone_matches) > 1 and not data.get("alternate_phone"):
|
||||
data["alternate_phone"] = phone_matches[1].strip()
|
||||
phone_values = []
|
||||
for phone_candidate in [data.get("phone"), data.get("alternate_phone")] + phone_matches:
|
||||
phone_value = self._clean_resume_phone_value(phone_candidate)
|
||||
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)
|
||||
if linkedin_match:
|
||||
|
|
@ -654,11 +661,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
if not data.get("total_experience_years"):
|
||||
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[
|
||||
"document.parser.service"
|
||||
].validate_explicit_skills(
|
||||
extracted_text,
|
||||
data.get("skills") or []
|
||||
resume_skills
|
||||
)
|
||||
data["education_history"] = self._normalize_resume_list(data.get("education_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)
|
||||
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):
|
||||
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()
|
||||
if not value:
|
||||
return False
|
||||
|
|
@ -2034,13 +2091,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
"<ul>",
|
||||
]
|
||||
for row in rows:
|
||||
html_parts.append(
|
||||
"<li><strong>%s</strong>: <span class='text-%s'>%s</span></li>" % (
|
||||
escape(row["filename"]),
|
||||
row["level"],
|
||||
escape(row["message"]),
|
||||
)
|
||||
)
|
||||
html_parts.append("<li><strong>%s</strong>: <span class='text-%s'>%s</span>" % (
|
||||
escape(row["filename"]),
|
||||
row["level"],
|
||||
escape(row["message"]),
|
||||
))
|
||||
html_parts.append("</li>")
|
||||
html_parts.extend(["</ul>", "</div>"])
|
||||
return "".join(html_parts)
|
||||
|
||||
|
|
@ -2072,3 +2128,128 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
|
|||
extracted_payload = fields.Text(readonly=True)
|
||||
candidate_id = fields.Many2one("hr.candidate", 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>
|
||||
|
||||
<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">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
|
|
|
|||
|
|
@ -34,9 +34,24 @@ class ShiftSwapRequest(models.Model):
|
|||
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):
|
||||
for rec in self:
|
||||
|
||||
rec._validate_roster_date_not_past()
|
||||
|
||||
if not rec.reason:
|
||||
raise UserError(
|
||||
_("Please enter reason.")
|
||||
|
|
|
|||
Loading…
Reference in New Issue