Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module
This commit is contained in:
commit
19669a7a3a
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -14,6 +14,6 @@
|
|||
"application": False,
|
||||
"auto_install": False,
|
||||
"external_dependencies": {
|
||||
"python": ["requests"],
|
||||
"python": ["requests","python-docx"],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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."))
|
||||
|
||||
|
|
|
|||
|
|
@ -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 & 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 & 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"/>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
<field name="currency_id" invisible="1"/>
|
||||
<field name="is_general_tax_statement" invisible="1"/>
|
||||
<group>
|
||||
<field name="is_hr_manager" invisible="0" force_save="1"/>
|
||||
<field name="is_hr_manager" invisible="1" force_save="1"/>
|
||||
<field name="employee_id" readonly="not is_hr_manager" options="{'no_edit': True, 'no_create': True}"/>
|
||||
<field name="contract_id" readonly="1" force_save="1" invisible="0"/>
|
||||
</group>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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."))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@
|
|||
<field name="inherit_id" ref="hr_recruitment_extended.hr_applicant_view_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="action_open_auto_doc_wizard"
|
||||
string="Parse Resumes"
|
||||
type="object"
|
||||
class="btn-secondary"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||
<!-- <button name="action_open_auto_doc_wizard"-->
|
||||
<!-- string="Parse Resume"-->
|
||||
<!-- type="object"-->
|
||||
<!-- class="btn-secondary"-->
|
||||
<!-- groups="hr_recruitment.group_hr_recruitment_user"/>-->
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 `<span class="o_hr_match_chip ${className}">${escapeHtml(_t("None"))}</span>`;
|
||||
return `<span class="o_hr_match_chip ${className}">${escapeHtml(_t("No Skills Added"))}</span>`;
|
||||
}
|
||||
return skillNames
|
||||
.map((skillName) => `<span class="o_hr_match_chip ${className}">${escapeHtml(skillName)}</span>`)
|
||||
|
|
@ -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 = `
|
||||
<div class="o_hr_match_panel_backdropless">
|
||||
|
|
@ -295,7 +302,7 @@ patch(RecruitmentFormController.prototype, {
|
|||
<div>
|
||||
<p>${escapeHtml(_t("Candidate Pool"))}</p>
|
||||
<h3>${escapeHtml(payload.job_recruitment_name || _t("Recruitment Matches"))}</h3>
|
||||
<span>${escapeHtml(`${payload.candidate_count || 0} ${_t("pool candidates")} • ${payload.applicant_count || 0} ${_t("applicants")}`)}</span>
|
||||
<span>${escapeHtml(`${payload.candidate_count || 0} ${_t(" pool candidates ")} • ${payload.applicant_count || 0} ${_t(" applicants")}`)}</span>
|
||||
</div>
|
||||
<div class="o_hr_match_panel_actions">
|
||||
<button type="button" class="btn btn-light o_hr_match_refresh">${escapeHtml(_t("Refresh"))}</button>
|
||||
|
|
@ -322,28 +329,88 @@ patch(RecruitmentFormController.prototype, {
|
|||
</label>
|
||||
</section>
|
||||
<section class="o_hr_match_panel_skill_summary">
|
||||
<div>
|
||||
<label>${escapeHtml(_t("Primary Skills"))}</label>
|
||||
<div class="o_hr_match_chip_row">
|
||||
${this._renderSkillTags(payload.primary_skill_names || [], "o_hr_match_chip_primary")}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>${escapeHtml(_t("Secondary Skills"))}</label>
|
||||
<div class="o_hr_match_chip_row">
|
||||
${this._renderSkillTags(payload.secondary_skill_names || [], "o_hr_match_chip_secondary")}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
${
|
||||
payload.primary_skill_names?.length
|
||||
? `
|
||||
<div class="o_hr_match_skill_header"
|
||||
data-toggle="primary">
|
||||
<span>
|
||||
${this._primarySkillsExpanded ? "▼" : "▶"}
|
||||
${escapeHtml(_t("Primary Skills"))}
|
||||
</span>
|
||||
|
||||
<span class="o_hr_match_skill_count">
|
||||
(${payload.primary_skill_names.length})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
${
|
||||
this._primarySkillsExpanded
|
||||
? `
|
||||
<div class="o_hr_match_chip_row">
|
||||
${this._renderSkillTags(payload.primary_skill_names, "o_hr_match_chip_primary")}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
payload.secondary_skill_names?.length
|
||||
? `
|
||||
<div class="o_hr_match_skill_header"
|
||||
data-toggle="secondary">
|
||||
<span>
|
||||
${this._secondarySkillsExpanded ? "▼" : "▶"}
|
||||
${escapeHtml(_t("Secondary Skills"))}
|
||||
</span>
|
||||
|
||||
<span class="o_hr_match_skill_count">
|
||||
(${payload.secondary_skill_names.length})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
${
|
||||
this._secondarySkillsExpanded
|
||||
? `
|
||||
<div class="o_hr_match_chip_row">
|
||||
${this._renderSkillTags(payload.secondary_skill_names, "o_hr_match_chip_secondary")}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
</section>
|
||||
<section class="o_hr_match_panel_body ${this._matchPanelLoading ? "o_hr_match_panel_body_loading" : ""}">
|
||||
${this._matchPanelLoading ? `<div class="o_hr_match_loading">${escapeHtml(_t("Refreshing matches..."))}</div>` : activeCards}
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="decoration-muted">hold_state == 'hold'</attribute>
|
||||
<attribute name="default_order">id desc</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='message_needaction']" position="after">
|
||||
<field name="is_on_hold" column_invisible="1"/>
|
||||
|
|
@ -16,7 +17,7 @@
|
|||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="after">
|
||||
<field name="recruitment_stage_id"/>
|
||||
<field name="hold_state" widget="badge" decoration-muted="hold_state == 'hold'" optional="show" nolabel="1"/>
|
||||
<field name="hold_state" string="Hold State" widget="badge" decoration-muted="hold_state == 'hold'" optional="show"/>
|
||||
<field name="submitted_to_client" optional="show"/>
|
||||
<field name="client_submission_date" optional="show"/>
|
||||
<field name="submitted_stage" optional="hide"/>
|
||||
|
|
@ -289,6 +290,26 @@
|
|||
<xpath expr="//field[@name='kanban_state']" position="attributes">
|
||||
<attribute name="invisible">is_on_hold</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-name='card']/field[@name='categ_ids']" position="replace"/>
|
||||
<xpath expr="//t[@t-name='card']/field[@name='applicant_properties']" position="replace"/>
|
||||
<xpath expr="//t[@t-name='card']/field[@name='job_id']" position="replace">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<field name="job_id" invisible="context.get('search_default_job_id', False)" on_change="1"/>
|
||||
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field name="applicant_properties" widget="properties"/>
|
||||
<!-- <field name="partner_name" class="fw-bold fs-5"/>-->
|
||||
</div>
|
||||
<t t-if="record.candidate_image.raw_value">
|
||||
<field name="candidate_image"
|
||||
widget="image"
|
||||
class="rounded-circle ms-2"
|
||||
options="{'size': [48, 48]}"/>
|
||||
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_kanban_view_applicant_inherit" model="ir.ui.view">
|
||||
|
|
@ -297,7 +318,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<kanban highlight_color="color" default_group_by="recruitment_stage_id"
|
||||
class="o_kanban_applicant o_search_matching_applicant"
|
||||
quick_create_view="hr_recruitment.quick_create_applicant_form" sample="1">
|
||||
quick_create_view="hr_recruitment.quick_create_applicant_form" sample="1" default_order="id desc">
|
||||
<field name="recruitment_stage_id" options='{"group_by_tooltip": {"requirements": "Requirements"}}'/>
|
||||
<field name="legend_normal"/>
|
||||
<field name="legend_blocked"/>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
name="action_validate_personal_details"
|
||||
type="object"
|
||||
class="btn btn-danger"
|
||||
invisible="not employee_id">
|
||||
invisible="employee_id">
|
||||
<div>
|
||||
Click here to save, Validate and Update into Employee Data
|
||||
<field name="personal_details_status"
|
||||
|
|
|
|||
|
|
@ -276,6 +276,16 @@
|
|||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_candidate_view_tree_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.candidate.view.tree.inherit</field>
|
||||
<field name="model">hr.candidate</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="default_order">id desc</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="hr_candidate_view_kanban_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.candidate.view.kanban.inherit</field>
|
||||
<field name="model">hr.candidate</field>
|
||||
|
|
@ -284,14 +294,30 @@
|
|||
<!-- Ensure applicant_ids is included in the kanban field list -->
|
||||
<xpath expr="//kanban" position="inside">
|
||||
<field name="applicant_ids" context="{'active_test': False}"/>
|
||||
<field name="candidate_image"/>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//kanban" position="attributes">
|
||||
<attribute name="context">{'active_test': False, 'kanban': True}</attribute>
|
||||
<attribute name="default_order">id desc</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//field[@name='partner_name']" position="before">
|
||||
<field t-if="record.candidate_sequence.raw_value" name="candidate_sequence" class="fw-bold fs-4"/>
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<field t-if="record.candidate_sequence.raw_value"
|
||||
name="candidate_sequence"
|
||||
class="fw-bold fs-4"/>
|
||||
<!-- <field name="partner_name" class="fw-bold fs-5"/>-->
|
||||
</div>
|
||||
<t t-if="record.candidate_image.raw_value">
|
||||
<field name="candidate_image"
|
||||
widget="image"
|
||||
class="rounded-circle ms-2"
|
||||
options="{'size': [48, 48]}"/>
|
||||
|
||||
</t>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//t[@t-name='card']" position="inside">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from . import stages
|
||||
from . import offer_letter
|
||||
from . import hr_applicant
|
||||
from . import hr_candidate
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
<xpath expr="//button[@name='action_share_applicant']" position="before">
|
||||
<button name="action_request_offer_release" string="Request Offer Release" type="object" class="btn-primary"
|
||||
groups="hr_recruitment.group_hr_recruitment_user"
|
||||
invisible="not id"/>
|
||||
invisible="not id or not request_offer_release"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook/page[@name='request_forms']" position="after">
|
||||
<page string="Offer Letters" name="offer_letters">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="hr_recruitment_stage_offer_request_form_extended">
|
||||
<field name="name">hr.recruitment.stage.form.offer.request.extended</field>
|
||||
<field name="model">hr.recruitment.stage</field>
|
||||
<field name="inherit_id" ref="hr_recruitment.hr_recruitment_stage_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='fold']" position="after">
|
||||
<field name="request_offer_release"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -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"<p><a href=\"{offer_url}\">Review Offer Letter Request</a></p>"
|
||||
)
|
||||
|
||||
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'}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,4 +7,8 @@
|
|||
color: var(--body-color, #374151);
|
||||
padding: 0.35rem 0.75rem;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,42 @@
|
|||
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"
|
||||
options="{'no_quick_create': True}"
|
||||
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 +170,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 +186,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 > 0"/>
|
||||
|
|
@ -252,7 +248,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 > 0"/>
|
||||
|
|
@ -260,7 +256,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 > 0"/>
|
||||
|
|
@ -273,7 +269,7 @@
|
|||
<!-- EXPERIENCE TAB -->
|
||||
<page string="Past Experience">
|
||||
|
||||
<field name="employer_history" nolabel="1">
|
||||
<field name="employer_history" nolabel="1" force_save="1">
|
||||
|
||||
<kanban create="1" class="o_kanban_mobile">
|
||||
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
@ -84,6 +84,232 @@
|
|||
options="{'color_field':'color','no_create':True}"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
|
||||
<!-- EXPERIENCE TAB -->
|
||||
<page string="Past Experience">
|
||||
|
||||
<field name="employer_history" nolabel="1" force_save="1">
|
||||
|
||||
<kanban create="1" class="o_kanban_mobile">
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="card">
|
||||
|
||||
<div class="oe_kanban_global_click shadow-sm rounded-4 p-3 mb-3 bg-white border"
|
||||
style="border-left:4px solid #4F46E5 !important;">
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
|
||||
<div class="fw-bold fs-4 text-dark">
|
||||
<field name="designation"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<i class="fa fa-building me-1 text-primary"/>
|
||||
<strong>Company:</strong>
|
||||
<field name="company_name"/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-muted">
|
||||
<i class="fa fa-calendar me-1 text-dark"/>
|
||||
<field name="date_of_joining"/>
|
||||
-
|
||||
|
||||
<field name="last_working_day"/>
|
||||
</div>
|
||||
<div class="mt-2 text-muted">
|
||||
<i class="fa fa-money me-1 text-success"/>
|
||||
<span class="fw-bold text-dark me-1">
|
||||
CTC :
|
||||
</span>
|
||||
<field name="ctc"/>
|
||||
</div>
|
||||
<div class="mt-3 p-3 rounded-3 border bg-light"
|
||||
style="border-left:4px solid #875A7B !important;">
|
||||
<div class="fw-bold mb-2 text-primary fs-6">
|
||||
<i class="fa fa-file-text-o me-1"/>
|
||||
Summary
|
||||
</div>
|
||||
<div style="font-size:13px; line-height:1.7;"
|
||||
class="text-dark">
|
||||
<field name="summary"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
|
||||
<!-- FORM -->
|
||||
<form string="Employer Details">
|
||||
|
||||
<sheet>
|
||||
|
||||
<group>
|
||||
|
||||
<group>
|
||||
<field name="company_name"/>
|
||||
<field name="designation"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_of_joining"/>
|
||||
<field name="last_working_day"/>
|
||||
<field name="ctc"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="summary"
|
||||
widget="html"/>
|
||||
</group>
|
||||
|
||||
</group>
|
||||
|
||||
</sheet>
|
||||
|
||||
</form>
|
||||
|
||||
</field>
|
||||
|
||||
</page>
|
||||
|
||||
<!-- EDUCATION TAB -->
|
||||
<page string="Education">
|
||||
|
||||
<field name="education_history" nolabel="1">
|
||||
|
||||
<kanban create="1" class="o_kanban_mobile">
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="card">
|
||||
|
||||
<div class="oe_kanban_global_click shadow-sm rounded-4 p-3 mb-3 bg-white border"
|
||||
style="border-left:4px solid #4F46E5 !important;">
|
||||
|
||||
<div class="fw-bold fs-5 text-dark">
|
||||
<field name="name"/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<i class="fa fa-university me-1 text-primary"/>
|
||||
<strong>University:</strong>
|
||||
<field name="university"/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<strong>Education:</strong>
|
||||
<span class="badge rounded-pill text-bg-light border px-3 py-2">
|
||||
<field name="education_type"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-muted">
|
||||
<i class="fa fa-calendar me-1 text-dark"/>
|
||||
|
||||
|
||||
<field name="start_year"/>
|
||||
-
|
||||
|
||||
<field name="end_year"/>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-3 p-2 rounded-3 bg-light">
|
||||
|
||||
<span class="fw-bold text-dark">
|
||||
Grade:
|
||||
</span>
|
||||
<field name="marks_or_grade"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="education_type"/>
|
||||
<field name="name"/>
|
||||
<field name="university"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="start_year"/>
|
||||
<field name="end_year"/>
|
||||
<field name="marks_or_grade"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Certificates">
|
||||
|
||||
<field name="certificate_ids" nolabel="1">
|
||||
|
||||
<kanban create="1" class="o_kanban_mobile">
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="card">
|
||||
|
||||
<div class="oe_kanban_global_click shadow-sm rounded-4 p-3 mb-3 bg-white border"
|
||||
style="border-left:4px solid #F97316 !important;">
|
||||
|
||||
<div class="fw-bold fs-5 text-dark">
|
||||
|
||||
<i class="fa fa-certificate text-warning me-2"/>
|
||||
|
||||
<field name="name"/>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
|
||||
<field name="certificate_file"
|
||||
filename="certificate_filename"
|
||||
widget="binary"/>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
</kanban>
|
||||
|
||||
<!-- FORM -->
|
||||
<form string="Certificate">
|
||||
|
||||
<sheet>
|
||||
|
||||
<group>
|
||||
|
||||
<field name="name"/>
|
||||
|
||||
<field name="certificate_file"
|
||||
filename="certificate_filename"/>
|
||||
|
||||
<field name="certificate_filename"
|
||||
invisible="1"/>
|
||||
|
||||
</group>
|
||||
|
||||
</sheet>
|
||||
|
||||
</form>
|
||||
|
||||
</field>
|
||||
|
||||
</page>
|
||||
</notebook>
|
||||
|
||||
</page>
|
||||
<page string="Resume" name="resume_page">
|
||||
<group>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue