From 89295a4e5a87aab947739eda4b1e48acef795938 Mon Sep 17 00:00:00 2001 From: Pranay Date: Fri, 24 Jan 2025 11:56:28 +0530 Subject: [PATCH] FIX: Leaves bug --- .../hr_recruitment_extended/__init__.py | 4 + .../hr_recruitment_extended/__manifest__.py | 39 ++ .../controllers/__init__.py | 3 + .../controllers/controllers.py | 167 ++++++++ .../hr_recruitment_extended/data/cron.xml | 16 + .../data/mail_template.xml | 42 ++ .../hr_recruitment_extended/demo/demo.xml | 30 ++ .../models/__init__.py | 5 + .../models/hr_applicant.py | 26 ++ .../models/hr_recruitment.py | 197 ++++++++++ .../hr_recruitment_extended/models/stages.py | 106 +++++ .../security/ir.model.access.csv | 3 + .../src/js/website_hr_applicant_form.js | 188 +++++++++ .../views/hr_location.xml | 68 ++++ .../views/hr_recruitment.xml | 220 +++++++++++ .../hr_recruitment_extended/views/stages.xml | 25 ++ ...e_hr_recruitment_application_templates.xml | 364 ++++++++++++++++++ .../hr_timeoff_extended/__manifest__.py | 1 - .../hr_timeoff_extended/models/hr_timeoff.py | 2 +- addons_extensions/requisitions/__init__.py | 2 + .../requisitions/__manifest__.py | 23 ++ .../requisitions/data/ir_sequence.xml | 17 + .../requisitions/data/mail_templates.xml | 72 ++++ .../requisitions/models/__init__.py | 2 + .../requisitions/models/hr_requisition.py | 85 ++++ .../models/res_config_settings.py | 14 + .../requisitions/security/ir.model.access.csv | 5 + .../requisitions/views/hr_requisition.xml | 82 ++++ .../views/res_config_settings.xml | 19 + .../requisitions/wizard/__init__.py | 1 + .../wizard/recruitment_cancel_wizard.py | 18 + .../wizard/recruitment_cancel_wizard.xml | 20 + 32 files changed, 1864 insertions(+), 2 deletions(-) create mode 100644 addons_extensions/hr_recruitment_extended/__init__.py create mode 100644 addons_extensions/hr_recruitment_extended/__manifest__.py create mode 100644 addons_extensions/hr_recruitment_extended/controllers/__init__.py create mode 100644 addons_extensions/hr_recruitment_extended/controllers/controllers.py create mode 100644 addons_extensions/hr_recruitment_extended/data/cron.xml create mode 100644 addons_extensions/hr_recruitment_extended/data/mail_template.xml create mode 100644 addons_extensions/hr_recruitment_extended/demo/demo.xml create mode 100644 addons_extensions/hr_recruitment_extended/models/__init__.py create mode 100644 addons_extensions/hr_recruitment_extended/models/hr_applicant.py create mode 100644 addons_extensions/hr_recruitment_extended/models/hr_recruitment.py create mode 100644 addons_extensions/hr_recruitment_extended/models/stages.py create mode 100644 addons_extensions/hr_recruitment_extended/security/ir.model.access.csv create mode 100644 addons_extensions/hr_recruitment_extended/static/src/js/website_hr_applicant_form.js create mode 100644 addons_extensions/hr_recruitment_extended/views/hr_location.xml create mode 100644 addons_extensions/hr_recruitment_extended/views/hr_recruitment.xml create mode 100644 addons_extensions/hr_recruitment_extended/views/stages.xml create mode 100644 addons_extensions/hr_recruitment_extended/views/website_hr_recruitment_application_templates.xml create mode 100644 addons_extensions/requisitions/__init__.py create mode 100644 addons_extensions/requisitions/__manifest__.py create mode 100644 addons_extensions/requisitions/data/ir_sequence.xml create mode 100644 addons_extensions/requisitions/data/mail_templates.xml create mode 100644 addons_extensions/requisitions/models/__init__.py create mode 100644 addons_extensions/requisitions/models/hr_requisition.py create mode 100644 addons_extensions/requisitions/models/res_config_settings.py create mode 100644 addons_extensions/requisitions/security/ir.model.access.csv create mode 100644 addons_extensions/requisitions/views/hr_requisition.xml create mode 100644 addons_extensions/requisitions/views/res_config_settings.xml create mode 100644 addons_extensions/requisitions/wizard/__init__.py create mode 100644 addons_extensions/requisitions/wizard/recruitment_cancel_wizard.py create mode 100644 addons_extensions/requisitions/wizard/recruitment_cancel_wizard.xml 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 + + + +