recruitment Changes and fixes

This commit is contained in:
Pranay 2025-04-03 11:48:51 +05:30
parent c10892c62a
commit 6e1ce6a1ed
15 changed files with 456 additions and 48 deletions

View File

@ -42,6 +42,7 @@
'views/requisitions.xml',
'views/skills.xml',
'wizards/post_onboarding_attachment_wizard.xml',
'wizards/applicant_refuse_reason.xml',
# 'views/resume_pearser.xml',
],
'assets': {

View File

@ -8,5 +8,14 @@
<field name="padding">5</field>
<field name="number_next_actual">1</field>
</record>
<record id="seq_hr_candidate" model="ir.sequence">
<field name="name">HR Job Candidate Sequence</field>
<field name="code">hr.job.candidate.sequence</field>
<field name="prefix">C</field>
<field name="padding">5</field>
<field name="number_next_actual">1</field>
</record>
</data>
</odoo>

View File

@ -16,6 +16,9 @@ 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, readonly=True, tracking=True)
client_submission_date = fields.Datetime(string="Submission Date")
submitted_stage = fields.Many2one('hr.recruitment.stage')
refused_stage = fields.Many2one('hr.recruitment.stage')
refused_comments = fields.Text()
@api.model
def _read_group_recruitment_stage_ids(self, stages, domain):
@ -130,6 +133,7 @@ class HRApplicant(models.Model):
)
rec.submitted_to_client = True
rec.client_submission_date = fields.Datetime.now()
rec.submitted_stage = rec.recruitment_stage_id.id
def submit_for_approval(self):
for rec in self:

View File

