recruitment bug fixes
This commit is contained in:
parent
e2c8a25c7b
commit
7960d1926b
|
|
@ -14,6 +14,6 @@
|
||||||
"application": False,
|
"application": False,
|
||||||
"auto_install": False,
|
"auto_install": False,
|
||||||
"external_dependencies": {
|
"external_dependencies": {
|
||||||
"python": ["requests"],
|
"python": ["requests","python-docx"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -421,6 +421,91 @@ Document:
|
||||||
def _get_param(self, key):
|
def _get_param(self, key):
|
||||||
return self.env["ir.config_parameter"].sudo().get_param(key)
|
return self.env["ir.config_parameter"].sudo().get_param(key)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def validate_explicit_skills(self, resume_text, skills):
|
||||||
|
if not skills:
|
||||||
|
return []
|
||||||
|
|
||||||
|
prompt = f"""
|
||||||
|
You are validating resume skills.
|
||||||
|
|
||||||
|
Resume:
|
||||||
|
{resume_text[:30000]}
|
||||||
|
|
||||||
|
Extracted Skills:
|
||||||
|
{json.dumps(skills)}
|
||||||
|
|
||||||
|
Keep ONLY skills explicitly claimed by the candidate.
|
||||||
|
|
||||||
|
A skill is explicit if:
|
||||||
|
- It is presented as the candidate's expertise.
|
||||||
|
- It appears in a skill list or competency list.
|
||||||
|
|
||||||
|
Reject skills appearing only in:
|
||||||
|
- job responsibilities
|
||||||
|
- project descriptions
|
||||||
|
- achievements
|
||||||
|
- employer history
|
||||||
|
|
||||||
|
Return ONLY JSON.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
{{
|
||||||
|
"skills": ["Python", "Django"]
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
together_key = self._get_param(
|
||||||
|
"document_parser.together_ai_key"
|
||||||
|
) or self._get_param(
|
||||||
|
"document_parser.together_api_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
openrouter_key = self._get_param(
|
||||||
|
"document_parser.openrouter_ai_key"
|
||||||
|
) or self._get_param(
|
||||||
|
"document_parser.openrouter_api_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
if together_key:
|
||||||
|
result, provider_errors = self._call_provider(
|
||||||
|
provider_name="Together",
|
||||||
|
endpoint=self.TOGETHER_ENDPOINT,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {together_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
models=self.TOGETHER_MODELS,
|
||||||
|
prompt=prompt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result.get("skills", skills)
|
||||||
|
|
||||||
|
errors.extend(provider_errors)
|
||||||
|
|
||||||
|
if openrouter_key:
|
||||||
|
result, provider_errors = self._call_provider(
|
||||||
|
provider_name="OpenRouter",
|
||||||
|
endpoint=self.OPENROUTER_ENDPOINT,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {openrouter_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"HTTP-Referer": self._get_param("web.base.url") or "odoo.local",
|
||||||
|
"X-Title": "Document Parser",
|
||||||
|
},
|
||||||
|
models=self.OPENROUTER_MODELS,
|
||||||
|
prompt=prompt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result.get("skills", skills)
|
||||||
|
|
||||||
|
errors.extend(provider_errors)
|
||||||
|
|
||||||
|
return skills
|
||||||
def _normalize_required_fields(self, fields):
|
def _normalize_required_fields(self, fields):
|
||||||
if isinstance(fields, dict):
|
if isinstance(fields, dict):
|
||||||
normalized = {}
|
normalized = {}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ class EmpITDeclaration(models.Model):
|
||||||
_rec_name = 'employee_id'
|
_rec_name = 'employee_id'
|
||||||
_description = "IT Declaration"
|
_description = "IT Declaration"
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('name_emp_tax_period', 'unique(employee_id, period_id, tax_regime)',
|
||||||
|
"Avoid creating duplicate records for the same period_id and tax_regime."),
|
||||||
|
]
|
||||||
|
|
||||||
# @api.depends('period_id', 'employee_id')
|
# @api.depends('period_id', 'employee_id')
|
||||||
# def _compute_name(self):
|
# def _compute_name(self):
|
||||||
# for sheet in self:
|
# for sheet in self:
|
||||||
|
|
@ -56,7 +61,8 @@ class EmpITDeclaration(models.Model):
|
||||||
period_id = fields.Many2one(
|
period_id = fields.Many2one(
|
||||||
'payroll.period',
|
'payroll.period',
|
||||||
string="Payroll Period",
|
string="Payroll Period",
|
||||||
required=True
|
required=True,
|
||||||
|
copy=False
|
||||||
)
|
)
|
||||||
|
|
||||||
display_period_label = fields.Char(string="Period Label", compute='_compute_display_period_label')
|
display_period_label = fields.Char(string="Period Label", compute='_compute_display_period_label')
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,6 @@ class EmployeePayslipDownloadWizard(models.TransientModel):
|
||||||
|
|
||||||
def _compute_is_hr_manager(self):
|
def _compute_is_hr_manager(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
import pdb
|
|
||||||
pdb.set_trace()
|
|
||||||
rec.is_hr_manager = self.env.user.has_group('hr.group_hr_manager')
|
rec.is_hr_manager = self.env.user.has_group('hr.group_hr_manager')
|
||||||
|
|
||||||
@api.onchange('download_type', 'period_id')
|
@api.onchange('download_type', 'period_id')
|
||||||
|
|
|
||||||
|
|
@ -465,13 +465,21 @@
|
||||||
<field name="name">IT Declarations</field>
|
<field name="name">IT Declarations</field>
|
||||||
<field name="path">income-tax-declaration</field>
|
<field name="path">income-tax-declaration</field>
|
||||||
<field name="res_model">emp.it.declaration</field>
|
<field name="res_model">emp.it.declaration</field>
|
||||||
|
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
</record>
|
</record>
|
||||||
|
<record id="action_manager_it_declaration" model="ir.actions.act_window">
|
||||||
|
<field name="name">IT Declarations</field>
|
||||||
|
<field name="path">employees-tax-declarations</field>
|
||||||
|
<field name="res_model">emp.it.declaration</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<menuitem id="menu_hr_payroll_emp_root" name="Payroll" sequence="190" web_icon="hr_payroll,static/description/icon.png" groups="base.group_user"/>
|
<menuitem id="menu_hr_payroll_emp_root" name="Payroll" sequence="190" web_icon="hr_payroll,static/description/icon.png" groups="base.group_user"/>
|
||||||
|
|
||||||
<menuitem id="menu_it_declarations" name="IT Declarations"
|
<menuitem id="menu_it_declarations" name="IT Declarations"
|
||||||
parent="hr_payroll.menu_hr_payroll_root"
|
parent="hr_payroll.menu_hr_payroll_root"
|
||||||
action="action_emp_it_declaration" sequence="99"/>
|
action="action_manager_it_declaration" sequence="99"/>
|
||||||
<menuitem id="menu_it_declarations_emp" name="IT Declarations"
|
<menuitem id="menu_it_declarations_emp" name="IT Declarations"
|
||||||
parent="menu_hr_payroll_emp_root"
|
parent="menu_hr_payroll_emp_root"
|
||||||
action="action_emp_it_declaration" sequence="1"/>
|
action="action_emp_it_declaration" sequence="1"/>
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,11 @@ class HrApplicant(models.Model):
|
||||||
def action_open_auto_doc_wizard(self):
|
def action_open_auto_doc_wizard(self):
|
||||||
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_applicant").read()[0]
|
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_applicant").read()[0]
|
||||||
context = dict(self.env.context)
|
context = dict(self.env.context)
|
||||||
|
context['single_parser'] = True
|
||||||
|
action["context"]['current_id'] = self.id
|
||||||
if len(self) == 1 and self.hr_job_recruitment:
|
if len(self) == 1 and self.hr_job_recruitment:
|
||||||
context["default_job_recruitment_id"] = self.hr_job_recruitment.id
|
context["default_job_recruitment_id"] = self.hr_job_recruitment.id
|
||||||
action["context"] = context
|
action["context"] = context
|
||||||
action["name"] = _("Parse Resumes")
|
action["name"] = _("Parse Resumes")
|
||||||
|
|
||||||
return action
|
return action
|
||||||
|
|
@ -7,5 +7,7 @@ class HrCandidate(models.Model):
|
||||||
def action_open_auto_doc_wizard(self):
|
def action_open_auto_doc_wizard(self):
|
||||||
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_candidate").read()[0]
|
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_candidate").read()[0]
|
||||||
action["context"] = dict(self.env.context)
|
action["context"] = dict(self.env.context)
|
||||||
|
action["context"]['single_parser'] = True
|
||||||
|
action["context"]['current_id'] = self.id
|
||||||
action["name"] = _("Parse Resumes")
|
action["name"] = _("Parse Resumes")
|
||||||
return action
|
return action
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//header" position="inside">
|
<xpath expr="//header" position="inside">
|
||||||
<button name="action_open_auto_doc_wizard"
|
<button name="action_open_auto_doc_wizard"
|
||||||
string="Parse Resumes"
|
string="Parse Resume"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,10 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//header" position="inside">
|
<xpath expr="//header" position="inside">
|
||||||
<button name="action_open_auto_doc_wizard"
|
<button name="action_open_auto_doc_wizard"
|
||||||
string="Parse Resumes"
|
string="Parse Resume"
|
||||||
type="object"
|
type="object"
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
|
invisible="not resume"
|
||||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,9 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
job_recruitment_id = fields.Many2one("hr.job.recruitment", string="Job Request")
|
job_recruitment_id = fields.Many2one("hr.job.recruitment", string="Job Request")
|
||||||
create_missing_skills = fields.Boolean(default=True)
|
create_missing_skills = fields.Boolean(default=True)
|
||||||
update_existing_candidates = fields.Boolean(default=True)
|
update_existing_candidates = fields.Boolean(default=True)
|
||||||
|
single_parser = fields.Boolean(default=False)
|
||||||
|
resume_file = fields.Binary(string="Resume")
|
||||||
|
resume_filename = fields.Char(string="Filename")
|
||||||
attachment_ids = fields.Many2many(
|
attachment_ids = fields.Many2many(
|
||||||
"ir.attachment",
|
"ir.attachment",
|
||||||
"hr_recruitment_auto_doc_wizard_ir_attachment_rel",
|
"hr_recruitment_auto_doc_wizard_ir_attachment_rel",
|
||||||
|
|
@ -46,10 +49,14 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
created_count = fields.Integer(readonly=True)
|
created_count = fields.Integer(readonly=True)
|
||||||
updated_count = fields.Integer(readonly=True)
|
updated_count = fields.Integer(readonly=True)
|
||||||
skipped_count = fields.Integer(readonly=True)
|
skipped_count = fields.Integer(readonly=True)
|
||||||
|
parsed_document = fields.Boolean(readonly=True)
|
||||||
|
create_updated_records = fields.Boolean(default=False)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def default_get(self, fields_list):
|
def default_get(self, fields_list):
|
||||||
res = super().default_get(fields_list)
|
res = super().default_get(fields_list)
|
||||||
|
single_parser = self.env.context.get("single_parser")
|
||||||
|
res["single_parser"] = single_parser
|
||||||
active_model = self.env.context.get("active_model")
|
active_model = self.env.context.get("active_model")
|
||||||
active_ids = self.env.context.get("active_ids") or []
|
active_ids = self.env.context.get("active_ids") or []
|
||||||
default_job_recruitment_id = self.env.context.get("default_job_recruitment_id")
|
default_job_recruitment_id = self.env.context.get("default_job_recruitment_id")
|
||||||
|
|
@ -57,12 +64,30 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
if active_model == "hr.applicant":
|
if active_model == "hr.applicant":
|
||||||
res["target_model"] = "applicant"
|
res["target_model"] = "applicant"
|
||||||
applicants = self.env["hr.applicant"].browse(active_ids).exists()
|
applicants = self.env["hr.applicant"].browse(active_ids).exists()
|
||||||
|
candidate = False
|
||||||
|
if active_ids and active_ids[0]:
|
||||||
|
applicant = self.env["hr.applicant"].browse(active_ids[0])
|
||||||
|
candidate = applicant.candidate_id
|
||||||
|
if candidate and candidate.resume:
|
||||||
|
res.update({
|
||||||
|
"resume_file": candidate.resume,
|
||||||
|
"resume_filename": candidate.resume_name,
|
||||||
|
})
|
||||||
job_requests = applicants.mapped("hr_job_recruitment")
|
job_requests = applicants.mapped("hr_job_recruitment")
|
||||||
if default_job_recruitment_id:
|
if default_job_recruitment_id:
|
||||||
res["job_recruitment_id"] = default_job_recruitment_id
|
res["job_recruitment_id"] = default_job_recruitment_id
|
||||||
elif len(job_requests) == 1:
|
elif len(job_requests) == 1:
|
||||||
res["job_recruitment_id"] = job_requests.id
|
res["job_recruitment_id"] = job_requests.id
|
||||||
elif active_model == "hr.candidate":
|
elif active_model == "hr.candidate":
|
||||||
|
candidate = False
|
||||||
|
if active_ids and active_ids[0]:
|
||||||
|
candidate = self.env["hr.candidate"].browse(active_ids[0])
|
||||||
|
|
||||||
|
if candidate and candidate.resume:
|
||||||
|
res.update({
|
||||||
|
"resume_file": candidate.resume,
|
||||||
|
"resume_filename": candidate.resume_name,
|
||||||
|
})
|
||||||
res["target_model"] = "candidate"
|
res["target_model"] = "candidate"
|
||||||
elif active_model == "hr.job.recruitment":
|
elif active_model == "hr.job.recruitment":
|
||||||
res["target_model"] = "job_recruitment"
|
res["target_model"] = "job_recruitment"
|
||||||
|
|
@ -119,52 +144,66 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
line.extracted_payload = json.dumps(parsed_data, indent=2, ensure_ascii=False)
|
line.extracted_payload = json.dumps(parsed_data, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with self.env.cr.savepoint():
|
processed += 1
|
||||||
if self.target_model == "job_recruitment":
|
|
||||||
job_request, job_state = self._apply_jd_parse(parsed_data, parsed_payload)
|
|
||||||
self._attach_jd_document_to_job_request(job_request, line)
|
|
||||||
action_label = _("Created") if job_state == "created" else _("Updated")
|
|
||||||
job_message = _("%(action)s job request %(name)s from parsed JD.") % {
|
|
||||||
"action": action_label,
|
|
||||||
"name": job_request.display_name,
|
|
||||||
}
|
|
||||||
processed += 1
|
|
||||||
if job_state == "created":
|
|
||||||
created += 1
|
|
||||||
else:
|
|
||||||
updated += 1
|
|
||||||
line.write({
|
|
||||||
"state": "done",
|
|
||||||
"message": job_message,
|
|
||||||
})
|
|
||||||
summary_rows.append(self._build_summary_row(line, job_message, "success"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
candidate, candidate_state, candidate_message = self._find_or_create_candidate(line, parsed_data, parsed_payload)
|
line.write({
|
||||||
|
"state": "parsed",
|
||||||
|
"message": _("Document parsed successfully. Click Save to create/update records."),
|
||||||
|
})
|
||||||
|
|
||||||
linked_record_message = candidate_message
|
summary_rows.append(
|
||||||
|
self._build_summary_row(
|
||||||
if self.target_model == "candidate":
|
line,
|
||||||
line.candidate_id = candidate.id
|
_("Document parsed successfully. Click Save to create/update records."),
|
||||||
line.applicant_id = False
|
"info",
|
||||||
updated += 1 if candidate_state == "updated" else 0
|
)
|
||||||
created += 1 if candidate_state == "created" else 0
|
)
|
||||||
else:
|
# with self.env.cr.savepoint():
|
||||||
applicant, applicant_state, applicant_message = self._find_or_create_applicant(candidate, line, parsed_data)
|
# if self.target_model == "job_recruitment":
|
||||||
line.candidate_id = candidate.id
|
# job_request, job_state = self._apply_jd_parse(parsed_data, parsed_payload)
|
||||||
line.applicant_id = applicant.id
|
# self._attach_jd_document_to_job_request(job_request, line)
|
||||||
linked_record_message = f"{candidate_message} {applicant_message}".strip()
|
# action_label = _("Created") if job_state == "created" else _("Updated")
|
||||||
if applicant_state == "created":
|
# job_message = _("%(action)s job request %(name)s from parsed JD.") % {
|
||||||
created += 1
|
# "action": action_label,
|
||||||
elif candidate_state == "updated":
|
# "name": job_request.display_name,
|
||||||
updated += 1
|
# }
|
||||||
|
# processed += 1
|
||||||
processed += 1
|
# if job_state == "created":
|
||||||
line.write({
|
# created += 1
|
||||||
"state": "done",
|
# else:
|
||||||
"message": linked_record_message,
|
# updated += 1
|
||||||
})
|
# line.write({
|
||||||
summary_rows.append(self._build_summary_row(line, linked_record_message, "success"))
|
# "state": "done",
|
||||||
|
# "message": job_message,
|
||||||
|
# })
|
||||||
|
# summary_rows.append(self._build_summary_row(line, job_message, "success"))
|
||||||
|
# continue
|
||||||
|
#
|
||||||
|
# candidate, candidate_state, candidate_message = self._find_or_create_candidate(line, parsed_data, parsed_payload)
|
||||||
|
#
|
||||||
|
# linked_record_message = candidate_message
|
||||||
|
#
|
||||||
|
# if self.target_model == "candidate":
|
||||||
|
# line.candidate_id = candidate.id
|
||||||
|
# line.applicant_id = False
|
||||||
|
# updated += 1 if candidate_state == "updated" else 0
|
||||||
|
# created += 1 if candidate_state == "created" else 0
|
||||||
|
# else:
|
||||||
|
# applicant, applicant_state, applicant_message = self._find_or_create_applicant(candidate, line, parsed_data)
|
||||||
|
# line.candidate_id = candidate.id
|
||||||
|
# line.applicant_id = applicant.id
|
||||||
|
# linked_record_message = f"{candidate_message} {applicant_message}".strip()
|
||||||
|
# if applicant_state == "created":
|
||||||
|
# created += 1
|
||||||
|
# elif candidate_state == "updated":
|
||||||
|
# updated += 1
|
||||||
|
#
|
||||||
|
# processed += 1
|
||||||
|
# line.write({
|
||||||
|
# "state": "done",
|
||||||
|
# "message": linked_record_message,
|
||||||
|
# })
|
||||||
|
# summary_rows.append(self._build_summary_row(line, linked_record_message, "success"))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
line.write({
|
line.write({
|
||||||
"state": "error",
|
"state": "error",
|
||||||
|
|
@ -181,6 +220,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"updated_count": updated,
|
"updated_count": updated,
|
||||||
"skipped_count": skipped,
|
"skipped_count": skipped,
|
||||||
"result_html": self._build_summary_html(summary_rows),
|
"result_html": self._build_summary_html(summary_rows),
|
||||||
|
"parsed_document": True
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -191,8 +231,180 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"target": "new",
|
"target": "new",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def action_create_or_update_records(self):
|
||||||
|
self.ensure_one()
|
||||||
|
processed = created = updated = skipped = 0
|
||||||
|
summary_rows = []
|
||||||
|
|
||||||
|
for line in self.line_ids:
|
||||||
|
if not line.extracted_payload:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed_data = json.loads(line.extracted_payload)
|
||||||
|
|
||||||
|
with self.env.cr.savepoint():
|
||||||
|
|
||||||
|
if self.target_model == "job_recruitment":
|
||||||
|
job_request, job_state = self._apply_jd_parse(
|
||||||
|
parsed_data,
|
||||||
|
{"text": ""}
|
||||||
|
)
|
||||||
|
|
||||||
|
self._attach_jd_document_to_job_request(
|
||||||
|
job_request,
|
||||||
|
line
|
||||||
|
)
|
||||||
|
|
||||||
|
message = _(
|
||||||
|
"Job Request %s successfully."
|
||||||
|
) % (
|
||||||
|
"created"
|
||||||
|
if job_state == "created"
|
||||||
|
else "updated"
|
||||||
|
)
|
||||||
|
|
||||||
|
if job_state == "created":
|
||||||
|
created += 1
|
||||||
|
else:
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
line.write({
|
||||||
|
"state": "done",
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
|
||||||
|
summary_rows.append(
|
||||||
|
self._build_summary_row(
|
||||||
|
line,
|
||||||
|
message,
|
||||||
|
"success"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
processed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
candidate, candidate_state, candidate_message = (
|
||||||
|
self._find_or_create_candidate(
|
||||||
|
line,
|
||||||
|
parsed_data,
|
||||||
|
{
|
||||||
|
"mimetype": mimetypes.guess_type(
|
||||||
|
line.file_name or ""
|
||||||
|
)[0]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
final_message = candidate_message
|
||||||
|
|
||||||
|
if self.target_model == "candidate":
|
||||||
|
|
||||||
|
line.write({
|
||||||
|
"candidate_id": candidate.id,
|
||||||
|
"applicant_id": False,
|
||||||
|
})
|
||||||
|
|
||||||
|
if candidate_state == "created":
|
||||||
|
created += 1
|
||||||
|
else:
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
applicant, applicant_state, applicant_message = (
|
||||||
|
self._find_or_create_applicant(
|
||||||
|
candidate,
|
||||||
|
line,
|
||||||
|
parsed_data
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
line.write({
|
||||||
|
"candidate_id": candidate.id,
|
||||||
|
"applicant_id": applicant.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
final_message = (
|
||||||
|
f"{candidate_message} "
|
||||||
|
f"{applicant_message}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if candidate_state == "created":
|
||||||
|
created += 1
|
||||||
|
else:
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
if applicant_state == "created":
|
||||||
|
created += 1
|
||||||
|
|
||||||
|
line.write({
|
||||||
|
"state": "done",
|
||||||
|
"message": final_message,
|
||||||
|
})
|
||||||
|
|
||||||
|
summary_rows.append(
|
||||||
|
self._build_summary_row(
|
||||||
|
line,
|
||||||
|
final_message,
|
||||||
|
"success"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
processed += 1
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
skipped += 1
|
||||||
|
|
||||||
|
line.write({
|
||||||
|
"state": "error",
|
||||||
|
"message": str(exc),
|
||||||
|
})
|
||||||
|
|
||||||
|
summary_rows.append(
|
||||||
|
self._build_summary_row(
|
||||||
|
line,
|
||||||
|
str(exc),
|
||||||
|
"danger"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.write({
|
||||||
|
"processed_count": processed,
|
||||||
|
"created_count": created,
|
||||||
|
"updated_count": updated,
|
||||||
|
"skipped_count": skipped,
|
||||||
|
"result_html": self._build_summary_html(summary_rows),
|
||||||
|
"create_updated_records": True
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": self._name,
|
||||||
|
"res_id": self.id,
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "new",
|
||||||
|
}
|
||||||
|
|
||||||
|
def done_records(self):
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.client",
|
||||||
|
"tag": "reload",
|
||||||
|
}
|
||||||
|
|
||||||
def _sync_upload_lines(self):
|
def _sync_upload_lines(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
if self.single_parser and self.resume_file:
|
||||||
|
attachment = self.env["ir.attachment"].create({
|
||||||
|
"name": self.resume_filename or "Resume.pdf",
|
||||||
|
"datas": self.resume_file,
|
||||||
|
"res_model": self._name,
|
||||||
|
"res_id": self.id,
|
||||||
|
"type": "binary",
|
||||||
|
})
|
||||||
|
|
||||||
|
self.attachment_ids = [(6, 0, [attachment.id])]
|
||||||
existing_by_attachment = {
|
existing_by_attachment = {
|
||||||
line.attachment_id.id: line
|
line.attachment_id.id: line
|
||||||
for line in self.line_ids
|
for line in self.line_ids
|
||||||
|
|
@ -231,7 +443,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
||||||
"relevant_experience_years": {"type": "float", "description": "Relevant years of experience as a number"},
|
"relevant_experience_years": {"type": "float", "description": "Relevant years of experience as a number"},
|
||||||
"notice_period": {"type": "string", "description": "Notice period text"},
|
"notice_period": {"type": "string", "description": "Notice period text"},
|
||||||
"degree": {"type": "string", "description": "Highest degree or main qualification"},
|
"degree": {"type": "string", "description": "Highest degree or main qualification"},
|
||||||
"skills": {"type": "list", "description": "All explicit technical and functional skills that are mentioned in skills session and do not fetch the skills seperatly from the education and employeer history data"},
|
"skills": {"type": "list", "description": "Only fetch important skills "},
|
||||||
"summary": {"type": "string", "description": "Short professional summary from the resume"},
|
"summary": {"type": "string", "description": "Short professional summary from the resume"},
|
||||||
"education_history": {
|
"education_history": {
|
||||||
"type": "list",
|
"type": "list",
|
||||||
|
|
@ -412,7 +624,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)
|
||||||
|
|
||||||
data["skills"] = self._merge_resume_skills(data.get("skills") or [], extracted_text)
|
data["skills"] = self.env[
|
||||||
|
"document.parser.service"
|
||||||
|
].validate_explicit_skills(
|
||||||
|
extracted_text,
|
||||||
|
data.get("skills") or []
|
||||||
|
)
|
||||||
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"))
|
||||||
data["family_details"] = self._normalize_resume_list(data.get("family_details"))
|
data["family_details"] = self._normalize_resume_list(data.get("family_details"))
|
||||||
|
|
@ -1794,6 +2011,7 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
|
||||||
state = fields.Selection(
|
state = fields.Selection(
|
||||||
selection=[
|
selection=[
|
||||||
("draft", "Draft"),
|
("draft", "Draft"),
|
||||||
|
("parsed", "Parsed"),
|
||||||
("done", "Done"),
|
("done", "Done"),
|
||||||
("error", "Error"),
|
("error", "Error"),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Parse Recruitment Documents" create="0" edit="1">
|
<form string="Parse Recruitment Documents" create="0" edit="1">
|
||||||
<header>
|
<header>
|
||||||
<button name="action_parse_documents" string="Parse Documents" type="object" class="btn-primary"/>
|
<button name="action_parse_documents" string="Parse Documents" type="object" class="btn-primary" invisible="not attachment_ids and not resume_file"/>
|
||||||
<button string="Close" special="cancel" class="btn-secondary"/>
|
<!-- <button string="Close" special="cancel" class="btn-secondary"/>-->
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
<group>
|
<group>
|
||||||
|
|
@ -22,13 +22,23 @@
|
||||||
<field name="update_existing_candidates" invisible="target_model != 'candidate'"/>
|
<field name="update_existing_candidates" invisible="target_model != 'candidate'"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group string="Upload Documents">
|
<group string="Upload Documents" invisible="single_parser">
|
||||||
<field name="attachment_ids"
|
<field name="attachment_ids"
|
||||||
widget="many2many_binary_dropzone"
|
widget="many2many_binary_dropzone"
|
||||||
nolabel="1"
|
nolabel="1"
|
||||||
class="w-100"
|
class="w-100"
|
||||||
options="{'preview_images': true}"/>
|
options="{'preview_images': true}"/>
|
||||||
</group>
|
</group>
|
||||||
|
<group string="Resume"
|
||||||
|
invisible="not single_parser">
|
||||||
|
<field name="resume_file"
|
||||||
|
filename="resume_filename"
|
||||||
|
widget="binary"/>
|
||||||
|
|
||||||
|
<field name="resume_filename"
|
||||||
|
invisible="1"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
<group string="Uploaded Files">
|
<group string="Uploaded Files">
|
||||||
<field name="line_ids" nolabel="1" readonly="1">
|
<field name="line_ids" nolabel="1" readonly="1">
|
||||||
<kanban class="o_kanban_small_column">
|
<kanban class="o_kanban_small_column">
|
||||||
|
|
@ -70,8 +80,16 @@
|
||||||
</group>
|
</group>
|
||||||
<group string="Result">
|
<group string="Result">
|
||||||
<field name="result_html" readonly="1" nolabel="1" widget="html"/>
|
<field name="result_html" readonly="1" nolabel="1" widget="html"/>
|
||||||
|
<field name="create_updated_records" invisible="1"/>
|
||||||
|
<field name="parsed_document" invisible="1"/>
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
|
<footer>
|
||||||
|
<button name="action_create_or_update_records" string="Save" type="object" class="btn-primary" data-hotkey="q" invisible="create_updated_records or not parsed_document"/>
|
||||||
|
<button name="done_records" string="Done" type="object" class="btn-primary" data-hotkey="q" invisible="not create_updated_records"/>
|
||||||
|
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x" invisible="create_updated_records"/>
|
||||||
|
</footer>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,12 @@ class HrRecruitmentDashboard(models.AbstractModel):
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_filter_options(self):
|
def _get_filter_options(self):
|
||||||
|
recruiter_group = self.env.ref('hr_recruitment.group_hr_recruitment_user')
|
||||||
|
|
||||||
recruiters = self.env['res.users'].search([
|
recruiters = self.env['res.users'].search([
|
||||||
('share', '=', False),
|
('share', '=', False),
|
||||||
('active', '=', True),
|
('active', '=', True),
|
||||||
|
('groups_id', 'in', recruiter_group.id),
|
||||||
], order='name')
|
], order='name')
|
||||||
jobs = self.env['hr.job.recruitment'].with_context(active_test=False).search([], order='recruitment_sequence desc, id desc', limit=200)
|
jobs = self.env['hr.job.recruitment'].with_context(active_test=False).search([], order='recruitment_sequence desc, id desc', limit=200)
|
||||||
departments = self.env['hr.department'].search([], order='name')
|
departments = self.env['hr.department'].search([], order='name')
|
||||||
|
|
|
||||||
|
|
@ -338,17 +338,35 @@ export class HrRecruitmentDashboard extends Component {
|
||||||
|
|
||||||
onRangeChange(event) {
|
onRangeChange(event) {
|
||||||
this.state.filters.range = event.target.value;
|
this.state.filters.range = event.target.value;
|
||||||
|
|
||||||
if (event.target.value !== "custom") {
|
if (event.target.value !== "custom") {
|
||||||
this.state.filters.date_from = "";
|
this.state.filters.date_from = "";
|
||||||
this.state.filters.date_to = "";
|
this.state.filters.date_to = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputChange(key, event) {
|
onInputChange(key, event) {
|
||||||
this.state.filters[key] = event.target.value;
|
this.state.filters[key] = event.target.value;
|
||||||
|
|
||||||
if (key === "date_from" || key === "date_to") {
|
if (key === "date_from" || key === "date_to") {
|
||||||
this.state.filters.range = "custom";
|
this.state.filters.range = "custom";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleMultiFilter(key, value, checked) {
|
||||||
|
const values = this.state.filters[key] || [];
|
||||||
|
|
||||||
|
if (checked && !values.includes(value)) {
|
||||||
|
this.state.filters[key] = [...values, value];
|
||||||
|
} else if (!checked) {
|
||||||
|
this.state.filters[key] = values.filter((item) => item !== value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMultiChange(key, event) {
|
onMultiChange(key, event) {
|
||||||
|
|
@ -358,15 +376,6 @@ export class HrRecruitmentDashboard extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleMultiFilter(key, value, checked) {
|
|
||||||
const values = this.state.filters[key] || [];
|
|
||||||
if (checked && !values.includes(value)) {
|
|
||||||
this.state.filters[key] = [...values, value];
|
|
||||||
} else if (!checked) {
|
|
||||||
this.state.filters[key] = values.filter((item) => item !== value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedFilterItems(key, options) {
|
selectedFilterItems(key, options) {
|
||||||
const values = this.state.filters[key] || [];
|
const values = this.state.filters[key] || [];
|
||||||
return (options || []).filter((item) => values.includes(item.id));
|
return (options || []).filter((item) => values.includes(item.id));
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,7 @@
|
||||||
<p>Pipeline health, recruiter performance, client submissions, hiring conversion, and urgent openings in one place.</p>
|
<p>Pipeline health, recruiter performance, client submissions, hiring conversion, and urgent openings in one place.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_hr_recruitment_dashboard__actions">
|
<div class="o_hr_recruitment_dashboard__actions">
|
||||||
<button class="btn btn-light" t-on-click="clearFilters">Reset</button>
|
<button class="btn btn-primary" t-on-click="clearFilters"><i class="fa fa-refresh me-1"/>Reset</button>
|
||||||
<button class="btn btn-primary" t-on-click="loadDashboard">
|
|
||||||
<i class="fa fa-refresh me-1"/> Apply Filters
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -25,14 +22,27 @@
|
||||||
</t>
|
</t>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_filter_item">
|
<t t-if="state.filters.range === 'custom'">
|
||||||
<label>From</label>
|
<div class="o_filter_item">
|
||||||
<input type="date" class="form-control" t-att-value="state.filters.date_from" t-on-change="(ev) => this.onInputChange('date_from', ev)"/>
|
<label>From</label>
|
||||||
</div>
|
<input
|
||||||
<div class="o_filter_item">
|
type="date"
|
||||||
<label>To</label>
|
class="form-control"
|
||||||
<input type="date" class="form-control" t-att-value="state.filters.date_to" t-on-change="(ev) => this.onInputChange('date_to', ev)"/>
|
t-att-value="state.filters.date_from"
|
||||||
</div>
|
t-on-change="(ev) => this.onInputChange('date_from', ev)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_filter_item">
|
||||||
|
<label>To</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
class="form-control"
|
||||||
|
t-att-value="state.filters.date_to"
|
||||||
|
t-on-change="(ev) => this.onInputChange('date_to', ev)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
<div class="o_filter_item">
|
<div class="o_filter_item">
|
||||||
<label>Recruiters</label>
|
<label>Recruiters</label>
|
||||||
<details class="o_filter_dropdown">
|
<details class="o_filter_dropdown">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from odoo import api, fields, models,_
|
from odoo import api, fields, models,_
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class ApplicantCandidate(models.Model):
|
class ApplicantCandidate(models.Model):
|
||||||
|
|
@ -40,7 +41,6 @@ class ApplicantCandidate(models.Model):
|
||||||
|
|
||||||
if not self.resume:
|
if not self.resume:
|
||||||
return
|
return
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.act_url',
|
'type': 'ir.actions.act_url',
|
||||||
'url': f'/web/content/hr.candidate/{self.id}/resume/{self.resume_name}?download=false',
|
'url': f'/web/content/hr.candidate/{self.id}/resume/{self.resume_name}?download=false',
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
.container-fluid.mt-2.mb-2 {
|
.container-fluid.mt-2.mb-2 {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 100;
|
z-index: 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix notebook stacking context */
|
/* Fix notebook stacking context */
|
||||||
|
|
@ -43,3 +43,12 @@
|
||||||
.modal-open .o_field_many2one .o_m2o_dropdown {
|
.modal-open .o_field_many2one .o_m2o_dropdown {
|
||||||
z-index: 1061 !important;
|
z-index: 1061 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.candidate-name {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-name {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
@ -26,29 +26,6 @@
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Critical fix - make sure the dropdown appears above notebook */
|
|
||||||
.ui-autocomplete,
|
|
||||||
.ui-menu,
|
|
||||||
.o_m2o_dropdown,
|
|
||||||
.o_field_many2one .o_m2o_dropdown,
|
|
||||||
.dropdown-menu {
|
|
||||||
z-index: 9999 !important;
|
|
||||||
position: absolute !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix for the specific many2one field container */
|
|
||||||
.o_field_many2one {
|
|
||||||
position: relative !important;
|
|
||||||
z-index: 100 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* When dropdown is open, ensure it's on top */
|
|
||||||
.o_field_many2one.o_focused .o_m2o_dropdown {
|
|
||||||
z-index: 10000 !important;
|
|
||||||
position: fixed !important;
|
|
||||||
max-height: 300px !important;
|
|
||||||
overflow-y: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Override any overflow hidden on parent containers */
|
/* Override any overflow hidden on parent containers */
|
||||||
.container-fluid,
|
.container-fluid,
|
||||||
|
|
@ -65,6 +42,14 @@
|
||||||
.modal .o_field_many2one .o_m2o_dropdown {
|
.modal .o_field_many2one .o_m2o_dropdown {
|
||||||
z-index: 10001 !important;
|
z-index: 10001 !important;
|
||||||
}
|
}
|
||||||
|
.ui-autocomplete,
|
||||||
|
.ui-menu,
|
||||||
|
.o_m2o_dropdown,
|
||||||
|
.o_field_many2one .o_m2o_dropdown,
|
||||||
|
.dropdown-menu {
|
||||||
|
z-index: 9999 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
@ -105,10 +90,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container-fluid mt-2 mb-2">
|
<div class="container-fluid mt-2 mb-2">
|
||||||
<div class="card border-0 shadow-sm rounded p-3">
|
<div class="card border-0 shadow-sm rounded p-3 overflow-visible">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-start">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-start">
|
||||||
<div class="me-4">
|
<div class="me-4">
|
||||||
|
|
||||||
<field name="candidate_image"
|
<field name="candidate_image"
|
||||||
|
|
@ -116,31 +101,41 @@
|
||||||
class="rounded border"
|
class="rounded border"
|
||||||
options="{'size':[140,140]}"/>
|
options="{'size':[140,140]}"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="d-flex flex-column gap-2">
|
||||||
<h2 class="fw-bold text-dark mb-1">
|
|
||||||
<field name="candidate_id" nolabel="1"/>
|
<div class="candidate-name">
|
||||||
</h2>
|
<field name="candidate_id"
|
||||||
<div class="text-muted fs-5 mb-2">
|
nolabel="1"
|
||||||
<field name="job_id" nolabel="1"/>
|
can_create="True"
|
||||||
|
can_write="True"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="job-name text-muted">
|
||||||
|
<field name="job_id"
|
||||||
|
nolabel="1"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<field name="priority" widget="priority" options="{'max': 5}"/>
|
<field name="priority"
|
||||||
|
widget="priority"
|
||||||
|
options="{'max': 5}"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="d-flex flex-column align-items-start gap-2">
|
<div class="d-flex flex-column align-items-start gap-2">
|
||||||
<div class="d-flex align-items-center" invisible="not email_from">
|
<div class="d-flex align-items-start" invisible="not email_from">
|
||||||
<i class="fa fa-envelope me-3 text-primary"/>
|
<i class="fa fa-envelope me-3 text-primary"/>
|
||||||
<field name="email_from" widget="email" nolabel="1"/>
|
<field name="email_from" widget="email" nolabel="1"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center"
|
<div class="d-flex align-items-start"
|
||||||
invisible="not partner_phone">
|
invisible="not partner_phone">
|
||||||
<i class="fa fa-phone me-3 text-primary"/>
|
<i class="fa fa-phone me-3 text-primary"/>
|
||||||
<field name="partner_phone" widget="phone" nolabel="1"/>
|
<field name="partner_phone" widget="phone" nolabel="1"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center" invisible="not linkedin_profile">
|
<div class="d-flex align-items-start" invisible="not linkedin_profile">
|
||||||
<i class="fa fa-linkedin me-3 text-primary"/>
|
<i class="fa fa-linkedin me-3 text-primary"/>
|
||||||
<field name="linkedin_profile" nolabel="1"/>
|
<field name="linkedin_profile" nolabel="1"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,13 +169,13 @@
|
||||||
groups="hr_recruitment.group_hr_recruitment_user" col="1">
|
groups="hr_recruitment.group_hr_recruitment_user" col="1">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="current_ctc" class="fw-bold"/>
|
<label for="current_ctc" class="fw-bold"/>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-start gap-2">
|
||||||
<field name="current_ctc" class="w-50" placeholder="Current CTC"/>
|
<field name="current_ctc" class="w-50" placeholder="Current CTC"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="salary_expected" class="fw-bold"/>
|
<label for="salary_expected" class="fw-bold"/>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-start gap-2">
|
||||||
<field name="salary_expected" class="w-50" placeholder="Expected CTC"/>
|
<field name="salary_expected" class="w-50" placeholder="Expected CTC"/>
|
||||||
<span invisible="not salary_expected_extra">
|
<span invisible="not salary_expected_extra">
|
||||||
+
|
+
|
||||||
|
|
@ -190,7 +185,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="notice_period" string="Notice Period" class="fw-bold"/>
|
<label for="notice_period" string="Notice Period" class="fw-bold"/>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-start gap-2">
|
||||||
<field name="notice_period" class="w-25" placeholder="0"/>
|
<field name="notice_period" class="w-25" placeholder="0"/>
|
||||||
<field name="notice_period_type" class="w-50" placeholder="Type"
|
<field name="notice_period_type" class="w-50" placeholder="Type"
|
||||||
required="notice_period > 0"/>
|
required="notice_period > 0"/>
|
||||||
|
|
@ -252,7 +247,7 @@
|
||||||
<group string="Experience" name="applicant_experience" col="1">
|
<group string="Experience" name="applicant_experience" col="1">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="total_exp" string="Total Experience" class="fw-bold"/>
|
<label for="total_exp" string="Total Experience" class="fw-bold"/>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-start gap-2">
|
||||||
<field name="total_exp" class="w-25" placeholder="0"/>
|
<field name="total_exp" class="w-25" placeholder="0"/>
|
||||||
<field name="total_exp_type" class="w-50" placeholder="Type"
|
<field name="total_exp_type" class="w-50" placeholder="Type"
|
||||||
required="total_exp > 0"/>
|
required="total_exp > 0"/>
|
||||||
|
|
@ -260,7 +255,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<label for="relevant_exp" string="Relevant Experience" class="fw-bold"/>
|
<label for="relevant_exp" string="Relevant Experience" class="fw-bold"/>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-start gap-2">
|
||||||
<field name="relevant_exp" class="w-25" placeholder="0"/>
|
<field name="relevant_exp" class="w-25" placeholder="0"/>
|
||||||
<field name="relevant_exp_type" class="w-50" placeholder="Type"
|
<field name="relevant_exp_type" class="w-50" placeholder="Type"
|
||||||
required="relevant_exp > 0"/>
|
required="relevant_exp > 0"/>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="fw-bold text-dark mb-1">
|
<h2 class="fw-bold text-dark mb-1">
|
||||||
<field name="partner_name" nolabel="0" placeholder="Candidate Name"/>
|
<field name="partner_name" nolabel="0" required="1" placeholder="Candidate Name"/>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="d-flex align-items-center g-1">
|
<div class="d-flex align-items-center g-1">
|
||||||
<i class="fa fa-id-badge me-3 text-primary"/>
|
<i class="fa fa-id-badge me-3 text-primary"/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue