diff --git a/addons_extensions/hr_recruitment_extended/__init__.py b/addons_extensions/hr_recruitment_extended/__init__.py
new file mode 100644
index 000000000..aa4d0fd63
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import controllers
+from . import models
diff --git a/addons_extensions/hr_recruitment_extended/__manifest__.py b/addons_extensions/hr_recruitment_extended/__manifest__.py
new file mode 100644
index 000000000..6d54628c8
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/__manifest__.py
@@ -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',
+ ],
+ }
+}
+
diff --git a/addons_extensions/hr_recruitment_extended/controllers/__init__.py b/addons_extensions/hr_recruitment_extended/controllers/__init__.py
new file mode 100644
index 000000000..b0f26a9a6
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import controllers
diff --git a/addons_extensions/hr_recruitment_extended/controllers/controllers.py b/addons_extensions/hr_recruitment_extended/controllers/controllers.py
new file mode 100644
index 000000000..77df935d6
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/controllers/controllers.py
@@ -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
+
diff --git a/addons_extensions/hr_recruitment_extended/data/cron.xml b/addons_extensions/hr_recruitment_extended/data/cron.xml
new file mode 100644
index 000000000..4ab10d334
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/data/cron.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ JOB: End date
+
+ code
+ model.hr_job_end_date_update()
+
+ 1
+ days
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/hr_recruitment_extended/data/mail_template.xml b/addons_extensions/hr_recruitment_extended/data/mail_template.xml
new file mode 100644
index 000000000..889dba962
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/data/mail_template.xml
@@ -0,0 +1,42 @@
+
+
+
+
+ Recruitment Deadline Alert
+
+ {{ object.company_id.email or user.email_formatted }}
+ {{ object.user_id.email }}
+
+ Reminder: Recruitment Process Ending Soon - {{ object.name }}
+ Notification sent to recruiters when a job's recruitment deadline is approaching.
+
+
+
+
+
+ Dear Recruiter,
+
+ This is a friendly reminder that the recruitment process for the position
+ Job Title is approaching its end:
+
+ - Job Position: Job Name
+ - Target End Date: End Date
+ - Location(s): Locations
+
+
+ Please ensure all recruitment activities are completed before the deadline.
+
+
+ Click here to view the job details.
+
+
+ Regards,
+
System Admin
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/hr_recruitment_extended/demo/demo.xml b/addons_extensions/hr_recruitment_extended/demo/demo.xml
new file mode 100644
index 000000000..9baad55ca
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/demo/demo.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
diff --git a/addons_extensions/hr_recruitment_extended/models/__init__.py b/addons_extensions/hr_recruitment_extended/models/__init__.py
new file mode 100644
index 000000000..93353fa50
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/models/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+
+from . import hr_recruitment
+from . import stages
+from . import hr_applicant
diff --git a/addons_extensions/hr_recruitment_extended/models/hr_applicant.py b/addons_extensions/hr_recruitment_extended/models/hr_applicant.py
new file mode 100644
index 000000000..76c12e152
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/models/hr_applicant.py
@@ -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
diff --git a/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py b/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py
new file mode 100644
index 000000000..f9a2ed206
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/models/hr_recruitment.py
@@ -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. It’s 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. It’s 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
\ No newline at end of file
diff --git a/addons_extensions/hr_recruitment_extended/models/stages.py b/addons_extensions/hr_recruitment_extended/models/stages.py
new file mode 100644
index 000000000..e8a821450
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/models/stages.py
@@ -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()
diff --git a/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv b/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv
new file mode 100644
index 000000000..1669daead
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/security/ir.model.access.csv
@@ -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
\ No newline at end of file
diff --git a/addons_extensions/hr_recruitment_extended/static/src/js/website_hr_applicant_form.js b/addons_extensions/hr_recruitment_extended/static/src/js/website_hr_applicant_form.js
new file mode 100644
index 000000000..31b5b5984
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/static/src/js/website_hr_applicant_form.js
@@ -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 = `
+
+
+
+
+ `;
+
+ 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 = `
+
+
+
+
+ `;
+
+ 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
+// }
+});
diff --git a/addons_extensions/hr_recruitment_extended/views/hr_location.xml b/addons_extensions/hr_recruitment_extended/views/hr_location.xml
new file mode 100644
index 000000000..805c9dc39
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/views/hr_location.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+ hr.location.tree
+ hr.location
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.location.form
+ hr.location
+
+
+
+
+
+
+
+ Locations
+ hr.location
+ list,form
+
+
+ Create a new Location
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/hr_recruitment_extended/views/hr_recruitment.xml b/addons_extensions/hr_recruitment_extended/views/hr_recruitment.xml
new file mode 100644
index 000000000..9e4e74758
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/views/hr_recruitment.xml
@@ -0,0 +1,220 @@
+
+
+
+
+ hr.job.form.extended
+ hr.job
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.job.survey.extended
+ hr.job
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.job.form.extended
+ hr.job
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.applicant.view.form.extended
+ hr.applicant
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.candidate.view.form.inherit
+ hr.candidate
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/hr_recruitment_extended/views/stages.xml b/addons_extensions/hr_recruitment_extended/views/stages.xml
new file mode 100644
index 000000000..88836b85d
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/views/stages.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ hr.recruitment.stage.form.extended
+ hr.recruitment.stage
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/hr_recruitment_extended/views/website_hr_recruitment_application_templates.xml b/addons_extensions/hr_recruitment_extended/views/website_hr_recruitment_application_templates.xml
new file mode 100644
index 000000000..45907183d
--- /dev/null
+++ b/addons_extensions/hr_recruitment_extended/views/website_hr_recruitment_application_templates.xml
@@ -0,0 +1,364 @@
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/hr_timeoff_extended/__manifest__.py b/addons_extensions/hr_timeoff_extended/__manifest__.py
index 6dd333fca..f61987969 100644
--- a/addons_extensions/hr_timeoff_extended/__manifest__.py
+++ b/addons_extensions/hr_timeoff_extended/__manifest__.py
@@ -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'],
diff --git a/addons_extensions/hr_timeoff_extended/models/hr_timeoff.py b/addons_extensions/hr_timeoff_extended/models/hr_timeoff.py
index 680daf9d6..65a55b5ce 100644
--- a/addons_extensions/hr_timeoff_extended/models/hr_timeoff.py
+++ b/addons_extensions/hr_timeoff_extended/models/hr_timeoff.py
@@ -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")
\ No newline at end of file
diff --git a/addons_extensions/requisitions/__init__.py b/addons_extensions/requisitions/__init__.py
new file mode 100644
index 000000000..c536983e2
--- /dev/null
+++ b/addons_extensions/requisitions/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
\ No newline at end of file
diff --git a/addons_extensions/requisitions/__manifest__.py b/addons_extensions/requisitions/__manifest__.py
new file mode 100644
index 000000000..6644233f8
--- /dev/null
+++ b/addons_extensions/requisitions/__manifest__.py
@@ -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'
+ ],
+
+}
+
diff --git a/addons_extensions/requisitions/data/ir_sequence.xml b/addons_extensions/requisitions/data/ir_sequence.xml
new file mode 100644
index 000000000..3877573a3
--- /dev/null
+++ b/addons_extensions/requisitions/data/ir_sequence.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ Requisitions
+ hr.requisitions
+ REQ/
+ 5
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/requisitions/data/mail_templates.xml b/addons_extensions/requisitions/data/mail_templates.xml
new file mode 100644
index 000000000..eb99be74d
--- /dev/null
+++ b/addons_extensions/requisitions/data/mail_templates.xml
@@ -0,0 +1,72 @@
+
+
+
+ HR Requisition: Notification
+
+ {{ object.requested_by.email_formatted or user.email_formatted }}
+ {{ object.hr_manager_id.email }}
+ New Requisition Submitted: {{ object.name }}
+ Notification sent to HR Manager when a new requisition is submitted.
+
+
+
+
+ Dear John Doe,
+
+ A new requisition has been submitted for your review:
+
+ - Requisition Name: New
+ - Department: Sales
+ - Position Title: Sales Manager
+ - Number of Positions: 3
+ - Requested By: Emily Clark
+ - Requested On: 2024-12-31
+
+
+
+ Notes: Requirement
+
+
+
+ Please
click here to view the requisition details.
+
+ Regards,
+
+
Emily Clark
+
+
+
+
+
+
+
+
+
+
+ Recruitment Requisition Cancellation
+
+ {{ object.requested_by.email_formatted or user.email_formatted }}
+ {{ object.hr_manager_id.email }}
+ Requisition Cancelled: {{ object.name }}
+
+
+
Dear HR Manager,
+
The requisition New has been cancelled for the following reason:
+
+
+ You can view the requisition details by clicking the link below:
+
+
+ View Requisition
+
+
+
Regards,
+
Requested By
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/requisitions/models/__init__.py b/addons_extensions/requisitions/models/__init__.py
new file mode 100644
index 000000000..34f389eea
--- /dev/null
+++ b/addons_extensions/requisitions/models/__init__.py
@@ -0,0 +1,2 @@
+from . import hr_requisition
+from . import res_config_settings
\ No newline at end of file
diff --git a/addons_extensions/requisitions/models/hr_requisition.py b/addons_extensions/requisitions/models/hr_requisition.py
new file mode 100644
index 000000000..05c420efa
--- /dev/null
+++ b/addons_extensions/requisitions/models/hr_requisition.py
@@ -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)
diff --git a/addons_extensions/requisitions/models/res_config_settings.py b/addons_extensions/requisitions/models/res_config_settings.py
new file mode 100644
index 000000000..05df35469
--- /dev/null
+++ b/addons_extensions/requisitions/models/res_config_settings.py
@@ -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')
\ No newline at end of file
diff --git a/addons_extensions/requisitions/security/ir.model.access.csv b/addons_extensions/requisitions/security/ir.model.access.csv
new file mode 100644
index 000000000..af513bbf3
--- /dev/null
+++ b/addons_extensions/requisitions/security/ir.model.access.csv
@@ -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
\ No newline at end of file
diff --git a/addons_extensions/requisitions/views/hr_requisition.xml b/addons_extensions/requisitions/views/hr_requisition.xml
new file mode 100644
index 000000000..670ca6bc9
--- /dev/null
+++ b/addons_extensions/requisitions/views/hr_requisition.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+ view.hr.job.form
+ hr.job
+
+
+
+
+
+
+
+
+ requisition.form
+ recruitment.requisition
+
+
+
+
+
+
+ requisition.tree
+ recruitment.requisition
+
+
+
+
+
+
+
+
+
+
+ Recruitment Requisitions
+ recruitment.requisition
+ list,form
+
+
+
+
+
diff --git a/addons_extensions/requisitions/views/res_config_settings.xml b/addons_extensions/requisitions/views/res_config_settings.xml
new file mode 100644
index 000000000..ce1260a4a
--- /dev/null
+++ b/addons_extensions/requisitions/views/res_config_settings.xml
@@ -0,0 +1,19 @@
+
+
+
+ res.config.settings.view.form.requisitions.access
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/requisitions/wizard/__init__.py b/addons_extensions/requisitions/wizard/__init__.py
new file mode 100644
index 000000000..bc8af2b3a
--- /dev/null
+++ b/addons_extensions/requisitions/wizard/__init__.py
@@ -0,0 +1 @@
+from . import recruitment_cancel_wizard
\ No newline at end of file
diff --git a/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.py b/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.py
new file mode 100644
index 000000000..27554b810
--- /dev/null
+++ b/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.py
@@ -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)
diff --git a/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.xml b/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.xml
new file mode 100644
index 000000000..b315e1938
--- /dev/null
+++ b/addons_extensions/requisitions/wizard/recruitment_cancel_wizard.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ recruitment.requisition.cancel.wizard.form
+ recruitment.requisition.cancel.wizard
+
+
+
+
+
+