Compare commits
5 Commits
f47d38d3d5
...
56fd53e470
| Author | SHA1 | Date |
|---|---|---|
|
|
56fd53e470 | |
|
|
5807943958 | |
|
|
89295a4e5a | |
|
|
ac90760034 | |
|
|
9138f20aba |
|
|
@ -15,7 +15,6 @@
|
|||
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||
# for the full list
|
||||
'category': 'Flutter',
|
||||
'license': 'LGPL-3',
|
||||
'version': '0.1',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from logging import exception
|
||||
|
||||
from odoo import api, models, _, fields
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.misc import format_date
|
||||
|
|
@ -6,24 +8,43 @@ from datetime import datetime
|
|||
class HRLeave(models.Model):
|
||||
_inherit = "hr.leave"
|
||||
|
||||
def flutter_check_overlap_constrain(self,employee_id, to_date, from_date):
|
||||
def flutter_check_overlap_constrain(self,employee_id, to_date, from_date, fetched_leave_id=None):
|
||||
if self.env.context.get('leave_skip_date_check', False):
|
||||
return
|
||||
date_from = datetime.fromisoformat(from_date).replace(hour=0, minute=0, second=0)
|
||||
date_to = datetime.fromisoformat(to_date).replace(hour=23, minute=59, second=59)
|
||||
employee_id = int(employee_id)
|
||||
all_leaves = self.search([
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('employee_id', 'in', [employee_id]),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
])
|
||||
domain = [
|
||||
('employee_id', '=', employee_id),
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
]
|
||||
if fetched_leave_id and fetched_leave_id > 0:
|
||||
all_leaves = self.search([
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('employee_id', 'in', [employee_id]),
|
||||
('id','!=', fetched_leave_id),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
])
|
||||
|
||||
domain = [
|
||||
('employee_id', '=', employee_id),
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('id', '!=', fetched_leave_id),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
]
|
||||
else:
|
||||
all_leaves = self.search([
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('employee_id', 'in', [employee_id]),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
])
|
||||
domain = [
|
||||
('employee_id', '=', employee_id),
|
||||
('date_from', '<', date_to),
|
||||
('date_to', '>', date_from),
|
||||
('state', 'not in', ['cancel', 'refuse']),
|
||||
]
|
||||
|
||||
|
||||
conflicting_holidays = all_leaves.filtered_domain(domain)
|
||||
|
||||
if conflicting_holidays:
|
||||
|
|
@ -102,6 +123,51 @@ Attempting to double-book your time off won't magically make your vacation 2x be
|
|||
}
|
||||
|
||||
@api.model
|
||||
def submit_leave_flutter_odoo(self,leave_request_data):
|
||||
print(leave_request_data)
|
||||
pass
|
||||
def submit_leave_flutter_odoo(self,leave_request_data, existing_leave_id = None):
|
||||
try:
|
||||
date_from = datetime.fromisoformat(leave_request_data['date_from']).replace(hour=0, minute=0, second=0)
|
||||
date_to = datetime.fromisoformat(leave_request_data['date_to']).replace(hour=23, minute=59, second=59)
|
||||
if existing_leave_id and existing_leave_id > 0:
|
||||
leave_id = self.env['hr.leave'].sudo().browse(existing_leave_id)
|
||||
else:
|
||||
leave_id = False
|
||||
if leave_id:
|
||||
leave_id.sudo().write({
|
||||
'employee_id': leave_request_data['employee_id'],
|
||||
'holiday_status_id': leave_request_data['holiday_status_id'],
|
||||
'request_date_from': date_from.date(),
|
||||
'request_date_to':date_to.date(),
|
||||
'name': leave_request_data['description'],
|
||||
'request_unit_half': leave_request_data['is_half_day'],
|
||||
'request_date_from_period': 'pm' if leave_request_data['half_day_option'] == 'afternoon' else 'am',
|
||||
})
|
||||
leave = leave_id
|
||||
else:
|
||||
leave = self.env['hr.leave'].sudo().create({
|
||||
'employee_id': leave_request_data['employee_id'],
|
||||
'holiday_status_id': leave_request_data['holiday_status_id'],
|
||||
'request_date_from': date_from.date(),
|
||||
'request_date_to':date_to.date(),
|
||||
'name':leave_request_data['description'],
|
||||
'request_unit_half': leave_request_data['is_half_day'],
|
||||
'request_date_from_period': 'pm' if leave_request_data['half_day_option'] == 'afternoon' else 'am',
|
||||
})
|
||||
attachment_ids = []
|
||||
for attachment in leave_request_data['attachments']:
|
||||
attachment_record = self.env['ir.attachment'].create({
|
||||
'name': attachment['name'], # Attachment name
|
||||
'datas': attachment['datas'], # Base64 encoded data
|
||||
'res_model': 'hr.leave', # Model to link
|
||||
'res_id': leave.id, # ID of the leave record
|
||||
})
|
||||
attachment_ids.append(attachment_record.id)
|
||||
|
||||
# Link the attachments to the leave record
|
||||
if attachment_ids:
|
||||
leave.supported_attachment_ids = [(6, 0, attachment_ids)]
|
||||
|
||||
leave._check_validity()
|
||||
leave.state = 'confirm'
|
||||
return {'status': 'success', 'leave_id': leave.id, 'message': ''}
|
||||
except Exception as e:
|
||||
return {'status': 'error','leave_id': '', 'message': str(e)}
|
||||
|
|
@ -32,139 +32,21 @@ class HrEmployee(models.Model):
|
|||
|
||||
@api.model
|
||||
def get_user_employee_details(self):
|
||||
"""To fetch the details of employee"""
|
||||
uid = request.session.uid
|
||||
employee = self.env['hr.employee'].sudo().search_read(
|
||||
[('user_id', '=', uid)], limit=1)
|
||||
attendance = self.env['hr.attendance'].sudo().search_read(
|
||||
[('employee_id', '=', employee[0]['id'])],
|
||||
fields=['id', 'check_in', 'check_out', 'worked_hours'])
|
||||
attendance_line = []
|
||||
for line in attendance:
|
||||
if line['check_in'] and line['check_out']:
|
||||
val = {
|
||||
'id':line['id'],
|
||||
'date': line['check_in'].date(),
|
||||
'sign_in': line['check_in'].time().strftime('%H:%M'),
|
||||
'sign_out': line['check_out'].time().strftime('%H:%M'),
|
||||
'worked_hours': format_duration(line['worked_hours'])
|
||||
}
|
||||
attendance_line.append(val)
|
||||
leaves = self.env['hr.leave'].sudo().search_read(
|
||||
[('employee_id', '=', employee[0]['id'])],
|
||||
fields=['request_date_from', 'request_date_to', 'state',
|
||||
'holiday_status_id'])
|
||||
for line in leaves:
|
||||
line['type'] = line.pop('holiday_status_id')[1]
|
||||
if line['state'] == 'confirm':
|
||||
line['state'] = 'To Approve'
|
||||
line['color'] = 'orange'
|
||||
elif line['state'] == 'validate1':
|
||||
line['state'] = 'Second Approval'
|
||||
line['color'] = '#7CFC00'
|
||||
elif line['state'] == 'validate':
|
||||
line['state'] = 'Approved'
|
||||
line['color'] = 'green'
|
||||
elif line['state'] == 'cancel':
|
||||
line['state'] = 'Cancelled'
|
||||
line['color'] = 'red'
|
||||
else:
|
||||
line['state'] = 'Refused'
|
||||
line['color'] = 'red'
|
||||
expense =[]
|
||||
# self.env['hr.expense'].sudo().search_read(
|
||||
# [('employee_id', '=', employee[0]['id'])],
|
||||
# fields=['name', 'date', 'state', 'total_amount'])
|
||||
# for line in expense:
|
||||
# if line['state'] == 'draft':
|
||||
# line['state'] = 'To Report'
|
||||
# line['color'] = '#17A2B8'
|
||||
# elif line['state'] == 'reported':
|
||||
# line['state'] = 'To Submit'
|
||||
# line['color'] = '#17A2B8'
|
||||
# elif line['state'] == 'submitted':
|
||||
# line['state'] = 'Submitted'
|
||||
# line['color'] = '#FFAC00'
|
||||
# elif line['state'] == 'approved':
|
||||
# line['state'] = 'Approved'
|
||||
# line['color'] = '#28A745'
|
||||
# elif line['state'] == 'done':
|
||||
# line['state'] = 'Done'
|
||||
# line['color'] = '#28A745'
|
||||
# else:
|
||||
# line['state'] = 'Refused'
|
||||
# line['color'] = 'red'
|
||||
leaves_to_approve = self.env['hr.leave'].sudo().search_count(
|
||||
[('state', 'in', ['confirm', 'validate1'])])
|
||||
today = datetime.strftime(datetime.today(), '%Y-%m-%d')
|
||||
query = """
|
||||
select count(id)
|
||||
from hr_leave
|
||||
WHERE (hr_leave.date_from::DATE,hr_leave.date_to::DATE)
|
||||
OVERLAPS ('%s', '%s') and
|
||||
state='validate'""" % (today, today)
|
||||
cr = self._cr
|
||||
cr.execute(query)
|
||||
leaves_today = cr.fetchall()
|
||||
first_day = date.today().replace(day=1)
|
||||
last_day = (date.today() + relativedelta(months=1, day=1)) - timedelta(
|
||||
1)
|
||||
query = """
|
||||
select count(id)
|
||||
from hr_leave
|
||||
WHERE (hr_leave.date_from::DATE,hr_leave.date_to::DATE)
|
||||
OVERLAPS ('%s', '%s')
|
||||
and state='validate'""" % (first_day, last_day)
|
||||
cr = self._cr
|
||||
cr.execute(query)
|
||||
leaves_this_month = cr.fetchall()
|
||||
leaves_alloc_req = self.env['hr.leave.allocation'].sudo().search_count(
|
||||
[('state', 'in', ['confirm', 'validate1'])])
|
||||
timesheet_count = self.env['account.analytic.line'].sudo().search_count(
|
||||
[('project_id', '!=', False), ('user_id', '=', uid)])
|
||||
timesheet_view_id = self.env.ref(
|
||||
'hr_timesheet.hr_timesheet_line_search')
|
||||
job_applications = self.env['hr.applicant'].sudo().search_count([])
|
||||
if employee:
|
||||
# sql = """select broad_factor from hr_employee_broad_factor
|
||||
# where id =%s"""
|
||||
# self.env.cr.execute(sql, (employee[0]['id'],))
|
||||
# result = self.env.cr.dictfetchall()
|
||||
# broad_factor = result[0]['broad_factor'] if result[0][
|
||||
# 'broad_factor'] else False
|
||||
broad_factor = False
|
||||
if employee[0]['birthday']:
|
||||
diff = relativedelta(datetime.today(), employee[0]['birthday'])
|
||||
age = diff.years
|
||||
else:
|
||||
age = False
|
||||
if employee[0]['joining_date']:
|
||||
diff = relativedelta(datetime.today(),
|
||||
employee[0]['joining_date'])
|
||||
years = diff.years
|
||||
months = diff.months
|
||||
days = diff.days
|
||||
experience = '{} years {} months {} days'.format(years, months,
|
||||
days)
|
||||
else:
|
||||
experience = False
|
||||
if employee:
|
||||
data = {
|
||||
'broad_factor': broad_factor if broad_factor else 0,
|
||||
'leaves_to_approve': leaves_to_approve,
|
||||
'leaves_today': leaves_today,
|
||||
'leaves_this_month': leaves_this_month,
|
||||
'leaves_alloc_req': leaves_alloc_req,
|
||||
'emp_timesheets': timesheet_count,
|
||||
'job_applications': job_applications,
|
||||
'timesheet_view_id': timesheet_view_id,
|
||||
'experience': experience,
|
||||
'age': age,
|
||||
'attendance_lines': attendance_line,
|
||||
'leave_lines': leaves,
|
||||
'expense_lines': expense
|
||||
}
|
||||
employee[0].update(data)
|
||||
return employee
|
||||
else:
|
||||
return False
|
||||
"""To fetch the details of employee"""
|
||||
return self.env["hr.employee"].sudo().search_read(
|
||||
[("user_id", "=", uid)],
|
||||
[
|
||||
'name',
|
||||
'image_1920',
|
||||
'job_id',
|
||||
'employee_id',
|
||||
'current_company_exp',
|
||||
'doj',
|
||||
'birthday',
|
||||
'mobile_phone',
|
||||
'work_email',
|
||||
'private_street',
|
||||
'attendance_state',
|
||||
'id'
|
||||
])
|
||||
|
|
@ -108,13 +108,12 @@ export class NetflixProfileContainer extends Component {
|
|||
|
||||
try {
|
||||
|
||||
const employeeData = await this.orm.searchRead("hr.employee", [["user_id", "=", this.props.action.context.user_id]], ['name', 'image_1920','job_id','employee_id','current_company_exp','doj','birthday','mobile_phone','work_email','private_street','attendance_state' ,'id'
|
||||
] );
|
||||
const employeeData = await this.orm.call(
|
||||
"hr.employee",'get_user_employee_details');
|
||||
const attendanceLines = await this.orm.searchRead(
|
||||
'hr.attendance',
|
||||
[['employee_id', '=', employeeData[0].id]],
|
||||
['create_date', 'check_in', 'check_out', 'worked_hours']
|
||||
);
|
||||
['create_date', 'check_in', 'check_out', 'worked_hours']);
|
||||
const Leaves = await this.orm.searchRead('hr.leave',[['employee_id', '=', employeeData[0].id]],
|
||||
['request_date_from', 'request_date_to', 'state','holiday_status_id']);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
|
@ -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',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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&model=hr.job&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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import hr_recruitment
|
||||
from . import stages
|
||||
from . import hr_applicant
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 > 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 > 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 > 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class hrLeaveAccrualLevel(models.Model):
|
|||
('monthly', 'Monthly'),
|
||||
('yearly', 'Yearly'),
|
||||
], default='daily', required=True, string="Frequency")
|
||||
emp_type = fields.Many2one('hr.contract.type', "Employee Type", tracking=True)
|
||||
emp_type = fields.Many2many('hr.contract.type', string="Employee Type", tracking=True)
|
||||
|
||||
|
||||
max_start_count = fields.Integer(
|
||||
|
|
@ -98,16 +98,17 @@ class hrTimeoffAllocation(models.Model):
|
|||
|
||||
for level in level_ids:
|
||||
# Calculate the current frequency
|
||||
run_allocation = self._handel_weekly_frequency(level)
|
||||
if run_allocation:
|
||||
if level.emp_type:
|
||||
level_filtered_employees = employees.filtered(lambda emp: emp.emp_type == level.emp_type)
|
||||
else:
|
||||
level_filtered_employees = employees
|
||||
qualified_employees = level_filtered_employees.filtered(lambda emp: self._emp_filter_by_level(emp, level))
|
||||
|
||||
# After filtering, we create the leave allocation for each employee
|
||||
for emp in qualified_employees:
|
||||
if level.emp_type:
|
||||
level_filtered_employees = employees.filtered(lambda emp: emp.emp_type.id in level.emp_type.ids)
|
||||
else:
|
||||
level_filtered_employees = employees
|
||||
qualified_employees = level_filtered_employees.filtered(lambda emp: self._emp_filter_by_level(emp, level))
|
||||
|
||||
# After filtering, we create the leave allocation for each employee
|
||||
for emp in qualified_employees:
|
||||
run_allocation = self._handel_weekly_frequency(level,emp)
|
||||
if run_allocation:
|
||||
allocations = self.env['hr.leave.allocation'].sudo().search([('employee_id','=',emp.id),('holiday_status_id','=',accrual.time_off_type_id.id),('state','=','validate')])
|
||||
leaves = self.env['hr.leave'].sudo().search([('employee_id','=',emp.id),('holiday_status_id','=',accrual.time_off_type_id.id),('state','not in',['draft','refuse','cancel'])])
|
||||
emp_leave_balance = sum(allocation.number_of_days for allocation in allocations) - sum(leave.number_of_days for leave in leaves)
|
||||
|
|
@ -115,7 +116,7 @@ class hrTimeoffAllocation(models.Model):
|
|||
continue
|
||||
self._create_leave_allocation(emp, level, accrual)
|
||||
|
||||
def _handel_weekly_frequency(self,level):
|
||||
def _handel_weekly_frequency(self,level,emp):
|
||||
today_date = datetime.today().date()
|
||||
if level.level_frequency == 'weekly':
|
||||
weekday_map = {
|
||||
|
|
@ -125,14 +126,13 @@ class hrTimeoffAllocation(models.Model):
|
|||
elif level.level_frequency == 'daily':
|
||||
return True
|
||||
elif level.level_frequency == 'monthly':
|
||||
return True if level.first_day_display == str(today_date.day) else False
|
||||
return True if level.first_day_display == str(today_date.day) or (emp.doj and ((emp.doj + timedelta(days=2)) == today_date)) else False
|
||||
elif level.level_frequency == 'yearly':
|
||||
month_map = {
|
||||
'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
|
||||
'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12
|
||||
}
|
||||
return True if level.first_day_display == str(today_date.day) and today_date.month == month_map.get(level.yearly_month) else False
|
||||
|
||||
return True if (level.first_day_display == str(today_date.day) and today_date.month == month_map.get(level.yearly_month)) or (emp.doj and ((emp.doj + timedelta(days=2)) == today_date)) else False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
|
@ -140,11 +140,29 @@ class hrTimeoffAllocation(models.Model):
|
|||
"""
|
||||
Create leave allocation for a qualified employee based on the accrual level and added value.
|
||||
"""
|
||||
today_date = datetime.today().date()
|
||||
number_of_days = level.added_value
|
||||
if employee.doj and ((employee.doj + timedelta(days=2)) == today_date):
|
||||
if level.level_frequency == 'monthly':
|
||||
if employee.doj.day <= 10:
|
||||
number_of_days = level.added_value
|
||||
else:
|
||||
number_of_days = level.added_value/2
|
||||
elif level.level_frequency == 'yearly':
|
||||
start_month = int(level.yearly_month)
|
||||
joining_month = employee.doj.month
|
||||
# Compute remaining months in the allocation cycle
|
||||
remaining_months = (start_month - joining_month) % 12 or 12
|
||||
# Calculate proportional leaves
|
||||
number_of_days = (level.added_value / 12) * remaining_months
|
||||
if employee.doj.day > 10:
|
||||
number_of_days = number_of_days - ((level.added_value / 12)/2)
|
||||
|
||||
self.env['hr.leave.allocation'].sudo().create({
|
||||
'employee_id': employee.id,
|
||||
'holiday_status_id': accrual.time_off_type_id.id,
|
||||
'date_from': fields.Date.today(),
|
||||
'number_of_days': level.added_value,
|
||||
'number_of_days': number_of_days,
|
||||
'allocation_type': 'auto_allocation'
|
||||
}).action_approve()
|
||||
|
||||
|
|
@ -238,10 +256,40 @@ class HRLeave(models.Model):
|
|||
"\nThe status is 'Approved', when time off request is approved by manager." +
|
||||
"\nThe status is 'Cancelled', when time off request is cancelled.")
|
||||
|
||||
|
||||
def _check_validity(self):
|
||||
for rec in self:
|
||||
if rec.holiday_status_id.limit_leave_requests:
|
||||
if rec.holiday_status_id.limit_request_count and rec.holiday_status_id.limit_request_type and rec.holiday_status_id.limit_emp_type and rec.holiday_status_id.limit_request_count >= 0:
|
||||
if rec.employee_id.id in rec.holiday_status_id.limit_emp_type.ids:
|
||||
time_frame = {
|
||||
'week': timedelta(weeks=1),
|
||||
'month': timedelta(days=30),
|
||||
'year': timedelta(days=365),
|
||||
}.get(rec.holiday_status_id.limit_request_type, timedelta(weeks=1)) # Default to 1 week
|
||||
|
||||
restriction_start_date = datetime.now() - time_frame
|
||||
|
||||
# Count the leave requests made by the employee within the restriction period
|
||||
leave_count = self.env['hr.leave'].search_count([
|
||||
('employee_id', '=', rec.employee_id.id),
|
||||
('state', 'not in', ['cancel', 'refuse', 'draft']), # Adjust states if needed
|
||||
('holiday_status_id', '=', rec.holiday_status_id.id),
|
||||
('request_date_from', '>=', restriction_start_date),
|
||||
('id','!=',rec.id)
|
||||
])
|
||||
if leave_count >= rec.holiday_status_id.limit_request_count:
|
||||
raise ValidationError(_(
|
||||
"You have exceeded the maximum allowed leave requests (%s) for the selected period (%s)."
|
||||
) % (rec.holiday_status_id.limit_request_count, rec.holiday_status_id.limit_request_type))
|
||||
|
||||
return super(HRLeave, self)._check_validity()
|
||||
|
||||
def action_draft(self):
|
||||
for rec in self:
|
||||
if rec.employee_id.user_id.id != self.env.user.id:
|
||||
raise ValidationError(_("Only employee can submit his own leave"))
|
||||
|
||||
self._check_validity()
|
||||
rec.state = 'confirm'
|
||||
|
||||
|
|
@ -284,4 +332,17 @@ class HRLeaveType(models.Model):
|
|||
request_unit_type = fields.Selection([
|
||||
('day', 'Day'),
|
||||
('half_day', 'Half Day')], default='day', string='Take Time Off in', required=True)
|
||||
request_unit = fields.Selection(related="request_unit_type",store=True)
|
||||
request_unit = fields.Selection(related="request_unit_type",store=True)
|
||||
|
||||
limit_leave_requests = fields.Boolean(string='Limit Leave Requests', default=False)
|
||||
limit_request_count = fields.Integer(
|
||||
"limit Count",
|
||||
help="Defines the minimum number of leave requests after which the restriction will apply. For example, set 1 to start restrictions after the first request.",
|
||||
default="1")
|
||||
limit_request_type = fields.Selection(
|
||||
[('week', 'Week'),
|
||||
('month', 'Month'),
|
||||
('year', 'Year')],
|
||||
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")
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
<xpath expr="//div[@name='carryover']" position="after">
|
||||
<field name="time_off_type_id" required="1"/>
|
||||
<div>
|
||||
<strong>Allocation Starts</strong>
|
||||
<strong>Allocation Starts Running </strong>
|
||||
<field name="accrual_start_count" style="width: 2rem"/>
|
||||
<field name="accrual_start_type" style="width: 4.75rem"/>
|
||||
<strong>after employee joining date</strong>
|
||||
|
|
@ -27,96 +27,124 @@
|
|||
</xpath>
|
||||
<xpath expr="//field[@name='level_ids']/kanban" position="replace">
|
||||
<kanban default_order="sequence">
|
||||
<field name="sequence"/>
|
||||
<field name="action_with_unused_accruals"/>
|
||||
<field name="accrual_validity_count"/>
|
||||
<field name="accrual_validity_type"/>
|
||||
<field name="cap_accrued_time"/>
|
||||
<field name="accrual_validity"/>
|
||||
<templates>
|
||||
<div t-name="card" class="bg-transparent border-0">
|
||||
<div class="o_hr_holidays_body">
|
||||
<div class="o_hr_holidays_timeline text-center">
|
||||
<t t-if="record.start_count.raw_value > 0">
|
||||
Experience between <field name="start_count"/> <field name="start_type"/> and <field name="max_start_count"/> <field name="max_start_type"/>
|
||||
<field name="sequence"/>
|
||||
<field name="action_with_unused_accruals"/>
|
||||
<field name="accrual_validity_count"/>
|
||||
<field name="accrual_validity_type"/>
|
||||
<field name="cap_accrued_time"/>
|
||||
<field name="accrual_validity"/>
|
||||
<templates>
|
||||
<div t-name="card" class="bg-transparent border-0">
|
||||
<div class="o_hr_holidays_body">
|
||||
<div class="o_hr_holidays_timeline text-center">
|
||||
<t t-if="record.start_count.raw_value > 0">
|
||||
Experience between
|
||||
<field name="start_count"/>
|
||||
<field name="start_type"/>
|
||||
and
|
||||
<field name="max_start_count"/>
|
||||
<field name="max_start_type"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
initially
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="!read_only_mode">
|
||||
<a type="edit" t-attf-class="oe_kanban_action text-black">
|
||||
<t t-call="level_content"/>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="level_content"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<t t-name="level_content">
|
||||
<div class="o_hr_holidays_card">
|
||||
<div class="content container" style="width: 560px;">
|
||||
<div class="row w-100">
|
||||
<div class="pe-0 me-0" style="width: 6rem;">
|
||||
<field name="added_value" invisible="1"/>
|
||||
<span t-out="record.added_value.raw_value"/>
|
||||
<field name="added_value_type"/>,
|
||||
</div>
|
||||
<div class="col-auto m-0 p-0">
|
||||
<field name="level_frequency" class="ms-1"/>
|
||||
<t t-if="record.level_frequency.raw_value === 'weekly'">
|
||||
on
|
||||
<field name="week_day"/>
|
||||
</t>
|
||||
<t t-elif="record.level_frequency.raw_value === 'monthly'">
|
||||
on the
|
||||
<field name="first_day"/>
|
||||
day of the month
|
||||
</t>
|
||||
<t t-elif="record.level_frequency.raw_value === 'bimonthly'">
|
||||
on the
|
||||
<field name="first_day"/>
|
||||
and on the
|
||||
<field name="second_day"/>
|
||||
days of the months
|
||||
</t>
|
||||
<t t-elif="record.level_frequency.raw_value === 'biyearly'">
|
||||
on the
|
||||
<field name="first_month_day"/>
|
||||
<field name="first_month"/>
|
||||
and on the
|
||||
<field name="second_month_day"/>
|
||||
<field name="second_month"/>
|
||||
</t>
|
||||
<t t-elif="record.level_frequency.raw_value === 'yearly'">
|
||||
on
|
||||
<field name="yearly_day"/>
|
||||
<field name="yearly_month"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-muted">
|
||||
<div class="pe-0 me-0" style="width: 6rem;">
|
||||
Cap:
|
||||
</div>
|
||||
<div class="col-3 m-0 ps-1 d-flex">
|
||||
<t t-if="record.cap_accrued_time.raw_value and record.maximum_leave.raw_value > 0">
|
||||
<field name="maximum_leave" widget="float_without_trailing_zeros"/>
|
||||
<field class="ms-1" name="added_value_type"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
initially
|
||||
Unlimited
|
||||
</t>
|
||||
</div>
|
||||
<t t-if="!read_only_mode">
|
||||
<a type="edit" t-attf-class="oe_kanban_action text-black">
|
||||
<t t-call="level_content"/>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="level_content"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<t t-name="level_content">
|
||||
<div class="o_hr_holidays_card">
|
||||
<div class="content container" style="width: 560px;">
|
||||
<div class="row w-100">
|
||||
<div class="pe-0 me-0" style="width: 6rem;">
|
||||
<field name="added_value" invisible="1"/>
|
||||
<span t-out="record.added_value.raw_value"/> <field name="added_value_type"/>,
|
||||
</div>
|
||||
<div class="col-auto m-0 p-0">
|
||||
<field name="level_frequency" class="ms-1"/>
|
||||
<t t-if="record.level_frequency.raw_value === 'weekly'">
|
||||
on <field name="week_day"/>
|
||||
</t>
|
||||
<t t-elif="record.level_frequency.raw_value === 'monthly'">
|
||||
on the <field name="first_day"/> day of the month
|
||||
</t>
|
||||
<t t-elif="record.level_frequency.raw_value === 'bimonthly'">
|
||||
on the <field name="first_day"/> and on the <field name="second_day"/> days of the months
|
||||
</t>
|
||||
<t t-elif="record.level_frequency.raw_value === 'biyearly'">
|
||||
on the <field name="first_month_day"/> <field name="first_month"/> and on the <field name="second_month_day"/> <field name="second_month"/>
|
||||
</t>
|
||||
<t t-elif="record.level_frequency.raw_value === 'yearly'">
|
||||
on <field name="yearly_day"/> <field name="yearly_month"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-muted">
|
||||
<div class="pe-0 me-0" style="width: 6rem;">
|
||||
Cap:
|
||||
</div>
|
||||
<div class="col-3 m-0 ps-1 d-flex">
|
||||
<t t-if="record.cap_accrued_time.raw_value and record.maximum_leave.raw_value > 0">
|
||||
<field name="maximum_leave" widget="float_without_trailing_zeros"/> <field class="ms-1" name="added_value_type"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Unlimited
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-muted" invisible="1">
|
||||
<div class="pe-0 me-0" style="width: 6rem;">
|
||||
Carry over:
|
||||
</div>
|
||||
<div class="col-6 m-0 ps-1">
|
||||
<t t-if="record.action_with_unused_accruals.raw_value === 'all'">all
|
||||
<span invisible="not accrual_validity">
|
||||
- Valid for <field name="accrual_validity_count"/> <field name="accrual_validity_type"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="record.action_with_unused_accruals.raw_value === 'maximum'">up to <field name="postpone_max_days"/> <t t-esc="record.added_value_type.raw_value"/>
|
||||
<span invisible="not accrual_validity">
|
||||
- Valid for <field name="accrual_validity_count"/> <field name="accrual_validity_type"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">no</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-muted" invisible="1">
|
||||
<div class="pe-0 me-0" style="width: 6rem;">
|
||||
Carry over:
|
||||
</div>
|
||||
<div class="col-6 m-0 ps-1">
|
||||
<t t-if="record.action_with_unused_accruals.raw_value === 'all'">all
|
||||
<span invisible="not accrual_validity">
|
||||
- Valid for
|
||||
<field name="accrual_validity_count"/>
|
||||
<field name="accrual_validity_type"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-elif="record.action_with_unused_accruals.raw_value === 'maximum'">
|
||||
up to
|
||||
<field name="postpone_max_days"/>
|
||||
<t t-esc="record.added_value_type.raw_value"/>
|
||||
<span invisible="not accrual_validity">
|
||||
- Valid for
|
||||
<field name="accrual_validity_count"/>
|
||||
<field name="accrual_validity_type"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">no</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
@ -174,15 +202,17 @@
|
|||
<field name="inherit_id" ref="hr_holidays.hr_accrual_level_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='accrue']" position="replace">
|
||||
<group name="accrue" col="1" width="800px">
|
||||
<group name="accrue" col="1" width="800px">
|
||||
<div class="o_td_label">
|
||||
<label for="added_value" string="Employee accrue"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="accrued_gain_time" invisible="1"/>
|
||||
<field name="can_modify_value_type" invisible="1"/>
|
||||
<field name="added_value" widget="float_without_trailing_zeros" style="width: 4rem" class="me-1"/>
|
||||
<field name="added_value_type" style="width: 3.4rem" nolabel="1" readonly="not can_modify_value_type"/>
|
||||
<field name="added_value" widget="float_without_trailing_zeros" style="width: 4rem"
|
||||
class="me-1"/>
|
||||
<field name="added_value_type" style="width: 3.4rem" nolabel="1"
|
||||
readonly="not can_modify_value_type"/>
|
||||
</div>
|
||||
<div style="width: 5rem"/>
|
||||
<div name="daily" invisible="level_frequency != 'daily'">
|
||||
|
|
@ -190,18 +220,22 @@
|
|||
</div>
|
||||
<div name="weekly" invisible="level_frequency != 'weekly'">
|
||||
<field name="level_frequency" style="width: 4.5rem;"/>
|
||||
<label for="week_day" string="on" class="me-1"/><field name="week_day" style="width: 6.6rem"/>
|
||||
<label for="week_day" string="on" class="me-1"/>
|
||||
<field name="week_day" style="width: 6.6rem"/>
|
||||
</div>
|
||||
<div name="monthly" invisible="level_frequency != 'monthly'">
|
||||
<field name="level_frequency" style="width: 4.5rem"/>
|
||||
<label for="first_day_display" string="on the" class="me-1"/>
|
||||
<field name="first_day_display" required="1" style="width: 4rem"/>
|
||||
of the month
|
||||
of the month (or 2 days after employee joining date)
|
||||
</div>
|
||||
<div name="yearly" invisible="level_frequency != 'yearly'">
|
||||
<field name="level_frequency" style="width: 4rem"/>
|
||||
<label for="yearly_day_display" string="on the" class="me-1"/>
|
||||
<field name="yearly_day_display" required="1" style="width: 4rem"/> of <field name="yearly_month" required="1" style="width: 5.4rem"/>
|
||||
<field name="yearly_day_display" required="1" style="width: 4rem"/>
|
||||
of
|
||||
<field name="yearly_month" required="1" style="width: 5.4rem"/>
|
||||
(or 2 days after employee joining date)
|
||||
</div>
|
||||
</group>
|
||||
</xpath>
|
||||
|
|
@ -211,22 +245,27 @@
|
|||
<label for="start_count" string="Employee total Experience"/>
|
||||
</div>
|
||||
<div>Min
|
||||
<field name="start_count" style="width: 2rem" required="max_start_count > 0 or max_start_type != False"/>
|
||||
<field name="start_type" style="width: 4.75rem" required="max_start_count > 0 or max_start_type != False"/>
|
||||
<field name="start_count" style="width: 2rem"
|
||||
required="max_start_count > 0 or max_start_type != False"/>
|
||||
<field name="start_type" style="width: 4.75rem"
|
||||
required="max_start_count > 0 or max_start_type != False"/>
|
||||
&
|
||||
Max of
|
||||
<field name="max_start_count" style="width: 2rem" required="start_count > 0 or start_type != False"/>
|
||||
<field name="max_start_type" style="width: 4.75rem" required="start_count > 0 or start_type != False"/>
|
||||
<field name="max_start_count" style="width: 2rem"
|
||||
required="start_count > 0 or start_type != False"/>
|
||||
<field name="max_start_type" style="width: 4.75rem"
|
||||
required="start_count > 0 or start_type != False"/>
|
||||
|
||||
Experience is required
|
||||
</div>
|
||||
</group>
|
||||
<group name="emp_types">
|
||||
<div class="o_td_label">
|
||||
<label for="emp_type" string="Employee Type"/>
|
||||
<label for="emp_type" string="Employee Type" widget="many2many_tags"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="emp_type" style="width: 4" required="max_start_count > 0 or max_start_type != False"/>
|
||||
<field name="emp_type" style="width: 4" widget="many2many_tags"
|
||||
required="1"/>
|
||||
</div>
|
||||
</group>
|
||||
|
||||
|
|
@ -258,6 +297,15 @@
|
|||
</xpath>
|
||||
<xpath expr="//field[@name='request_unit']" position="after">
|
||||
<field name="request_unit_type"/>
|
||||
<field name="limit_leave_requests"/>
|
||||
<div invisible="not limit_leave_requests">
|
||||
<strong>Eligible to apply upto </strong>
|
||||
<field name="limit_request_count" style="width: 2rem" required="limit_leave_requests == True" invisible="not limit_leave_requests"/>
|
||||
<strong> Leaves Per </strong>
|
||||
<field name="limit_request_type" style="width: 4.75rem" required="limit_leave_requests == True" invisible="not limit_leave_requests"/>
|
||||
<strong> during</strong>
|
||||
<field name="limit_emp_type" widget="many2many_tags" required="limit_leave_requests == True" invisible="not limit_leave_requests"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -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'
|
||||
],
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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&model=recruitment.requisition&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&model=recruitment.requisition&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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import hr_requisition
|
||||
from . import res_config_settings
|
||||
|
|
@ -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)
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 & 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import recruitment_cancel_wizard
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue