recruitment bug fixes

This commit is contained in:
pranaysaidurga 2026-06-17 15:51:56 +05:30
parent e2c8a25c7b
commit 7960d1926b
18 changed files with 637 additions and 272 deletions

View File

@ -14,6 +14,6 @@
"application": False,
"auto_install": False,
"external_dependencies": {
"python": ["requests"],
"python": ["requests","python-docx"],
},
}

View File

@ -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 = {}

View File

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

View File

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

View File

@ -5,60 +5,60 @@
<field name="name">emp.it.declaration.list</field>
<field name="model">emp.it.declaration</field>
<field name="arch" type="xml">
<list>
<field name="employee_id"/>
<field name="period_id"/>
<field name="total_investment"/>
<field name="tax_regime"/>
<field name="state"/>
</list>
</field>
</record>
<list>
<field name="employee_id"/>
<field name="period_id"/>
<field name="total_investment"/>
<field name="tax_regime"/>
<field name="state"/>
</list>
</field>
</record>
<record id="view_emp_it_declaration_form" model="ir.ui.view">
<field name="name">emp.it.declarations.form</field>
<field name="model">emp.it.declaration</field>
<field name="arch" type="xml">
<form string="IT Declaration">
<header>
<button name="action_submit"
string="Submit"
type="object"
class="btn-primary"
invisible="state != 'draft' or not costing_details_generated"/>
<button name="action_download_submission_pdf"
string="Download PDF"
type="object"
class="btn-primary"
icon="fa-download"
invisible="state != 'submitted'"/>
<button name="action_return_to_draft"
string="Return to Draft"
type="object"
class="btn-secondary"
invisible="state != 'submitted'"
groups="hr_payroll.group_hr_payroll_manager"/>
<field name="state" widget="statusbar" statusbar_visible="draft,submitted"/>
</header>
<sheet>
<div class="oe_title mb24">
<div class="o_row">
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..." readonly="costing_details_generated or state == 'submitted'"/>
</div>
</div>
<group>
<group>
<field name="period_id" readonly="costing_details_generated or state == 'submitted'"/>
</group>
<group>
<field name="total_investment" readonly="state == 'submitted'"/>
<field name="return_reason" placeholder="Reason for returning the declaration to draft..." readonly="state != 'submitted' or not is_payroll_manager"/>
<field name="is_payroll_manager" invisible="1"/>
<field name="costing_details_generated" invisible="1" force_save="1"/>
<field name="house_rent_costing_id"/>
<field name="show_past_employment" invisible="1"/>
<field name="model">emp.it.declaration</field>
<field name="arch" type="xml">
<form string="IT Declaration">
<header>
<button name="action_submit"
string="Submit"
type="object"
class="btn-primary"
invisible="state != 'draft' or not costing_details_generated"/>
<button name="action_download_submission_pdf"
string="Download PDF"
type="object"
class="btn-primary"
icon="fa-download"
invisible="state != 'submitted'"/>
<button name="action_return_to_draft"
string="Return to Draft"
type="object"
class="btn-secondary"
invisible="state != 'submitted'"
groups="hr_payroll.group_hr_payroll_manager"/>
<field name="state" widget="statusbar" statusbar_visible="draft,submitted"/>
</header>
<sheet>
<div class="oe_title mb24">
<div class="o_row">
<field name="employee_id" widget="res_partner_many2one" placeholder="Employee Name..." readonly="costing_details_generated or state == 'submitted'"/>
</div>
</div>
<group>
<group>
<field name="period_id" readonly="costing_details_generated or state == 'submitted'"/>
</group>
<group>
<field name="total_investment" readonly="state == 'submitted'"/>
<field name="return_reason" placeholder="Reason for returning the declaration to draft..." readonly="state != 'submitted' or not is_payroll_manager"/>
<field name="is_payroll_manager" invisible="1"/>
<field name="costing_details_generated" invisible="1" force_save="1"/>
<field name="house_rent_costing_id"/>
<field name="show_past_employment" invisible="1"/>
<field name="show_us_80c" invisible="1"/>
<field name="show_us_80d" invisible="1"/>
<field name="show_us_10" invisible="1"/>
@ -73,9 +73,9 @@
<br/>
<br/>
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}" readonly="state == 'submitted'"/>
<field name="tax_regime" nolabel="1" widget="radio" options="{'horizontal': true}" readonly="state == 'submitted'"/>
<br/>
<button name="generate_declarations" type="object" class="btn-primary" string="Generate" confirm="Upon Confirming you won't be able to change the Period &amp; Employee" help="Generate Data to upload the declaration Costing" invisible="costing_details_generated or state == 'submitted'"/>
<button name="generate_declarations" type="object" class="btn-primary" string="Generate" confirm="Upon Confirming you won't be able to change the Period &amp; Employee" help="Generate Data to upload the declaration Costing" invisible="costing_details_generated or state == 'submitted'"/>
<field name="is_section_open" invisible="1"/> <!-- Store toggle state -->
<group invisible="not costing_details_generated">
@ -95,7 +95,7 @@
<page string="Total Investment Costing">
<group>
<group>
<field name="visible_investment_costing_ids" nolabel="1" readonly="state == 'submitted'">
<field name="visible_investment_costing_ids" nolabel="1" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0" edit="0">
<field name="investment_type_id"/>
<field name="amount"/>
@ -126,7 +126,7 @@
<!-- </page>-->
<page name="past_employment_costings" string="PAST EMPLOYMENT" invisible="not show_past_employment">
<field name="past_employment_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<field name="past_employment_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -136,7 +136,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="past_employment_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<field name="past_employment_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -150,7 +150,7 @@
<page name="us_80c_costings" string="US 80C" invisible="not show_us_80c">
<field name="us80c_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<field name="us80c_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -166,7 +166,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="us80c_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<field name="us80c_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -186,10 +186,10 @@
</page>
<page name="us_80d_costings" string="US 80D" invisible="not show_us_80d">
<group>
<field name="us80d_selection_type" widget="radio" options="{'horizontal': true}" required="tax_regime == 'old' and costing_details_generated" readonly="state == 'submitted'"/>
<field name="us80d_health_checkup" readonly="state == 'submitted'"/>
<field name="us80d_selection_type" widget="radio" options="{'horizontal': true}" required="tax_regime == 'old' and costing_details_generated" readonly="state == 'submitted'"/>
<field name="us80d_health_checkup" readonly="state == 'submitted'"/>
</group>
<field name="us80d_costings" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family'" readonly="state == 'submitted'">
<field name="us80d_costings" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -199,7 +199,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="us80d_costings_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family'" readonly="state == 'submitted'">
<field name="us80d_costings_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -209,7 +209,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="us80d_costings_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_parent'" readonly="state == 'submitted'">
<field name="us80d_costings_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_parent'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -220,7 +220,7 @@
</list>
</field>
<field name="us80d_costings_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_parent'" readonly="state == 'submitted'">
<field name="us80d_costings_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_parent'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -231,7 +231,7 @@
</list>
</field>
<field name="us80d_costings_senior_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_senior_parent'" readonly="state == 'submitted'">
<field name="us80d_costings_senior_parents" invisible="tax_regime != 'old' or us80d_selection_type != 'self_family_senior_parent'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -242,7 +242,7 @@
</list>
</field>
<field name="us80d_costings_senior_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_senior_parent'" readonly="state == 'submitted'">
<field name="us80d_costings_senior_parents_new" invisible="tax_regime != 'new' or us80d_selection_type != 'self_family_senior_parent'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -255,7 +255,7 @@
</field>
</page>
<page name="us_10_costing" string="US 10" invisible="not show_us_10">
<field name="us10_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<field name="us10_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -265,7 +265,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="us10_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<field name="us10_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -277,7 +277,7 @@
</field>
</page>
<page name="us_80g_costing" string="US 80G" invisible="not show_us_80g">
<field name="us80g_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<field name="us80g_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -287,7 +287,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="us80g_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<field name="us80g_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -300,7 +300,7 @@
</page>
<page name="chapter_via_costings" string="CHAPTER VIA" invisible="not show_chapter_via">
<field name="chapter_via_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<field name="chapter_via_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -310,7 +310,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="chapter_via_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<field name="chapter_via_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -323,7 +323,7 @@
</page>
<page name="us_17_costings" string="US 17" invisible="not show_us_17">
<field name="us17_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<field name="us17_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -333,7 +333,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="us17_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<field name="us17_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -346,7 +346,7 @@
</page>
<page name="house_rent_costings" string="HOUSE RENT" invisible="not show_house_rent">
<!-- <field name="house_rent_costing_line_ids"/>-->
<field name="house_rent_costings" readonly="state == 'submitted'" context="{
<field name="house_rent_costings" readonly="state == 'submitted'" context="{
'default_costing_type': house_rent_costing_id
}">
<list string="House Rent Declarations">
@ -397,7 +397,7 @@
</field>
</page>
<page name="other_i_or_l_costings" string="OTHER INCOME/LOSS" invisible="not show_other_i_or_l">
<field name="other_il_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<field name="other_il_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -414,7 +414,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="other_il_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<field name="other_il_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -433,7 +433,7 @@
</field>
</page>
<page name="other_declaration_costings" string="Other Declarations" invisible="not show_other_declaration">
<field name="other_declaration_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<field name="other_declaration_costings" invisible="tax_regime != 'old'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -443,7 +443,7 @@
<field name="limit" readonly="1" force_save="1"/>
</list>
</field>
<field name="other_declaration_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<field name="other_declaration_costings_new" invisible="tax_regime != 'new'" readonly="state == 'submitted'">
<list editable="bottom" create="0" delete="0">
<field name="investment_type_line_id" width="600px" readonly="1" force_save="1" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="declaration_amount" width="130px"/>
@ -465,13 +465,21 @@
<field name="name">IT Declarations</field>
<field name="path">income-tax-declaration</field>
<field name="res_model">emp.it.declaration</field>
<field name="domain">[('employee_id.user_id', '=', uid)]</field>
<field name="view_mode">list,form</field>
</record>
<record id="action_manager_it_declaration" model="ir.actions.act_window">
<field name="name">IT Declarations</field>
<field name="path">employees-tax-declarations</field>
<field name="res_model">emp.it.declaration</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_hr_payroll_emp_root" name="Payroll" sequence="190" web_icon="hr_payroll,static/description/icon.png" groups="base.group_user"/>
<menuitem id="menu_it_declarations" name="IT Declarations"
parent="hr_payroll.menu_hr_payroll_root"
action="action_emp_it_declaration" sequence="99"/>
action="action_manager_it_declaration" sequence="99"/>
<menuitem id="menu_it_declarations_emp" name="IT Declarations"
parent="menu_hr_payroll_emp_root"
action="action_emp_it_declaration" sequence="1"/>

View File

@ -7,8 +7,11 @@ 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
action["context"]['current_id'] = self.id
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

View File

@ -7,5 +7,7 @@ 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"]['current_id'] = self.id
action["name"] = _("Parse Resumes")
return action

View File

@ -40,7 +40,7 @@
<field name="arch" type="xml">
<xpath expr="//header" position="inside">
<button name="action_open_auto_doc_wizard"
string="Parse Resumes"
string="Parse Resume"
type="object"
class="btn-secondary"
groups="hr_recruitment.group_hr_recruitment_user"/>

View File

@ -29,9 +29,10 @@
<field name="arch" type="xml">
<xpath expr="//header" position="inside">
<button name="action_open_auto_doc_wizard"
string="Parse Resumes"
string="Parse Resume"
type="object"
class="btn-secondary"
invisible="not resume"
groups="hr_recruitment.group_hr_recruitment_user"/>
</xpath>
</field>

View File

@ -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"),
],

View File

@ -6,8 +6,8 @@
<field name="arch" type="xml">
<form string="Parse Recruitment Documents" create="0" edit="1">
<header>
<button name="action_parse_documents" string="Parse Documents" type="object" class="btn-primary"/>
<button string="Close" special="cancel" class="btn-secondary"/>
<button name="action_parse_documents" string="Parse Documents" type="object" class="btn-primary" invisible="not attachment_ids and not resume_file"/>
<!-- <button string="Close" special="cancel" class="btn-secondary"/>-->
</header>
<sheet>
<group>
@ -22,13 +22,23 @@
<field name="update_existing_candidates" invisible="target_model != 'candidate'"/>
</group>
</group>
<group string="Upload Documents">
<group string="Upload Documents" invisible="single_parser">
<field name="attachment_ids"
widget="many2many_binary_dropzone"
nolabel="1"
class="w-100"
options="{'preview_images': true}"/>
</group>
<group string="Resume"
invisible="not single_parser">
<field name="resume_file"
filename="resume_filename"
widget="binary"/>
<field name="resume_filename"
invisible="1"/>
</group>
<group string="Uploaded Files">
<field name="line_ids" nolabel="1" readonly="1">
<kanban class="o_kanban_small_column">
@ -70,8 +80,16 @@
</group>
<group string="Result">
<field name="result_html" readonly="1" nolabel="1" widget="html"/>
<field name="create_updated_records" invisible="1"/>
<field name="parsed_document" invisible="1"/>
</group>
</sheet>
<footer>
<button name="action_create_or_update_records" string="Save" type="object" class="btn-primary" data-hotkey="q" invisible="create_updated_records or not parsed_document"/>
<button name="done_records" string="Done" type="object" class="btn-primary" data-hotkey="q" invisible="not create_updated_records"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x" invisible="create_updated_records"/>
</footer>
</form>
</field>
</record>

View File

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

View File

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

View File

@ -9,10 +9,7 @@
<p>Pipeline health, recruiter performance, client submissions, hiring conversion, and urgent openings in one place.</p>
</div>
<div class="o_hr_recruitment_dashboard__actions">
<button class="btn btn-light" t-on-click="clearFilters">Reset</button>
<button class="btn btn-primary" t-on-click="loadDashboard">
<i class="fa fa-refresh me-1"/> Apply Filters
</button>
<button class="btn btn-primary" t-on-click="clearFilters"><i class="fa fa-refresh me-1"/>Reset</button>
</div>
</div>
@ -25,14 +22,27 @@
</t>
</select>
</div>
<div class="o_filter_item">
<label>From</label>
<input type="date" class="form-control" t-att-value="state.filters.date_from" t-on-change="(ev) => this.onInputChange('date_from', ev)"/>
</div>
<div class="o_filter_item">
<label>To</label>
<input type="date" class="form-control" t-att-value="state.filters.date_to" t-on-change="(ev) => this.onInputChange('date_to', ev)"/>
</div>
<t t-if="state.filters.range === 'custom'">
<div class="o_filter_item">
<label>From</label>
<input
type="date"
class="form-control"
t-att-value="state.filters.date_from"
t-on-change="(ev) => this.onInputChange('date_from', ev)"
/>
</div>
<div class="o_filter_item">
<label>To</label>
<input
type="date"
class="form-control"
t-att-value="state.filters.date_to"
t-on-change="(ev) => this.onInputChange('date_to', ev)"
/>
</div>
</t>
<div class="o_filter_item">
<label>Recruiters</label>
<details class="o_filter_dropdown">