@ -18,17 +18,20 @@ class HRJobRecruitment(models.Model):
]
def _get_first_stage(self):
"""This function is used to fetch the starting stage"""
self.ensure_one()
return self.env['hr.recruitment.stage'].search([
('job_recruitment_ids', '=', self.id)], order='sequence asc', limit=1)
def _compute_application_count(self):
"""this function is used to compute the application count"""
read_group_result = self.env['hr.applicant']._read_group([('hr_job_recruitment', 'in', self.ids)], ['hr_job_recruitment'], ['__count'])
result = {job.id: count for job, count in read_group_result}
for job in self:
job.application_count = result.get(job.id, 0)
def _compute_all_application_count(self):
"this function is used to compute all the applicants count including inactive applicants"
read_group_result = self.env['hr.applicant'].with_context(active_test=False)._read_group([
('hr_job_recruitment', 'in', self.ids),
'|',
@ -41,6 +44,7 @@ class HRJobRecruitment(models.Model):
job.all_application_count = result.get(job.id, 0)
def _compute_applicant_hired(self):
"""this function is used to compute the hired applicants count"""
hired_stages = self.env['hr.recruitment.stage'].search([('hired_stage', '=', True)])
hired_data = self.env['hr.applicant']._read_group([
('hr_job_recruitment', 'in', self.ids),
@ -51,6 +55,7 @@ class HRJobRecruitment(models.Model):
job.applicant_hired = job_hires.get(job.id, 0)
def _compute_new_application_count(self):
"""sthis function is used to fetch the count of applicants those who are in starting stage"""
self.env.cr.execute(
"""
WITH job_stage AS (
@ -78,19 +83,20 @@ class HRJobRecruitment(models.Model):
for job in self:
job.new_application_count = new_applicant_count.get(job.id, 0)
# # display_name = fields.Char(string='Name', compute='_compute_display_name', store=True)
application_count = fields.Integer(compute='_compute_application_count', string="Application Count")
all_application_count = fields.Integer(compute='_compute_all_application_count', string="All Application Count")
new_application_count = fields.Integer(
compute='_compute_new_application_count', string="New Application",
help="Number of applications that are new in the flow (typically at first step of the flow)")
applicant_hired = fields.Integer(compute='_compute_applicant_hired', string="Applicants Hired")
def _get_default_favorite_user_ids(self):
"""this function is used to set the default users i.e current user"""
return [(6, 0, [self.env.uid])]
@api.model
def _default_address_id(self):
"""this function is used to set the sefault company id """
last_used_address = self.env['hr.job.recruitment'].search([('company_id', 'in', self.env.companies.ids)], order='id desc',
limit=1)
if last_used_address:
@ -115,9 +121,11 @@ class HRJobRecruitment(models.Model):
no_of_eligible_submissions = fields.Integer(string='Eligible Submissions', copy=False,
help='Number of Submissions you expected to send.', default=1)
submission_status = fields.Selection([('zero','Zero Submissions'),('partial','Partial Submissions'),('filled','Filled Submissions')],default='zero', compute="_compute_submission_status", store=True)
@api.onchange("no_of_recruitment")
def onchange_no_of_recruitments(self):
"""this function is used to set the no_of_eligible submissions"""
for rec in self:
if rec.no_of_eligible_submissions <= 1:
rec.no_of_eligible_submissions = rec.no_of_recruitment
@ -186,7 +194,7 @@ class HRJobRecruitment(models.Model):
store=True)
no_of_submissions = fields.Integer(
compute='_compute_no_of_submissions',
string='Hired', copy=False,
string='Submitted', copy=False, store=True,
help='Number of Application submissions for this job position during recruitment phase.',
)
no_of_refused_submissions = fields.Integer(
@ -213,6 +221,18 @@ class HRJobRecruitment(models.Model):
if rec.job_category and rec.job_category.default_user:
rec.user_id = rec.job_category.default_user.id
@api.depends('no_of_submissions','no_of_eligible_submissions')
def _compute_submission_status(self):
for rec in self:
if rec.no_of_submissions == 0:
rec.submission_status = 'zero'
elif rec.no_of_submissions > 0 and rec.no_of_submissions < rec.no_of_eligible_submissions:
rec.submission_status = 'partial'
elif rec.no_of_submissions > 0 and rec.no_of_submissions >= rec.no_of_eligible_submissions:
rec.submission_status = 'filled'
else:
rec.submission_status = 'zero'
@api.depends('application_ids.submitted_to_client')
def _compute_no_of_submissions(self):
@ -318,6 +338,10 @@ class HRJobRecruitment(models.Model):
result.append((record.id, name))
return result
@api.depends('job_id', 'recruitment_sequence')
def _compute_display_name(self):
for rec in self:
rec.display_name = False if not rec.recruitment_sequence else f"{rec.recruitment_sequence} ({rec.job_id.name})"
def buttion_view_applicants(self):
if self.skill_ids:
@ -334,11 +358,11 @@ class HRJobRecruitment(models.Model):
action['context'] = dict(self._context)
return action
def hr_job_recruitment_end_date_update(self):
def hr_job_recruitment_end_date_update(self):
tomorrow_date = fields.Date.today() + timedelta(days=1)
jobs_ending_tomorrow = self.sudo().search([('target_to', '=', tomorrow_date)])
jobs_unpublish_needed = self.sudo().search([('target_to','!=',False),('target_to', '<', fields.Date.today()),('website_published','=',True)])
for job in jobs_ending_tomorrow:
# Fetch recruiters (assuming job has a field recruiter_id or similar)
recruiter = job.sudo().user_id # Replacne with the appropriate field name
@ -348,13 +372,13 @@ class HRJobRecruitment(models.Model):
'hr_recruitment_extended.template_recruitment_deadline_alert') # Replace with your email template XML ID
if template:
template.sudo().send_mail(recruiter.id, force_send=True)
for job in jobs_unpublish_needed:
job.sudo().write({'website_published': False})
return True
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
vals["favorite_user_ids"] = vals.get("favorite_user_ids", [])
if vals.get('recruitment_sequence', '/') == '/':
@ -377,8 +401,6 @@ class HRJobRecruitment(models.Model):
return jobs
def buttion_view_applicants(self):
pass
def action_open_attachments(self):
return {

View File

@ -1,4 +1,4 @@
from odoo import models, fields, api, _
from odoo import models, fields, api, _, tools
from odoo.exceptions import ValidationError, UserError
from datetime import date
from datetime import timedelta
@ -14,7 +14,12 @@ import datetime
class HrCandidate(models.Model):
_inherit = "hr.candidate"
_sql_constraints = [
('unique_candidate_sequence', 'UNIQUE(candidate_sequence)', 'Candidate sequence must be unique!'),
]
#personal Details
candidate_sequence = fields.Char(string='Candidate Sequence', readonly=False, default='/', copy=False)
first_name = fields.Char(string='First Name',required=False, help="This is the person's first name, given at birth or during a naming ceremony. Its the name people use to address you.")
middle_name = fields.Char(string='Middle Name', help="This is an extra name that comes between the first name and last name. Not everyone has a middle name")
last_name = fields.Char(string='Last Name',required=False, help="This is the family name, shared with other family members. Its usually the last name.")
@ -23,6 +28,62 @@ class HrCandidate(models.Model):
employee_code = fields.Char(related="employee_id.employee_id")
resume = fields.Binary()
applications_stages_stat = fields.Many2many('application.stage.status',string="Applications History", compute="_compute_applications_stages_stat")
# availability_status = fields.Selection([('available','Available'),('not_available','Not Available'),('hired','Hired'),('abscond','Abscond')])
@api.depends('applicant_ids')
def _compute_applications_stages_stat(self):
for rec in self:
if rec.applicant_ids:
stage_status_records = self.env['application.stage.status'].with_context(active_test=False).search([
('applicant_id', 'in', rec.applicant_ids.ids)
])
rec.applications_stages_stat = [(6, 0, stage_status_records.ids)]
else:
rec.applications_stages_stat = [(5,)]
@api.depends('partner_name', 'candidate_sequence')
def _compute_display_name(self):
for rec in self:
rec.display_name = rec.partner_name if not rec.candidate_sequence else f"{rec.partner_name} ({rec.candidate_sequence})"
@api.constrains('email_from', 'partner_phone', 'alternate_phone')
def _candidate_unique_constraints(self):
for rec in self:
# Check for unique email
if rec.email_from:
existing_email = self.sudo().search([('id', '!=', rec.id), ('email_from', '=', rec.email_from)],
limit=1)
if existing_email:
raise ValidationError(_("A candidate with the email '%s' already exists, sourced by %s %s.") % (
existing_email.email_from, existing_email.user_id.name, existing_email.candidate_sequence if existing_email.candidate_sequence else ''))
# Check for unique phone number (partner_phone or alternate_phone)
if rec.partner_phone:
existing_phone = self.sudo().search(
[('id', '!=', rec.id), '|', ('partner_phone', '=', rec.partner_phone),
('alternate_phone', '=', rec.partner_phone)], limit=1)
if existing_phone:
raise ValidationError(_("A candidate with the phone number '%s' already exists, sourced by %s %s.") % (
existing_phone.partner_phone, existing_phone.user_id.name, existing_phone.candidate_sequence if existing_phone.candidate_sequence else ''))
if rec.alternate_phone:
existing_al_phone = self.sudo().search(
[('id', '!=', rec.id), '|', ('partner_phone', '=', rec.alternate_phone),
('alternate_phone', '=', rec.alternate_phone)], limit=1)
if existing_al_phone:
raise ValidationError(
_("A candidate with the alternate phone number '%s' already exists, sourced by %s %s.") % (
existing_al_phone.alternate_phone, existing_al_phone.user_id.name, existing_al_phone.candidate_sequence if existing_al_phone.candidate_sequence else ''))
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('candidate_sequence', '/') == '/':
vals['candidate_sequence'] = self.env['ir.sequence'].next_by_code(
'hr.job.candidate.sequence') or '/'
return super(HrCandidate, self).create(vals_list)
def create_employee_from_candidate(self):
@ -134,12 +195,12 @@ class HRApplicant(models.Model):
exp_type = fields.Selection([('fresher','Fresher'),('experienced','Experienced')], default='fresher', required=True)
total_exp = fields.Integer(string="Total Experience")
relevant_exp = fields.Integer(string="Relevant Experience")
total_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")])
relevant_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")])
total_exp = fields.Float(string="Total Experience")
relevant_exp = fields.Float(string="Relevant Experience")
total_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year')
relevant_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year')
notice_period = fields.Integer(string="Notice Period")
notice_period_type = fields.Selection([('day',"Day's"),('month',"Month's"),('year',"Year's")], string='Type')
notice_period_type = fields.Selection([('day',"Day's"),('month',"Month's"),('year',"Year's")], string='Type', default='day')
current_ctc = fields.Float(string="Current CTC", aggregator="avg", help="Applicant Current Salary", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")
salary_expected = fields.Float("Expected CTC", aggregator="avg", help="Salary Expected by Applicant", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")
@ -148,7 +209,7 @@ class HRApplicant(models.Model):
holding_offer = fields.Char(string="Holding Offer")
applicant_comments = fields.Text(string='Applicant Comments')
recruiter_comments = fields.Text(string='Recruiter Comments')
medium_id = fields.Many2one(string='Mode')
doj = fields.Date(tracking=True)
gender = fields.Selection([
('male', 'Male'),
@ -422,4 +483,57 @@ class RecruitmentCategory(models.Model):
_rec_name = "category_name"
category_name = fields.Char(string="Category Name")
default_user = fields.Many2one('res.users')
default_user = fields.Many2one('res.users')
class ApplicationsStageStatus(models.Model):
_name = 'application.stage.status'
_rec_name = 'applicant_id'
_auto = False
applicant_id = fields.Many2one('hr.applicant')
job_request = fields.Many2one('hr.job.recruitment', related='applicant_id.hr_job_recruitment')
stage_id = fields.Many2one('hr.recruitment.stage', related="applicant_id.recruitment_stage_id")
application_status = fields.Selection([
('ongoing', 'Ongoing'),
('hired', 'Hired'),
('refused', 'Refused'),
('archived', 'Archived'),
], related='applicant_id.application_status')
stage_color = fields.Char(related='applicant_id.stage_color', widget='color')
stage_color_int = fields.Integer("Stage Color Int", compute="_compute_stage_color")
@api.depends("application_status")
def _compute_stage_color(self):
for record in self:
if record.application_status == 'hired':
record.stage_color_int = 10
elif record.application_status == 'ongoing':
record.stage_color_int = 3
elif record.application_status == 'refused':
record.stage_color_int = 1
elif record.application_status == 'archived':
record.stage_color_int = 9
else:
record.stage_color_int = 0
@api.depends('applicant_id', 'stage_id', 'application_status', 'job_request', 'stage_color_int')
def _compute_display_name(self):
for rec in self:
rec.display_name = rec.applicant_id.display_name if not rec.stage_id else f"({rec.job_request.recruitment_sequence}){rec.applicant_id.display_name} {rec.stage_color_int} - ({rec.stage_id.name}, {rec.application_status})"
def init(self):
""" Create the SQL view for application.stage.status """
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute("""
CREATE OR REPLACE VIEW %s AS (
SELECT
a.id AS id, -- Ensuring a unique primary key
a.id AS applicant_id
FROM
hr_applicant a
WHERE
a.active = 't' or a.active = 'f'
);
""" % (self._table))

View File

@ -4,4 +4,4 @@ from odoo import models, fields, api, _
class ResPartner(models.Model):
_inherit = 'res.partner'
contact_type = fields.Selection([('internal','Internal'),('external','External')], required=True, default='internal')
contact_type = fields.Selection([('internal','Internal'),('external','External')], required=True, default='external')

View File

@ -109,6 +109,6 @@ class Job(models.Model):
def unlink(self):
# Remove stage from all related jobs when stage is deleted
for job in self:
for stage in stage.recruitment_stage_ids:
for stage in job.recruitment_stage_ids:
stage.write({'job_recruitment_ids': [(3, job.id)]})
return super(Job, self).unlink()

View File

@ -20,3 +20,9 @@ access_recruitment_attachments_user,access.recruitment.attachments.user,model_re
access_post_onboarding_attachment_wizard,access.post.onboarding.attachment.wizard,model_post_onboarding_attachment_wizard,base.group_user,1,1,1,1
access_employee_recruitment_attachments,employee.recruitment.attachments,model_employee_recruitment_attachments,base.group_user,1,1,1,1
hr_recruitment.access_hr_applicant_interviewer,hr.applicant.interviewer,hr_recruitment.model_hr_applicant,hr_recruitment.group_hr_recruitment_interviewer,1,1,1,0
hr_recruitment.access_hr_recruitment_stage_user,hr.recruitment.stage.user,hr_recruitment.model_hr_recruitment_stage,hr_recruitment.group_hr_recruitment_user,1,1,1,0
access_application_stage_status,application.stage.status,model_application_stage_status,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
20
21
22
23
24
25
26
27
28

View File

@ -1,6 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="hr_applicant_view_list_inherit" model="ir.ui.view">
<field name="name">hr.applicant.view.list</field>
<field name="inherit_id" ref="hr_recruitment.crm_case_tree_view_job"/>
<field name="model">hr.applicant</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='stage_id']" position="attributes">
<attribute name="column_invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='stage_id']" position="after">
<field name="recruitment_stage_id"/>
</xpath>
</field>
</record>
<record id="hr_applicant_view_form_inherit" model="ir.ui.view">
<field name="name">hr.applicant.view.form</field>
<field name="model">hr.applicant</field>
@ -106,6 +118,16 @@
</group>
</page>
</xpath>
<xpath expr="//notebook" position="after">
<sheet invisible="application_status != 'refused'">
<group invisible="application_status != 'refused'">
<field name="refused_stage" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
<field name="refuse_date" string="Refused On" readonly="1" force_save="1" invisible="application_status != 'refused'"/>
<field name="refused_comments" invisible="application_status != 'refused'"/>
</group>
</sheet>
</xpath>
</field>
</record>
<record id="hr_applicant_view_search_bis_inherit" model="ir.ui.view">
@ -128,7 +150,6 @@
<filter string="Job Recruitment Stage" name="job_recruitment_stage" domain="[]"
context="{'group_by': 'recruitment_stage_id'}"/>
</xpath>
</field>
</record>
@ -172,10 +193,36 @@
invisible="application_status != 'refused'"/>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-secondary"
invisible="application_status != 'archived'"/>
<field t-if="record.partner_name.raw_value" class="fw-bold fs-5" name="partner_name"/>
<field name="job_id" invisible="context.get('search_default_job_id', False)"/>
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="applicant_properties" widget="properties"/>
<div class="d-flex align-items-baseline gap-1 ms-2">
<field t-if="record.partner_name.raw_value" class="fw-bold fs-5" name="partner_name"/>
<field name="job_id" invisible="context.get('search_default_job_id', False)"/>
</div>
<div class="row g-0 mt-0 mt-sm-3 ms-2">
<div class="col-7">
<!-- Categories and applicant properties -->
<field name="categ_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<field name="applicant_properties" widget="properties"/>
</div>
<div class="col-5">
<!-- Refused information, visible when status is 'refused' -->
<div invisible="application_status != 'refused'">
<sheet>
<!-- Refused Stage -->
<div class="form-group">
<label for="refuse_reason_id"><strong>Refuse Details</strong></label>
<field name="refuse_reason_id" readonly="1" force_save="1"
invisible="application_status != 'refused'"/>
</div>
<!-- Refuse Date -->
<div class="form-group">
<field name="refuse_date" string="Refused On" readonly="1" force_save="1"
invisible="application_status != 'refused'" widget="date"/>
</div>
</sheet>
</div>
</div>
</div>
<footer>
<field name="priority" widget="priority"/>
<field class="ms-1 align-items-center" name="activity_ids" widget="kanban_activity"/>
@ -251,7 +298,7 @@
<record id="hr_recruitment.crm_case_categ0_act_job" model="ir.actions.act_window">
<field name="search_view_id" ref="hr_applicant_view_search_bis_inherit"/>
<field name="context">
{"search_default_job_recruitment_stage":1,"search_default_job_recruitment":1,"search_default_my_applications":1}
{"search_default_job_recruitment_stage":0,"search_default_job_recruitment":1,"search_default_my_applications":1}
</field>
</record>

View File

@ -24,6 +24,7 @@
<field name="alias_id" invisible="not alias_name" column_invisible="True" optional="hide"/>
<field name="user_id" widget="many2one_avatar_user" optional="hide"/>
<field name="no_of_employee"/>
<field name="submission_status" optional="hide"/>
</list>
</field>
@ -121,7 +122,7 @@
<field name="requested_by" can_create="True" can_write="True"/>
<field name="department_id" can_create="True" can_write="True" invisible="1"/>
<label for="address_id"/>
<label for="address_id" string="Requested By Company"/>
<div class="o_row">
<span invisible="address_id" class="oe_read_only">Remote</span>
<field name="address_id" context="{'show_address': 1}" placeholder="Remote"
@ -184,6 +185,7 @@
</group>
</group>
<field name="job_properties" columns="2"/>
<field name="submission_status" invisible="1" force_save="1"/>
</page>
<page string="Job Summary" name="job_description_page" invisible="0">
<field name="description" options="{'collaborative': true}"
@ -210,9 +212,26 @@
<field name="department_id" operator="child_of"/>
<field name="user_id" string="Primary Recruiter"/>
<field name="interviewer_ids" string="Secondary Recruiters"/>
<field name="no_of_submissions"/>
<field name="no_of_eligible_submissions"/>
<field name="submission_status"/>
<separator/>
<filter string="My Assignments" name="my_assignments" domain="['|',('user_id', '=', uid),('interviewer_ids','in',uid)]"/>
<filter name="message_needaction" string="Unread Messages"
<filter string="Published Records" name="published_records" domain="[('website_published','=',True)]"/>
<separator/>
<filter string="My Assignments" name="my_assignments" domain="['|',('user_id', '=', uid), ('interviewer_ids','in',uid)]"/>
<separator/>
<filter string="Zero Submissions" name="zero_submissions"
domain="[('submission_status', '=', 'zero')]"/>
<!-- Partial Submissions -->
<filter string="Partial Submissions" name="partial_submissions"
domain="[('submission_status', '=', 'partial')]"/>
<!-- Filled Submissions -->
<filter string="Filled Submissions" name="filled_submissions"
domain="[('submission_status', '=', 'filled')]"/>
<separator/>
<filter name="message_needaction" string="Unread Messages"
domain="[('message_needaction', '=', True)]"
groups="mail.group_mail_notification_type_inbox"/>
<separator/>
@ -224,6 +243,9 @@
groups="base.group_multi_company"/>
<filter string="Employment Type" name="employment_type" domain="[]"
context="{'group_by': 'contract_type_id'}"/>
<filter string="Submission Status" name="submission_status" domain="[]"
context="{'group_by': 'submission_status'}"/>
</group>
</search>
</field>
@ -238,9 +260,9 @@
<field name="active"/>
<field name="alias_email" invisible="1"/>
<templates>
<t t-name="menu" groups="hr_recruitment.group_hr_recruitment_user">
<t t-name="menu">
<div class="container">
<div class="row">
<div class="row" groups="hr_recruitment.group_hr_recruitment_user">
<div class="col-6">
<h5 role="menuitem" class="o_kanban_card_manage_title">
<span>View</span>
@ -278,15 +300,19 @@
</div>
<div class="col-6" role="menuitem">
<a class="dropdown-item" t-if="widget.editable" name="edit_job" type="open">Configuration</a>
<a class="dropdown-item" t-if="record.active.raw_value" type="archive">Archive</a>
<a class="dropdown-item" t-if="!record.active.raw_value" name="toggle_active" type="object">Unarchive</a>
<a class="dropdown-item" t-if="record.active.raw_value" type="archive" groups="hr_recruitment.group_hr_recruitment_user">Archive</a>
<a class="dropdown-item" t-if="!record.active.raw_value" name="toggle_active" type="object" groups="hr_recruitment.group_hr_recruitment_user">Unarchive</a>
</div>
</div>
</div>
</t>
<t t-name="card">
<div class="d-flex align-items-baseline gap-1 ms-2">
<field name="is_favorite" widget="boolean_favorite" nolabel="1"/>
<div class="d-flex align-items-baseline gap-1 ms-2"
t-att-style="
record.submission_status.raw_value == 'zero' ? 'background-color: #f8d7da; padding: 5px; border-radius: 4px;' :
record.submission_status.raw_value == 'partial' ? 'background-color: #fff3cd; padding: 5px; border-radius: 4px;' :
record.submission_status.raw_value == 'filled' ? 'background-color: #d4edda; padding: 5px; border-radius: 4px;' : 'padding: 5px; border-radius: 4px;'">
<field name="is_favorite" widget="boolean_favorite" style="color:red" nolabel="1"/>
<div class="o_kanban_card_header_title d-flex flex-column">
<div class="oe_row">
<field name="recruitment_sequence" class="fw-bold fs-4"/> -
@ -306,6 +332,12 @@
<button class="btn btn-primary" name="%(action_hr_job_recruitment_applications)d" type="action">
<field name="new_application_count"/> New Applications
</button>
<br/>
<div t-if="widget.editable" style="padding-top: 5px">
<button class="btn btn-primary" name="edit_job" type="open">
Open
</button>
</div>
<br/><br/>
<div t-if="record.budget.value">
<strong>Budget : </strong> <field name="budget"/>
@ -318,6 +350,9 @@
<span t-attf-class="{{ record.no_of_recruitment.raw_value &gt; 0 ? 'text-primary fw-bolder' : 'text-secondary' }}" groups="!hr_recruitment.group_hr_recruitment_user">
<field name="no_of_recruitment"/> To Recruit
</span>
<div t-if="record.no_of_eligible_submissions.raw_value &gt; 0">
<field name="no_of_eligible_submissions"/> To Submit
</div>
<div t-if="record.application_count.raw_value &gt; 0">
<field name="application_count"/> Applications
</div>
@ -329,7 +364,8 @@
<field name="no_of_submissions"/>
Submissions
</div>
<field name="no_of_submissions" force_save="1" invisible="1"/>
<field name="submission_status" invisible="1" force_save="1"/>
<div t-if="record.no_of_refused_submissions.raw_value &gt; 0">
<field name="no_of_refused_submissions"/>
Refused Submissions
@ -354,7 +390,7 @@
<field name="res_model">hr.job.recruitment</field>
<field name="view_mode">kanban,list,form,search</field>
<field name="search_view_id" ref="view_job_recruitment_filter"/>
<field name="context">{"search_default_Current":1,"search_default_my_assignments":1}</field>
<field name="context">{"search_default_Current":1,"search_default_my_assignments":1,"search_default_published_records":1,'no_of_eligible_submissions': 0}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Ready to recruit more efficiently?
@ -367,21 +403,27 @@
<menuitem
name="Applications"
parent="hr_recruitment.menu_hr_recruitment_root"
id="hr_recruitment.menu_crm_case_categ0_act_job"
active="0"
sequence="2"/>
<menuitem
name="Job Positions Recruitment"
name="Job Description"
id="menu_hr_job_recruitment_interviewer"
parent="hr_recruitment.menu_crm_case_categ0_act_job"
parent="hr_recruitment.menu_hr_recruitment_root"
action="action_hr_job_recruitment"
sequence="1"
groups="base.group_user"/>
<menuitem
name="By Job Positions"
name="Job Positions"
id="hr_recruitment.menu_hr_job_position"
parent="hr_recruitment.menu_crm_case_categ0_act_job"
parent="hr_recruitment.menu_hr_recruitment_config_jobs"
action="hr_recruitment.action_hr_job"
sequence="30"
sequence="0"
groups="hr_recruitment.group_hr_recruitment_user"/>
<!-- <menuitem-->

View File

@ -176,8 +176,9 @@
<xpath expr="//field[@name='linkedin_profile']" position="after">
<field name="exp_type"/>
<field name="resume" force_save="1"/>
<field name="submitted_to_client" force_save="1" readonly="1"/>
<field name="client_submission_date" force_save="1" readonly="1"/>
<field name="submitted_to_client" force_save="1" readonly="1" invisible="not submitted_to_client"/>
<field name="client_submission_date" force_save="1" readonly="1" invisible="not submitted_to_client"/>
<field name="submitted_stage" force_save="1" readonly="1" invisible="not submitted_to_client"/>
</xpath>
<xpath expr="//group[@name='recruitment_contract']/label[@for='salary_expected']" position="before">
<field name="current_ctc"/>
@ -234,12 +235,20 @@
<!-- <xpath expr="//field[@name='partner_name']" position="attributes">-->
<!-- <attribute name="readonly">1</attribute>-->
<!-- </xpath>-->
<!-- <xpath expr="" position="before">-->
<!-- <field name="candidate_sequence"/>-->
<!-- </xpath>-->
<xpath expr="//widget[@name='web_ribbon']" position="after">
<div class="o_employee_avatar m-0 p-0">
<field name="candidate_image" widget="image" class="oe_avatar m-0"
options="{&quot;zoom&quot;: true, &quot;preview_image&quot;:&quot;candidate_image&quot;}"/>
</div>
<div class="oe_title mw-75 ps-0 pe-2">
<h1>
<field name="candidate_sequence"/>
</h1>
</div>
</xpath>
<xpath expr="//form/sheet/group" position="before">
@ -254,13 +263,82 @@
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="alternate_phone"/>
</xpath>
<xpath expr="//field[@name='categ_ids']" position="after">
<field name="resume"/>
</xpath>
<xpath expr="//field[@name='categ_ids']" position="after">
<field name="resume"/>
</xpath>
</field>
</record>
<record id="hr_candidate_view_search_inherit" model="ir.ui.view">
<field name="name">hr.candidate.view.search.inherit</field>
<field name="model">hr.candidate</field>
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_name']" position="after">
<field name="candidate_sequence"/>
</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>
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_kanban"/>
<field name="arch" type="xml">
<!-- Ensure applicant_ids is included in the kanban field list -->
<xpath expr="//kanban" position="inside">
<field name="applicant_ids" context="{'active_test': False}"/>
</xpath>
<xpath expr="//kanban" position="attributes">
<attribute name="context">{'active_test': False, 'kanban': True}</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"/>
</xpath>
<xpath expr="//t[@t-name='card']" position="inside">
<t t-if="record.applicant_ids and record.applicant_ids.value">
<div class="mt-2">
<strong>Application History:</strong>
<div class="o_kanban_application_history">
<field name="applications_stages_stat" widget="many2many_tags" context="{'active_test': False}"
options="{'color_field':'stage_color_int'}"/>
</div>
</div>
</t>
</xpath>
<!-- <t t-if="record.applicant_ids and record.applicant_ids.value">-->
<!-- <field name="applicant_ids" widget="kanban_one2many" options="{'display_field': ['job_id']}" />-->
<!--&lt;!&ndash; <t t-foreach="record.applicant_ids.value" t-as="application">&ndash;&gt;-->
<!--&lt;!&ndash; <div class="d-flex align-items-center">&ndash;&gt;-->
<!--&lt;!&ndash; <span class="badge bg-info">&ndash;&gt;-->
<!--&lt;!&ndash; <t t-if="application.job_id">&ndash;&gt;-->
<!--&lt;!&ndash; <t t-esc="application.job_id.value"/>&ndash;&gt;-->
<!--&lt;!&ndash; </t>&ndash;&gt;-->
<!--&lt;!&ndash; <t t-else="">No Job</t>&ndash;&gt;-->
<!--&lt;!&ndash; </span>&ndash;&gt;-->
<!--&lt;!&ndash; <span class="ms-2 text-muted">&ndash;&gt;-->
<!--&lt;!&ndash; <t t-if="application.stage_id">&ndash;&gt;-->
<!--&lt;!&ndash; <t t-esc="application.stage_id.value"/>&ndash;&gt;-->
<!--&lt;!&ndash; </t>&ndash;&gt;-->
<!--&lt;!&ndash; <t t-else="">No Stage</t>&ndash;&gt;-->
<!--&lt;!&ndash; </span>&ndash;&gt;-->
<!--&lt;!&ndash; </div>&ndash;&gt;-->
<!--&lt;!&ndash; </t>&ndash;&gt;-->
<!-- </t>-->
<!-- <t t-else="">-->
<!-- <span class="text-muted">No application history</span>-->
<!-- </t>-->
<!-- </div>-->
<!-- </div>-->
<!-- </xpath>-->
</field>
</record>
<!-- explicit list view definition -->
<!--
<record model="ir.ui.view" id="hr_recruitment_extended.list">
@ -340,9 +418,23 @@
parent="hr_recruitment.menu_crm_case_categ0_act_job"
action="hr_recruitment.action_hr_job_interviewer"
sequence="31"
active="0"
groups="base.group_no_one"/>
<menuitem
name="Applications"
parent="hr_recruitment.menu_hr_recruitment_root"
id="hr_recruitment.menu_crm_case_categ_all_app"
action="hr_recruitment.crm_case_categ0_act_job"
sequence="2"/>
<menuitem
name="Candidates"
parent="hr_recruitment.menu_hr_recruitment_root"
id="hr_recruitment.menu_hr_candidate"
action="hr_recruitment.action_hr_candidate"
sequence="3"/>
</data>
</odoo>

View File

@ -1 +1,2 @@
from . import post_onboarding_attachment_wizard
from . import post_onboarding_attachment_wizard
from . import applicant_refuse_reason

View File

@ -0,0 +1,55 @@
from datetime import datetime
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.osv import expression
class ApplicantGetRefuseReason(models.TransientModel):
_inherit = 'applicant.get.refuse.reason'
refused_comments = fields.Text()
def action_refuse_reason_apply(self):
if self.send_mail:
if not self.template_id:
raise UserError(_("Email template must be selected to send a mail"))
if any(not (applicant.email_from or applicant.partner_id.email) for applicant in self.applicant_ids):
raise UserError(_("At least one applicant doesn't have a email; you can't use send email option."))
refused_applications = self.applicant_ids
# duplicates_count can be true only if only one application is selected
if self.duplicates_count and self.duplicates:
applicant_id = self.applicant_ids[0]
duplicate_domain = applicant_id.candidate_id._get_similar_candidates_domain()
duplicates = self.env['hr.candidate'].search(duplicate_domain).applicant_ids
refused_applications |= duplicates
url = applicant_id._get_html_link()
message = _(
"Refused automatically because this application has been identified as a duplicate of %(link)s",
link=url)
duplicates._message_log_batch(bodies={duplicate.id: message for duplicate in duplicates})
refused_applications.write({'refuse_reason_id': self.refuse_reason_id.id, 'active': False, 'refuse_date': datetime.now(),'refused_comments': self.refused_comments})
for applicant in refused_applications:
applicant.write({'refused_stage': applicant.recruitment_stage_id.id})
if self.send_mail:
# TDE note: keeping 16.0 behavior, clean me please
message_values = {
'email_layout_xmlid' : 'hr_recruitment.mail_notification_light_without_background',
}
if len(self.applicant_ids) > 1:
self.applicant_ids.with_context(active_test=True).message_mail_with_source(
self.template_id,
auto_delete_keep_log=True,
**message_values
)
else:
self.applicant_ids.with_context(active_test=True).message_post_with_source(
self.template_id,
subtype_xmlid='mail.mt_note',
**message_values
)

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="applicant_get_refuse_reason_view_form_inherit" model="ir.ui.view">
<field name="name">applicant.get.refuse.reason.form.inherit</field>
<field name="model">applicant.get.refuse.reason</field>
<field name="inherit_id" ref="hr_recruitment.applicant_get_refuse_reason_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='refuse_reason_id']" position="after">
<field name="refused_comments" placeholder="Comments if any..."/>
</xpath>
</field>
</record>
</odoo>

View File

@ -468,6 +468,8 @@ class WebsiteJobHrRecruitment(WebsiteHrRecruitment):
# candidate_skills_list.append(skill.id)
if candidate.candidate_skill_ids and candidate_skills['skill_id'] not in candidate.candidate_skill_ids.skill_id.ids:
candidate.write({'candidate_skill_ids':[(0,4,candidate_skills)]})
else:
candidate.write({'candidate_skill_ids':[(0,4,candidate_skills)]})
else: