FIX: Leaves bug

This commit is contained in:
Pranay 2025-01-24 11:56:28 +05:30
parent ac90760034
commit 89295a4e5a
32 changed files with 1864 additions and 2 deletions

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
{
'name': "HR Recruitment Extended",
'summary': "Extention of HR Recruitment Module",
'description': """
Extention of HR Recruitment MOdule
""",
'author': "Pranay",
'website': "https://www.ftprotech.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Human Resources/Recruitment',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['hr_recruitment','hr','hr_recruitment_skills','website_hr_recruitment','requisitions'],
# always loaded
'data': [
'security/ir.model.access.csv',
'data/cron.xml',
'data/mail_template.xml',
'views/hr_recruitment.xml',
'views/hr_location.xml',
'views/website_hr_recruitment_application_templates.xml',
'views/stages.xml',
],
'assets': {
'web.assets_frontend': [
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
],
}
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers

View File

@ -0,0 +1,167 @@
import warnings
from datetime import datetime
from dateutil.relativedelta import relativedelta
from operator import itemgetter
from werkzeug.urls import url_encode
from odoo import http, _
from odoo.addons.website_hr_recruitment.controllers.main import WebsiteHrRecruitment
from odoo.osv.expression import AND
from odoo.http import request
from odoo.tools import email_normalize
from odoo.tools.misc import groupby
class WebsiteRecruitmentApplication(WebsiteHrRecruitment):
@http.route('/hr_recruitment_extended/fetch_hr_recruitment_degree', type='json', auth="public", website=True)
def fetch_recruitment_degrees(self):
degrees = {}
all_degrees = http.request.env['hr.recruitment.degree'].sudo().search([])
if all_degrees:
for degree in all_degrees:
degrees[degree.id] = degree.name
return degrees
@http.route('/hr_recruitment_extended/fetch_preferred_locations', type='json', auth="public", website=True)
def fetch_preferred_locations(self, loc_ids):
locations = {}
for id in loc_ids:
location = http.request.env['hr.location'].sudo().browse(id)
if location:
locations[location.id] = location.location_name
return locations
@http.route('/website_hr_recruitment/check_recent_application', type='json', auth="public", website=True)
def check_recent_application(self, field, value, job_id):
def refused_applicants_condition(applicant):
return not applicant.active \
and applicant.job_id.id == int(job_id) \
and applicant.create_date >= (datetime.now() - relativedelta(months=6))
field_domain = {
'name': [('partner_name', '=ilike', value)],
'email': [('email_normalized', '=', email_normalize(value))],
'phone': [('partner_phone', '=', value)],
'linkedin': [('linkedin_profile', '=ilike', value)],
}.get(field, [])
applications_by_status = http.request.env['hr.applicant'].sudo().search(AND([
field_domain,
[
('job_id.website_id', 'in', [http.request.website.id, False]),
'|',
('application_status', '=', 'ongoing'),
'&',
('application_status', '=', 'refused'),
('active', '=', False),
]
]), order='create_date DESC').grouped('application_status')
refused_applicants = applications_by_status.get('refused', http.request.env['hr.applicant'])
if any(applicant for applicant in refused_applicants if refused_applicants_condition(applicant)):
return {
'message': _(
'We\'ve found a previous closed application in our system within the last 6 months.'
' Please consider before applying in order not to duplicate efforts.'
)
}
if 'ongoing' not in applications_by_status:
return {'message': None}
ongoing_application = applications_by_status.get('ongoing')[0]
if ongoing_application.job_id.id == int(job_id):
recruiter_contact = "" if not ongoing_application.user_id else _(
' In case of issue, contact %(contact_infos)s',
contact_infos=", ".join(
[value for value in itemgetter('name', 'email', 'phone')(ongoing_application.user_id) if value]
))
return {
'message': _(
'An application already exists for %(value)s.'
' Duplicates might be rejected. %(recruiter_contact)s',
value=value,
recruiter_contact=recruiter_contact
)
}
return {
'message': _(
'We found a recent application with a similar name, email, phone number.'
' You can continue if it\'s not a mistake.'
)
}
def _should_log_authenticate_message(self, record):
if record._name == "hr.applicant" and not request.session.uid:
return False
return super()._should_log_authenticate_message(record)
def extract_data(self, model, values):
candidate = False
current_ctc = values.pop('current_ctc', None)
expected_ctc = values.pop('expected_ctc', None)
exp_type = values.pop('exp_type', None)
current_location = values.pop('current_location', None)
preferred_locations_str = values.pop('preferred_locations', '')
preferred_locations = [int(x) for x in preferred_locations_str.split(',')] if len(preferred_locations_str) > 0 else []
current_organization = values.pop('current_organization', None)
notice_period = values.pop('notice_period',0)
notice_period_type = values.pop('notice_period_type', 'day')
if model.model == 'hr.applicant':
# pop the fields since there are only useful to generate a candidate record
# partner_name = values.pop('partner_name')
first_name = values.pop('first_name', None)
middle_name = values.pop('middle_name', None)
last_name = values.pop('last_name', None)
partner_phone = values.pop('partner_phone', None)
alternate_phone = values.pop('alternate_phone', None)
partner_email = values.pop('email_from', None)
degree = values.pop('degree',None)
if partner_phone and partner_email:
candidate = request.env['hr.candidate'].sudo().search([
('email_from', '=', partner_email),
('partner_phone', '=', partner_phone),
], limit=1)
if candidate:
candidate.sudo().write({
'partner_name': f"{first_name + ' '+ ((middle_name + ' ') if middle_name else '') + last_name}",
'first_name': first_name,
'middle_name': middle_name,
'last_name': last_name,
'alternate_phone': alternate_phone,
'type_id': int(degree) if degree.isdigit() else False
})
if not candidate:
candidate = request.env['hr.candidate'].sudo().create({
'partner_name': f"{first_name + ' '+ ((middle_name + ' ') if middle_name else '') + last_name}",
'email_from': partner_email,
'partner_phone': partner_phone,
'first_name': first_name,
'middle_name': middle_name,
'last_name': last_name,
'alternate_phone': alternate_phone,
'type_id': int(degree) if degree.isdigit() else False
})
values['partner_name'] = f"{first_name + ' '+ ((middle_name + ' ') if middle_name else '') + last_name}"
if partner_phone:
values['partner_phone'] = partner_phone
if partner_email:
values['email_from'] = partner_email
data = super().extract_data(model, values)
data['record']['current_ctc'] = float(current_ctc if current_ctc else 0)
data['record']['salary_expected'] = float(expected_ctc if expected_ctc else 0)
data['record']['exp_type'] = exp_type if exp_type else 'fresher'
data['record']['current_location'] =current_location if current_location else ''
data['record']['current_organization'] = current_organization if current_organization else ''
data['record']['notice_period'] = notice_period if notice_period else 0
data['record']['notice_period_type'] = notice_period_type if notice_period_type else 'day'
if len(preferred_locations_str) > 0:
data['record']['preferred_location'] = preferred_locations
if candidate:
data['record']['candidate_id'] = candidate.id
data['record']['type_id'] = candidate.type_id.id
return data

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="1">
<record model="ir.cron" id="hr_job_end_date_update">
<field name="name">JOB: End date</field>
<field name="model_id" ref="hr.model_hr_job"/>
<field name="state">code</field>
<field name="code">model.hr_job_end_date_update()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall" eval="(DateTime.now().replace(hour=1, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="template_recruitment_deadline_alert" model="mail.template">
<field name="name">Recruitment Deadline Alert</field>
<field name="model_id" ref="hr.model_hr_job"/>
<field name="email_from">{{ object.company_id.email or user.email_formatted }}</field>
<field name="email_to">{{ object.user_id.email }}
</field>
<field name="subject">Reminder: Recruitment Process Ending Soon - {{ object.name }}</field>
<field name="description">Notification sent to recruiters when a job's recruitment deadline is approaching.</field>
<field name="body_html" type="html">
<t t-set="user_names" t-value="', '.join(object.user_id.mapped('name')) if object.user_id else 'Recruiter'"/>
<t t-set="location_names" t-value="', '.join(object.locations.mapped('name')) if object.locations else 'N/A'"/>
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="user_names">Recruiter</t>,
<br /><br />
This is a friendly reminder that the recruitment process for the position
<strong><t t-esc="object.name or ''">Job Title</t></strong> is approaching its end:
<ul>
<li><strong>Job Position:</strong> <t t-esc="object.name or ''">Job Name</t></li>
<li><strong>Target End Date:</strong> <t t-esc="object.target_to or ''">End Date</t></li>
<li><strong>Location(s):</strong> <t t-esc="location_names">Locations</t></li>
</ul>
<br />
Please ensure all recruitment activities are completed before the deadline.
<br /><br />
<a t-att-href="'%s/web#id=%d&amp;model=hr.job&amp;view_type=form' % (object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)" target="_blank">
Click here to view the job details.
</a>
<br /><br />
Regards,<br />
<t t-esc="user.name or 'System Admin'">System Admin</t>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,30 @@
<odoo>
<data>
<!--
<record id="object0" model="hr_recruitment_extended.hr_recruitment_extended">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="hr_recruitment_extended.hr_recruitment_extended">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="hr_recruitment_extended.hr_recruitment_extended">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="hr_recruitment_extended.hr_recruitment_extended">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="hr_recruitment_extended.hr_recruitment_extended">
<field name="name">Object 4</field>
<field name="value">40</field>
</record>
-->
</data>
</odoo>

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import hr_recruitment
from . import stages
from . import hr_applicant

View File

@ -0,0 +1,26 @@
from odoo import models, fields, api, _
class HRApplicant(models.Model):
_inherit = 'hr.applicant'
refused_state = fields.Many2one('hr.recruitment.stage', readonly=True, force_save=True)
def reset_applicant(self):
""" Reinsert the applicant into the recruitment pipe in the first stage"""
res = super(HRApplicant, self).reset_applicant()
for applicant in self:
applicant.write(
{'refused_state': False})
return res
class ApplicantGetRefuseReason(models.TransientModel):
_inherit = 'applicant.get.refuse.reason'
def action_refuse_reason_apply(self):
res = super(ApplicantGetRefuseReason, self).action_refuse_reason_apply()
refused_applications = self.applicant_ids
refused_applications.write({'refused_state': refused_applications.stage_id.id})
return res

View File

@ -0,0 +1,197 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from datetime import date
from datetime import timedelta
import datetime
class Job(models.Model):
_inherit = 'hr.job'
secondary_skill_ids = fields.Many2many('hr.skill', "hr_job_secondary_hr_skill_rel",
'hr_job_id', 'hr_skill_id', "Secondary Skills")
locations = fields.Many2many('hr.location')
target_from = fields.Date(string="This is the date in which we starting the recruitment process", default=fields.Date.today)
target_to = fields.Date(string='This is the target end date')
hiring_history = fields.One2many('recruitment.status.history', 'job_id', string='History')
def buttion_view_applicants(self):
if self.skill_ids:
a = self.env['hr.candidate'].search([])
applicants = self.env['hr.candidate']
for i in a:
if all(skill in i.skill_ids for skill in self.skill_ids):
applicants += i
else:
applicants = self.env['hr.candidate'].search([])
action = self.env['ir.actions.act_window']._for_xml_id('hr_recruitment.action_hr_candidate')
action['domain'] = [('id', 'in', applicants.ids)]
action['context'] = dict(self._context)
return action
def hr_job_end_date_update(self):
# Find all jobs where the target_to is today's date
hr_jobs = self.sudo().search([('target_to', '=', fields.Date.today() - timedelta(days=1))])
# stage_ids = self.env['hr.recruitment.stage'].sudo().search([('hired_stage','=',True)])
for job in hr_jobs:
# Determine the hiring period
date_from = job.target_from
date_end = job.target_to
# Fetch hired applicants related to this job
hired_applicants = self.env['hr.applicant'].search([
('job_id', '=', job.id),
('stage_id.hired_stage', '=', True)
])
# Get today's date in the datetime format (with time set to midnight)
today_start = fields.Datetime.today()
# Get today's date at the end of the day (23:59:59) to include all records created today
today_end = fields.Datetime.today().now()
# Search for records where create_date is today
hiring_history_today = self.env['recruitment.status.history'].sudo().search([
('create_date', '>=', today_start),
('create_date', '<=', today_end),
('job_id','=',job.id)
])
# Create a hiring history record
if not hiring_history_today:
self.env['recruitment.status.history'].create({
'date_from': date_from,
'date_end': date_end,
'target': len(hired_applicants), # Number of hired applicants
'job_id': job.id,
'hired': [(6, 0, hired_applicants.ids)] # Many2many write operation
})
tomorrow_date = fields.Date.today() + timedelta(days=1)
jobs_ending_tomorrow = self.sudo().search([('target_to', '=', tomorrow_date)])
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
if recruiter:
# Send mail
template = self.env.ref(
'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)
recruitment_history = self.env['recruitment.status.history'].sudo().search([('job_id','!=',False)])
for recruitment in recruitment_history:
# Determine the hiring period
if recruitment.date_from and recruitment.job_id:
# Use `date_end` or today's date if `date_end` is not provided
date_end = fields.Datetime.to_datetime(fields.Date.to_string(recruitment.date_end)) + datetime.timedelta(days=1,seconds=-1) or fields.Datetime.today().now()
current_hired_applicants = recruitment.hired
# Search for applicants matching the conditions
hired_applicants = self.env['hr.applicant'].search([
('date_closed', '>=', fields.Datetime.to_datetime(fields.Date.to_string(recruitment.date_from))),
('date_closed', '<=', date_end),
('job_id', '=', recruitment.job_id.id)
])
# Filter out the applicants that are already in the 'hired' field
new_hired_applicants = hired_applicants - current_hired_applicants
# Add the missing applicants to the 'hired' field
recruitment.hired = current_hired_applicants | new_hired_applicants
return True
class HrCandidate(models.Model):
_inherit = "hr.candidate"
first_name = fields.Char(string='First Name',required=True, 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=True, help="This is the family name, shared with other family members. Its usually the last name.")
alternate_phone = fields.Char(string='Alternate Phone')
@api.constrains('partner_name')
def partner_name_constrain(self):
for rec in self:
if any(char.isdigit() for char in rec.partner_name):
raise ValidationError(_("Enter Valid Name"))
class HRApplicant(models.Model):
_inherit = 'hr.applicant'
current_location = fields.Char('Current Location')
preferred_location = fields.Many2many('hr.location',string="Preferred Location's")
current_organization = fields.Char('Current Organization')
alternate_phone = fields.Char(related="candidate_id.alternate_phone", readonly=False)
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")])
notice_period = fields.Integer(string="Notice Period")
notice_period_type = fields.Selection([('day',"Day's"),('month',"Month's"),('year',"Year's")], string='Type')
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")
salary_negotiable = fields.Boolean(string="Salary Negotiable")
np_negotiable = fields.Boolean(string="NP Negotiable")
holding_offer = fields.Char(string="Holding Offer")
applicant_comments = fields.Text(string='Applicant Comments')
recruiter_comments = fields.Text(string='Recruiter Comments')
class Location(models.Model):
_name = 'hr.location'
_rec_name = 'location_name'
# SQL Constraint to ensure the combination of location_name, zip_code, country_id, and state is unique
_sql_constraints = [
('unique_location_zip_state_country',
'UNIQUE(location_name, zip_code, country_id, state)',
'The selected Location, Zip Code, Country, and State combination already exists.')
]
location_name = fields.Char(string='Location', required=True)
zip_code = fields.Char(string = 'Zip Code')
country_id = fields.Many2one('res.country','Country',groups="hr.group_hr_user")
state = fields.Many2one("res.country.state", string="State",
domain="[('country_id', '=?', country_id)]",
groups="hr.group_hr_user")
@api.constrains('location_name')
def _check_location_name(self):
for record in self:
if record.location_name.isdigit():
raise ValidationError("Location name should not be a number. Please enter a valid location name.")
@api.constrains('zip_code')
def _check_zip_code(self):
for record in self:
if record.zip_code and not record.zip_code.isdigit(): # Check if zip_code exists and is not digit
raise ValidationError("Zip Code should contain only numeric characters. Please enter a valid zip code.")
class RecruitmentHistory(models.Model):
_name='recruitment.status.history'
date_from = fields.Date(string='Date From')
date_end = fields.Date(string='Date End')
target = fields.Integer(string='Target')
job_id = fields.Many2one('hr.job', string='Job Position') # Ensure this field exists
hired = fields.Many2many('hr.applicant')
@api.depends('date_from', 'date_end', 'job_id')
def _total_hired_users(self):
for rec in self:
if rec.date_from:
# Use `date_end` or today's date if `date_end` is not provided
date_end = rec.date_end or date.today()
# Search for applicants matching the conditions
hired_applicants = self.env['hr.applicant'].search([
('date_closed', '>=', rec.date_from),
('date_closed', '<=', date_end),
('job_id', '=', rec.job_id.id)
])
rec.hired = hired_applicants
else:
rec.hired = False

View File

@ -0,0 +1,106 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class RecruitmentStage(models.Model):
_inherit = 'hr.recruitment.stage'
is_default_field = fields.Boolean(string='Is Default', help='Upon Activating this it will automatically come upon JD Creation', default=True)
@api.model
def create(self, vals):
res = super(RecruitmentStage, self).create(vals)
if 'job_ids' in vals:
jobs = list()
for job_id in vals['job_ids']:
jobs.append(job_id[1] if len(job_id)>1 else job_id)
job_ids = self.env['hr.job'].browse(jobs)
for job_id in job_ids:
job_id.write({'recruitment_stage_ids': [(4, res.id)]})
return res
def write(self, vals, model=None):
res = super(RecruitmentStage, self).write(vals)
if model:
if 'job_ids' in vals:
previous_job_ids = self.job_ids
jobs = list()
for job_id in vals['job_ids']:
jobs.append(job_id[1] if len(job_id)>1 else job_id)
new_job_ids = self.env['hr.job'].browse(jobs)
for stage_id in new_job_ids:
stage_id.write({'recruitment_stage_ids': [(4, self.id)]})
# Remove jobs from stages no longer related
for stage in previous_job_ids:
if stage.id not in new_job_ids.ids:
stage.write({'recruitment_stage_ids': [(3, self.id)]})
return res
def unlink(self):
# Remove stage from all related jobs when stage is deleted
for stage in self:
for job in stage.job_ids:
job.write({'recruitment_stage_ids': [(3, stage.id)]})
return super(RecruitmentStage, self).unlink()
class Job(models.Model):
_inherit = 'hr.job'
recruitment_stage_ids = fields.Many2many('hr.recruitment.stage')
@api.model
def default_get(self, fields_list):
""" Override default_get to set default values """
res = super(Job, self).default_get(fields_list)
# Check if recruitment_stage_ids is not set
if 'recruitment_stage_ids' in fields_list:
# Search for the default stage (e.g., a stage called 'Default Stage')
default_stages = self.env['hr.recruitment.stage'].search([('is_default_field', '=', True)],order='sequence')
if default_stages:
# Set the default stage in the recruitment_stage_ids field
res['recruitment_stage_ids'] = default_stages # Add the default stage to the many2many field
return res
@api.model
def create(self,vals):
res = super(Job, self).create(vals)
if 'recruitment_stage_ids' in vals:
stages = list()
for stage_id in vals['recruitment_stage_ids']:
stages.append(stage_id[1] if len(stage_id)>1 else stage_id)
stage_ids = self.env['hr.recruitment.stage'].browse(stages)
for stage_id in stage_ids:
stage_id.write({'job_ids': [(4, res.id)]})
return res
def write(self, vals, model=None):
res = super(Job, self).write(vals)
if model:
if 'recruitment_stage_ids' in vals:
previous_stage_ids = self.recruitment_stage_ids.ids
stages = list()
for stage_id in vals['recruitment_stage_ids']:
stages.append(stage_id[1] if len(stage_id)>1 else stage_id)
new_stage_ids = self.env['hr.recruitment.stage'].browse(stages)
for stage_id in new_stage_ids:
stage_id.write({'job_ids': [(4, self.id)]})
# Remove jobs from stages no longer related
for stage in previous_stage_ids:
if stage.id not in new_stage_ids.ids:
stage.write({'job_ids': [(3, self.id)]})
return res
def unlink(self):
# Remove stage from all related jobs when stage is deleted
for job in self:
for stage in stage.recruitment_stage_ids:
stage.write({'job_ids': [(3, job.id)]})
return super(Job, self).unlink()

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_location,hr.location,model_hr_location,base.group_user,1,1,1,1
access_recruitment_status_history,recruitment.status.history,model_recruitment_status_history,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_location hr.location model_hr_location base.group_user 1 1 1 1
3 access_recruitment_status_history recruitment.status.history model_recruitment_status_history base.group_user 1 1 1 1

View File

@ -0,0 +1,188 @@
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
import { _t } from "@web/core/l10n/translation";
import { rpc } from "@web/core/network/rpc";
// Inherit the hrRecruitment widget
publicWidget.registry.CustomHrRecruitment = publicWidget.registry.hrRecruitment.extend({
selector: '#hr_recruitment_form',
events: {
'focusout #recruitmentctc' : '_onFocusOutCTC',
'focusout #recruitmentctc2' : '_onFocusOutCTC2',
'focusout #recruitmentphone' : '_onFocusOutPhoneNumber',
'focusout #recruitmentphone2': '_onFocusOutPhone2Number',
'change [name="exp_type"]': '_onExperienceTypeChange' // Add event listener for Experience Type change
},
async _onFocusOutCTC(ev) {
const regex = /^[\d]*$/;
const field='ctc'
const value = ev.currentTarget.value;
const messageContainerId = "#ctcwarning-message";
if (value && !regex.test(value)) {
this.showWarningMessage(ev.currentTarget, messageContainerId, "InValid CTC");
} else {
this.hideWarningMessage(ev.currentTarget, messageContainerId);
}
},
async _onFocusOutCTC2(ev) {
const regex = /^[\d]*$/;
const field='ctc'
const value = ev.currentTarget.value;
const messageContainerId = "#ctc2warning-message";
if (value && !regex.test(value)) {
this.showWarningMessage(ev.currentTarget, messageContainerId, "InValid CTC");
} else {
this.hideWarningMessage(ev.currentTarget, messageContainerId);
}
},
async _onFocusOutPhoneNumber (ev) {
const regex = /^[\d\+\-\s]*$/;
const field = "phone"
const value = ev.currentTarget.value;
const messageContainerId = "#phone1-warning";
await this.checkRedundant(ev.currentTarget, field, messageContainerId);
if (value && !regex.test(value)) {
this.showWarningMessage(ev.currentTarget, messageContainerId, "Invalid Number");
} else {
this.hideWarningMessage(ev.currentTarget, messageContainerId);
}
},
async _onFocusOutPhone2Number (ev) {
const regex = /^[\d\+\-\s]*$/;
const field = "phone"
const value = ev.currentTarget.value;
const messageContainerId = "#phone2-warning";
await this.checkRedundant(ev.currentTarget, field, messageContainerId);
if (value && !regex.test(value)) {
this.showWarningMessage(ev.currentTarget, messageContainerId, "Invalid Number");
} else {
this.hideWarningMessage(ev.currentTarget, messageContainerId);
}
},
// Function to toggle visibility of current_ctc and current_organization based on Experience Type
_onExperienceTypeChange(ev) {
const expType = ev.currentTarget.value; // Get selected Experience Type
const currentCtcField = $('#current_ctc_field');
const currentOrgField = $('#current_organization_field');
const noticePeriodField = $('#notice_period_field');
const ctcInput = $('#recruitmentctc');
const orgInput = $('#current_organization');
const noticePeriodInput = $('#notice_period')
if (expType === 'fresher') {
currentCtcField.hide();
currentOrgField.hide();
noticePeriodField.hide();
ctcInput.val('')
orgInput.val('')
noticePeriodInput.val('')
} else {
currentCtcField.show();
currentOrgField.show();
noticePeriodField.show();
}
},
async _renderPreferredLocations() {
console.log("hello world")
console.log(this)
const value = $('#preferred_locations_container').data('job_id');
console.log(value)
console.log("Job ID:", value); // You can now use this jobId
if (value){
let locationsArray = value.match(/\d+/g).map(Number); // [1, 2, 4, 5]
console.log(locationsArray)
const locations_data = await rpc("/hr_recruitment_extended/fetch_preferred_locations", {
loc_ids : locationsArray
});
try {
// Assuming location_ids is a many2many field in hr.job
const locationsField = $('#preferred_locations_container');
locationsField.empty(); // Clear any existing options
// Add checkboxes for each location
Object.keys(locations_data).forEach(key => {
const value = locations_data[key]; // value for the current key
const checkboxHtml = `
<div class="checkbox-container">
<input type="checkbox" name="preferred_locations" value="${key}" id="location_${key}">
<label for="location_${key}">${value}</label>
</div>
`;
locationsField.append(checkboxHtml);
});
} catch (error) {
console.error('Error fetching locations:', error);
}
} else {
console.log("no values")
const preferredLocation = $('#preferred_location_field');
preferredLocation.hide();
}
},
async _hrRecruitmentDegrees() {
try {
const degrees_data = await rpc("/hr_recruitment_extended/fetch_hr_recruitment_degree", {
});
// Assuming location_ids is a many2many field in hr.job
const degreesSelection = $('#fetch_hr_recruitment_degree');
degreesSelection.empty(); // Clear any existing options
// Add checkboxes for each location
Object.keys(degrees_data).forEach(key => {
const value = degrees_data[key]; // value for the current key
const checkboxHtml = `
<option value="${key}">${value}</option>
`;
degreesSelection.append(checkboxHtml);
});
} catch (error) {
console.error('Error fetching degrees:', error);
}
},
async start() {
this._super(...arguments);
await this._renderPreferredLocations(); // Render the preferred locations checkboxes
await this._hrRecruitmentDegrees();
const currentCtcField = $('#current_ctc_field');
const currentOrgField = $('#current_organization_field');
const noticePeriodField = $('#notice_period_field');
const ctcInput = $('#recruitmentctc');
const orgInput = $('#current_organization');
const noticePeriodInput = $('#notice_period')
currentCtcField.hide();
currentOrgField.hide();
noticePeriodField.hide();
ctcInput.val('')
orgInput.val('')
noticePeriodInput.val('')
},
// You can also override the _onClickApplyButton method if needed
// _onClickApplyButton(ev) {
// this._super(ev); // Call the parent method if you want to retain its functionality
// console.log("Custom behavior after clicking apply button!");
// // Add your custom logic here if needed
// }
});

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<!-- Tree View (List View) -->
<record id="view_hr_location_tree" model="ir.ui.view">
<field name="name">hr.location.tree</field>
<field name="model">hr.location</field>
<field name="arch" type="xml">
<list string="Location">
<field name="location_name"/>
<field name="zip_code"/>
<field name="country_id"/>
<field name="state"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_hr_location_form" model="ir.ui.view">
<field name="name">hr.location.form</field>
<field name="model">hr.location</field>
<field name="arch" type="xml">
<form string="Location">
<sheet>
<div class="row justify-content-between position-relative w-100 m-0 mb-2">
<div class="oe_title mw-75 ps-0 pe-2">
<h1 class="d-flex flex-row align-items-center">
<field name="location_name" placeholder="Location Name" required="True" style="font-size: min(4vw, 2.6rem);"/>
</h1>
</div>
</div>
<group>
<group>
<field name="country_id"/>
<field name="state"/>
</group>
<group>
<field name="zip_code"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Action to open Locations -->
<record id="action_hr_location" model="ir.actions.act_window">
<field name="name">Locations</field>
<field name="res_model">hr.location</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new Location
</p>
</field>
</record>
<menuitem
id="hr_location_menu"
name="Locations"
action="action_hr_location"
parent="hr_recruitment.menu_hr_recruitment_config_jobs"
sequence="5"/>
</data>
</odoo>

View File

@ -0,0 +1,220 @@
<odoo>
<data>
<record model="ir.ui.view" id="hr_view_hr_job_form_extended">
<field name="name">hr.job.form.extended</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr.view_hr_job_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="History" name="hiring_history_page">
<field name="hiring_history">
<list editable="bottom">
<field name="date_from"/>
<field name="date_end"/>
<field name="target"/>
<field name="hired" widget="many2many_tags"/>
<field name="job_id" invisible="1"/>
</list>
</field>
</page>
</xpath>
<!-- <xpath expr="//page[@name='job_description_page']" position="after">-->
<!-- <page string="History" name="hiring_history_page">-->
<!-- <field name="hiring_history">-->
<!-- <tree editable="top">-->
<!-- <field name="date_from"/>-->
<!-- <field name="date_end"/>-->
<!-- <field name="target"/>-->
<!-- <field name="hired"/>-->
<!-- </tree>-->
<!-- </field>-->
<!-- </page>-->
<!-- </xpath>-->
</field>
</record>
<record model="ir.ui.view" id="hr_recruitment_hr_job_survey_extended">
<field name="name">hr.job.survey.extended</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr_recruitment.hr_job_survey"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date_from']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='date_from']" position="before">
<field name="target_from" widget="daterange" string="Mission Dates"
options="{'end_date_field': 'target_to'}"/>
<field name="target_to" invisible="1"/>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="hr_job_form_extended">
<field name="name">hr.job.form.extended</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr_recruitment_skills.hr_job_form_inherit_hr_recruitment_skills"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button name="buttion_view_applicants" type="object" class="oe_stat_button" string="Candidates" widget="statinfo" icon="fa-th-large"/>
</xpath>
<xpath expr="//field[@name='skill_ids']" position="after">
<field name="secondary_skill_ids" widget="many2many_tags" options="{'color_field': 'color'}"
context="{'search_default_group_skill_type_id': 1}"/>
</xpath>
<xpath expr="//group[@name='recruitment2']" position="inside">
<field name="locations" widget="many2many_tags"/>
<field name="recruitment_stage_ids" widget="many2many_tags"/>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="hr_recruitment_hr_applicant_view_form_extend">
<field name="name">hr.applicant.view.form.extended</field>
<field name="model">hr.applicant</field>
<field name="inherit_id" ref="hr_recruitment.hr_applicant_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="alternate_phone"/>
</xpath>
<xpath expr="//field[@name='refuse_reason_id']" position="after">
<field name="refused_state" invisible="not refuse_reason_id"/>
</xpath>
<xpath expr="//field[@name='linkedin_profile']" position="after">
<field name="exp_type"/>
</xpath>
<xpath expr="//group[@name='recruitment_contract']/label[@for='salary_expected']" position="before">
<field name="current_ctc"/>
</xpath>
<xpath expr="//page[@name='application_details']" position="inside">
<group>
<group string="Location" name="location_details">
<field name="current_location"/>
<field name="preferred_location" widget="many2many_tags"/>
<field name="current_organization"/>
</group>
<group string="Experience" name="applicant_experience">
<label for="total_exp" string="Total Experience"/>
<div class="o_row">
<field name="total_exp" placeholder="Total Experience"/>
<field name="total_exp_type" placeholder="Experience Type" required="total_exp &gt; 0"/>
</div>
<label for="relevant_exp" string="Relevant Experience"/>
<div class="o_row">
<field name="relevant_exp" placeholder="Relevant Experience"/>
<field name="relevant_exp_type" placeholder="Experience Type" required="relevant_exp &gt; 0"/>
</div>
<label for="notice_period" string="Notice Period"/>
<div class="o_row">
<field name="notice_period" placeholder="Relevant Experience"/>
<field name="notice_period_type" placeholder="Experience Type" required="relevant_exp &gt; 0"/>
</div>
</group>
</group>
<group>
<group string="Negotiation" name="negotiation_details">
<field name="salary_negotiable"/>
<field name="np_negotiable"/>
<field name="holding_offer"/>
</group>
<group string="Comments" name="comments">
<field name="applicant_comments"/>
<field name="recruiter_comments"/>
</group>
</group>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="hr_candidate_view_form_inherit">
<field name="name">hr.candidate.view.form.inherit</field>
<field name="model">hr.candidate</field>
<field name="inherit_id" ref="hr_recruitment.hr_candidate_view_form"/>
<field name="arch" type="xml">
<!-- <xpath expr="//field[@name='partner_name']" position="attributes">-->
<!-- <attribute name="readonly">1</attribute>-->
<!-- </xpath>-->
<xpath expr="//form/sheet/group" position="before">
<group>
<group string="Candidate's Name">
<field name="first_name"/>
<field name="middle_name"/>
<field name="last_name"/>
</group>
</group>
</xpath>
<xpath expr="//field[@name='partner_phone']" position="after">
<field name="alternate_phone"/>
</xpath>
</field>
</record>
<!-- explicit list view definition -->
<!--
<record model="ir.ui.view" id="hr_recruitment_extended.list">
<field name="name">hr_recruitment_extended list</field>
<field name="model">hr_recruitment_extended.hr_recruitment_extended</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="value"/>
<field name="value2"/>
</tree>
</field>
</record>
-->
<!-- actions opening views on models -->
<!--
<record model="ir.actions.act_window" id="hr_recruitment_extended.action_window">
<field name="name">hr_recruitment_extended window</field>
<field name="res_model">hr_recruitment_extended.hr_recruitment_extended</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="hr_recruitment_extended.action_server">
<field name="name">hr_recruitment_extended server</field>
<field name="model_id" ref="model_hr_recruitment_extended_hr_recruitment_extended"/>
<field name="state">code</field>
<field name="code">
action = {
"type": "ir.actions.act_window",
"view_mode": "tree,form",
"res_model": model._name,
}
</field>
</record>
-->
<!-- Top menu item -->
<!--
<menuitem name="hr_recruitment_extended" id="hr_recruitment_extended.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="hr_recruitment_extended.menu_1" parent="hr_recruitment_extended.menu_root"/>
<menuitem name="Menu 2" id="hr_recruitment_extended.menu_2" parent="hr_recruitment_extended.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="hr_recruitment_extended.menu_1_list" parent="hr_recruitment_extended.menu_1"
action="hr_recruitment_extended.action_window"/>
<menuitem name="Server to list" id="hr_recruitment_extended" parent="hr_recruitment_extended.menu_2"
action="hr_recruitment_extended.action_server"/>
-->
</data>
</odoo>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="ir.ui.view" id="hr_recruitment_stage_form_extended">
<field name="name">hr.recruitment.stage.form.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="before">
<field name="is_default_field" string="Is Default"/>
</xpath>
</field>
</record>
<menuitem
id="hr_recruitment.menu_hr_recruitment_stage"
name="Stages"
parent="hr_recruitment.menu_hr_recruitment_config_jobs"
action="hr_recruitment.hr_recruitment_stage_act"
groups="hr_recruitment.group_hr_recruitment_manager"
sequence="1"/>
</odoo>

View File

@ -0,0 +1,364 @@
<odoo>
<template id="apply_extend" inherit_id="website_hr_recruitment.apply">
<xpath expr="//form[@id='hr_recruitment_form']" position="replace">
<!-- Your custom content here -->
<form id="hr_recruitment_form" action="/website/form/" method="post"
enctype="multipart/form-data" class="o_mark_required row"
data-mark="*" data-model_name="hr.applicant"
data-success-mode="redirect" data-success-page="/job-thank-you"
hide-change-model="true">
<div class="s_website_form_rows s_col_no_bgcolor">
<div class="s_website_form_rows row s_col_no_bgcolor">
<!-- Main Heading for Name Group -->
<!-- <div class="col-12">-->
<!-- <h3 class="section-heading">Name</h3>-->
<!-- </div>-->
<!-- First Name, Middle Name, Last Name in a row -->
<div class="col-12 col-sm-4 mb-0 py-2 s_website_form_field s_website_form_required s_website_form_model_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-12 s_website_form_label" for="recruitment1">
<span class="s_website_form_label_content">First Name</span>
<span class="s_website_form_mark">*</span>
</label>
<div class="col-12">
<input id="recruitment1" type="text"
class="form-control s_website_form_input"
name="first_name" required=""
data-fill-with="first_name"
placeholder="e.g. Pranay"/>
</div>
</div>
</div>
<div class="col-12 col-sm-4 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-12 s_website_form_label" for="recruitment2">
<span class="s_website_form_label_content">Middle Name</span>
</label>
<div class="col-12">
<input id="recruitment2" type="text"
class="form-control s_website_form_input"
name="middle_name"
data-fill-with="middle_name"
placeholder="e.g. Kumar"/>
</div>
</div>
</div>
<div class="col-12 col-sm-4 mb-0 py-2 s_website_form_field s_website_form_required s_website_form_model_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-12 s_website_form_label" for="recruitment3">
<span class="s_website_form_label_content">Last Name</span>
<span class="s_website_form_mark">*</span>
</label>
<div class="col-12">
<input id="recruitment3" type="text"
class="form-control s_website_form_input"
name="last_name" required=""
data-fill-with="last_name"
placeholder="e.g. Gadi (SURNAME)"/>
</div>
</div>
</div>
</div>
<!-- Contact Section (Email, Phone, Alternate Phone) -->
<div>
<div class="row s_col_no_resize s_col_no_bgcolor">
<!-- Main Heading for Contact Group -->
<!-- <div class="col-12">-->
<!-- <h3 class="section-heading">Contact</h3>-->
<!-- </div>-->
<!-- Email Field -->
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="email" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment2">
<span class="s_website_form_label_content">Email</span>
<span class="s_website_form_mark">*</span>
</label>
<div class="col-sm">
<input id="recruitment2" type="email"
class="form-control s_website_form_input"
name="email_from" required=""
placeholder="e.g. abc@gmail.com"
data-fill-with="email_from"/>
</div>
</div>
</div>
<!-- Phone Number Field -->
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitmentphone">
<span class="s_website_form_label_content">Phone Number</span>
<span class="s_website_form_mark">*</span>
</label>
<div class="col-sm">
<input id="recruitmentphone" type="tel"
class="form-control s_website_form_input"
name="partner_phone" required=""
placeholder="+91 1112223334"
data-fill-with="partner_phone"/>
<div class="alert alert-warning mt-2 d-none" id="phone1-warning"></div>
</div>
</div>
</div>
<!-- Alternate Phone Field -->
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitmentphone2">
<span class="s_website_form_label_content">Alternate Number</span>
</label>
<div class="col-sm">
<input id="recruitmentphone2" type="tel"
class="form-control s_website_form_input"
name="alternate_phone"
placeholder="+91 1112223334"
data-fill-with="alternate_phone"/>
<div class="alert alert-warning mt-2 d-none" id="phone2-warning"></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Degree</span>
</label>
<div class="col-sm">
<select id="fetch_hr_recruitment_degree" class="form-control s_website_form_input"
name="degree">
</select>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Experience Type</span>
</label>
<div class="col-sm">
<div class="o-row">
<select class="form-control s_website_form_input"
name="exp_type" required="">
<option value="fresher" selected="">Fresher</option>
<option value="experienced">Experienced</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field" id="current_organization_field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Current Organization</span>
</label>
<div class="col-sm">
<input type="text"
class="form-control s_website_form_input"
name="current_organization"
/>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field" id="current_ctc_field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitmentctc">
<span class="s_website_form_label_content">Current CTC (LPA)</span>
</label>
<div class="col-sm">
<input id="recruitmentctc" type="number"
class="form-control s_website_form_input"
name="current_ctc"
data-fill-with="ctc"/>
<div class="alert alert-warning mt-2 d-none" id="ctcwarning-message"></div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitmentctc2">
<span class="s_website_form_label_content">Expected CTC (LPA)</span>
</label>
<div class="col-sm">
<input id="recruitmentctc2" type="number"
class="form-control s_website_form_input"
name="expected_ctc"
/>
<div class="alert alert-warning mt-2 d-none" id="ctc2warning-message"></div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Current Location</span>
</label>
<div class="col-sm">
<input type="text"
class="form-control s_website_form_input"
name="current_location"
/>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field" id="preferred_location_field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<t t-set="job_id" t-value="job.id"/>
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="preferred_locations">
<span class="s_website_form_label_content">Preferred Locations</span>
</label>
<div class="col-sm">
<div id="preferred_locations_container" t-att-data-job_id="job.locations">
</div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="char" data-name="Field" id="notice_period_field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px">
<span class="s_website_form_label_content">Notice Period</span>
</label>
<div class="col-sm">
<div class="o-row">
<input type="number"
class="form-control s_website_form_input"
name="notice_period"
/>
<select class="form-control s_website_form_input"
name="notice_period_type">
<option value="day">Day's</option>
<option value="month">Month's</option>
<option value="year">Year's</option>
</select>
</div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_required"
data-type="char" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment4">
<span class="s_website_form_label_content">LinkedIn Profile</span>
</label>
<div class="col-sm">
<i class="fa fa-linkedin fa-2x o_linkedin_icon"></i>
<input id="recruitment4" type="text"
class="form-control s_website_form_input pl64"
placeholder="e.g. https://www.linkedin.com/in/fpodoo"
style="padding-inline-start: calc(40px + 0.375rem)"
name="linkedin_profile"
data-fill-with="linkedin_profile"/>
<div class="alert alert-warning mt-2 d-none" id="linkedin-message"></div>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_custom"
data-type="binary" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment6">
<span class="s_website_form_label_content">Resume</span>
</label>
<div class="col-sm">
<input id="recruitment6" type="file"
class="form-control s_website_form_input o_resume_input"
name="Resume"/>
<span class="text-muted small">Provide either a resume file or a linkedin profile</span>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field"
data-type="text" data-name="Field">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment5">
<span class="s_website_form_label_content">Short Introduction</span>
</label>
<div class="col-sm">
<textarea id="recruitment5"
class="form-control s_website_form_input"
placeholder="Optional introduction, or any question you might have about the job…"
name="applicant_notes" rows="5"></textarea>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_dnone"
data-type="record" data-model="hr.job">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment7">
<span class="s_website_form_label_content">Job</span>
</label>
<div class="col-sm">
<input id="recruitment7" type="hidden"
class="form-control s_website_form_input"
name="job_id"/>
</div>
</div>
</div>
<div class="col-12 mb-0 py-2 s_website_form_field s_website_form_dnone"
data-type="record" data-model="hr.department">
<div class="row s_col_no_resize s_col_no_bgcolor">
<label class="col-4 col-sm-auto s_website_form_label" style="width: 200px"
for="recruitment8">
<span class="s_website_form_label_content">Department</span>
</label>
<div class="col-sm">
<input id="recruitment8" type="hidden"
class="form-control s_website_form_input"
name="department_id"/>
</div>
</div>
</div>
<div class="col-12 s_website_form_submit mb64" data-name="Submit Button">
<div class="alert alert-warning mt-2 d-none" id="warning-message"></div>
<div style="width: 200px" class="s_website_form_label"/>
<a href="#" role="button" class="btn btn-primary btn-lg s_website_form_send" id="apply-btn">I'm
feeling lucky
</a>
<span id="s_website_form_result"></span>
</div>
</div>
</form>
</xpath>
</template>
</odoo>

View File

@ -16,7 +16,6 @@
# for the full list
'category': 'Human Resources/Time Off',
'version': '0.1',
'license': 'LGPL-3',
# any module necessary for this one to work correctly
'depends': ['base','hr','hr_holidays','hr_employee_extended'],

View File

@ -343,6 +343,6 @@ class HRLeaveType(models.Model):
[('week', 'Week'),
('month', 'Month'),
('year', 'Year')],
default='day', string="Limit Type", required=True,
default='month', string="Limit Type", required=True,
help="Specifies the type of time period (days, months, or years) for applying the leave request")
limit_emp_type = fields.Many2many('hr.contract.type', string="Employee Type")

View File

@ -0,0 +1,2 @@
from . import models
from . import wizard

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
{
'name': "Recruitment Requisition",
'summary': "To Raise a requist for recruitment",
'description': """
To Raise a requist for recruitment""",
'author': "Raman Marikanti",
'website': "https://www.ftprotech.com",
'category': 'Recruitment',
'version': '0.1',
'depends': ['hr_recruitment', 'mail','base','hr'],
'data': [
'security/ir.model.access.csv',
'data/ir_sequence.xml',
'data/mail_templates.xml',
'wizard/recruitment_cancel_wizard.xml',
'views/hr_requisition.xml',
'views/res_config_settings.xml'
],
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="seq_requisition" model="ir.sequence">
<field name="name">Requisitions</field>
<field name="code">hr.requisitions</field>
<field name="prefix">REQ/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
<!-- <record id="group_requisition_approver" model="res.groups">-->
<!-- <field name="name">Requisitions Approver</field>-->
<!-- <field name="category_id" ref="base.module_category_hidden"/>-->
<!-- </record>-->
</odoo>

View File

@ -0,0 +1,72 @@
<odoo>
<data>
<record id="mail_template_recruitment_requisition_notification" model="mail.template">
<field name="name">HR Requisition: Notification</field>
<field name="model_id" ref="requisitions.model_recruitment_requisition"/>
<field name="email_from">{{ object.requested_by.email_formatted or user.email_formatted }}</field>
<field name="email_to">{{ object.hr_manager_id.email }}</field>
<field name="subject">New Requisition Submitted: {{ object.name }}</field>
<field name="description">Notification sent to HR Manager when a new requisition is submitted.</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-out="object.hr_manager_id.name or ''">John Doe</t>,
<br /><br />
A new requisition has been submitted for your review:
<ul>
<li><strong>Requisition Name:</strong> <t t-out="object.name or ''">New</t></li>
<li><strong>Department:</strong> <t t-out="object.department_id.name or ''">Sales</t></li>
<li><strong>Position Title:</strong> <t t-out="object.position_title or ''">Sales Manager</t></li>
<li><strong>Number of Positions:</strong> <t t-out="object.number_of_positions or 0">3</t></li>
<li><strong>Requested By:</strong> <t t-out="object.requested_by.name or ''">Emily Clark</t></li>
<li><strong>Requested On:</strong> <t t-out="object.requested_on or ''">2024-12-31</t></li>
</ul>
<br />
<t t-if="object.notes">
<strong>Notes:</strong> <t t-out="object.notes or ''">Requirement</t>
<br />
</t>
<br />
Please <a t-att-href="'%s/web#id=%d&amp;model=recruitment.requisition&amp;view_type=form' % (object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id)" target="_blank">click here</a> to view the requisition details.
<br /><br />
Regards,
<br />
<t t-out="object.requested_by.name or ''">Emily Clark</t>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
<odoo>
<data>
<record id="mail_template_recruitment_requisition_cancellation" model="mail.template">
<field name="name">Recruitment Requisition Cancellation</field>
<field name="model_id" ref="requisitions.model_recruitment_requisition"/>
<field name="email_from">{{ object.requested_by.email_formatted or user.email_formatted }}</field>
<field name="email_to">{{ object.hr_manager_id.email }}</field>
<field name="subject">Requisition Cancelled: {{ object.name }}</field>
<field name="body_html" type="html">
<div>
<p>Dear <t t-out="object.hr_manager_id.name or ''">HR Manager</t>,</p>
<p>The requisition <strong><t t-out="object.name or ''">New</t></strong> has been cancelled for the following reason:</p>
<p><t t-out="object.notes or 'No reason provided.'"/></p>
<p>
You can view the requisition details by clicking the link below:
<br/>
<a t-att-href="'/web#id=%d&amp;model=recruitment.requisition&amp;view_type=form' % object.id" style="color: #1a73e8; text-decoration: none;">
View Requisition
</a>
</p>
<p>Regards,</p>
<p><t t-out="object.requested_by.name or ''">Requested By</t></p>
</div>
</field>
</record>
</data>
</odoo>
</data>
</odoo>

View File

@ -0,0 +1,2 @@
from . import hr_requisition
from . import res_config_settings

View File

@ -0,0 +1,85 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
class RecruitmentRequisition(models.Model):
_name = 'recruitment.requisition'
_description = 'Recruitment Requisition'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string="Requisition Name", required=True, default="New")
department_id = fields.Many2one('hr.department', string="Department", required=True)
requested_by = fields.Many2one('res.users', string="Requested By", default=lambda self: self.env.user)
requested_on = fields.Date(string='Requested On')
hr_manager_id = fields.Many2one('res.users', string="HR Manager", compute='_compute_hr_manager')
hr_job = fields.Many2one('')
position_title = fields.Char(string="Position Title", required=True)
number_of_positions = fields.Integer(string="Number of Positions", required=True)
job_description = fields.Html(string="Job Summary")
job_id = fields.Many2one('hr.job',"JD ID")
state = fields.Selection([
('draft', 'Draft'),
('waiting_approval', 'Waiting Approval'),
('approved', 'Approved'),
('done', 'Done'),
('cancel', 'Cancelled')
], default='draft', track_visibility='onchange')
notes = fields.Text(string="Notes")
primary_skill_ids = fields.Many2many('hr.skill', "recruitment_requisition_primary_hr_skill_rel",
'hr_job_id', 'hr_skill_id', "Primary Skills", required=True)
secondary_skill_ids = fields.Many2many('hr.skill', "recruitment_requisition_secondary_hr_skill_rel",
'hr_job_id', 'hr_skill_id', "Secondary Skills")
assign_to = fields.Many2one('res.users', domain=lambda self: [
('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_user').id)])
@api.depends('requested_by')
def _compute_hr_manager(self):
hr_id = self.env['ir.config_parameter'].sudo().get_param('requisitions.requisition_hr_id')
self.hr_manager_id = self.env['res.users'].sudo().browse(int(hr_id)) if hr_id else False
def action_submit(self):
self.name = self.env['ir.sequence'].next_by_code('hr.requisitions')
self.requested_on = fields.Date.today()
self.state = 'waiting_approval'
template = self.env.ref('requisitions.mail_template_recruitment_requisition_notification') # Replace `module_name` with your module name
if template:
template.send_mail(self.id, force_send=True)
def action_approve(self):
for rec in self:
if not rec.assign_to:
raise ValidationError(_("Please Assign a recruitment user"))
rec.state = 'approved'
rec.button_create_jd()
def action_cancel(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'recruitment.requisition.cancel.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'active_id': self.id},
}
def button_create_jd(self):
self.job_id = self.env['hr.job'].create({
'name': self.position_title,
'department_id': self.department_id.id,
'no_of_recruitment':self.number_of_positions,
'description':self.job_description,
'skill_ids': [(6, 0, self.primary_skill_ids.ids)],
'secondary_skill_ids': [(6, 0, self.secondary_skill_ids.ids)],
'requested_by': self.requested_by.partner_id.id,
'user_id': self.assign_to.id
})
self.state ='done'
class HRJob(models.Model):
_inherit = 'hr.job'
requested_by = fields.Many2one('res.partner', string="Requested By", default=lambda self: self.env.user.partner_id)

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
requisition_hr_id = fields.Many2one('res.users',config_parameter='requisitions.requisition_hr_id', string='Requisition HR',
domain=lambda self: [
('groups_id', 'in', self.env.ref('hr_recruitment.group_hr_recruitment_manager').id)])
# requisition_md_id = fields.Many2one('res.users', string='Requisition MD')

View File

@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_requisition_user,access_requisition_user,model_recruitment_requisition,base.group_user,1,1,1,1
access_requisition_manager,access_requisition_manager,model_recruitment_requisition,base.group_user,1,1,1,1
access_recruitment_requisition_cancel_wizard,recruitment.requisition.cancel.wizard,model_recruitment_requisition_cancel_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_requisition_user access_requisition_user model_recruitment_requisition base.group_user 1 1 1 1
3 access_requisition_manager access_requisition_manager model_recruitment_requisition base.group_user 1 1 1 1
4 access_recruitment_requisition_cancel_wizard recruitment.requisition.cancel.wizard model_recruitment_requisition_cancel_wizard base.group_user 1 1 1 1

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_hr_job_form" model="ir.ui.view">
<field name="name">view.hr.job.form</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr.view_hr_job_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='department_id']" position="before">
<field name="requested_by"/>
</xpath>
</field>
</record>
<record id="view_requisition_form" model="ir.ui.view">
<field name="name">requisition.form</field>
<field name="model">recruitment.requisition</field>
<field name="arch" type="xml">
<form string="Recruitment Requisition">
<header>
<button string="Submit" type="object" name="action_submit" invisible=" state != 'draft'"/>
<button string="Approve &amp; Create JD" type="object" name="action_approve" invisible=" state != 'waiting_approval'" groups="hr_recruitment.group_hr_recruitment_manager"/>
<button name="button_create_jd" type="object" string="Create JD" invisible=" state != 'approved'"/>
<button string="Cancel" type="object" name="action_cancel" invisible=" state in ('draft','done','cancel')"/>
<field name="state" widget="statusbar" statusbar_visible="draft,waiting_approval,done" readonly="1"/>
</header>
<sheet>
<div class="row justify-content-between position-relative w-100 m-0 mb-2">
<div class="oe_title mw-75 ps-0 pe-2">
<h1 class="d-flex flex-row align-items-center">
<field name="name" placeholder="Serial Number" required="True" readonly="1" force_save="1"/>
</h1>
</div>
</div>
<group>
<group>
<field name="department_id" readonly="state != 'draft'"/>
<field name="requested_by" readonly="1" widget="many2one_avatar_user"/>
<field name="requested_on" invisible="requested_on == False"/>
<field name="hr_manager_id" widget="many2one_avatar_user"/>
</group>
<group>
<field name="position_title" readonly="state != 'draft'"/>
<field name="number_of_positions" readonly="state != 'draft'"/>
<field name="primary_skill_ids" widget="many2many_tags" options="{'color_field': 'color'}" readonly="state != 'draft'"/>
<field name="secondary_skill_ids" widget="many2many_tags" options="{'color_field': 'color'}" readonly="state != 'draft'"/>
<field name="assign_to" invisible="state not in ['approved','waiting_approval','done']" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
<field name="job_id" invisible="job_id == False" readonly="1" force_save="1"/>
</group>
<field name="notes" placeholder="Remarks" readonly="state == 'done'"/>
</group>
<group string="Job Summary">
<field name="job_description" readonly="state == 'done'" nolabel="1" placeholder="Please enter the JD Hear"/>
</group>
</sheet>
<chatter reload_on_follower="True"/>
</form>
</field>
</record>
<record id="view_requisition_tree" model="ir.ui.view">
<field name="name">requisition.tree</field>
<field name="model">recruitment.requisition</field>
<field name="arch" type="xml">
<list string="Recruitment Requisitions">
<field name="name"/>
<field name="department_id"/>
<field name="requested_by"/>
<field name="state"/>
</list>
</field>
</record>
<record id="action_recruitment_requisition" model="ir.actions.act_window">
<field name="name">Recruitment Requisitions</field>
<field name="res_model">recruitment.requisition</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_recruitment_requisition" name="Recruitment Requisition" parent="hr_recruitment.menu_hr_recruitment_root"/>
<menuitem id="menu_recruitment_requisition_main" name="Requisitions" parent="menu_recruitment_requisition" action="action_recruitment_requisition"/>
</odoo>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form_inherit" model="ir.ui.view">
<field name="name">res.config.settings.view.form.requisitions.access</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="hr_recruitment.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//block[@name='recruitment_in_app_purchases']" position="after">
<block title="Requisition Access Control" name="requisition_access_block">
<setting string="Requisition HR Approval Access"
help="Select the HR responsible for approving the recruitment requisitions."
id="requisition_hr_access_control">
<field name="requisition_hr_id" options="{'no_quick_create': True, 'no_create_edit': True, 'no_open': True}"/>
</setting>
</block>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
from . import recruitment_cancel_wizard

View File

@ -0,0 +1,18 @@
from odoo import models, fields, api
class RecruitmentRequisitionCancelWizard(models.TransientModel):
_name = 'recruitment.requisition.cancel.wizard'
_description = 'Requisition Cancellation Wizard'
cancellation_reason = fields.Text(string="Cancellation Reason", required=True)
def submit_cancellation(self):
requisition = self.env['recruitment.requisition'].browse(self.env.context.get('active_id'))
requisition.write({
'state': 'cancel',
'notes': self.cancellation_reason,
})
# Send cancellation email
template = self.env.ref('requisitions.mail_template_recruitment_requisition_cancellation') # Replace with your module name
if template:
template.send_mail(requisition.id, force_send=True)

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record id="view_recruitment_requisition_cancel_wizard_form" model="ir.ui.view">
<field name="name">recruitment.requisition.cancel.wizard.form</field>
<field name="model">recruitment.requisition.cancel.wizard</field>
<field name="arch" type="xml">
<form string="Cancel Requisition">
<group>
<field name="cancellation_reason" placeholder="Enter the reason for cancellation..."/>
</group>
<footer>
<button string="Submit" type="object" name="submit_cancellation" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</data>
</odoo>