Updated grace period, recruitment auto doc, and shift swap

This commit is contained in:
karuna 2026-07-28 10:49:12 +05:30
parent 14427d20fe
commit f698203a7f
5 changed files with 430 additions and 131 deletions

View File

@ -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

View File

@ -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'"/>

View File

@ -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

View File

@ -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 &amp; 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">

View File

@ -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.")