diff --git a/addons_extensions/hr_recruitment_auto_doc/views/hr_candidate_views.xml b/addons_extensions/hr_recruitment_auto_doc/views/hr_candidate_views.xml
index d39010ca4..ba411c577 100644
--- a/addons_extensions/hr_recruitment_auto_doc/views/hr_candidate_views.xml
+++ b/addons_extensions/hr_recruitment_auto_doc/views/hr_candidate_views.xml
@@ -29,9 +29,10 @@
diff --git a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py
index 6e9ede0e2..70dec4289 100644
--- a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py
+++ b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard.py
@@ -29,6 +29,9 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
job_recruitment_id = fields.Many2one("hr.job.recruitment", string="Job Request")
create_missing_skills = 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(
"ir.attachment",
"hr_recruitment_auto_doc_wizard_ir_attachment_rel",
@@ -46,10 +49,14 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
created_count = fields.Integer(readonly=True)
updated_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
def default_get(self, 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_ids = self.env.context.get("active_ids") or []
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":
res["target_model"] = "applicant"
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")
if default_job_recruitment_id:
res["job_recruitment_id"] = default_job_recruitment_id
elif len(job_requests) == 1:
res["job_recruitment_id"] = job_requests.id
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"
elif active_model == "hr.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)
try:
- with self.env.cr.savepoint():
- 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
+ processed += 1
- 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
-
- 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"))
+ summary_rows.append(
+ self._build_summary_row(
+ line,
+ _("Document parsed successfully. Click Save to create/update records."),
+ "info",
+ )
+ )
+ # with self.env.cr.savepoint():
+ # 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)
+ #
+ # 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:
line.write({
"state": "error",
@@ -181,6 +220,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"updated_count": updated,
"skipped_count": skipped,
"result_html": self._build_summary_html(summary_rows),
+ "parsed_document": True
})
return {
@@ -191,8 +231,180 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"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):
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 = {
line.attachment_id.id: line
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"},
"notice_period": {"type": "string", "description": "Notice period text"},
"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"},
"education_history": {
"type": "list",
@@ -412,7 +624,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
if not data.get("total_experience_years"):
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["employer_history"] = self._normalize_resume_list(data.get("employer_history"))
data["family_details"] = self._normalize_resume_list(data.get("family_details"))
@@ -1794,6 +2011,7 @@ class HrRecruitmentAutoDocWizardLine(models.TransientModel):
state = fields.Selection(
selection=[
("draft", "Draft"),
+ ("parsed", "Parsed"),
("done", "Done"),
("error", "Error"),
],
diff --git a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml
index 5af4548fa..5bcf5ced6 100644
--- a/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml
+++ b/addons_extensions/hr_recruitment_auto_doc/wizard/hr_recruitment_auto_doc_wizard_views.xml
@@ -6,8 +6,8 @@
diff --git a/addons_extensions/hr_recruitment_dashboards/models/recruitment_dashboard.py b/addons_extensions/hr_recruitment_dashboards/models/recruitment_dashboard.py
index cf022f1ec..f9b950379 100644
--- a/addons_extensions/hr_recruitment_dashboards/models/recruitment_dashboard.py
+++ b/addons_extensions/hr_recruitment_dashboards/models/recruitment_dashboard.py
@@ -67,9 +67,12 @@ class HrRecruitmentDashboard(models.AbstractModel):
@api.model
def _get_filter_options(self):
+ recruiter_group = self.env.ref('hr_recruitment.group_hr_recruitment_user')
+
recruiters = self.env['res.users'].search([
('share', '=', False),
('active', '=', True),
+ ('groups_id', 'in', recruiter_group.id),
], order='name')
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')
diff --git a/addons_extensions/hr_recruitment_dashboards/static/src/js/recruitment_dashboard.js b/addons_extensions/hr_recruitment_dashboards/static/src/js/recruitment_dashboard.js
index 7d6bb86fb..8b024345c 100644
--- a/addons_extensions/hr_recruitment_dashboards/static/src/js/recruitment_dashboard.js
+++ b/addons_extensions/hr_recruitment_dashboards/static/src/js/recruitment_dashboard.js
@@ -338,17 +338,35 @@ export class HrRecruitmentDashboard extends Component {
onRangeChange(event) {
this.state.filters.range = event.target.value;
+
if (event.target.value !== "custom") {
this.state.filters.date_from = "";
this.state.filters.date_to = "";
}
+
+ this.loadDashboard();
}
onInputChange(key, event) {
this.state.filters[key] = event.target.value;
+
if (key === "date_from" || key === "date_to") {
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) {
@@ -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) {
const values = this.state.filters[key] || [];
return (options || []).filter((item) => values.includes(item.id));
diff --git a/addons_extensions/hr_recruitment_dashboards/static/src/xml/recruitment_dashboard.xml b/addons_extensions/hr_recruitment_dashboards/static/src/xml/recruitment_dashboard.xml
index f8e5d42af..65dd9dd01 100644
--- a/addons_extensions/hr_recruitment_dashboards/static/src/xml/recruitment_dashboard.xml
+++ b/addons_extensions/hr_recruitment_dashboards/static/src/xml/recruitment_dashboard.xml
@@ -9,10 +9,7 @@
Pipeline health, recruiter performance, client submissions, hiring conversion, and urgent openings in one place.
-
-
+
@@ -25,14 +22,27 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/web_portal_form_custom/models/application_candidate_changes.py b/addons_extensions/web_portal_form_custom/models/application_candidate_changes.py
index 915f6c211..7765427cf 100644
--- a/addons_extensions/web_portal_form_custom/models/application_candidate_changes.py
+++ b/addons_extensions/web_portal_form_custom/models/application_candidate_changes.py
@@ -1,4 +1,5 @@
from odoo import api, fields, models,_
+from odoo.exceptions import ValidationError
class ApplicantCandidate(models.Model):
@@ -40,7 +41,6 @@ class ApplicantCandidate(models.Model):
if not self.resume:
return
-
return {
'type': 'ir.actions.act_url',
'url': f'/web/content/hr.candidate/{self.id}/resume/{self.resume_name}?download=false',
diff --git a/addons_extensions/web_portal_form_custom/static/src/css/candidate_card.css b/addons_extensions/web_portal_form_custom/static/src/css/candidate_card.css
index f57e17ee8..92ad5230e 100644
--- a/addons_extensions/web_portal_form_custom/static/src/css/candidate_card.css
+++ b/addons_extensions/web_portal_form_custom/static/src/css/candidate_card.css
@@ -30,7 +30,7 @@
.container-fluid.mt-2.mb-2 {
overflow: visible !important;
position: relative;
- z-index: 100;
+ z-index: 30;
}
/* Fix notebook stacking context */
@@ -42,4 +42,13 @@
/* Ensure dropdown appears above everything */
.modal-open .o_field_many2one .o_m2o_dropdown {
z-index: 1061 !important;
+}
+
+.candidate-name {
+ font-size: 1.4rem;
+ font-weight: 500;
+}
+
+.job-name {
+ font-size: 0.9rem;
}
\ No newline at end of file
diff --git a/addons_extensions/web_portal_form_custom/views/hr_applicant_form.xml b/addons_extensions/web_portal_form_custom/views/hr_applicant_form.xml
index b8d8767bd..0e1632d1f 100644
--- a/addons_extensions/web_portal_form_custom/views/hr_applicant_form.xml
+++ b/addons_extensions/web_portal_form_custom/views/hr_applicant_form.xml
@@ -26,29 +26,6 @@
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 */
.container-fluid,
@@ -65,6 +42,14 @@
.modal .o_field_many2one .o_m2o_dropdown {
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;
+ }
@@ -105,10 +90,10 @@