diff --git a/addons_extensions/cwf_timesheet/models/timesheet.py b/addons_extensions/cwf_timesheet/models/timesheet.py
index 4d257ab38..8245e95d0 100644
--- a/addons_extensions/cwf_timesheet/models/timesheet.py
+++ b/addons_extensions/cwf_timesheet/models/timesheet.py
@@ -115,7 +115,6 @@ class CwfTimesheet(models.Model):
external_group_id = self.env.ref("hr_employee_extended.group_external_user")
users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)])
employees = self.env['hr.employee'].search([('user_id', 'in', users.ids),'|',('doj','=',False),('doj','>=', self.week_start_date)])
- print(employees)
# Loop through each day of the week and create timesheet lines for each employee
while current_date <= end_date:
for employee in employees:
diff --git a/addons_extensions/document_parser/__manifest__.py b/addons_extensions/document_parser/__manifest__.py
index 05d96bc4f..65eec6e90 100644
--- a/addons_extensions/document_parser/__manifest__.py
+++ b/addons_extensions/document_parser/__manifest__.py
@@ -14,6 +14,6 @@
"application": False,
"auto_install": False,
"external_dependencies": {
- "python": ["requests"],
+ "python": ["requests","python-docx"],
},
}
diff --git a/addons_extensions/document_parser/models/document_parser_service.py b/addons_extensions/document_parser/models/document_parser_service.py
index a672c9706..ff5d8da16 100644
--- a/addons_extensions/document_parser/models/document_parser_service.py
+++ b/addons_extensions/document_parser/models/document_parser_service.py
@@ -421,6 +421,91 @@ Document:
def _get_param(self, 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):
if isinstance(fields, dict):
normalized = {}
diff --git a/addons_extensions/employee_it_declaration/models/emp_it_declaration.py b/addons_extensions/employee_it_declaration/models/emp_it_declaration.py
index 2df9f0f4d..76035b6b5 100644
--- a/addons_extensions/employee_it_declaration/models/emp_it_declaration.py
+++ b/addons_extensions/employee_it_declaration/models/emp_it_declaration.py
@@ -37,38 +37,44 @@ class ITDeclarationSubmittedLockMixin(models.AbstractModel):
class EmpITDeclaration(models.Model):
- _name = 'emp.it.declaration'
- _rec_name = 'employee_id'
- _description = "IT Declaration"
-
- # @api.depends('period_id', 'employee_id')
- # def _compute_name(self):
- # for sheet in self:
- # # sheet.name = _('%(period_id)s, %(emp_name)s', period_id=sheet.period_id.name, emp_name=sheet.employee_id.name)
- # sheet.name='hello world'
-
- employee_id = fields.Many2one(
- 'hr.employee',
- string="Employee",
- default=lambda self: self.env.user.employee_id,
- required=True
- )
- period_id = fields.Many2one(
- 'payroll.period',
- string="Payroll Period",
- required=True
- )
-
- display_period_label = fields.Char(string="Period Label", compute='_compute_display_period_label')
-
- @api.depends('period_id.name')
- def _compute_display_period_label(self):
- for rec in self:
- if rec.period_id:
- rec.display_period_label = f"Financial Year {rec.period_id.name}"
- else:
- rec.display_period_label = ""
-
+ _name = 'emp.it.declaration'
+ _rec_name = 'employee_id'
+ _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')
+ # def _compute_name(self):
+ # for sheet in self:
+ # # sheet.name = _('%(period_id)s, %(emp_name)s', period_id=sheet.period_id.name, emp_name=sheet.employee_id.name)
+ # sheet.name='hello world'
+
+ employee_id = fields.Many2one(
+ 'hr.employee',
+ string="Employee",
+ default=lambda self: self.env.user.employee_id,
+ required=True
+ )
+ period_id = fields.Many2one(
+ 'payroll.period',
+ string="Payroll Period",
+ required=True,
+ copy=False
+ )
+
+ display_period_label = fields.Char(string="Period Label", compute='_compute_display_period_label')
+
+ @api.depends('period_id.name')
+ def _compute_display_period_label(self):
+ for rec in self:
+ if rec.period_id:
+ rec.display_period_label = f"Financial Year {rec.period_id.name}"
+ else:
+ rec.display_period_label = ""
+
tax_regime = fields.Selection([
('new', 'New Regime'),
('old', 'Old Regime')
@@ -119,8 +125,8 @@ class EmpITDeclaration(models.Model):
for rec in self:
if rec.investment_costing_ids and rec.costing_details_generated:
rec.house_rent_costing_id = rec.investment_costing_ids.filtered(
- lambda e: e.investment_type_id.investment_type == 'house_rent'
- )[:1]
+ lambda e: e.investment_type_id.investment_type == 'house_rent'
+ )[:1]
else:
rec.house_rent_costing_id = False
@@ -166,13 +172,13 @@ class EmpITDeclaration(models.Model):
other_il_costings_new = fields.One2many('other.il.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
other_declaration_costings = fields.One2many('other.declaration.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['old', 'both'])])
other_declaration_costings_new = fields.One2many('other.declaration.costing.type','it_declaration_id',domain=[('investment_type_line_id.tax_regime', 'in', ['new', 'both'])])
- show_past_employment = fields.Boolean(compute="_compute_show_records")
- show_us_80c = fields.Boolean(compute="_compute_show_records")
- show_us_80d = fields.Boolean(compute="_compute_show_records")
- show_us_10 = fields.Boolean(compute="_compute_show_records")
- show_us_80g = fields.Boolean(compute="_compute_show_records")
- show_chapter_via = fields.Boolean(compute="_compute_show_records")
- show_us_17 = fields.Boolean(compute="_compute_show_records")
+ show_past_employment = fields.Boolean(compute="_compute_show_records")
+ show_us_80c = fields.Boolean(compute="_compute_show_records")
+ show_us_80d = fields.Boolean(compute="_compute_show_records")
+ show_us_10 = fields.Boolean(compute="_compute_show_records")
+ show_us_80g = fields.Boolean(compute="_compute_show_records")
+ show_chapter_via = fields.Boolean(compute="_compute_show_records")
+ show_us_17 = fields.Boolean(compute="_compute_show_records")
show_house_rent = fields.Boolean(compute="_compute_show_records")
show_other_i_or_l = fields.Boolean(compute="_compute_show_records")
show_other_declaration = fields.Boolean(compute="_compute_show_records")
@@ -353,7 +359,7 @@ class EmpITDeclaration(models.Model):
rec[field_name] = bool(visible_investment_types.filtered(
lambda inv: inv.investment_type == investment_type_key
))
-
+
def toggle_section_visibility(self):
for rec in self:
rec.is_section_open = not rec.is_section_open
@@ -372,37 +378,37 @@ class EmpITDeclaration(models.Model):
# self.fields_get()
if self.tax_regime == 'new':
domain = [('investment_type_line_id.tax_regime', 'in', ['new', 'both'])]
- elif self.tax_regime == 'old':
- domain = [('investment_type_line_id.tax_regime', 'in', ['old', 'both'])]
- else:
- domain = [] # Default case, although 'tax_regime' is required
- return {'domain': {'past_employment_costings': domain}}
- else:
- return {'domain': {'past_employment_costings': []}} # Handle potential empty state
-
- # def fields_get(self, allfields=None, attributes=None):
- # import pdb
- # pdb.set_trace()
- # res = super(empITDeclaration, self).fields_get(allfields, attributes)
- # print(res)
- #
- # # Example: Modify domain of field_1 based on field_2
- # if 'tax_regime' in res:
- # if self.tax_regime == '':
- # res['field_1']['domain'] = [('some_field', '=', 123)]
- # else:
- # res['field_1']['domain'] = [('some_field', '=', 456)]
- #
- # return res
- # import pdb
- # pdb.set_trace()
- # if rec.tax_regime:
- # return {'domain': {'past_employment_costings': [('investment_type_line_id.tax_regime', 'in', ['new','both'])]}}
- # return {
- # 'domain': {
- # 'past_employment_costings': [('investment_type_line_id.tax_regime', 'in', ['new','both'])]
- # }
- # }
+ elif self.tax_regime == 'old':
+ domain = [('investment_type_line_id.tax_regime', 'in', ['old', 'both'])]
+ else:
+ domain = [] # Default case, although 'tax_regime' is required
+ return {'domain': {'past_employment_costings': domain}}
+ else:
+ return {'domain': {'past_employment_costings': []}} # Handle potential empty state
+
+ # def fields_get(self, allfields=None, attributes=None):
+ # import pdb
+ # pdb.set_trace()
+ # res = super(empITDeclaration, self).fields_get(allfields, attributes)
+ # print(res)
+ #
+ # # Example: Modify domain of field_1 based on field_2
+ # if 'tax_regime' in res:
+ # if self.tax_regime == '':
+ # res['field_1']['domain'] = [('some_field', '=', 123)]
+ # else:
+ # res['field_1']['domain'] = [('some_field', '=', 456)]
+ #
+ # return res
+ # import pdb
+ # pdb.set_trace()
+ # if rec.tax_regime:
+ # return {'domain': {'past_employment_costings': [('investment_type_line_id.tax_regime', 'in', ['new','both'])]}}
+ # return {
+ # 'domain': {
+ # 'past_employment_costings': [('investment_type_line_id.tax_regime', 'in', ['new','both'])]
+ # }
+ # }
def generate_declarations(self):
for rec in self:
diff --git a/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py b/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py
index 358b0da2f..2e82ad7cd 100644
--- a/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py
+++ b/addons_extensions/employee_it_declaration/models/employee_payslip_download_wiz.py
@@ -42,8 +42,6 @@ class EmployeePayslipDownloadWizard(models.TransientModel):
def _compute_is_hr_manager(self):
for rec in self:
- import pdb
- pdb.set_trace()
rec.is_hr_manager = self.env.user.has_group('hr.group_hr_manager')
@api.onchange('download_type', 'period_id')
diff --git a/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py b/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py
index 6ebe1fd82..3f19c27df 100644
--- a/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py
+++ b/addons_extensions/employee_it_declaration/models/it_tax_statement_wiz.py
@@ -756,9 +756,9 @@ class ITTaxStatementWizard(models.TransientModel):
if not self.employee_id or not self.contract_id or not self.period_id or not self.period_line:
raise ValidationError(_("Select employee, period, and period line before checking comparison."))
- values = self._get_tax_base_values(include_comparison=True)
+ values = self.sudo()._get_tax_base_values(include_comparison=True)
if not values['comparison_available']:
- self._reset_regime_comparison()
+ self.sudo()._reset_regime_comparison()
raise ValidationError(_("Tax comparison is available only when both old and new regime slabs are configured."))
self.write({
@@ -938,7 +938,7 @@ class ITTaxStatementWizard(models.TransientModel):
return {'data': data}
def action_generate_report(self):
- report_data = self._prepare_income_tax_data(include_comparison=False)
+ report_data = self.sudo()._prepare_income_tax_data(include_comparison=False)
return self.env.ref('employee_it_declaration.income_tax_statement_action_report').report_action(
self,
@@ -946,7 +946,7 @@ class ITTaxStatementWizard(models.TransientModel):
)
def action_generate_comparison_report(self):
- report_data = self._prepare_income_tax_data(include_comparison=True)
+ report_data = self.sudo()._prepare_income_tax_data(include_comparison=True)
if not report_data['data']['comparison']['available']:
raise ValidationError(_("Tax comparison is available only when both old and new regime slabs are configured."))
diff --git a/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml b/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml
index b19816111..af6d56b98 100644
--- a/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml
+++ b/addons_extensions/employee_it_declaration/views/emp_it_declaration.xml
@@ -5,60 +5,60 @@
emp.it.declaration.list
emp.it.declaration
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
emp.it.declarations.form
- emp.it.declaration
-
-
+
+ IT Declarations
+ employees-tax-declarations
+ emp.it.declaration
+ list,form
+
+
+ action="action_manager_it_declaration" sequence="99"/>
diff --git a/addons_extensions/employee_it_declaration/views/it_tax_menu_and_wizard_view.xml b/addons_extensions/employee_it_declaration/views/it_tax_menu_and_wizard_view.xml
index 59bfaa342..203375040 100644
--- a/addons_extensions/employee_it_declaration/views/it_tax_menu_and_wizard_view.xml
+++ b/addons_extensions/employee_it_declaration/views/it_tax_menu_and_wizard_view.xml
@@ -43,7 +43,7 @@
-
+
diff --git a/addons_extensions/employee_it_declaration/wizards/letout_house_property.py b/addons_extensions/employee_it_declaration/wizards/letout_house_property.py
index 72510de3e..45d116d9f 100644
--- a/addons_extensions/employee_it_declaration/wizards/letout_house_property.py
+++ b/addons_extensions/employee_it_declaration/wizards/letout_house_property.py
@@ -1,10 +1,10 @@
from odoo import models, fields, api
import math
-class LetoutHouseProperty(models.Model):
- _name = 'letout.house.property'
- _inherit = ['it.declaration.submitted.lock.mixin']
- _description = 'Letout House Property Details'
+class LetoutHouseProperty(models.Model):
+ _name = 'letout.house.property'
+ _inherit = ['it.declaration.submitted.lock.mixin']
+ _description = 'Letout House Property Details'
it_declaration_id = fields.Many2one('emp.it.declaration', string="IT Declaration")
other_il_id = fields.Many2one('other.il.costing.type', string="Other Income/Loss Costing Type")
@@ -36,9 +36,6 @@ class LetoutHouseProperty(models.Model):
deduction = net_annual_value * 0.30
income_loss = net_annual_value - deduction - record.interest_paid_to
- print(net_annual_value)
- print(deduction)
- print(income_loss)
record.net_annual_value = net_annual_value
record.deduction_for_repairs = round(deduction)
- record.income_loss = round(income_loss)
+ record.income_loss = round(income_loss)
diff --git a/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py b/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py
index d745b1592..7a1e4687b 100644
--- a/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py
+++ b/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py
@@ -209,7 +209,6 @@ ORDER BY
except Exception as e:
error_msg = f"Error executing attendance report query: {str(e)}"
- print(error_msg)
raise UserError(
_("An error occurred while generating the attendance report. Please check the logs for details."))
diff --git a/addons_extensions/hr_recruitment_auto_doc/models/hr_applicant.py b/addons_extensions/hr_recruitment_auto_doc/models/hr_applicant.py
index f88e9d81b..2c09d56b2 100644
--- a/addons_extensions/hr_recruitment_auto_doc/models/hr_applicant.py
+++ b/addons_extensions/hr_recruitment_auto_doc/models/hr_applicant.py
@@ -7,8 +7,12 @@ class HrApplicant(models.Model):
def action_open_auto_doc_wizard(self):
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_applicant").read()[0]
context = dict(self.env.context)
+ context['single_parser'] = True
+ context['applicant_id'] = self.id
+ context['target_model'] = 'applicant'
if len(self) == 1 and self.hr_job_recruitment:
context["default_job_recruitment_id"] = self.hr_job_recruitment.id
action["context"] = context
action["name"] = _("Parse Resumes")
- return action
+
+ return action
\ No newline at end of file
diff --git a/addons_extensions/hr_recruitment_auto_doc/models/hr_candidate.py b/addons_extensions/hr_recruitment_auto_doc/models/hr_candidate.py
index d691152e5..3526071c2 100644
--- a/addons_extensions/hr_recruitment_auto_doc/models/hr_candidate.py
+++ b/addons_extensions/hr_recruitment_auto_doc/models/hr_candidate.py
@@ -7,5 +7,8 @@ class HrCandidate(models.Model):
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["context"] = dict(self.env.context)
+ action["context"]['single_parser'] = True
+ action["context"]['target_model'] = 'candidate'
+ action["context"]['candidate_id'] = self.id
action["name"] = _("Parse Resumes")
return action
diff --git a/addons_extensions/hr_recruitment_auto_doc/models/hr_job_recruitment.py b/addons_extensions/hr_recruitment_auto_doc/models/hr_job_recruitment.py
index 9eae68c54..96a0aa4da 100644
--- a/addons_extensions/hr_recruitment_auto_doc/models/hr_job_recruitment.py
+++ b/addons_extensions/hr_recruitment_auto_doc/models/hr_job_recruitment.py
@@ -7,6 +7,7 @@ class HrJobRecruitment(models.Model):
def action_open_auto_doc_wizard(self):
action = self.env.ref("hr_recruitment_auto_doc.action_hr_recruitment_auto_doc_wizard_job_recruitment").read()[0]
context = dict(self.env.context)
+ context['target_model'] = 'job_recruitment'
if len(self) == 1:
context["default_job_recruitment_id"] = self.id
action["context"] = context
diff --git a/addons_extensions/hr_recruitment_auto_doc/views/hr_applicant_views.xml b/addons_extensions/hr_recruitment_auto_doc/views/hr_applicant_views.xml
index 76ca68968..0e4b76753 100644
--- a/addons_extensions/hr_recruitment_auto_doc/views/hr_applicant_views.xml
+++ b/addons_extensions/hr_recruitment_auto_doc/views/hr_applicant_views.xml
@@ -39,11 +39,11 @@
-
+
+
+
+
+
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..3979d0c06 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"
@@ -70,10 +95,21 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
res["job_recruitment_id"] = default_job_recruitment_id
elif len(active_ids) == 1:
res["job_recruitment_id"] = active_ids[0]
+ if self.env.context.get("target_model"):
+ res["target_model"] = self.env.context.get("target_model")
+ if res["target_model"] == "candidate" and self.env.context.get("candidate_id"):
+ candidate = self.env["hr.candidate"].browse(int(self.env.context.get("candidate_id")))
+
+ if candidate and candidate.resume:
+ res.update({
+ "resume_file": candidate.resume,
+ "resume_filename": candidate.resume_name,
+ })
return res
def action_parse_documents(self):
self.ensure_one()
+ self.line_ids.unlink()
self._sync_upload_lines()
if not self.line_ids:
raise UserError(_("Please add at least one document to parse."))
@@ -119,52 +155,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 +231,8 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"updated_count": updated,
"skipped_count": skipped,
"result_html": self._build_summary_html(summary_rows),
+ "parsed_document": True,
+ "create_updated_records": False,
})
return {
@@ -191,8 +243,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 +455,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",
@@ -304,15 +528,24 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
def _find_or_create_candidate(self, line, parsed_data, parsed_payload):
candidate = self._find_existing_candidate(parsed_data)
+ existing_candidate = True
candidate_vals = self._prepare_candidate_vals(line, parsed_data, parsed_payload)
-
+ if self.env.context.get('candidate_id') and not candidate:
+ existing_candidate = False
+ candidate = self.env['hr.candidate'].sudo().browse(int(self.env.context.get('candidate_id')))
if candidate:
if self.update_existing_candidates:
- candidate.write(self._prepare_sparse_update_vals(candidate, candidate_vals))
+ candidate.write(self._prepare_sparse_update_vals(candidate, candidate_vals, False))
self._sync_candidate_skills(candidate, parsed_data.get("skills") or [])
if self.target_model == "candidate":
self._sync_candidate_resume_histories(candidate, parsed_data)
- message = _("Matched existing candidate: %s") % candidate.display_name
+ if existing_candidate:
+ message = _("Matched existing candidate: %s") % candidate.display_name
+ if self.env.context.get('candidate_id'):
+ self.env['hr.candidate'].sudo().browse(int(self.env.context.get('candidate_id'))).unlink()
+ else:
+ candidate.write(self._prepare_sparse_update_vals(candidate, candidate_vals, True))
+ message = _("Details Updated Successfully: %s") % candidate.display_name
return candidate, "updated", message
self._ensure_resume_creation_allowed(parsed_data, line.file_name)
@@ -374,7 +607,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
candidate = False
if email_value:
- candidate = search_model.search(['|',('email_from','=', email_value),("email_normalized", "=", email_value)], limit=1)
+ candidate = search_model.search([('id','!=',self.env.context.get('candidate_id')),'|',('email_from','=', email_value),("email_normalized", "=", email_value)], limit=1)
if not candidate and phone_values:
candidate = search_model.search([
"|","|",('partner_phone',"in",phone_values),
@@ -412,7 +645,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"))
@@ -756,7 +994,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
if not skills:
raise ValidationError(_("No reliable skills were found in the resume. Record creation was skipped."))
- def _prepare_sparse_update_vals(self, candidate, values):
+ def _prepare_sparse_update_vals(self, candidate, values, current_update:False):
update_vals = {}
field_map = {
"partner_name": not candidate.partner_name,
@@ -771,8 +1009,13 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
"resume_type": not getattr(candidate, "resume_type", False),
}
for field_name, can_update in field_map.items():
- if can_update and values.get(field_name):
+ if can_update and values.get(field_name) and not current_update:
update_vals[field_name] = values[field_name]
+ elif values.get(field_name) and current_update:
+ if field_name == 'partner_name':
+ update_vals[field_name] = values.get('partner_name')
+ else:
+ update_vals[field_name] = values[field_name]
return update_vals
def _sync_candidate_resume_histories(self, candidate, parsed_data):
@@ -1794,6 +2037,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/hr_recruitment_extended/models/hr_applicant.py b/addons_extensions/hr_recruitment_extended/models/hr_applicant.py
index ba2bb0b8c..4010d4888 100644
--- a/addons_extensions/hr_recruitment_extended/models/hr_applicant.py
+++ b/addons_extensions/hr_recruitment_extended/models/hr_applicant.py
@@ -9,6 +9,18 @@ from odoo.tools.mimetypes import guess_mimetype, fix_filename_extension
class HRApplicant(models.Model):
_inherit = 'hr.applicant'
_track_duration_field = 'recruitment_stage_id'
+ _rec_names_search = ['candidate_id', 'candidate_id.candidate_sequence', 'candidate_id.partner_phone','partner_name','job_id.name','hr_job_recruitment.recruitment_sequence']
+ _sql_constraints = [
+ ('name_applicant_job_id', 'unique(candidate_id, hr_job_recruitment)',
+ "Avoid creating Multiple applications for the same job")]
+
+ @api.constrains('candidate_id','hr_job_recruitment')
+ def _check_name_applicant_job_id(self):
+ for rec in self:
+ if rec.candidate_id and rec.hr_job_recruitment:
+ candidate_history = self.sudo().search([('id','!=',rec.id),('candidate_id','=',rec.candidate_id.id),('hr_job_recruitment','=',rec.hr_job_recruitment.id)])
+ if candidate_history:
+ raise ValidationError(_("Avoid creating Multiple applications for the same Job Position."))
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
primary_skill_match_percentage = fields.Float(
@@ -31,24 +43,24 @@ class HRApplicant(models.Model):
)
candidate_image = fields.Image(related='candidate_id.candidate_image', readonly=False, compute_sudo=True)
- submitted_to_client = fields.Boolean(string="Submitted to Client", default=False, tracking=True)
- client_submission_date = fields.Datetime(string="Submission Date", tracking=True)
- submitted_stage = fields.Many2one('hr.recruitment.stage', string="Submitted Stage", tracking=True)
+ submitted_to_client = fields.Boolean(string="Submitted to Client", default=False, tracking=True)
+ client_submission_date = fields.Datetime(string="Submission Date", tracking=True)
+ submitted_stage = fields.Many2one('hr.recruitment.stage', string="Submitted Stage", tracking=True)
refused_stage = fields.Many2one('hr.recruitment.stage', string="Reject Stage")
refused_comments = fields.Text(string='Reject Comments')
is_on_hold = fields.Boolean(string="Is On Hold", default=False)
- hold_state = fields.Selection(
- [('hold', 'On Hold')],
- string="Hold Status",
- compute='_compute_hold_state',
- )
- stage_comment_ids = fields.One2many(
- 'hr.applicant.stage.comment',
- 'applicant_id',
- string='Stage Comments',
- )
- stage_comment_count = fields.Integer(compute='_compute_stage_comment_count')
- stage_comment_tooltips = fields.Json(compute='_compute_stage_comment_tooltips')
+ hold_state = fields.Selection(
+ [('hold', 'On Hold')],
+ string="Hold Status",
+ compute='_compute_hold_state',
+ )
+ stage_comment_ids = fields.One2many(
+ 'hr.applicant.stage.comment',
+ 'applicant_id',
+ string='Stage Comments',
+ )
+ stage_comment_count = fields.Integer(compute='_compute_stage_comment_count')
+ stage_comment_tooltips = fields.Json(compute='_compute_stage_comment_tooltips')
@api.depends('is_on_hold')
def _compute_hold_state(self):
@@ -63,63 +75,63 @@ class HRApplicant(models.Model):
rec.is_on_hold = True
return {'type': 'ir.actions.client', 'tag': 'reload'}
- def action_toggle_chatter_visibility(self):
- for record in self:
- record.hide_chatter_suggestion = not record.hide_chatter_suggestion
- return {'type': 'ir.actions.client', 'tag': 'reload'}
-
- @api.depends('stage_comment_ids')
- def _compute_stage_comment_count(self):
- for applicant in self:
- applicant.stage_comment_count = len(applicant.stage_comment_ids)
-
- @api.depends('stage_comment_ids.comment', 'stage_comment_ids.stage_id', 'stage_comment_ids.user_id', 'stage_comment_ids.comment_date')
- def _compute_stage_comment_tooltips(self):
- for applicant in self:
- grouped_comments = {}
- for comment in applicant.stage_comment_ids.sorted(lambda item: item.comment_date or fields.Datetime.now()):
- if not comment.stage_id:
- continue
- line = '%s: %s' % (comment.user_id.name, comment.comment)
- grouped_comments.setdefault(comment.stage_id.id, []).append(line)
- applicant.stage_comment_tooltips = {
- stage_id: '\n'.join(comments[-4:])
- for stage_id, comments in grouped_comments.items()
- }
-
- def action_open_stage_comment_wizard(self):
- self.ensure_one()
- return {
- 'type': 'ir.actions.act_window',
- 'name': _('Stage Comments'),
- 'res_model': 'applicant.stage.comment.wizard',
- 'view_mode': 'form',
- 'view_id': self.env.ref('hr_recruitment_extended.view_applicant_stage_comment_wizard_form').id,
- 'target': 'new',
- 'context': {
- 'active_id': self.id,
- 'default_applicant_id': self.id,
- 'default_stage_id': self.recruitment_stage_id.id,
- },
- }
-
- @api.onchange('submitted_to_client')
- def _onchange_submitted_to_client(self):
- for applicant in self:
- if applicant.submitted_to_client:
- applicant.client_submission_date = applicant.client_submission_date or fields.Datetime.now()
- applicant.submitted_stage = applicant.submitted_stage or applicant.recruitment_stage_id
- else:
- applicant.client_submission_date = False
- applicant.submitted_stage = False
-
- @api.model_create_multi
- def create(self, vals_list):
- for vals in vals_list:
- if vals.get('submitted_to_client'):
- vals.setdefault('client_submission_date', fields.Datetime.now())
- vals.setdefault('submitted_stage', vals.get('recruitment_stage_id'))
- return super().create(vals_list)
+ def action_toggle_chatter_visibility(self):
+ for record in self:
+ record.hide_chatter_suggestion = not record.hide_chatter_suggestion
+ return {'type': 'ir.actions.client', 'tag': 'reload'}
+
+ @api.depends('stage_comment_ids')
+ def _compute_stage_comment_count(self):
+ for applicant in self:
+ applicant.stage_comment_count = len(applicant.stage_comment_ids)
+
+ @api.depends('stage_comment_ids.comment', 'stage_comment_ids.stage_id', 'stage_comment_ids.user_id', 'stage_comment_ids.comment_date')
+ def _compute_stage_comment_tooltips(self):
+ for applicant in self:
+ grouped_comments = {}
+ for comment in applicant.stage_comment_ids.sorted(lambda item: item.comment_date or fields.Datetime.now()):
+ if not comment.stage_id:
+ continue
+ line = '%s: %s' % (comment.user_id.name, comment.comment)
+ grouped_comments.setdefault(comment.stage_id.id, []).append(line)
+ applicant.stage_comment_tooltips = {
+ stage_id: '\n'.join(comments[-4:])
+ for stage_id, comments in grouped_comments.items()
+ }
+
+ def action_open_stage_comment_wizard(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': _('Stage Comments'),
+ 'res_model': 'applicant.stage.comment.wizard',
+ 'view_mode': 'form',
+ 'view_id': self.env.ref('hr_recruitment_extended.view_applicant_stage_comment_wizard_form').id,
+ 'target': 'new',
+ 'context': {
+ 'active_id': self.id,
+ 'default_applicant_id': self.id,
+ 'default_stage_id': self.recruitment_stage_id.id,
+ },
+ }
+
+ @api.onchange('submitted_to_client')
+ def _onchange_submitted_to_client(self):
+ for applicant in self:
+ if applicant.submitted_to_client:
+ applicant.client_submission_date = applicant.client_submission_date or fields.Datetime.now()
+ applicant.submitted_stage = applicant.submitted_stage or applicant.recruitment_stage_id
+ else:
+ applicant.client_submission_date = False
+ applicant.submitted_stage = False
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if vals.get('submitted_to_client'):
+ vals.setdefault('client_submission_date', fields.Datetime.now())
+ vals.setdefault('submitted_stage', vals.get('recruitment_stage_id'))
+ return super().create(vals_list)
@api.depends('hr_job_recruitment.skill_ids', 'hr_job_recruitment.secondary_skill_ids', 'candidate_id.skill_ids')
def _compute_skill_match_percentages(self):
@@ -153,27 +165,27 @@ class HRApplicant(models.Model):
stage_ids = stages.sudo()._search(search_domain, order=stages._order)
return stages.browse(stage_ids)
- def write(self, vals):
- if vals.get('submitted_to_client') and 'submitted_stage' not in vals and len(self) > 1:
- for applicant in self:
- applicant_vals = dict(vals)
- applicant_vals.setdefault('client_submission_date', fields.Datetime.now())
- applicant_vals['submitted_stage'] = vals.get('recruitment_stage_id') or applicant.recruitment_stage_id.id or False
- applicant.write(applicant_vals)
- return True
- if vals.get('submitted_to_client'):
- vals.setdefault('client_submission_date', fields.Datetime.now())
- if 'submitted_stage' not in vals:
- submitted_stage = vals.get('recruitment_stage_id') or self[:1].recruitment_stage_id.id
- if submitted_stage:
- vals['submitted_stage'] = submitted_stage
- elif vals.get('submitted_to_client') is False:
- vals.setdefault('client_submission_date', False)
- vals.setdefault('submitted_stage', False)
- if 'recruitment_stage_id' in vals:
- blocked_records = self.filtered(
- lambda applicant: applicant.is_on_hold and applicant.recruitment_stage_id.id != vals.get('recruitment_stage_id')
- )
+ def write(self, vals):
+ if vals.get('submitted_to_client') and 'submitted_stage' not in vals and len(self) > 1:
+ for applicant in self:
+ applicant_vals = dict(vals)
+ applicant_vals.setdefault('client_submission_date', fields.Datetime.now())
+ applicant_vals['submitted_stage'] = vals.get('recruitment_stage_id') or applicant.recruitment_stage_id.id or False
+ applicant.write(applicant_vals)
+ return True
+ if vals.get('submitted_to_client'):
+ vals.setdefault('client_submission_date', fields.Datetime.now())
+ if 'submitted_stage' not in vals:
+ submitted_stage = vals.get('recruitment_stage_id') or self[:1].recruitment_stage_id.id
+ if submitted_stage:
+ vals['submitted_stage'] = submitted_stage
+ elif vals.get('submitted_to_client') is False:
+ vals.setdefault('client_submission_date', False)
+ vals.setdefault('submitted_stage', False)
+ if 'recruitment_stage_id' in vals:
+ blocked_records = self.filtered(
+ lambda applicant: applicant.is_on_hold and applicant.recruitment_stage_id.id != vals.get('recruitment_stage_id')
+ )
if blocked_records:
raise ValidationError(_("You cannot change the stage of an applicant while it is on hold. Please unhold it first."))
if 'stage_id' in vals:
diff --git a/addons_extensions/hr_recruitment_extended/models/hr_employee_education_employer_family.py b/addons_extensions/hr_recruitment_extended/models/hr_employee_education_employer_family.py
index 2397a119e..160cc9c2b 100644
--- a/addons_extensions/hr_recruitment_extended/models/hr_employee_education_employer_family.py
+++ b/addons_extensions/hr_recruitment_extended/models/hr_employee_education_employer_family.py
@@ -2,14 +2,23 @@ from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
+class HRCandidate(models.Model):
+ _inherit = 'hr.candidate'
+
+ education_history = fields.Many2many('education.history', string='Education Details')
+
+ employer_history = fields.Many2many('employer.history', string='Education Details')
+
+ family_details = fields.Many2many('family.details', string='Family Details')
+
class HRApplicant(models.Model):
_inherit = 'hr.applicant'
- education_history = fields.One2many('education.history', 'applicant_id', string='Education Details')
+ education_history = fields.Many2many('education.history', related='candidate_id.education_history', readonly=False, string='Education Details')
- employer_history = fields.One2many('employer.history', 'applicant_id', string='Education Details')
+ employer_history = fields.Many2many('employer.history', related='candidate_id.employer_history', readonly=False, string='Education Details')
- family_details = fields.One2many('family.details', 'applicant_id', string='Family Details')
+ family_details = fields.Many2many('family.details', related='candidate_id.family_details', readonly=False, string='Family Details')
family_education_employer_details_status = fields.Selection([('pending', 'Pending'),
@@ -27,15 +36,6 @@ class HRApplicant(models.Model):
raise ValidationError(_("No Data to Validate"))
-class HRCandidate(models.Model):
- _inherit = 'hr.candidate'
-
- education_history = fields.One2many('education.history', 'candidate_id', string='Education Details')
-
- employer_history = fields.One2many('employer.history', 'candidate_id', string='Education Details')
-
- family_details = fields.One2many('family.details', 'candidate_id', string='Family Details')
-
class FamilyDetails(models.Model):
_inherit = "family.details"
diff --git a/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py b/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py
index b610af552..9b9f38701 100644
--- a/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py
+++ b/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py
@@ -10,6 +10,8 @@ class HrCandidate(models.Model):
_sql_constraints = [
('unique_candidate_sequence', 'UNIQUE(candidate_sequence)', 'Candidate sequence must be unique!'),
]
+ _rec_names_search = ['partner_name', 'candidate_sequence', 'partner_phone']
+
#personal Details
candidate_sequence = fields.Char(string='Candidate Sequence', readonly=False, default='/', copy=False)
hide_chatter_suggestion = fields.Boolean(string="Hide Chatter Suggestions", default=False, tracking=True)
diff --git a/addons_extensions/hr_recruitment_extended/static/src/js/recruitment_match_panel.js b/addons_extensions/hr_recruitment_extended/static/src/js/recruitment_match_panel.js
index d5bc89689..e28dd0a52 100644
--- a/addons_extensions/hr_recruitment_extended/static/src/js/recruitment_match_panel.js
+++ b/addons_extensions/hr_recruitment_extended/static/src/js/recruitment_match_panel.js
@@ -27,6 +27,12 @@ patch(RecruitmentFormController.prototype, {
this._matchPanelSearchTerm = "";
this._matchPanelActiveTab = "candidates";
this._matchPanelAddingCandidateId = null;
+ this._primarySkillsExpanded = false;
+ this._secondarySkillsExpanded = false;
+ this._matchPanelScrollPosition = {
+ candidates: 0,
+ applicants: 0,
+ };
onMounted(() => this._syncRecruitmentMatchPanel());
onPatched(() => this._syncRecruitmentMatchPanel());
@@ -161,7 +167,7 @@ patch(RecruitmentFormController.prototype, {
_renderSkillTags(skillNames, className) {
if (!skillNames.length) {
- return `${escapeHtml(_t("None"))}`;
+ return `${escapeHtml(_t("No Skills Added"))}`;
}
return skillNames
.map((skillName) => `${escapeHtml(skillName)}`)
@@ -288,6 +294,7 @@ patch(RecruitmentFormController.prototype, {
? _t("Applicants added to this recruitment will appear here with the same match insights.")
: _t("Try refreshing after updating the recruitment skills or candidate pool.")
);
+ const previousScroll = this._matchPanelScrollPosition[this._matchPanelActiveTab] || 0;
panel.innerHTML = `
@@ -295,7 +302,7 @@ patch(RecruitmentFormController.prototype, {
${escapeHtml(_t("Candidate Pool"))}
${escapeHtml(payload.job_recruitment_name || _t("Recruitment Matches"))}
-
${escapeHtml(`${payload.candidate_count || 0} ${_t("pool candidates")} • ${payload.applicant_count || 0} ${_t("applicants")}`)}
+
${escapeHtml(`${payload.candidate_count || 0} ${_t(" pool candidates ")} • ${payload.applicant_count || 0} ${_t(" applicants")}`)}
@@ -322,28 +329,88 @@ patch(RecruitmentFormController.prototype, {
-
-
-
- ${this._renderSkillTags(payload.primary_skill_names || [], "o_hr_match_chip_primary")}
-
-
-
-
-
- ${this._renderSkillTags(payload.secondary_skill_names || [], "o_hr_match_chip_secondary")}
-
-
-
+
+ ${
+ payload.primary_skill_names?.length
+ ? `
+
+
+ ${
+ this._primarySkillsExpanded
+ ? `
+
+ ${this._renderSkillTags(payload.primary_skill_names, "o_hr_match_chip_primary")}
+
+ `
+ : ""
+ }
+ `
+ : ""
+ }
+
+ ${
+ payload.secondary_skill_names?.length
+ ? `
+
+
+ ${
+ this._secondarySkillsExpanded
+ ? `
+
+ ${this._renderSkillTags(payload.secondary_skill_names, "o_hr_match_chip_secondary")}
+
+ `
+ : ""
+ }
+ `
+ : ""
+ }
+
+
${this._matchPanelLoading ? `${escapeHtml(_t("Refreshing matches..."))}
` : activeCards}
`;
panel.classList.add("o_hr_match_panel_open");
+ panel.querySelector('[data-toggle="primary"]')?.addEventListener("click", () => {
+ this._primarySkillsExpanded = !this._primarySkillsExpanded;
+ this._renderRecruitmentMatchPanel();
+ });
+ panel.querySelector('[data-toggle="secondary"]')?.addEventListener("click", () => {
+ this._secondarySkillsExpanded = !this._secondarySkillsExpanded;
+ this._renderRecruitmentMatchPanel();
+ });
panel.querySelector(".o_hr_match_close")?.addEventListener("click", () => this._closeRecruitmentMatchPanel());
- panel.querySelector(".o_hr_match_refresh")?.addEventListener("click", () => this._loadRecruitmentMatchPanelData());
+ panel.querySelector(".o_hr_match_refresh")?.addEventListener("click", () => {
+ this._matchPanelScrollPosition = {
+ candidates: 0,
+ applicants: 0,
+ };
+ this._loadRecruitmentMatchPanelData()
+ }
+ );
panel.querySelectorAll(".o_hr_match_tab").forEach((tabButton) => {
tabButton.addEventListener("click", () => {
this._matchPanelActiveTab = tabButton.dataset.tab || "candidates";
@@ -401,7 +468,17 @@ patch(RecruitmentFormController.prototype, {
}
});
}
+ const body = panel.querySelector(".o_hr_match_panel_body");
+ if (body) {
+ // Restore previous scroll for this tab
+ body.scrollTop = previousScroll;
+
+ // Continuously save scroll position
+ body.addEventListener("scroll", () => {
+ this._matchPanelScrollPosition[this._matchPanelActiveTab] = body.scrollTop;
+ });
+ }
const button = this._getRecruitmentMatchPanelButton();
if (button) {
button.classList.add("o_hr_match_fab_hidden");
diff --git a/addons_extensions/hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss b/addons_extensions/hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss
index 208544dec..e9732c4ca 100644
--- a/addons_extensions/hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss
+++ b/addons_extensions/hr_recruitment_extended/static/src/scss/recruitment_match_panel.scss
@@ -100,6 +100,7 @@
margin-top: 6px;
font-size: 22px;
font-weight: 700;
+ color: white;
}
.o_hr_match_panel_header span {
@@ -416,6 +417,44 @@
display: grid;
gap: 14px;
}
+.o_hr_match_collapsible {
+ margin-bottom: 12px;
+}
+
+.o_hr_match_collapsible_header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ cursor: pointer;
+ padding: 8px 12px;
+ border-radius: 8px;
+ background: #f5f5f5;
+ font-weight: 600;
+}
+
+.o_hr_match_collapsible_header:hover {
+ background: #ececec;
+}
+.o_hr_match_skill_header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .08em;
+ margin-bottom: 10px;
+ color: #334155;
+}
+
+.o_hr_match_skill_header:hover {
+ color: var(--o-brand-primary);
+}
+
+.o_hr_match_skill_count {
+ color: #64748b;
+ font-weight: 600;
+}
@media (min-width: 768px) {
.o_hr_match_skill_split {
diff --git a/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml b/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml
index 4412341cd..3528775de 100644
--- a/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml
+++ b/addons_extensions/hr_recruitment_extended/views/hr_applicant_views.xml
@@ -7,6 +7,7 @@
hold_state == 'hold'
+ id desc
@@ -16,7 +17,7 @@
-
+
@@ -289,6 +290,26 @@
is_on_hold
+
+
+
+
+
+
@@ -297,7 +318,7 @@
+ quick_create_view="hr_recruitment.quick_create_applicant_form" sample="1" default_order="id desc">
diff --git a/addons_extensions/hr_recruitment_extended/views/hr_employee_education_employer_family.xml b/addons_extensions/hr_recruitment_extended/views/hr_employee_education_employer_family.xml
index ebda49b45..4cfe126ae 100644
--- a/addons_extensions/hr_recruitment_extended/views/hr_employee_education_employer_family.xml
+++ b/addons_extensions/hr_recruitment_extended/views/hr_employee_education_employer_family.xml
@@ -29,7 +29,7 @@
name="action_validate_personal_details"
type="object"
class="btn btn-danger"
- invisible="not employee_id">
+ invisible="employee_id">
Click here to save, Validate and Update into Employee Data
+
+ hr.candidate.view.tree.inherit
+ hr.candidate
+
+
+
+ id desc
+
+
+
hr.candidate.view.kanban.inherit
hr.candidate
@@ -284,14 +294,30 @@
+
{'active_test': False, 'kanban': True}
+ id desc
-
+
diff --git a/addons_extensions/hr_recruitment_extended/wizards/post_onboarding_attachment_wizard.py b/addons_extensions/hr_recruitment_extended/wizards/post_onboarding_attachment_wizard.py
index b7aefcee0..145505d9e 100644
--- a/addons_extensions/hr_recruitment_extended/wizards/post_onboarding_attachment_wizard.py
+++ b/addons_extensions/hr_recruitment_extended/wizards/post_onboarding_attachment_wizard.py
@@ -98,7 +98,6 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
lambda a: a.attachment_type == 'previous_employer').mapped('name')
other_docs = rec.req_attachment_ids.filtered(lambda a: a.attachment_type == 'others').mapped('name')
- print(request_upload_url)
email_context = {
'applicant_request_form_id': rec.request_form_id.id,
'applicant_request_form_token': request_token,
diff --git a/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py b/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py
index 6c6ee5e0f..150aa9f59 100644
--- a/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py
+++ b/addons_extensions/hrms_emp_dashboard/controllers/hrms_emp_dashboard.py
@@ -37,7 +37,6 @@ class HrmsEmployeeDashboard(http.Controller):
calendar_attendances = self._get_attendances(employee, calendar_start, calendar_end)
calendar_leaves = self._get_leaves(employee, calendar_start, calendar_end)
calendar_public_holidays = self._get_public_holidays(employee, calendar_start, calendar_end)
- print(self._expense_data(employee, range_start, range_end))
return {
"success": True,
"employee": self._employee_card(employee),
@@ -325,12 +324,6 @@ class HrmsEmployeeDashboard(http.Controller):
("date", "<=", date_to),
])
for expense in emp_expenses:
- print(
- "Expense: %s | Date: %s | Amount: %s",
- expense.name,
- expense.date,
- expense.total_amount
- )
totals[expense.date.strftime("%Y-%m")] += expense.total_amount or 0.0
state_totals[expense.state or "draft"] += expense.total_amount or 0.0
diff --git a/addons_extensions/offer_letters/__manifest__.py b/addons_extensions/offer_letters/__manifest__.py
index b7a27c728..3104f490b 100644
--- a/addons_extensions/offer_letters/__manifest__.py
+++ b/addons_extensions/offer_letters/__manifest__.py
@@ -12,6 +12,7 @@
'data': [
'security/ir.model.access.csv',
'data/mail_template.xml',
+ 'views/stages.xml',
'views/offer_letter_views.xml',
'views/hr_applicant_offer_views.xml',
'views/offer_response_templates.xml',
diff --git a/addons_extensions/offer_letters/models/__init__.py b/addons_extensions/offer_letters/models/__init__.py
index 3afed8da1..2e82655fc 100644
--- a/addons_extensions/offer_letters/models/__init__.py
+++ b/addons_extensions/offer_letters/models/__init__.py
@@ -1,3 +1,4 @@
+from . import stages
from . import offer_letter
from . import hr_applicant
from . import hr_candidate
\ No newline at end of file
diff --git a/addons_extensions/offer_letters/models/hr_applicant.py b/addons_extensions/offer_letters/models/hr_applicant.py
index b47e5e51c..5f768a093 100644
--- a/addons_extensions/offer_letters/models/hr_applicant.py
+++ b/addons_extensions/offer_letters/models/hr_applicant.py
@@ -26,6 +26,8 @@ class HRApplicant(models.Model):
store=False,
)
+ request_offer_release = fields.Boolean(related='recruitment_stage_id.request_offer_release')
+
@api.depends('offer_letter_ids', 'offer_letter_ids.create_date', 'offer_letter_ids.state')
def _compute_current_offer_letter(self):
for applicant in self:
diff --git a/addons_extensions/offer_letters/models/offer_letter.py b/addons_extensions/offer_letters/models/offer_letter.py
index d8b18eea7..25287fccb 100644
--- a/addons_extensions/offer_letters/models/offer_letter.py
+++ b/addons_extensions/offer_letters/models/offer_letter.py
@@ -174,12 +174,12 @@ class OfferLetter(models.Model):
first_day = today.replace(day=1)
last_day = today.replace(day=calendar.monthrange(today.year, today.month)[1])
- payslip = self.env['hr.payslip'].new({
+ payslip = self.env['hr.payslip'].sudo().new({
'date_from': first_day,
'date_to': last_day,
})
- contract = self.env['hr.contract'].new({
+ contract = self.env['hr.contract'].sudo().new({
'date_start': first_day,
'date_end': last_day,
'l10n_in_medical_insurance':self.mi,
@@ -205,7 +205,7 @@ class OfferLetter(models.Model):
'inputs': {},
}
blacklisted_ids = set(self.env.context.get('prevent_payslip_computation_line_ids', []))
- for rule in sorted(self.pay_struct_id.rule_ids, key=lambda r: r.sequence):
+ for rule in sorted(self.sudo().pay_struct_id.rule_ids, key=lambda r: r.sequence):
if rule.id in blacklisted_ids or not rule._satisfy_condition(localdict):
continue
qty = 1.0
@@ -223,15 +223,15 @@ class OfferLetter(models.Model):
except Exception as e:
raise UserError(_("Error in rule %s: %s") % (rule.name, str(e)))
- total = payslip._get_payslip_line_total(amount, qty, rate, rule)
+ total = payslip.sudo()._get_payslip_line_total(amount, qty, rate, rule)
rule_code = rule.code
previous_amount = localdict.get(rule.code, 0.0)
category_code = rule.category_id.code
- tot_rule = payslip._get_payslip_line_total(amount, qty, rate, rule)
+ tot_rule = payslip.sudo()._get_payslip_line_total(amount, qty, rate, rule)
# Make sure _sum_salary_rule_category method exists
if hasattr(rule.category_id, '_sum_salary_rule_category'):
- localdict = rule.category_id._sum_salary_rule_category(localdict, tot_rule - previous_amount)
+ localdict = rule.category_id.sudo()._sum_salary_rule_category(localdict, tot_rule - previous_amount)
localdict[rule_code] = total
rules_dict[rule_code] = rule
diff --git a/addons_extensions/offer_letters/models/stages.py b/addons_extensions/offer_letters/models/stages.py
new file mode 100644
index 000000000..8699343c0
--- /dev/null
+++ b/addons_extensions/offer_letters/models/stages.py
@@ -0,0 +1,6 @@
+from odoo import models, fields, api, _
+
+class RecruitmentStage(models.Model):
+ _inherit = 'hr.recruitment.stage'
+
+ request_offer_release = fields.Boolean(string="Request Offer Release")
\ No newline at end of file
diff --git a/addons_extensions/offer_letters/views/hr_applicant_offer_views.xml b/addons_extensions/offer_letters/views/hr_applicant_offer_views.xml
index 34cb2153c..ff04f7765 100644
--- a/addons_extensions/offer_letters/views/hr_applicant_offer_views.xml
+++ b/addons_extensions/offer_letters/views/hr_applicant_offer_views.xml
@@ -26,7 +26,7 @@
+ invisible="not id or not request_offer_release"/>
diff --git a/addons_extensions/offer_letters/views/stages.xml b/addons_extensions/offer_letters/views/stages.xml
new file mode 100644
index 000000000..b9915559d
--- /dev/null
+++ b/addons_extensions/offer_letters/views/stages.xml
@@ -0,0 +1,14 @@
+
+
+
+ hr.recruitment.stage.form.offer.request.extended
+ hr.recruitment.stage
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/offer_letters/wizards/offer_release_request_wizard.py b/addons_extensions/offer_letters/wizards/offer_release_request_wizard.py
index 960bf1ed5..461836a76 100644
--- a/addons_extensions/offer_letters/wizards/offer_release_request_wizard.py
+++ b/addons_extensions/offer_letters/wizards/offer_release_request_wizard.py
@@ -70,7 +70,7 @@ class OfferReleaseRequestWizard(models.TransientModel):
def _get_default_pay_structure(self, applicant):
company = applicant.company_id or self.env.company
- return self.env['hr.payroll.structure'].search([
+ return self.env['hr.payroll.structure'].sudo().search([
], limit=1)
def _get_default_manager(self, applicant):
@@ -104,7 +104,7 @@ class OfferReleaseRequestWizard(models.TransientModel):
f"Review Offer Letter Request
"
)
- mail = self.env['mail.mail'].create({
+ mail = self.env['mail.mail'].sudo().create({
'email_from': self.email_from,
'email_to': self.email_to,
'email_cc': self.email_cc,
@@ -114,5 +114,5 @@ class OfferReleaseRequestWizard(models.TransientModel):
'model': 'offer.letter',
'res_id': offer_letter.id,
})
- mail.send()
+ mail.sudo().send()
return {'type': 'ir.actions.act_window_close'}
diff --git a/addons_extensions/project_dashboards_management/controllers/project_dashboard_controller.py b/addons_extensions/project_dashboards_management/controllers/project_dashboard_controller.py
index c40efffd5..a9465b408 100644
--- a/addons_extensions/project_dashboards_management/controllers/project_dashboard_controller.py
+++ b/addons_extensions/project_dashboards_management/controllers/project_dashboard_controller.py
@@ -20,19 +20,15 @@ class ProjectDashboardController(http.Controller):
# 1. Get project data
project_data = self._get_project_data(project)
- print(project_data)
# 2. Get tasks data
tasks_data = self._get_tasks_data(project)
- print(tasks_data)
# 3. Get employee performance
employee_performance = self._get_employee_performance(project)
- print(employee_performance)
# 4. Get budget data
budget_data = self._get_budget_data(project)
- print(budget_data)
return {
'success': True,
diff --git a/addons_extensions/search_view_extension/static/src/search_tabs/search_tabs.scss b/addons_extensions/search_view_extension/static/src/search_tabs/search_tabs.scss
index 6e4f70c97..0807c9a0c 100644
--- a/addons_extensions/search_view_extension/static/src/search_tabs/search_tabs.scss
+++ b/addons_extensions/search_view_extension/static/src/search_tabs/search_tabs.scss
@@ -7,4 +7,8 @@
color: var(--body-color, #374151);
padding: 0.35rem 0.75rem;
}
+
+ .nav-link.active {
+ color: #fff !important;
+ }
}
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..cb1d6edac 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):
@@ -35,12 +36,17 @@ class ApplicantCandidate(models.Model):
string="Resume Type"
)
+
+ certificate_ids = fields.Many2many(
+ 'applicant.certificate',
+ string='Certificates'
+ )
+
def action_open_resume_candidate(self):
self.ensure_one()
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',
@@ -152,9 +158,10 @@ class CandidateApplicant(models.Model):
string="Online Tests"
)
- certificate_ids = fields.One2many(
+ certificate_ids = fields.Many2many(
'applicant.certificate',
- 'applicant_id',
+ related='candidate_id.certificate_ids',
+ readonly=False,
string='Certificates'
)
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..cddc626f3 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,34 @@
/* 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;
+}
+
+/* Make the email field occupy the available width */
+.o_field_widget[name="email_from"] {
+ width: 100% !important;
+ max-width: none !important;
+ flex: 1 1 auto !important;
+}
+
+/* Input inside the email field */
+.o_field_widget[name="email_from"] input {
+ width: 100% !important;
+ max-width: none !important;
+ min-width: 350px !important; /* adjust as needed */
+}
+
+/* If rendered as a link instead of input */
+.o_field_widget[name="email_from"] a {
+ white-space: nowrap !important;
+ overflow: visible !important;
+ text-overflow: clip !important;
}
\ 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..c1febe092 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 @@
-
-
+
+
-
+
-
-
+
-
-
+
@@ -174,13 +170,13 @@
groups="hr_recruitment.group_hr_recruitment_user" col="1">
-
-
+
+
@@ -190,7 +186,7 @@
-
+
@@ -252,7 +248,7 @@
-
+
@@ -260,7 +256,7 @@
-
+
@@ -273,7 +269,7 @@
-
+
diff --git a/addons_extensions/web_portal_form_custom/views/hr_candidate_form.xml b/addons_extensions/web_portal_form_custom/views/hr_candidate_form.xml
index f321b596a..7a8930a14 100644
--- a/addons_extensions/web_portal_form_custom/views/hr_candidate_form.xml
+++ b/addons_extensions/web_portal_form_custom/views/hr_candidate_form.xml
@@ -23,7 +23,7 @@
-
+
@@ -84,6 +84,232 @@
options="{'color_field':'color','no_create':True}"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Company:
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ CTC :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ University:
+
+
+
+
+ Education:
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ Grade:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/website_hr_recruitment_extended/controllers/main.py b/addons_extensions/website_hr_recruitment_extended/controllers/main.py
index 118002c53..1f34f5ef4 100644
--- a/addons_extensions/website_hr_recruitment_extended/controllers/main.py
+++ b/addons_extensions/website_hr_recruitment_extended/controllers/main.py
@@ -956,7 +956,6 @@ class WebsiteJobHrRecruitment(WebsiteHrRecruitment):
))
error_message = 'An application already exists for %s Duplicates might be rejected. %s '%(value,recruiter_contact)
- print(error_message)
return {
'message': _(error_message)
}
diff --git a/addons_extensions/website_hr_recruitment_extended/models/website.py b/addons_extensions/website_hr_recruitment_extended/models/website.py
index 62b15cb31..56c8926a5 100644
--- a/addons_extensions/website_hr_recruitment_extended/models/website.py
+++ b/addons_extensions/website_hr_recruitment_extended/models/website.py
@@ -16,6 +16,4 @@ class Website(models.Model):
result = super()._search_get_details(search_type, order, options)
if search_type in ['job_requests', 'all']:
result.append(self.env['hr.job.recruitment']._search_get_detail(self, order, options))
- print(result)
- print("hello result")
return result