View File

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

View File

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

View File

@ -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;
}
</style>
</div>
</xpath>
@ -105,10 +90,10 @@
</div>
<div class="container-fluid mt-2 mb-2">
<div class="card border-0 shadow-sm rounded p-3">
<div class="row align-items-center">
<div class="card border-0 shadow-sm rounded p-3 overflow-visible">
<div class="row align-items-start">
<div class="col-lg-8">
<div class="d-flex align-items-center">
<div class="d-flex align-items-start">
<div class="me-4">
<field name="candidate_image"
@ -116,31 +101,41 @@
class="rounded border"
options="{'size':[140,140]}"/>
</div>
<div>
<h2 class="fw-bold text-dark mb-1">
<field name="candidate_id" nolabel="1"/>
</h2>
<div class="text-muted fs-5 mb-2">
<field name="job_id" nolabel="1"/>
<div class="d-flex flex-column gap-2">
<div class="candidate-name">
<field name="candidate_id"
nolabel="1"
can_create="True"
can_write="True"/>
</div>
<div class="job-name text-muted">
<field name="job_id"
nolabel="1"/>
</div>
<div>
<field name="priority" widget="priority" options="{'max': 5}"/>
<field name="priority"
widget="priority"
options="{'max': 5}"/>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="d-flex flex-column align-items-start gap-2">
<div class="d-flex align-items-center" invisible="not email_from">
<div class="d-flex align-items-start" invisible="not email_from">
<i class="fa fa-envelope me-3 text-primary"/>
<field name="email_from" widget="email" nolabel="1"/>
</div>
<div class="d-flex align-items-center"
<div class="d-flex align-items-start"
invisible="not partner_phone">
<i class="fa fa-phone me-3 text-primary"/>
<field name="partner_phone" widget="phone" nolabel="1"/>
</div>
<div class="d-flex align-items-center" invisible="not linkedin_profile">
<div class="d-flex align-items-start" invisible="not linkedin_profile">
<i class="fa fa-linkedin me-3 text-primary"/>
<field name="linkedin_profile" nolabel="1"/>
</div>
@ -174,13 +169,13 @@
groups="hr_recruitment.group_hr_recruitment_user" col="1">
<div class="mb-2">
<label for="current_ctc" class="fw-bold"/>
<div class="d-flex align-items-center gap-2">
<div class="d-flex align-items-start gap-2">
<field name="current_ctc" class="w-50" placeholder="Current CTC"/>
</div>
</div>
<div class="mb-2">
<label for="salary_expected" class="fw-bold"/>
<div class="d-flex align-items-center gap-2">
<div class="d-flex align-items-start gap-2">
<field name="salary_expected" class="w-50" placeholder="Expected CTC"/>
<span invisible="not salary_expected_extra">
+
@ -190,7 +185,7 @@
</div>
<div class="mb-2">
<label for="notice_period" string="Notice Period" class="fw-bold"/>
<div class="d-flex align-items-center gap-2">
<div class="d-flex align-items-start gap-2">
<field name="notice_period" class="w-25" placeholder="0"/>
<field name="notice_period_type" class="w-50" placeholder="Type"
required="notice_period &gt; 0"/>
@ -252,7 +247,7 @@
<group string="Experience" name="applicant_experience" col="1">
<div class="mb-2">
<label for="total_exp" string="Total Experience" class="fw-bold"/>
<div class="d-flex align-items-center gap-2">
<div class="d-flex align-items-start gap-2">
<field name="total_exp" class="w-25" placeholder="0"/>
<field name="total_exp_type" class="w-50" placeholder="Type"
required="total_exp &gt; 0"/>
@ -260,7 +255,7 @@
</div>
<div class="mb-2">
<label for="relevant_exp" string="Relevant Experience" class="fw-bold"/>
<div class="d-flex align-items-center gap-2">
<div class="d-flex align-items-start gap-2">
<field name="relevant_exp" class="w-25" placeholder="0"/>
<field name="relevant_exp_type" class="w-50" placeholder="Type"
required="relevant_exp &gt; 0"/>

View File

@ -23,7 +23,7 @@
</div>
<div>
<h2 class="fw-bold text-dark mb-1">
<field name="partner_name" nolabel="0" placeholder="Candidate Name"/>
<field name="partner_name" nolabel="0" required="1" placeholder="Candidate Name"/>
</h2>
<div class="d-flex align-items-center g-1">
<i class="fa fa-id-badge me-3 text-primary"/>