Recruitment & Attendance module changes
This commit is contained in:
parent
96e99017c8
commit
be91a849f4
|
|
@ -4,11 +4,12 @@
|
||||||
'category': 'Human Resources',
|
'category': 'Human Resources',
|
||||||
'summary': 'Manage and update weekly timesheets for CWF department',
|
'summary': 'Manage and update weekly timesheets for CWF department',
|
||||||
'author': 'Your Name or Company',
|
'author': 'Your Name or Company',
|
||||||
'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard'],
|
'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard','hr_employee_extended'],
|
||||||
'data': [
|
'data': [
|
||||||
# 'views/timesheet_form.xml',
|
# 'views/timesheet_form.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'views/timesheet_view.xml',
|
'views/timesheet_view.xml',
|
||||||
|
'views/timesheet_weekly_view.xml',
|
||||||
'data/email_template.xml',
|
'data/email_template.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<odoo>
|
<odoo>
|
||||||
<data noupdate="1">
|
<data noupdate="0">
|
||||||
<record id="email_template_timesheet_update" model="mail.template">
|
<record id="email_template_timesheet_update" model="mail.template">
|
||||||
<field name="name">Timesheet Update Reminder</field>
|
<field name="name">Timesheet Update Reminder</field>
|
||||||
<field name="email_from">${(user.email or '')}</field>
|
<field name="email_from">${(user.email or '')}</field>
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,72 @@
|
||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
from odoo.exceptions import ValidationError, UserError
|
from odoo.exceptions import ValidationError, UserError
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import datetime as dt
|
||||||
from odoo import _
|
from odoo import _
|
||||||
|
|
||||||
|
class CwfTimesheetYearly(models.Model):
|
||||||
|
_name = 'cwf.timesheet.calendar'
|
||||||
|
_description = "CWF Timesheet Calendar"
|
||||||
|
_rec_name = 'name'
|
||||||
|
|
||||||
|
name = fields.Char(string='Year Name', required=True)
|
||||||
|
week_period = fields.One2many('cwf.timesheet','cwf_calendar_id')
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('unique_year', 'unique(name)', 'The year must be unique!')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@api.constrains('name')
|
||||||
|
def _check_year_format(self):
|
||||||
|
for record in self:
|
||||||
|
if not record.name.isdigit() or len(record.name) != 4:
|
||||||
|
raise ValidationError("Year Name must be a 4-digit number.")
|
||||||
|
|
||||||
|
def generate_week_period(self):
|
||||||
|
for record in self:
|
||||||
|
record.week_period.unlink()
|
||||||
|
year = int(record.name)
|
||||||
|
|
||||||
|
# Find the first Monday of the year
|
||||||
|
start_date = datetime(year, 1, 1)
|
||||||
|
while start_date.weekday() != 0: # Monday is 0 in weekday()
|
||||||
|
start_date += timedelta(days=1)
|
||||||
|
|
||||||
|
# Generate weeks from Monday to Sunday
|
||||||
|
while start_date.year == year or (start_date - timedelta(days=1)).year == year:
|
||||||
|
end_date = start_date + timedelta(days=6)
|
||||||
|
|
||||||
|
self.env['cwf.timesheet'].create({
|
||||||
|
'name': f'Week {start_date.strftime("%W")}, {year}',
|
||||||
|
'week_start_date': start_date.date(),
|
||||||
|
'week_end_date': end_date.date(),
|
||||||
|
'cwf_calendar_id': record.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
start_date += timedelta(days=7)
|
||||||
|
|
||||||
|
def action_generate_weeks(self):
|
||||||
|
self.generate_week_period()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'reload',
|
||||||
|
}
|
||||||
|
|
||||||
class CwfTimesheet(models.Model):
|
class CwfTimesheet(models.Model):
|
||||||
_name = 'cwf.timesheet'
|
_name = 'cwf.timesheet'
|
||||||
_description = 'CWF Weekly Timesheet'
|
_description = 'CWF Weekly Timesheet'
|
||||||
|
_rec_name = 'name'
|
||||||
|
|
||||||
name = fields.Char(string='Week Name', required=True)
|
name = fields.Char(string='Week Name', required=True)
|
||||||
department_id = fields.Many2one('hr.department', string='Department')
|
|
||||||
week_start_date = fields.Date(string='Week Start Date', required=True)
|
week_start_date = fields.Date(string='Week Start Date', required=True)
|
||||||
week_end_date = fields.Date(string='Week End Date', required=True)
|
week_end_date = fields.Date(string='Week End Date', required=True)
|
||||||
total_hours = fields.Float(string='Total Hours', required=True)
|
|
||||||
status = fields.Selection([
|
status = fields.Selection([
|
||||||
('draft', 'Draft'),
|
('draft', 'Draft'),
|
||||||
('submitted', 'Submitted')
|
('submitted', 'Submitted')
|
||||||
], default='draft', string='Status')
|
], default='draft', string='Status')
|
||||||
lines = fields.One2many('cwf.timesheet.line','week_id')
|
lines = fields.One2many('cwf.timesheet.line','week_id')
|
||||||
|
cwf_calendar_id = fields.Many2one('cwf.timesheet.calendar')
|
||||||
|
|
||||||
|
|
||||||
def send_timesheet_update_email(self):
|
def send_timesheet_update_email(self):
|
||||||
template = self.env.ref('cwf_timesheet.email_template_timesheet_update')
|
template = self.env.ref('cwf_timesheet.email_template_timesheet_update')
|
||||||
|
|
@ -31,11 +78,18 @@ class CwfTimesheet(models.Model):
|
||||||
raise UserError('The start date cannot be after the end date.')
|
raise UserError('The start date cannot be after the end date.')
|
||||||
|
|
||||||
# Get all employees in the department
|
# Get all employees in the department
|
||||||
employees = self.env['hr.employee'].search([('department_id', '=', self.department_id.id)])
|
external_group_id = self.env.ref("hr_employee_extended.group_external_user")
|
||||||
|
users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)])
|
||||||
|
employees = self.env['hr.employee'].search([('user_id', 'in', users.ids),'|',('doj','=',False),('doj','>=', self.week_start_date)])
|
||||||
|
print(employees)
|
||||||
# Loop through each day of the week and create timesheet lines for each employee
|
# Loop through each day of the week and create timesheet lines for each employee
|
||||||
while current_date <= end_date:
|
while current_date <= end_date:
|
||||||
for employee in employees:
|
for employee in employees:
|
||||||
|
existing_record = self.env['cwf.weekly.timesheet'].search([
|
||||||
|
('week_id', '=', self.id),
|
||||||
|
('employee_id', '=', employee.id)
|
||||||
|
], limit=1)
|
||||||
|
if not existing_record:
|
||||||
self.env['cwf.timesheet.line'].create({
|
self.env['cwf.timesheet.line'].create({
|
||||||
'week_id': self.id,
|
'week_id': self.id,
|
||||||
'employee_id': employee.id,
|
'employee_id': employee.id,
|
||||||
|
|
@ -44,27 +98,199 @@ class CwfTimesheet(models.Model):
|
||||||
current_date += timedelta(days=1)
|
current_date += timedelta(days=1)
|
||||||
self.status = 'submitted'
|
self.status = 'submitted'
|
||||||
for employee in employees:
|
for employee in employees:
|
||||||
|
weekly_timesheet_exists = self.env['cwf.weekly.timesheet'].sudo().search([('week_id','=',self.id),('employee_id','=',employee.id)])
|
||||||
|
|
||||||
|
if not weekly_timesheet_exists:
|
||||||
|
weekly_timesheet = self.env['cwf.weekly.timesheet'].sudo().create({
|
||||||
|
'week_id': self.id,
|
||||||
|
'employee_id': employee.id,
|
||||||
|
'status': 'draft'
|
||||||
|
})
|
||||||
|
|
||||||
|
# Generate the URL for the newly created weekly_timesheet
|
||||||
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||||
|
record_url = f"{base_url}/web#id={weekly_timesheet.id}&view_type=form&model=cwf.weekly.timesheet"
|
||||||
|
|
||||||
|
weekly_timesheet.update_attendance()
|
||||||
|
|
||||||
if employee.work_email:
|
if employee.work_email:
|
||||||
email_values = {
|
email_values = {
|
||||||
'email_to': employee.work_email,
|
'email_to': employee.work_email,
|
||||||
'body_html': template.body_html, # Email body from template
|
'body_html': template.body_html.replace(
|
||||||
|
'https://ftprotech.in/odoo/action-261',
|
||||||
|
record_url
|
||||||
|
), # Email body from template
|
||||||
'subject': 'Timesheet Update Notification',
|
'subject': 'Timesheet Update Notification',
|
||||||
}
|
}
|
||||||
|
|
||||||
template.send_mail(self.id, email_values=email_values, force_send=True)
|
template.send_mail(self.id, email_values=email_values, force_send=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CwfWeeklyTimesheet(models.Model):
|
||||||
|
_name = "cwf.weekly.timesheet"
|
||||||
|
_description = "CWF Weekly Timesheet"
|
||||||
|
_rec_name = 'employee_id'
|
||||||
|
|
||||||
|
week_id = fields.Many2one('cwf.timesheet', 'Week')
|
||||||
|
employee_id = fields.Many2one('hr.employee', default=lambda self: self.env.user.employee_id.id)
|
||||||
|
cwf_timesheet_lines = fields.One2many('cwf.timesheet.line' ,'weekly_timesheet')
|
||||||
|
status = fields.Selection([('draft','Draft'),('submitted','Submitted')], default='draft')
|
||||||
|
week_start_date = fields.Date(related='week_id.week_start_date')
|
||||||
|
week_end_date = fields.Date(related='week_id.week_end_date')
|
||||||
|
|
||||||
|
@api.constrains('week_id', 'employee_id')
|
||||||
|
def _check_unique_week_employee(self):
|
||||||
|
for record in self:
|
||||||
|
# Search for existing records with the same week_id and employee_id
|
||||||
|
existing_record = self.env['cwf.weekly.timesheet'].search([
|
||||||
|
('week_id', '=', record.week_id.id),
|
||||||
|
('employee_id', '=', record.employee_id.id)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
# If an existing record is found and it's not the current record (in case of update), raise an error
|
||||||
|
if existing_record and existing_record.id != record.id:
|
||||||
|
raise ValidationError("A timesheet for this employee already exists for the selected week.")
|
||||||
|
|
||||||
|
def update_attendance(self):
|
||||||
|
for rec in self:
|
||||||
|
# Get the week start and end date
|
||||||
|
week_start_date = rec.week_id.week_start_date
|
||||||
|
week_end_date = rec.week_id.week_end_date
|
||||||
|
|
||||||
|
# Convert start and end dates to datetime objects for proper filtering
|
||||||
|
week_start_datetime = datetime.combine(week_start_date, datetime.min.time())
|
||||||
|
week_end_datetime = datetime.combine(week_end_date, datetime.max.time())
|
||||||
|
|
||||||
|
# Delete timesheet lines that are outside the week range
|
||||||
|
rec.cwf_timesheet_lines.filtered(lambda line:
|
||||||
|
line.week_day < week_start_date or line.week_day > week_end_date
|
||||||
|
).unlink()
|
||||||
|
|
||||||
|
# Search for attendance records that fall within the week period and match the employee
|
||||||
|
hr_attendance_records = self.env['hr.attendance'].sudo().search([
|
||||||
|
('check_in', '>=', week_start_datetime),
|
||||||
|
('check_out', '<=', week_end_datetime),
|
||||||
|
('employee_id', '=', rec.employee_id.id)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Group the attendance records by date
|
||||||
|
attendance_by_date = {}
|
||||||
|
for attendance in hr_attendance_records:
|
||||||
|
attendance_date = attendance.check_in.date()
|
||||||
|
if attendance_date not in attendance_by_date:
|
||||||
|
attendance_by_date[attendance_date] = []
|
||||||
|
attendance_by_date[attendance_date].append(attendance)
|
||||||
|
|
||||||
|
# Get all the dates within the week period
|
||||||
|
all_week_dates = [week_start_date + timedelta(days=i) for i in
|
||||||
|
range((week_end_date - week_start_date).days + 1)]
|
||||||
|
|
||||||
|
# Create or update timesheet lines for each day in the week
|
||||||
|
for date in all_week_dates:
|
||||||
|
# Check if there is attendance for this date
|
||||||
|
if date in attendance_by_date:
|
||||||
|
# If there are multiple attendance records, take the earliest check_in and latest check_out
|
||||||
|
earliest_check_in = min(attendance.check_in for attendance in attendance_by_date[date])
|
||||||
|
latest_check_out = max(attendance.check_out for attendance in attendance_by_date[date])
|
||||||
|
|
||||||
|
if (earliest_check_in + timedelta(hours=5, minutes=30)).date() > date:
|
||||||
|
earliest_check_in = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
|
||||||
|
|
||||||
|
if (latest_check_out + timedelta(hours=5, minutes=30)).date() > date:
|
||||||
|
latest_check_out = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
|
||||||
|
|
||||||
|
# Check if a timesheet line for this employee, week, and date already exists
|
||||||
|
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
|
||||||
|
('week_day', '=', date),
|
||||||
|
('employee_id', '=', rec.employee_id.id),
|
||||||
|
('week_id', '=', rec.week_id.id),
|
||||||
|
('weekly_timesheet', '=', rec.id)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if existing_timesheet_line:
|
||||||
|
# If it exists, update the existing record
|
||||||
|
existing_timesheet_line.write({
|
||||||
|
'check_in_date': earliest_check_in,
|
||||||
|
'check_out_date': latest_check_out,
|
||||||
|
'state_type': 'present',
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# If it doesn't exist, create a new timesheet line with present state_type
|
||||||
|
self.env['cwf.timesheet.line'].create({
|
||||||
|
'weekly_timesheet': rec.id,
|
||||||
|
'employee_id': rec.employee_id.id,
|
||||||
|
'week_id': rec.week_id.id,
|
||||||
|
'week_day': date,
|
||||||
|
'check_in_date': earliest_check_in,
|
||||||
|
'check_out_date': latest_check_out,
|
||||||
|
'state_type': 'present',
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
# If no attendance exists for this date, create a new timesheet line with time_off state_type
|
||||||
|
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
|
||||||
|
('week_day', '=', date),
|
||||||
|
('employee_id', '=', rec.employee_id.id),
|
||||||
|
('week_id', '=', rec.week_id.id),
|
||||||
|
('weekly_timesheet', '=', rec.id)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if not existing_timesheet_line:
|
||||||
|
if date.weekday() != 5 and date.weekday() != 6:
|
||||||
|
# If no record exists for this date, create a new timesheet line with time_off state_type
|
||||||
|
self.env['cwf.timesheet.line'].create({
|
||||||
|
'weekly_timesheet': rec.id,
|
||||||
|
'employee_id': rec.employee_id.id,
|
||||||
|
'week_id': rec.week_id.id,
|
||||||
|
'week_day': date,
|
||||||
|
'state_type': 'time_off',
|
||||||
|
})
|
||||||
|
|
||||||
|
def action_submit(self):
|
||||||
|
for rec in self:
|
||||||
|
for timesheet in rec.cwf_timesheet_lines:
|
||||||
|
timesheet.action_submit()
|
||||||
|
rec.status = 'submitted'
|
||||||
|
|
||||||
class CwfTimesheetLine(models.Model):
|
class CwfTimesheetLine(models.Model):
|
||||||
_name = 'cwf.timesheet.line'
|
_name = 'cwf.timesheet.line'
|
||||||
_description = 'CWF Weekly Timesheet Lines'
|
_description = 'CWF Weekly Timesheet Lines'
|
||||||
|
_rec_name = 'employee_id'
|
||||||
|
|
||||||
employee_id = fields.Many2one('hr.employee', string='Employee')
|
weekly_timesheet = fields.Many2one('cwf.weekly.timesheet')
|
||||||
week_id = fields.Many2one('cwf.timesheet', 'Week')
|
employee_id = fields.Many2one('hr.employee', string='Employee', related='weekly_timesheet.employee_id')
|
||||||
|
week_id = fields.Many2one('cwf.timesheet', 'Week', related='weekly_timesheet.week_id')
|
||||||
week_day = fields.Date(string='Date')
|
week_day = fields.Date(string='Date')
|
||||||
check_in_date = fields.Datetime(string='Checkin')
|
check_in_date = fields.Datetime(string='Checkin')
|
||||||
check_out_date = fields.Datetime(string='Checkout ')
|
check_out_date = fields.Datetime(string='Checkout ')
|
||||||
is_updated = fields.Boolean('Attendance Updated')
|
is_updated = fields.Boolean('Attendance Updated')
|
||||||
state_type = fields.Selection([('draft','Draft'),('holiday', 'Holiday'),('time_off','Time Off'),('present','Present')], default='draft')
|
state_type = fields.Selection([('draft','Draft'),('holiday', 'Holiday'),('time_off','Time Off'),('half_day','Half Day'),('present','Present')], default='draft', required=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@api.constrains('week_day', 'check_in_date', 'check_out_date')
|
||||||
|
def _check_week_day_and_times(self):
|
||||||
|
for record in self:
|
||||||
|
# Ensure week_day is within the week range
|
||||||
|
if record.week_id:
|
||||||
|
if record.week_day < record.week_id.week_start_date or record.week_day > record.week_id.week_end_date:
|
||||||
|
raise ValidationError(
|
||||||
|
"The selected 'week_day' must be within the range of the week from %s to %s." %
|
||||||
|
(record.week_id.week_start_date, record.week_id.week_end_date)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure check_in_date and check_out_date are on the selected week_day
|
||||||
|
if record.check_in_date:
|
||||||
|
if record.check_in_date.date() != record.week_day:
|
||||||
|
raise ValidationError(
|
||||||
|
"The 'check_in_date' must be on the selected Date."
|
||||||
|
)
|
||||||
|
|
||||||
|
if record.check_out_date:
|
||||||
|
if record.check_out_date.date() != record.week_day:
|
||||||
|
raise ValidationError(
|
||||||
|
"The 'check_out_date' must be on the selected Date."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def action_submit(self):
|
def action_submit(self):
|
||||||
if self.state_type == 'draft' or not self.state_type:
|
if self.state_type == 'draft' or not self.state_type:
|
||||||
|
|
@ -77,9 +303,23 @@ class CwfTimesheetLine(models.Model):
|
||||||
def _update_attendance(self):
|
def _update_attendance(self):
|
||||||
attendance_obj = self.env['hr.attendance']
|
attendance_obj = self.env['hr.attendance']
|
||||||
for record in self:
|
for record in self:
|
||||||
|
if record.check_in_date != False and record.check_out_date != False and record.employee_id:
|
||||||
|
first_check_in = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
|
||||||
|
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
|
||||||
|
limit=1, order="check_in")
|
||||||
|
last_check_out = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
|
||||||
|
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
|
||||||
|
limit=1, order="check_out desc")
|
||||||
|
|
||||||
|
if first_check_in or last_check_out:
|
||||||
|
if first_check_in.sudo().check_in != record.check_in_date:
|
||||||
|
first_check_in.sudo().check_in = record.check_in_date
|
||||||
|
if last_check_out.sudo().check_out != record.check_out_date:
|
||||||
|
last_check_out.sudo().check_out = record.check_out_date
|
||||||
|
else:
|
||||||
attendance_obj.sudo().create({
|
attendance_obj.sudo().create({
|
||||||
'employee_id': record.employee_id.id,
|
'employee_id': record.employee_id.id,
|
||||||
'check_in': record.check_in_date,
|
'check_in': record.check_in_date - timedelta(hours=5, minutes=30),
|
||||||
'check_out': record.check_out_date,
|
'check_out': record.check_out_date - timedelta(hours=5, minutes=30),
|
||||||
})
|
})
|
||||||
record.is_updated = True
|
record.is_updated = True
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
access_cwf_timesheet_user,access.cwf.timesheet,model_cwf_timesheet,,1,1,1,1
|
access_cwf_timesheet_user,access.cwf.timesheet,model_cwf_timesheet,base.group_user,1,0,0,0
|
||||||
|
access_cwf_timesheet_manager,access.cwf.timesheet,model_cwf_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
|
||||||
|
|
||||||
|
|
||||||
|
access_cwf_timesheet_calendar,cwf_timesheet_calendar,model_cwf_timesheet_calendar,hr_attendance.group_hr_attendance_manager,1,1,1,1
|
||||||
|
access_cwf_timesheet_calendar_user,cwf_timesheet_calendar_user,model_cwf_timesheet_calendar,base.group_user,1,0,0,0
|
||||||
|
|
||||||
|
|
||||||
access_cwf_timesheet_line_user,access.cwf.timesheet.line,model_cwf_timesheet_line,,1,1,1,1
|
access_cwf_timesheet_line_user,access.cwf.timesheet.line,model_cwf_timesheet_line,,1,1,1,1
|
||||||
|
|
||||||
|
access_cwf_weekly_timesheet_user,cwf.weekly.timesheet access,model_cwf_weekly_timesheet,,1,1,1,1
|
||||||
|
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { patch } from "@web/core/utils/patch";
|
import { patch } from "@web/core/utils/patch";
|
||||||
import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
|
import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
|
||||||
|
import { user } from "@web/core/user";
|
||||||
|
|
||||||
// Apply patch to NetflixProfileContainer prototype
|
// Apply patch to NetflixProfileContainer prototype
|
||||||
patch(NetflixProfileContainer.prototype, {
|
patch(NetflixProfileContainer.prototype, {
|
||||||
|
|
@ -24,9 +25,14 @@ patch(NetflixProfileContainer.prototype, {
|
||||||
/**
|
/**
|
||||||
* Override the hr_timesheets method
|
* Override the hr_timesheets method
|
||||||
*/
|
*/
|
||||||
hr_timesheets() {
|
async hr_timesheets() {
|
||||||
|
const isExternalUser = await user.hasGroup("hr_employee_extended.group_external_user");
|
||||||
// Check the department of the logged-in employee
|
// Check the department of the logged-in employee
|
||||||
if (this.state.login_employee.department_id == 'CWF') {
|
console.log(isExternalUser);
|
||||||
|
console.log("is external user");
|
||||||
|
debugger;
|
||||||
|
if (isExternalUser && this.state.login_employee.department_id) {
|
||||||
|
console.log("hello external");
|
||||||
// If the department is 'CWF', perform the action to open the timesheets
|
// If the department is 'CWF', perform the action to open the timesheets
|
||||||
this.action.doAction({
|
this.action.doAction({
|
||||||
name: "Timesheets",
|
name: "Timesheets",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,52 @@
|
||||||
<odoo>
|
<odoo>
|
||||||
|
<record id="view_cwf_timesheet_calendar_form" model="ir.ui.view">
|
||||||
|
<field name="name">cwf.timesheet.calendar.form</field>
|
||||||
|
<field name="model">cwf.timesheet.calendar</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="CWF Timesheet Calendar">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<button name="action_generate_weeks" string="Generate Week Periods" type="object" class="oe_highlight"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Weeks">
|
||||||
|
<field name="week_period" context="{'order': 'week_start_date asc'}">
|
||||||
|
<list editable="bottom" decoration-success="status == 'submitted'">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="week_start_date"/>
|
||||||
|
<field name="week_end_date"/>
|
||||||
|
<field name="status"/>
|
||||||
|
<button name="send_timesheet_update_email" string="Send Update Email" invisible="status == 'submitted'" type="object" confirm="You can't revert this action. Please check twice before Submitting?" class="oe_highlight"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record id="action_cwf_timesheet" model="ir.actions.act_window">
|
<record id="view_cwf_timesheet_calendar_list" model="ir.ui.view">
|
||||||
<field name="name">CWF Timesheet</field>
|
<field name="name">cwf.timesheet.calendar.list</field>
|
||||||
<field name="res_model">cwf.timesheet</field>
|
<field name="model">cwf.timesheet.calendar</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_cwf_timesheet_calendar" model="ir.actions.act_window">
|
||||||
|
<field name="name">CWF Timesheet Calendar</field>
|
||||||
|
<field name="res_model">cwf.timesheet.calendar</field>
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_timesheet_calendar_form" name="CWF Timesheet Calendar" parent="hr_attendance_extended.menu_attendance_attendance" action="cwf_timesheet.action_cwf_timesheet_calendar"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<record id="view_timesheet_form" model="ir.ui.view">
|
<record id="view_timesheet_form" model="ir.ui.view">
|
||||||
<field name="name">cwf.timesheet.form</field>
|
<field name="name">cwf.timesheet.form</field>
|
||||||
<field name="model">cwf.timesheet</field>
|
<field name="model">cwf.timesheet</field>
|
||||||
|
|
@ -21,63 +63,21 @@
|
||||||
<group>
|
<group>
|
||||||
<!-- Section for Employee and Date Range -->
|
<!-- Section for Employee and Date Range -->
|
||||||
<group>
|
<group>
|
||||||
<field name="department_id" readonly="status != 'draft'"/>
|
|
||||||
<field name="week_start_date" readonly="status != 'draft'"/>
|
<field name="week_start_date" readonly="status != 'draft'"/>
|
||||||
<field name="week_end_date" readonly="status != 'draft'"/>
|
<field name="week_end_date" readonly="status != 'draft'"/>
|
||||||
</group>
|
</group>
|
||||||
<!-- Section for Hours and Status -->
|
|
||||||
<group>
|
|
||||||
<field name="total_hours" readonly="status != 'draft'"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<menuitem id="menu_timesheet_form" name="Timesheet Form" parent="hr_attendance_extended.menu_attendance_attendance" action="action_cwf_timesheet"/>
|
<record id="action_cwf_timesheet" model="ir.actions.act_window">
|
||||||
|
<field name="name">CWF Timesheet</field>
|
||||||
<record id="action_cwf_timesheet_line" model="ir.actions.act_window">
|
<field name="res_model">cwf.timesheet</field>
|
||||||
<field name="name">CWF Timesheet Lines</field>
|
<field name="view_mode">list,form</field>
|
||||||
<field name="res_model">cwf.timesheet.line</field>
|
|
||||||
<field name="view_mode">list</field>
|
|
||||||
<field name="context">{'search_default_group_by_employee_id':1}</field>
|
|
||||||
</record>
|
|
||||||
<record id="view_cwf_timesheet_line_list" model="ir.ui.view">
|
|
||||||
<field name="name">cwf.timesheet.line.list</field>
|
|
||||||
<field name="model">cwf.timesheet.line</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<list editable="bottom" create="0" delete="0" decoration-success="is_updated == True">
|
|
||||||
<field name="employee_id" readonly="1" force_save="1"/>
|
|
||||||
<field name="week_day" readonly="1" force_save="1"/>
|
|
||||||
<field name="check_in_date" readonly="is_updated == True"/>
|
|
||||||
<field name="check_out_date" readonly="is_updated == True"/>
|
|
||||||
<field name="state_type" readonly="is_updated == True"/>
|
|
||||||
<button name="action_submit" type="object" string="Submit" class="btn btn-outline-primary" invisible="is_updated == True"/>
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<menuitem id="menu_timesheet_form_line" name="Week Timesheet " parent="hr_attendance_extended.menu_attendance_attendance" action="action_cwf_timesheet_line"/>
|
|
||||||
|
|
||||||
|
|
||||||
<record id="view_cwf_timesheet_line_search" model="ir.ui.view">
|
|
||||||
<field name="name">cwf.timesheet.line.search</field>
|
|
||||||
<field name="model">cwf.timesheet.line</field>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<search string="Timesheets">
|
|
||||||
<field name="employee_id"/>
|
|
||||||
<field name="week_id"/>
|
|
||||||
<field name="week_day"/>
|
|
||||||
<field name="check_in_date"/>
|
|
||||||
<field name="check_out_date"/>
|
|
||||||
<field name="state_type"/>
|
|
||||||
<group expand="0" string="Group By">
|
|
||||||
<filter string="Employee" name="employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
|
|
||||||
<filter string="Week" name="week_id" domain="[]" context="{'group_by':'week_id'}"/>
|
|
||||||
<filter string="Status" name ="state_type" domain="[]" context="{'group_by': 'state_type'}"/>
|
|
||||||
</group>
|
|
||||||
</search>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_cwf_weekly_timesheet_form" model="ir.ui.view">
|
||||||
|
<field name="name">cwf.weekly.timesheet.form</field>
|
||||||
|
<field name="model">cwf.weekly.timesheet</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="CWF Weekly Timesheet" create="0">
|
||||||
|
<header>
|
||||||
|
<button name="update_attendance" string="Update"
|
||||||
|
type="object" class="oe_highlight" invisible="status != 'draft'"/>
|
||||||
|
<button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"
|
||||||
|
class="oe_highlight" invisible="status != 'draft'"/>
|
||||||
|
<field name="status" readonly="1" widget="statusbar"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="week_id" readonly="1"/>
|
||||||
|
<field name="employee_id" readonly="1"/>
|
||||||
|
<label for="week_start_date" string="Dates"/>
|
||||||
|
<div class="o_row">
|
||||||
|
<field name="week_start_date" widget="daterange" options="{'end_date_field': 'week_end_date'}"/>
|
||||||
|
<field name="week_end_date" invisible="1"/>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Timesheet Lines">
|
||||||
|
<field name="cwf_timesheet_lines">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="week_day"/>
|
||||||
|
<field name="check_in_date"/>
|
||||||
|
<field name="check_out_date"/>
|
||||||
|
<field name="state_type"/>
|
||||||
|
<!-- <button name="action_submit" string="Submit" type="object"-->
|
||||||
|
<!-- confirm="Are you sure you want to submit?" class="oe_highlight"/>-->
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_cwf_weekly_timesheet_list" model="ir.ui.view">
|
||||||
|
<field name="name">cwf.weekly.timesheet.list</field>
|
||||||
|
<field name="model">cwf.weekly.timesheet</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list create="0">
|
||||||
|
<field name="week_id"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="status"/>
|
||||||
|
<!-- <button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"-->
|
||||||
|
<!-- class="oe_highlight"/>-->
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_cwf_weekly_timesheet_search" model="ir.ui.view">
|
||||||
|
<field name="name">cwf.weekly.timesheet.search</field>
|
||||||
|
<field name="model">cwf.weekly.timesheet</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<!-- Search by Week ID -->
|
||||||
|
<field name="week_id"/>
|
||||||
|
|
||||||
|
<!-- Search by Employee -->
|
||||||
|
<field name="employee_id"/>
|
||||||
|
|
||||||
|
<!-- Search by Status -->
|
||||||
|
<field name="status"/>
|
||||||
|
|
||||||
|
<!-- Optional: Add custom filters if needed -->
|
||||||
|
<filter string="Draft" name="filter_draft" domain="[('status','=','draft')]"/>
|
||||||
|
<filter string="Submitted" name="filter_submitted" domain="[('status','=','submit')]"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter string="Week" name="week_id" domain="[]" context="{'group_by':'week_id'}"/>
|
||||||
|
<filter string="Employee" name="employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
|
||||||
|
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'status'}"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="action_cwf_weekly_timesheet" model="ir.actions.act_window">
|
||||||
|
<field name="name">CWF Weekly Timesheet</field>
|
||||||
|
<field name="res_model">cwf.weekly.timesheet</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="context">
|
||||||
|
{
|
||||||
|
"search_default_week_id": 1,
|
||||||
|
"search_default_employee_id": 2
|
||||||
|
}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
<field name="search_view_id" ref="cwf_timesheet.view_cwf_weekly_timesheet_search"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="view_cwf_timesheet_line_list" model="ir.ui.view">
|
||||||
|
<field name="name">cwf.timesheet.line.list</field>
|
||||||
|
<field name="model">cwf.timesheet.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list editable="bottom" create="0" delete="0" decoration-success="is_updated == True">
|
||||||
|
<field name="employee_id" readonly="1" force_save="1"/>
|
||||||
|
<field name="week_day" readonly="1" force_save="1"/>
|
||||||
|
<field name="check_in_date" readonly="is_updated == True"/>
|
||||||
|
<field name="check_out_date" readonly="is_updated == True"/>
|
||||||
|
<field name="state_type" readonly="is_updated == True"/>
|
||||||
|
<!-- <button name="action_submit" type="object" string="Submit" class="btn btn-outline-primary"-->
|
||||||
|
<!-- invisible="is_updated == True"/>-->
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_cwf_timesheet_line_search" model="ir.ui.view">
|
||||||
|
<field name="name">cwf.timesheet.line.search</field>
|
||||||
|
<field name="model">cwf.timesheet.line</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Timesheets">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="week_id"/>
|
||||||
|
<field name="week_day"/>
|
||||||
|
<field name="check_in_date"/>
|
||||||
|
<field name="check_out_date"/>
|
||||||
|
<field name="state_type"/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter string="Employee" name="employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
|
||||||
|
<filter string="Week" name="week_id" domain="[]" context="{'group_by':'week_id'}"/>
|
||||||
|
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'state_type'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="action_cwf_timesheet_line" model="ir.actions.act_window">
|
||||||
|
<field name="name">CWF Timesheet Lines</field>
|
||||||
|
<field name="res_model">cwf.timesheet.line</field>
|
||||||
|
<field name="view_mode">list</field>
|
||||||
|
<field name="context">{"search_default_week_id": 1, "search_default_employee_id": 1}</field>
|
||||||
|
<field name="search_view_id" ref="cwf_timesheet.view_cwf_timesheet_line_search"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<menuitem id="menu_timesheet_form" name="CWF Weekly Timesheet "
|
||||||
|
parent="hr_attendance_extended.menu_attendance_attendance" action="action_cwf_weekly_timesheet"/>
|
||||||
|
<menuitem id="menu_timesheet_form_line" name="CWF Timesheet Lines"
|
||||||
|
parent="hr_attendance_extended.menu_attendance_attendance" action="action_cwf_timesheet_line"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -12,10 +12,10 @@
|
||||||
'website': "https://www.ftprotech.com",
|
'website': "https://www.ftprotech.com",
|
||||||
|
|
||||||
# Categories can be used to filter modules in modules listing
|
# 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
|
# for the full list
|
||||||
'category': 'Human Resources/Attendances',
|
'category': 'Human Resources/Attendances',
|
||||||
'version': '0.1',
|
'version': '0.1',
|
||||||
'license': 'LGPL-3',
|
|
||||||
|
|
||||||
# any module necessary for this one to work correctly
|
# any module necessary for this one to work correctly
|
||||||
'depends': ['base','hr','hr_attendance','hr_holidays','hr_employee_extended'],
|
'depends': ['base','hr','hr_attendance','hr_holidays','hr_employee_extended'],
|
||||||
|
|
@ -23,21 +23,11 @@
|
||||||
# always loaded
|
# always loaded
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
|
'security/security.xml',
|
||||||
'data/cron.xml',
|
'data/cron.xml',
|
||||||
|
'data/sequence.xml',
|
||||||
'views/hr_attendance.xml',
|
'views/hr_attendance.xml',
|
||||||
'views/day_attendance_report.xml',
|
'views/on_duty_form.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
|
||||||
'web.assets_backend': [
|
|
||||||
'hr_attendance_extended/static/src/xml/attendance_report.xml',
|
|
||||||
'hr_attendance_extended/static/src/js/attendance_report.js',
|
|
||||||
|
|
||||||
],
|
|
||||||
'web.assets_frontend': [
|
|
||||||
'web/static/lib/jquery/jquery.js',
|
|
||||||
'hr_attendance_extended/static/src/js/jquery-ui.min.js',
|
|
||||||
'hr_attendance_extended/static/src/js/jquery-ui.min.css',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Sequences for lead mining requests -->
|
||||||
|
<record id="ir_sequence_on_duty_form" model="ir.sequence">
|
||||||
|
<field name="name">On Duty Request</field>
|
||||||
|
<field name="code">on.duty.form</field>
|
||||||
|
<field name="prefix">OD</field>
|
||||||
|
<field name="padding">4</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
from . import hr_attendance
|
from . import hr_attendance
|
||||||
from . import hr_attendance_report
|
from . import on_duty_form
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
from odoo import models, fields, api
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import xlwt
|
|
||||||
from io import BytesIO
|
|
||||||
import base64
|
|
||||||
from odoo.exceptions import UserError
|
|
||||||
|
|
||||||
|
|
||||||
def convert_to_date(date_string):
|
|
||||||
# Use strptime to parse the date string in 'dd/mm/yyyy' format
|
|
||||||
return datetime.strptime(date_string, '%Y/%m/%d')
|
|
||||||
class AttendanceReport(models.Model):
|
|
||||||
_name = 'attendance.report'
|
|
||||||
_description = 'Attendance Report'
|
|
||||||
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def get_attendance_report(self, employee_id, start_date, end_date):
|
|
||||||
# Ensure start_date and end_date are in the correct format (datetime)
|
|
||||||
|
|
||||||
if employee_id == '-':
|
|
||||||
employee_id = False
|
|
||||||
else:
|
|
||||||
employee_id = int(employee_id)
|
|
||||||
if isinstance(start_date, str):
|
|
||||||
start_date = datetime.strptime(start_date, '%d/%m/%Y')
|
|
||||||
if isinstance(end_date, str):
|
|
||||||
end_date = datetime.strptime(end_date, '%d/%m/%Y')
|
|
||||||
|
|
||||||
# Convert the dates to 'YYYY-MM-DD' format for PostgreSQL
|
|
||||||
start_date_str = start_date.strftime('%Y-%m-%d')
|
|
||||||
end_date_str = end_date.strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# Define the base where condition
|
|
||||||
if employee_id:
|
|
||||||
case = """WHERE emp.id = %s AND at.check_in >= %s AND at.check_out <= %s"""
|
|
||||||
params = (employee_id, start_date_str, end_date_str)
|
|
||||||
else:
|
|
||||||
case = """WHERE at.check_in >= %s AND at.check_out <= %s"""
|
|
||||||
params = (start_date_str, end_date_str)
|
|
||||||
|
|
||||||
# Define the query with improved date handling
|
|
||||||
query = """
|
|
||||||
WITH daily_checkins AS (
|
|
||||||
SELECT
|
|
||||||
emp.id,
|
|
||||||
emp.name,
|
|
||||||
DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') AS date,
|
|
||||||
at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_in,
|
|
||||||
at.check_out AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_out,
|
|
||||||
at.worked_hours,
|
|
||||||
ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS first_checkin_row,
|
|
||||||
ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in DESC) AS last_checkout_row
|
|
||||||
FROM
|
|
||||||
hr_attendance at
|
|
||||||
LEFT JOIN
|
|
||||||
hr_employee emp ON at.employee_id = emp.id
|
|
||||||
""" + case + """
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
date,
|
|
||||||
MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in,
|
|
||||||
MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out,
|
|
||||||
SUM(worked_hours) AS total_worked_hours
|
|
||||||
FROM
|
|
||||||
daily_checkins
|
|
||||||
GROUP BY
|
|
||||||
id, name, date
|
|
||||||
ORDER BY
|
|
||||||
id, date;
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Execute the query with parameters
|
|
||||||
self.env.cr.execute(query, params)
|
|
||||||
rows = self.env.cr.dictfetchall()
|
|
||||||
data = []
|
|
||||||
a = 0
|
|
||||||
for r in rows:
|
|
||||||
a += 1
|
|
||||||
# Calculate worked hours in Python, but here it's better done in the query itself.
|
|
||||||
worked_hours = r['last_check_out'] - r['first_check_in'] if r['first_check_in'] and r[
|
|
||||||
'last_check_out'] else 0
|
|
||||||
|
|
||||||
data.append({
|
|
||||||
'id': a,
|
|
||||||
'employee_id': r['id'],
|
|
||||||
'employee_name': r['name'],
|
|
||||||
'date': r['date'],
|
|
||||||
'check_in': r['first_check_in'],
|
|
||||||
'check_out': r['last_check_out'],
|
|
||||||
'worked_hours': worked_hours,
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def export_to_excel(self, employee_id, start_date, end_date):
|
|
||||||
# Fetch the attendance data (replace with your logic to fetch attendance data)
|
|
||||||
attendance_data = self.get_attendance_report(employee_id, start_date, end_date)
|
|
||||||
|
|
||||||
if not attendance_data:
|
|
||||||
raise UserError("No data to export!")
|
|
||||||
|
|
||||||
# Create an Excel workbook and a sheet
|
|
||||||
workbook = xlwt.Workbook()
|
|
||||||
sheet = workbook.add_sheet('Attendance Report')
|
|
||||||
|
|
||||||
# Define the column headers
|
|
||||||
headers = ['Employee Name', 'Check-in', 'Check-out', 'Worked Hours']
|
|
||||||
|
|
||||||
# Write headers to the first row
|
|
||||||
for col_num, header in enumerate(headers):
|
|
||||||
sheet.write(0, col_num, header)
|
|
||||||
|
|
||||||
# Write the attendance data to the sheet
|
|
||||||
for row_num, record in enumerate(attendance_data, start=1):
|
|
||||||
sheet.write(row_num, 0, record['employee_name'])
|
|
||||||
sheet.write(row_num, 1, record['check_in'].strftime("%Y-%m-%d %H:%M:%S"))
|
|
||||||
sheet.write(row_num, 2, record['check_out'].strftime("%Y-%m-%d %H:%M:%S"))
|
|
||||||
if isinstance(record['worked_hours'], timedelta):
|
|
||||||
hours = record['worked_hours'].seconds // 3600
|
|
||||||
minutes = (record['worked_hours'].seconds % 3600) // 60
|
|
||||||
# Format as "X hours Y minutes"
|
|
||||||
worked_hours_str = f"{record['worked_hours'].days * 24 + hours} hours {minutes} minutes"
|
|
||||||
sheet.write(row_num, 3, worked_hours_str)
|
|
||||||
else:
|
|
||||||
sheet.write(row_num, 3, record['worked_hours'])
|
|
||||||
# Save the workbook to a BytesIO buffer
|
|
||||||
output = BytesIO()
|
|
||||||
workbook.save(output)
|
|
||||||
|
|
||||||
# Convert the output to base64 for saving in Odoo
|
|
||||||
file_data = base64.b64encode(output.getvalue())
|
|
||||||
|
|
||||||
# Create an attachment record to save the Excel file in Odoo
|
|
||||||
attachment = self.env['ir.attachment'].create({
|
|
||||||
'name': 'attendance_report.xls',
|
|
||||||
'type': 'binary',
|
|
||||||
'datas': file_data,
|
|
||||||
'mimetype': 'application/vnd.ms-excel',
|
|
||||||
})
|
|
||||||
|
|
||||||
# Return the attachment's URL to allow downloading in the Odoo UI
|
|
||||||
return '/web/content/%d/%s' % (attachment.id, attachment.name),
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
class OnDutyForm(models.Model):
|
||||||
|
_name = "on.duty.form"
|
||||||
|
_description = "On Duty Form"
|
||||||
|
_rec_name = "unique_id"
|
||||||
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
|
||||||
|
unique_id = fields.Char(string='Reference', readonly=True, copy=False, default='/')
|
||||||
|
employee_id = fields.Many2one('hr.employee', string="Employee", required=True , default=lambda self: self.env.user.employee_id.id)
|
||||||
|
department_id = fields.Many2one('hr.department', string="Department", related='employee_id.department_id', readonly=True)
|
||||||
|
start_date = fields.Datetime(string='Start Date', required=True)
|
||||||
|
end_date = fields.Datetime(string='End Date', required=True)
|
||||||
|
reason = fields.Text(string='Reason', required=True)
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Draft'),
|
||||||
|
('submitted', 'Submitted'),
|
||||||
|
('approved', 'Approved'),
|
||||||
|
('rejected', 'Rejected')
|
||||||
|
], string='Status', default='draft', tracking=True)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
vals['unique_id'] = self.env['ir.sequence'].next_by_code('on.duty.form') or '/'
|
||||||
|
return super(OnDutyForm, self).create(vals)
|
||||||
|
|
||||||
|
def action_submit(self):
|
||||||
|
for record in self:
|
||||||
|
|
||||||
|
overlapping_requests = self.env['on.duty.form'].search([
|
||||||
|
('employee_id', '=', record.employee_id.id),
|
||||||
|
('state', '=', 'submitted'),
|
||||||
|
'|', '|',
|
||||||
|
'&', ('start_date', '<=', record.start_date), ('end_date', '>=', record.start_date), # Overlap at Start
|
||||||
|
'&', ('start_date', '<=', record.end_date), ('end_date', '>=', record.end_date), # Overlap at End
|
||||||
|
'&', ('start_date', '>=', record.start_date), ('end_date', '<=', record.end_date) # Complete Overlap
|
||||||
|
])
|
||||||
|
|
||||||
|
if overlapping_requests:
|
||||||
|
raise ValidationError("The selected date range conflicts with an existing submitted On-Duty request.")
|
||||||
|
record.state = 'submitted'
|
||||||
|
|
||||||
|
def action_approve(self):
|
||||||
|
for rec in self:
|
||||||
|
if self.env.user.has_group('hr_attendance.group_hr_attendance_manager') or rec.employee_id.parent_id.id == self.env.user.id:
|
||||||
|
rec.state = 'approved'
|
||||||
|
else:
|
||||||
|
raise ValidationError(_("You don't have access to Approve this request"))
|
||||||
|
|
||||||
|
def action_reject(self):
|
||||||
|
for rec in self:
|
||||||
|
if self.env.user.has_group('hr_attendance.group_hr_attendance_manager') or rec.employee_id.parent_id.id == self.env.user.id:
|
||||||
|
rec.state = 'rejected'
|
||||||
|
else:
|
||||||
|
raise ValidationError(_("You don't have access to Reject this request"))
|
||||||
|
|
||||||
|
def action_reset(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.employee_id.user_id.id == self.env.user.id:
|
||||||
|
rec.state = 'draft'
|
||||||
|
else:
|
||||||
|
raise ValidationError(_("Only Employee can Reset"))
|
||||||
|
|
||||||
|
@api.constrains('start_date', 'end_date')
|
||||||
|
def _check_dates(self):
|
||||||
|
for record in self:
|
||||||
|
if record.start_date >= record.end_date:
|
||||||
|
raise ValidationError("End Date must be greater than Start Date.")
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
access_attendance_attendance_user,attendance.attendance.access,model_attendance_attendance,base.group_user,1,1,1,1
|
access_attendance_attendance_user,attendance.attendance.access,model_attendance_attendance,base.group_user,1,1,1,1
|
||||||
access_attendance_data_user,attendance.data.access,model_attendance_data,base.group_user,1,1,1,1
|
access_attendance_data_user,attendance.data.access,model_attendance_data,base.group_user,1,1,1,1
|
||||||
|
access_on_duty_form_user,on_duty_form_user,model_on_duty_form,base.group_user,1,1,1,1
|
||||||
|
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0">
|
||||||
|
<record id="on_duty_form_user_rule" model="ir.rule">
|
||||||
|
<field name="name">On Duty Form User Rule</field>
|
||||||
|
<field ref="model_on_duty_form" name="model_id"/>
|
||||||
|
<field name="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="on_duty_form_manager_rule" model="ir.rule">
|
||||||
|
<field name="name">On Duty Form Manager Rule</field>
|
||||||
|
<field ref="model_on_duty_form" name="model_id"/>
|
||||||
|
<field name="domain_force">[('employee_id.parent_id.user_id.id','=',user.id),('state','!=','draft')]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('hr.group_hr_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="on_duty_form_admin_rule" model="ir.rule">
|
||||||
|
<field name="name">On Duty Form Admin Rule</field>
|
||||||
|
<field ref="model_on_duty_form" name="model_id"/>
|
||||||
|
<field name="domain_force">[('state','!=','draft'),'|',('employee_id.parent_id.user_id.id','=',user.id),('employee_id.parent_id.user_id.id','!=',user.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
import { useService } from "@web/core/utils/hooks";
|
|
||||||
import { Component, xml, useState, onMounted } from "@odoo/owl";
|
|
||||||
import { registry } from "@web/core/registry";
|
|
||||||
import { getOrigin } from "@web/core/utils/urls";
|
|
||||||
export default class AttendanceReport extends Component {
|
|
||||||
static props = ['*'];
|
|
||||||
static template = 'attendance_report_template';
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
super.setup(...arguments);
|
|
||||||
this.orm = useService("orm");
|
|
||||||
this.state = useState({
|
|
||||||
startDate: "", // To store the start date parameter
|
|
||||||
endDate: "",
|
|
||||||
attendanceData: [], // Initialized as an empty array
|
|
||||||
groupedData: [], // To store the grouped attendance data by employee_id
|
|
||||||
employeeIDS: [] // List of employee IDs to bind with select dropdown
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
this.loademployeeIDS();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async loademployeeIDS() {
|
|
||||||
try {
|
|
||||||
const employee = await this.orm.searchRead('hr.employee', [], ['id', 'display_name']);
|
|
||||||
this.state.employeeIDS = employee;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.initializeSelect2();
|
|
||||||
this.render();// Initialize Select2 after data is loaded
|
|
||||||
this.reload();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading employeeIDS:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Select2 with error handling and ensuring it's initialized only once
|
|
||||||
initializeSelect2() {
|
|
||||||
const employeeIDS = this.state.employeeIDS;
|
|
||||||
|
|
||||||
// Ensure the <select> element is initialized only once
|
|
||||||
const $empSelect = $('#emp');
|
|
||||||
const from_date = $("#from_date").datepicker({
|
|
||||||
dateFormat: "dd/mm/yy", // Date format
|
|
||||||
showAnim: "slideDown", // Animation
|
|
||||||
changeMonth: true, // Allow month selection
|
|
||||||
changeYear: true, // Allow year selection
|
|
||||||
yearRange: "2010:2030", // Year range
|
|
||||||
// ... other options
|
|
||||||
});
|
|
||||||
|
|
||||||
const to_date = $("#to_date").datepicker({
|
|
||||||
dateFormat: "dd/mm/yy", // Date format
|
|
||||||
showAnim: "slideDown", // Animation
|
|
||||||
changeMonth: true, // Allow month selection
|
|
||||||
changeYear: true, // Allow year selection
|
|
||||||
yearRange: "2010:2030", // Year range
|
|
||||||
// ... other options
|
|
||||||
});
|
|
||||||
// Debugging the employeeIDS array to verify its structure
|
|
||||||
console.log("employeeIDS:", employeeIDS);
|
|
||||||
|
|
||||||
// Check if employeeIDS is an array and has the necessary properties
|
|
||||||
if (Array.isArray(employeeIDS) && employeeIDS.length > 0) {
|
|
||||||
// Clear the current options (if any)
|
|
||||||
$empSelect.empty();
|
|
||||||
|
|
||||||
// Add options for each employee
|
|
||||||
employeeIDS.forEach(emp => {
|
|
||||||
$empSelect.append(
|
|
||||||
`<option value="${emp.id}">${emp.display_name}</option>`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
$empSelect.append(
|
|
||||||
`<option value="-">All</option>`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize the select with the 'multiple' attribute for multi-select
|
|
||||||
// $empSelect.attr('multiple', 'multiple');
|
|
||||||
|
|
||||||
// Enable tagging (you can manually add tags as well)
|
|
||||||
$empSelect.on('change', (ev) => {
|
|
||||||
const selectedEmployeeIds = $(ev.target).val();
|
|
||||||
console.log('Selected Employee IDs: ', selectedEmployeeIds);
|
|
||||||
|
|
||||||
const selectedEmployees = employeeIDS.filter(emp => selectedEmployeeIds.includes(emp.id.toString()));
|
|
||||||
console.log('Selected Employees: ', selectedEmployees);
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
console.error("Invalid employee data format:", employeeIDS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method called when a date is selected in the input fields
|
|
||||||
async ExportToExcel() {
|
|
||||||
|
|
||||||
var startdate = $('#from_date').val()
|
|
||||||
var enddate = $('#to_date').val()
|
|
||||||
let domain = [
|
|
||||||
['check_in', '>=', startdate],
|
|
||||||
['check_in', '<=', enddate],
|
|
||||||
];
|
|
||||||
|
|
||||||
// If employee(s) are selected, filter the data by employee_id
|
|
||||||
if ($('#emp').val() && $('#emp').val().length > 0 && $('#emp').val() !== '-') {
|
|
||||||
domain.push(['employee_id', '=', parseInt($('#emp').val())]);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
debugger;
|
|
||||||
// Fetch the attendance data based on the date range and selected employees
|
|
||||||
const URL = await this.orm.call('attendance.report', 'export_to_excel', [$('#emp').val(), startdate, enddate]);
|
|
||||||
window.open(getOrigin()+URL, '_blank');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error generating report:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateReport() {
|
|
||||||
|
|
||||||
let { startDate, endDate, selectedEmployeeIds } = this.state;
|
|
||||||
startDate = $('#from_date').val()
|
|
||||||
endDate = $('#to_date').val()
|
|
||||||
|
|
||||||
if (!startDate || !endDate) {
|
|
||||||
alert("Please specify both start and end dates!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the domain for the search query
|
|
||||||
let domain = [
|
|
||||||
['check_in', '>=', startDate],
|
|
||||||
['check_in', '<=', endDate],
|
|
||||||
];
|
|
||||||
|
|
||||||
// If employee(s) are selected, filter the data by employee_id
|
|
||||||
if ($('#emp').val() && $('#emp').val().length > 0 && $('#emp').val() !== '-') {
|
|
||||||
domain.push(['employee_id', '=', parseInt($('#emp').val())]);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fetch the attendance data based on the date range and selected employees
|
|
||||||
// const attendanceData = await this.orm.searchRead('hr.attendance', domain, ['employee_id', 'check_in', 'check_out', 'worked_hours']);
|
|
||||||
const attendanceData = await this.orm.call('attendance.report','get_attendance_report',[$('#emp').val(),startDate,endDate]);
|
|
||||||
|
|
||||||
// Group data by employee_id
|
|
||||||
const groupedData = this.groupDataByEmployee(attendanceData);
|
|
||||||
|
|
||||||
// Update state with the fetched and grouped data
|
|
||||||
this.state.attendanceData = attendanceData;
|
|
||||||
this.state.groupedData = groupedData;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error generating report:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Helper function to group data by employee_id
|
|
||||||
groupDataByEmployee(data) {
|
|
||||||
const grouped = {};
|
|
||||||
|
|
||||||
data.forEach(record => {
|
|
||||||
const employeeId = record.employee_id; // employee_id[1] is the name
|
|
||||||
|
|
||||||
if (employeeId) {
|
|
||||||
if (!grouped[employeeId]) {
|
|
||||||
grouped[employeeId] = [];
|
|
||||||
}
|
|
||||||
grouped[employeeId].push(record);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert the grouped data into an array to be used in t-foreach
|
|
||||||
return Object.values(grouped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the action in the actions registry
|
|
||||||
registry.category("actions").add("AttendanceReport", AttendanceReport);
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,91 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
|
||||||
|
|
||||||
<templates xml:space="preserve">
|
|
||||||
<t t-name="attendance_report_template">
|
|
||||||
<script t-att-src="'/web/static/lib/jquery/jquery.js'"></script>
|
|
||||||
<script t-att-src="'/hr_attendance_extended/static/src/js/jquery-ui.min.js'"></script>
|
|
||||||
<link rel="stylesheet" type="text/css" href="/hr_attendance_extended/static/src/js/jquery-ui.min.css"/>
|
|
||||||
<style>
|
|
||||||
.ui-datepicker {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
.ui-state-highlight {
|
|
||||||
background-color: #ffcc00;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="header pt-5" style="text-align:center">
|
|
||||||
<h1>Attendance Report</h1>
|
|
||||||
<div class="navbar navbar-expand-lg container">
|
|
||||||
<h4 class="p-3 text-nowrap">Employee </h4>
|
|
||||||
<div class="input-group input-group-lg">
|
|
||||||
<select type="text" id="emp" class="form-control" />
|
|
||||||
</div>
|
|
||||||
<h4 class="p-3 text-nowrap"> From Date</h4>
|
|
||||||
<div class="input-group input-group-lg">
|
|
||||||
<input type="text" id="from_date" class="form-control" value="DD/MM/YYYY"/>
|
|
||||||
</div>
|
|
||||||
<h4 class="p-3 text-nowrap"> To Date</h4>
|
|
||||||
<div class="input-group input-group-lg">
|
|
||||||
<input type="text" id="to_date" class="form-control" value="DD/MM/YYYY"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-outline-success" t-on-click="generateReport" >Generate Report</button>
|
|
||||||
<button class="btn btn-outline-success" t-on-click="ExportToExcel">Export Report</button>
|
|
||||||
|
|
||||||
<div t-if="this.state.groupedData.length > 0" style="max-height: 800px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;">
|
|
||||||
<div t-foreach="this.state.groupedData" t-as="group" t-key="group[0].employee_id">
|
|
||||||
<div class="employee-group">
|
|
||||||
<h3 style="text-align:left" class="p-2">
|
|
||||||
Employee:
|
|
||||||
<t t-if="group[0].employee_id">
|
|
||||||
<t t-esc="group[0].employee_name"/>
|
|
||||||
</t>
|
|
||||||
<t t-else="">
|
|
||||||
<span>Unknown Employee</span>
|
|
||||||
</t>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Scrollable Container for the Table -->
|
|
||||||
<div class="scrollable-table-container">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Employee</th>
|
|
||||||
<th>Check In</th>
|
|
||||||
<th>Check Out</th>
|
|
||||||
<th>Worked Hours</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr t-foreach="group" t-as="data" t-key="data.id">
|
|
||||||
<td><t t-esc="data.date"/></td>
|
|
||||||
<td>
|
|
||||||
<t t-if="data.employee_id">
|
|
||||||
<t t-esc="data.employee_name"/>
|
|
||||||
</t>
|
|
||||||
<t t-else="">
|
|
||||||
<span>Unknown Employee</span>
|
|
||||||
</t>
|
|
||||||
</td>
|
|
||||||
<td><t t-esc="data.check_in"/></td>
|
|
||||||
<td><t t-esc="data.check_out"/></td>
|
|
||||||
<td><t t-esc="data.worked_hours"/></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div t-else="">
|
|
||||||
<p>No data available for the selected date range.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</t>
|
|
||||||
</templates>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<odoo>
|
|
||||||
<record id="action_attendance_report_s" model="ir.actions.client">
|
|
||||||
<field name="name">Attendance Report</field>
|
|
||||||
<field name="tag">AttendanceReport</field>
|
|
||||||
</record>
|
|
||||||
<menuitem action="action_attendance_report_s" id="menu_hr_attendance_day" groups="hr_attendance.group_hr_attendance_officer" parent="hr_attendance_extended.menu_attendance_attendance"/>
|
|
||||||
</odoo>
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<odoo>
|
||||||
|
<record id="view_on_duty_form_list" model="ir.ui.view">
|
||||||
|
<field name="name">on.duty.form.list</field>
|
||||||
|
<field name="model">on.duty.form</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="unique_id"/>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="start_date"/>
|
||||||
|
<field name="end_date"/>
|
||||||
|
<field name="state" widget="badge" decoration-success="state == 'approved'" decoration-warning="state == 'rejected'" decoration-info="state == 'submitted'"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_on_duty_form_form" model="ir.ui.view">
|
||||||
|
<field name="name">on.duty.form.form</field>
|
||||||
|
<field name="model">on.duty.form</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="On Duty Form">
|
||||||
|
<header>
|
||||||
|
<button name="action_submit" type="object" string="Submit" class="btn btn-primary"
|
||||||
|
invisible="state != 'draft'"/>
|
||||||
|
<button name="action_approve" type="object" string="Approve" class="btn btn-primary"
|
||||||
|
invisible="state != 'submitted'" groups="hr.group_hr_user"/>
|
||||||
|
<button name="action_reject" type="object" string="Reject"
|
||||||
|
invisible="state not in ['submitted','approved']" groups="hr.group_hr_user"/>
|
||||||
|
<button name="action_reset" type="object" string="Reset"
|
||||||
|
invisible="state != 'rejected'"/>
|
||||||
|
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,submitted"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="unique_id" readonly="1"/>
|
||||||
|
<field name="employee_id" readonly="1"/>
|
||||||
|
<field name="department_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group col="2"> <!-- Two-column layout -->
|
||||||
|
<field name="start_date" readonly="state != 'draft'"/>
|
||||||
|
<field name="end_date" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="reason" readonly="state != 'draft'"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
<chatter reload_on_post="True" reload_on_attachment="True"/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="action_on_duty_form" model="ir.actions.act_window">
|
||||||
|
<field name="name">On Duty Forms</field>
|
||||||
|
<field name="res_model">on.duty.form</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<menuitem id="menu_on_duty_form_root"
|
||||||
|
name="On Duty Forms"
|
||||||
|
parent="hr_attendance.menu_hr_attendance_root"
|
||||||
|
sequence="10"/>
|
||||||
|
<!-- Menu for Attendance -->
|
||||||
|
<menuitem id="menu_on_duty_form"
|
||||||
|
name="On Duty Requests"
|
||||||
|
action="action_on_duty_form"
|
||||||
|
parent="menu_on_duty_form_root"
|
||||||
|
sequence="20"/>
|
||||||
|
</odoo>
|
||||||
|
|
@ -7,12 +7,12 @@
|
||||||
<field name="sequence">17</field>
|
<field name="sequence">17</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="group_external_user" model="res.groups">
|
<record id="group_external_user" model="res.groups">
|
||||||
<field name="name">External User</field>
|
<field name="name">Client-Side User</field>
|
||||||
<field name="category_id" ref="hr_employee_extended.module_internal_user_category"/>
|
<field name="category_id" ref="hr_employee_extended.module_internal_user_category"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="group_internal_user" model="res.groups">
|
<record id="group_internal_user" model="res.groups">
|
||||||
<field name="name">Internal User</field>
|
<field name="name">In-House User</field>
|
||||||
<field name="implied_ids" eval="[(4, ref('group_external_user'))]"/>
|
<field name="implied_ids" eval="[(4, ref('group_external_user'))]"/>
|
||||||
<field name="category_id" ref="hr_employee_extended.module_internal_user_category"/>
|
<field name="category_id" ref="hr_employee_extended.module_internal_user_category"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
'security/security.xml',
|
'security/security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/cron.xml',
|
'data/cron.xml',
|
||||||
|
'data/data.xml',
|
||||||
'data/sequence.xml',
|
'data/sequence.xml',
|
||||||
'data/mail_template.xml',
|
'data/mail_template.xml',
|
||||||
'views/job_category.xml',
|
'views/job_category.xml',
|
||||||
|
|
@ -43,6 +44,8 @@
|
||||||
'views/skills.xml',
|
'views/skills.xml',
|
||||||
'wizards/post_onboarding_attachment_wizard.xml',
|
'wizards/post_onboarding_attachment_wizard.xml',
|
||||||
'wizards/applicant_refuse_reason.xml',
|
'wizards/applicant_refuse_reason.xml',
|
||||||
|
'wizards/ats_invite_mail_template_wizard.xml',
|
||||||
|
'wizards/client_submission_mail_template_wizard.xml',
|
||||||
# 'views/resume_pearser.xml',
|
# 'views/resume_pearser.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
|
|
@ -51,6 +54,7 @@
|
||||||
],
|
],
|
||||||
'web.assets_frontend': [
|
'web.assets_frontend': [
|
||||||
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
|
||||||
|
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
|
||||||
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,64 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@http.route(['/FTPROTECH/DocRequests/<int:applicant_id>'], type='http', auth="public",
|
||||||
|
website=True)
|
||||||
|
def doc_request_form(self, applicant_id, **kwargs):
|
||||||
|
"""Renders the website form for applicants to submit additional details."""
|
||||||
|
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||||
|
if not applicant.exists():
|
||||||
|
return request.not_found()
|
||||||
|
if applicant:
|
||||||
|
if applicant.doc_requests_form_status == 'done':
|
||||||
|
return request.render("hr_recruitment_extended.thank_you_template")
|
||||||
|
else:
|
||||||
|
return request.render("hr_recruitment_extended.doc_request_form_template", {
|
||||||
|
'applicant': applicant
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
@http.route(['/FTPROTECH/submit/<int:applicant_id>/docRequest'], type='http', auth="public",
|
||||||
|
methods=['POST'], website=True, csrf=False)
|
||||||
|
def process_applicant_doc_submission_form(self, applicant_id, **post):
|
||||||
|
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
|
||||||
|
if not applicant.exists():
|
||||||
|
return request.not_found() # Return 404 if applicant doesn't exist
|
||||||
|
|
||||||
|
if applicant.doc_requests_form_status == 'done':
|
||||||
|
return request.render("hr_recruitment_extended.thank_you_template")
|
||||||
|
|
||||||
|
applicant_data = {
|
||||||
|
'applicant_id': int(post.get('applicant_id', 0)),
|
||||||
|
'candidate_image': post.get('candidate_image_base64', ''),
|
||||||
|
'doc_requests_form_status': 'done'
|
||||||
|
}
|
||||||
|
|
||||||
|
applicant_data = {k: v for k, v in applicant_data.items() if v != '' and v != 0}
|
||||||
|
# attachments
|
||||||
|
attachments_data_json = post.get('attachments_data_json', '[]')
|
||||||
|
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
|
||||||
|
|
||||||
|
if attachments_data:
|
||||||
|
applicant_data['joining_attachment_ids'] = [
|
||||||
|
(4, existing_id) for existing_id in
|
||||||
|
(applicant.joining_attachment_ids).ids
|
||||||
|
] + [
|
||||||
|
(0, 0, {
|
||||||
|
'name': attachment.get('file_name', ''),
|
||||||
|
'recruitment_attachment_id': attachment.get(
|
||||||
|
'attachment_rec_id', ''),
|
||||||
|
'file': attachment.get('file_content', '')
|
||||||
|
}) for attachment in attachments_data if
|
||||||
|
attachment.get('attachment_rec_id')
|
||||||
|
]
|
||||||
|
|
||||||
|
applicant.write(applicant_data)
|
||||||
|
|
||||||
|
return request.render("hr_recruitment_extended.thank_you_template")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@http.route(['/FTPROTECH/JoiningForm/<int:applicant_id>'], type='http', auth="public",
|
@http.route(['/FTPROTECH/JoiningForm/<int:applicant_id>'], type='http', auth="public",
|
||||||
website=True)
|
website=True)
|
||||||
|
|
@ -102,6 +160,7 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
else:
|
else:
|
||||||
return request.not_found()
|
return request.not_found()
|
||||||
|
|
||||||
|
|
||||||
@http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
|
@http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
|
||||||
methods=['POST'], website=True, csrf=False)
|
methods=['POST'], website=True, csrf=False)
|
||||||
def process_employee_joining_form(self,applicant_id,**post):
|
def process_employee_joining_form(self,applicant_id,**post):
|
||||||
|
|
@ -209,11 +268,16 @@ class website_hr_recruitment_applications(http.Controller):
|
||||||
|
|
||||||
if attachments_data:
|
if attachments_data:
|
||||||
applicant_data['joining_attachment_ids'] = [
|
applicant_data['joining_attachment_ids'] = [
|
||||||
(0,0,{
|
(4, existing_id) for existing_id in
|
||||||
'name': attachment.get('file_name',''),
|
(applicant.joining_attachment_ids).ids
|
||||||
'recruitment_attachment_id': attachment.get('attachment_rec_id',''),
|
] + [
|
||||||
'file': attachment.get('file_content','')
|
(0, 0, {
|
||||||
}) for attachment in attachments_data if attachment.get('attachment_rec_id')
|
'name': attachment.get('file_name', ''),
|
||||||
|
'recruitment_attachment_id': attachment.get(
|
||||||
|
'attachment_rec_id', ''),
|
||||||
|
'file': attachment.get('file_content', '')
|
||||||
|
}) for attachment in attachments_data if
|
||||||
|
attachment.get('attachment_rec_id')
|
||||||
]
|
]
|
||||||
|
|
||||||
applicant.write(applicant_data)
|
applicant.write(applicant_data)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- employee.recruitment.attachments-->
|
||||||
|
<record model="ir.attachment" id="employee_recruitment_attachments_preview">
|
||||||
|
<field name="name">Attachment Preview</field>
|
||||||
|
<field name="type">binary</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -191,6 +191,104 @@
|
||||||
<field name="auto_delete" eval="True"/>
|
<field name="auto_delete" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="email_template_request_documents" model="mail.template">
|
||||||
|
<field name="name">Document Submission Request</field>
|
||||||
|
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||||
|
<field name="email_from">{{ user.company_id.email or user.email_formatted }}</field>
|
||||||
|
<field name="email_to">{{ object.email_from }}</field>
|
||||||
|
<field name="subject">Important: Document Submission Request | FTPROTECH</field>
|
||||||
|
<field name="description">
|
||||||
|
Email requesting necessary documents from applicants.
|
||||||
|
</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<t t-set="applicant_name" t-value="object.candidate_id.partner_name or 'Applicant'"/>
|
||||||
|
|
||||||
|
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
|
||||||
|
<p>Dear
|
||||||
|
<strong>
|
||||||
|
<t t-esc="applicant_name">Applicant</t>
|
||||||
|
</strong>
|
||||||
|
,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>We hope you are doing well! As part of our recruitment process, we kindly request you to submit
|
||||||
|
the following documents at the earliest.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<t t-if="ctx.get('personal_docs') or ctx.get('education_docs') or ctx.get('previous_employer_docs') or ctx.get('other_docs')">
|
||||||
|
<p>Please ensure to provide soft copies of the required documents:</p>
|
||||||
|
|
||||||
|
<!-- Personal Documents -->
|
||||||
|
<t t-if="ctx.get('personal_docs')">
|
||||||
|
<strong>Personal Documents:</strong>
|
||||||
|
<ul>
|
||||||
|
<t t-foreach="ctx.get('personal_docs')" t-as="doc">
|
||||||
|
<li t-esc="doc"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Education Documents -->
|
||||||
|
<t t-if="ctx.get('education_docs')">
|
||||||
|
<strong>Education Documents:</strong>
|
||||||
|
<ul>
|
||||||
|
<t t-foreach="ctx.get('education_docs')" t-as="doc">
|
||||||
|
<li t-esc="doc"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Previous Employer Documents -->
|
||||||
|
<t t-if="ctx.get('previous_employer_docs')">
|
||||||
|
<strong>Previous Employer Documents:</strong>
|
||||||
|
<ul>
|
||||||
|
<t t-foreach="ctx.get('previous_employer_docs')" t-as="doc">
|
||||||
|
<li t-esc="doc"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Additional Documents -->
|
||||||
|
<t t-if="ctx.get('other_docs')">
|
||||||
|
<strong>Additional Documents:</strong>
|
||||||
|
<ul>
|
||||||
|
<t t-foreach="ctx.get('other_docs')" t-as="doc">
|
||||||
|
<li t-esc="doc"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<p>Please upload your documents via the following link:</p>
|
||||||
|
|
||||||
|
<t t-set="base_url" t-value="object.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
|
||||||
|
<t t-set="upload_url" t-value="base_url + '/FTPROTECH/DocRequests/%s' % object.id"/>
|
||||||
|
|
||||||
|
<p style="text-align: center; margin-top: 20px;">
|
||||||
|
<a t-att-href="upload_url" target="_blank"
|
||||||
|
style="background-color: #007bff; color: #fff; padding: 10px 20px; text-decoration: none;
|
||||||
|
font-weight: bold; border-radius: 5px; display: inline-block;">
|
||||||
|
Upload Your Documents
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>If you have any questions or need assistance, please contact us at
|
||||||
|
<a href="mailto:hr@ftprotech.com" style="color: #007bff; text-decoration: none;">
|
||||||
|
hr@ftprotech.com</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>We appreciate your cooperation and look forward to completing the process smoothly.</p>
|
||||||
|
|
||||||
|
<p>Best Regards,
|
||||||
|
<br/>
|
||||||
|
<strong>
|
||||||
|
<t t-esc="object.company_id.name or 'HR Team'">HR Team</t>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
<field name="auto_delete" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record id="email_template_post_onboarding_form" model="mail.template">
|
<record id="email_template_post_onboarding_form" model="mail.template">
|
||||||
<field name="name">Joining Formalities Notification</field>
|
<field name="name">Joining Formalities Notification</field>
|
||||||
|
|
@ -421,5 +519,104 @@
|
||||||
<field name="auto_delete" eval="True"/>
|
<field name="auto_delete" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="email_template_recruiter_assignment_template" model="mail.template">
|
||||||
|
<field name="name">Recruitment Assignment Notification</field>
|
||||||
|
<field name="model_id" ref="model_hr_job_recruitment"/>
|
||||||
|
<field name="email_from">{{ user.email_formatted }}</field>
|
||||||
|
<field name="email_to">{{ object.user_id.email }}</field>
|
||||||
|
<field name="subject">Job Assignment - {{ object.job_id.name }}</field>
|
||||||
|
<field name="description">
|
||||||
|
Notification to recruiter regarding new job requisition assignment.
|
||||||
|
</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||||
|
Dear <t t-esc="ctx['recruiter_name']">Recruiter</t>,
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
A new job requisition has been assigned to you.
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<ul>
|
||||||
|
<t t-if="object.recruitment_sequence">
|
||||||
|
<li>
|
||||||
|
<strong>Job ID:</strong>
|
||||||
|
<t t-out="object.recruitment_sequence"/>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
<t t-if="object.job_id">
|
||||||
|
<li>
|
||||||
|
<strong>Job Title:</strong>
|
||||||
|
<t t-out="object.job_id.name"/>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
<t t-if="object.job_priority">
|
||||||
|
<li>
|
||||||
|
<strong>Priority Level:</strong>
|
||||||
|
<t t-out="object.job_priority"/>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
<t t-if="object.locations and object.locations.mapped('location_name')">
|
||||||
|
<li>
|
||||||
|
<strong>Location:</strong>
|
||||||
|
<t t-out="', '.join(object.locations.mapped('location_name'))"/>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
<t t-if="object.budget">
|
||||||
|
<li>
|
||||||
|
<strong>Budget:</strong>
|
||||||
|
<t t-out="object.budget"/>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
<t t-if="object.id">
|
||||||
|
<li>
|
||||||
|
<strong>Job Description:</strong>
|
||||||
|
<a t-att-href="'%s/web#id=%d&model=hr.job.recruitment&action=%s&view_type=form' %
|
||||||
|
(object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id, object.env.ref('hr_recruitment_extended.action_hr_job_recruitment').id)"
|
||||||
|
target="_blank">
|
||||||
|
View Job Details
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
<br/>
|
||||||
|
Kindly review the requisition and start sourcing suitable candidates in the ATS.
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
Regards,
|
||||||
|
<br/>
|
||||||
|
<t t-out="user.name or 'Hiring Manager'">Hiring Manager</t>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="application_client_submission_email_template" model="mail.template">
|
||||||
|
<field name="name">Applicant Client Submissions</field>
|
||||||
|
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
|
||||||
|
<field name="email_from">{{ user.email_formatted }}</field>
|
||||||
|
<field name="email_to">{{ object.hr_job_recruitment.requested_by.email }}</field>
|
||||||
|
<field name="subject">Applicant Submission</field>
|
||||||
|
<field name="description">
|
||||||
|
Submitting the Applicant Details to Client.
|
||||||
|
</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||||
|
Dear <t t-esc="ctx['client_name']">Sir/Madam</t>,
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
Submitting new applicant.
|
||||||
|
<br/>
|
||||||
|
Kindly review the Applicant.
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
Regards,
|
||||||
|
<br/>
|
||||||
|
<t t-out="user.name or 'Hiring Manager'">Hiring Manager</t>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -20,6 +20,12 @@ class HRApplicant(models.Model):
|
||||||
refused_stage = fields.Many2one('hr.recruitment.stage')
|
refused_stage = fields.Many2one('hr.recruitment.stage')
|
||||||
refused_comments = fields.Text()
|
refused_comments = fields.Text()
|
||||||
|
|
||||||
|
@api.constrains('candidate_id','hr_job_recruitment')
|
||||||
|
def hr_applicant_constrains(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.candidate_id and rec.hr_job_recruitment:
|
||||||
|
self.sudo().search([('candidate_id','=',rec.candidate_id.id),('hr_job_recruitment','=',rec.hr_job_recruitment.id),('id','!=',rec.id)])
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _read_group_recruitment_stage_ids(self, stages, domain):
|
def _read_group_recruitment_stage_ids(self, stages, domain):
|
||||||
# retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
|
# retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
|
||||||
|
|
@ -101,7 +107,7 @@ class HRApplicant(models.Model):
|
||||||
second_application_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
second_application_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
||||||
send_post_onboarding_form = fields.Boolean(related='recruitment_stage_id.post_onboarding_form')
|
send_post_onboarding_form = fields.Boolean(related='recruitment_stage_id.post_onboarding_form')
|
||||||
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
||||||
|
doc_requests_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
|
||||||
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
|
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
|
||||||
legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid')
|
legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid')
|
||||||
legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing')
|
legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing')
|
||||||
|
|
@ -129,11 +135,19 @@ class HRApplicant(models.Model):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Max no of submissions for this JD has been reached",
|
"Max no of submissions for this JD has been reached",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=2,
|
|
||||||
)
|
)
|
||||||
rec.submitted_to_client = True
|
return {
|
||||||
rec.client_submission_date = fields.Datetime.now()
|
'type': 'ir.actions.act_window',
|
||||||
rec.submitted_stage = rec.recruitment_stage_id.id
|
'name': 'Submission',
|
||||||
|
'res_model': 'client.submission.mails.template.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'view_id': self.env.ref('hr_recruitment_extended.view_client_submission_mails_template_wizard_form').id,
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'default_template_id': self.env.ref(
|
||||||
|
"hr_recruitment_extended.application_client_submission_email_template").id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def submit_for_approval(self):
|
def submit_for_approval(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -208,7 +222,16 @@ class HRApplicant(models.Model):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def send_pre_onboarding_doc_request_form_to_candidate(self):
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': 'Select Attachments',
|
||||||
|
'res_model': 'post.onboarding.attachment.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'view_type': 'form',
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'default_attachment_ids': [],'default_is_pre_onboarding_attachment_request': True}
|
||||||
|
}
|
||||||
def _track_template(self, changes):
|
def _track_template(self, changes):
|
||||||
res = super(HRApplicant, self)._track_template(changes)
|
res = super(HRApplicant, self)._track_template(changes)
|
||||||
applicant = self[0]
|
applicant = self[0]
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ class HRJobRecruitment(models.Model):
|
||||||
'res.partner', "Job Location", default=_default_address_id,
|
'res.partner', "Job Location", default=_default_address_id,
|
||||||
domain="[('is_company','=',True),('contact_type','=',recruitment_type)]",
|
domain="[('is_company','=',True),('contact_type','=',recruitment_type)]",
|
||||||
help="Select the location where the applicant will work. Addresses listed here are defined on the company's contact information.", exportable=False)
|
help="Select the location where the applicant will work. Addresses listed here are defined on the company's contact information.", exportable=False)
|
||||||
recruitment_type = fields.Selection([('internal','Internal'),('external','External')], required=True, default='internal')
|
recruitment_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal')
|
||||||
requested_by = fields.Many2one('res.partner', string="Requested By",
|
requested_by = fields.Many2one('res.partner', string="Requested By",
|
||||||
default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]")
|
default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]")
|
||||||
|
|
||||||
|
|
@ -167,6 +167,26 @@ class HRJobRecruitment(models.Model):
|
||||||
self.requested_by = False
|
self.requested_by = False
|
||||||
self.address_id = False
|
self.address_id = False
|
||||||
|
|
||||||
|
def send_mail_to_recruiters(self):
|
||||||
|
for rec in self:
|
||||||
|
""" Open the email wizard """
|
||||||
|
users = rec.interviewer_ids.ids
|
||||||
|
primary_user = rec.user_id.id
|
||||||
|
# template = self.env.ref('hr_recruitment_extended.email_template_recruiter_assignment_template')
|
||||||
|
# template.sudo().send_mail(rec.id, force_send=True)
|
||||||
|
|
||||||
|
users.append(primary_user)
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': 'Send Email',
|
||||||
|
'res_model': 'ats.invite.mail.template.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'view_id': self.env.ref('hr_recruitment_extended.view_ats_invite_mail_template_wizard_form').id,
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'default_partner_ids': [(6, 0, users)],'default_template_id': self.env.ref("hr_recruitment_extended.email_template_recruiter_assignment_template").id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
@api.onchange('requested_by')
|
@api.onchange('requested_by')
|
||||||
def _onchange_requested_by(self):
|
def _onchange_requested_by(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -209,7 +229,7 @@ class HRJobRecruitment(models.Model):
|
||||||
|
|
||||||
job_category = fields.Many2one("job.category", string="Category")
|
job_category = fields.Many2one("job.category", string="Category")
|
||||||
|
|
||||||
|
job_priority = fields.Selection([('low','Low'),('medium','Medium'),('high','High')], string="Pirority")
|
||||||
|
|
||||||
@api.onchange('job_id','job_category')
|
@api.onchange('job_id','job_category')
|
||||||
def onchange_job_id(self):
|
def onchange_job_id(self):
|
||||||
|
|
|
||||||
|
|
@ -199,8 +199,8 @@ class HRApplicant(models.Model):
|
||||||
relevant_exp = fields.Float(string="Relevant Experience")
|
relevant_exp = fields.Float(string="Relevant Experience")
|
||||||
total_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year')
|
total_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year')
|
||||||
relevant_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year')
|
relevant_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year')
|
||||||
notice_period = fields.Integer(string="Notice Period")
|
notice_period = fields.Char(string="Notice Period")
|
||||||
notice_period_type = fields.Selection([('day',"Day's"),('month',"Month's"),('year',"Year's")], string='Type', default='day')
|
notice_period_type = fields.Selection([('day',"Day's"),('month',"Month's"),('year',"Year's")], string='Type', default='day', invisible=True)
|
||||||
|
|
||||||
current_ctc = fields.Float(string="Current CTC", aggregator="avg", help="Applicant Current Salary", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")
|
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_expected = fields.Float("Expected CTC", aggregator="avg", help="Salary Expected by Applicant", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,21 @@ class EmployeeRecruitmentAttachments(models.Model):
|
||||||
recruitment_attachment_id = fields.Many2one('recruitment.attachments')
|
recruitment_attachment_id = fields.Many2one('recruitment.attachments')
|
||||||
recruitment_attachment_type = fields.Selection([('personal','Personal Documents'),('education','Education Documents'),('previous_employer','Previous Employer'),('others','Others')],related='recruitment_attachment_id.attachment_type')
|
recruitment_attachment_type = fields.Selection([('personal','Personal Documents'),('education','Education Documents'),('previous_employer','Previous Employer'),('others','Others')],related='recruitment_attachment_id.attachment_type')
|
||||||
file = fields.Binary(string='File', required=True)
|
file = fields.Binary(string='File', required=True)
|
||||||
|
review_status = fields.Selection([('draft','Under Review'),('pass','PASS'),('fail','FAIL')], default='draft')
|
||||||
|
review_comments = fields.Char()
|
||||||
|
|
||||||
|
def action_preview_file(self):
|
||||||
|
""" Returns a URL to preview the attachment in a popup """
|
||||||
|
for record in self:
|
||||||
|
if record.file:
|
||||||
|
attachment = self.env.ref("hr_recruitment_extended.employee_recruitment_attachments_preview")
|
||||||
|
attachment.datas = record.file
|
||||||
|
return {
|
||||||
|
'name': "File Preview",
|
||||||
|
'type': 'ir.actions.act_url',
|
||||||
|
'url': f'/web/content/{attachment.id}?download=false',
|
||||||
|
'target': 'current', # Opens in a new tab
|
||||||
|
}
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,4 @@ from odoo import models, fields, api, _
|
||||||
class ResPartner(models.Model):
|
class ResPartner(models.Model):
|
||||||
_inherit = 'res.partner'
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
contact_type = fields.Selection([('internal','Internal'),('external','External')], required=True, default='external')
|
contact_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='external')
|
||||||
|
|
@ -26,3 +26,7 @@ hr_recruitment.access_hr_recruitment_stage_user,hr.recruitment.stage.user,hr_rec
|
||||||
|
|
||||||
|
|
||||||
access_application_stage_status,application.stage.status,model_application_stage_status,base.group_user,1,1,1,1
|
access_application_stage_status,application.stage.status,model_application_stage_status,base.group_user,1,1,1,1
|
||||||
|
|
||||||
|
|
||||||
|
access_ats_invite_mail_template_wizard,ats.invite.mail.template.wizard.user,hr_recruitment_extended.model_ats_invite_mail_template_wizard,,1,1,1,1
|
||||||
|
access_client_submission_mails_template_wizard,client.submission.mails.template.wizard.user,hr_recruitment_extended.model_client_submission_mails_template_wizard,,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,436 @@
|
||||||
|
import publicWidget from "@web/legacy/js/public/public_widget";
|
||||||
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
import { rpc } from "@web/core/network/rpc";
|
||||||
|
import { assets, loadCSS, loadJS } from "@web/core/assets";
|
||||||
|
|
||||||
|
publicWidget.registry.hrRecruitmentDocs = publicWidget.Widget.extend({
|
||||||
|
selector: "#doc_request_form",
|
||||||
|
|
||||||
|
events: {
|
||||||
|
"change [name='candidate_image']": "previewApplicantPhoto",
|
||||||
|
"click #delete-photo-btn": "deleteCandidatePhoto",
|
||||||
|
"click #preview-photo-btn": "previewFullImage",
|
||||||
|
'change .attachment-input': 'handleAttachmentUpload',
|
||||||
|
'click .upload-new-btn': 'handleUploadNewFile',
|
||||||
|
'click .delete-file-btn': 'handleDeleteUploadedFile',
|
||||||
|
'input .file-name-input': 'handleFileNameChange',
|
||||||
|
"click .remove-file": "removeFile",
|
||||||
|
"click .preview-file": "previewFile",
|
||||||
|
"click .view-attachments-btn": "openAttachmentModal", // Opens modal with files
|
||||||
|
"click .close-modal-btn": "closeAttachmentModal", // Close modal
|
||||||
|
"submit": "handleFormSubmit",
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
uploadedFiles: {}, // Store files per attachment ID
|
||||||
|
addUploadedFileRow(attachmentId, file, base64String) {
|
||||||
|
const tableBody = this.$(`#preview_body_${attachmentId}`);
|
||||||
|
|
||||||
|
if (!this.uploadedFiles[attachmentId]) {
|
||||||
|
this.uploadedFiles[attachmentId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique file ID using attachmentId and a timestamp
|
||||||
|
const fileId = `${attachmentId}-${Date.now()}`;
|
||||||
|
|
||||||
|
const fileRecord = {
|
||||||
|
attachment_rec_id : attachmentId,
|
||||||
|
id: fileId, // Unique file ID
|
||||||
|
name: file.name,
|
||||||
|
base64: base64String,
|
||||||
|
type: file.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.uploadedFiles[attachmentId].push(fileRecord);
|
||||||
|
|
||||||
|
const fileIndex = this.uploadedFiles[attachmentId].length - 1;
|
||||||
|
const previewImageId = `preview_image_${fileId}`;
|
||||||
|
const fileNameInputId = `file_name_input_${fileId}`;
|
||||||
|
let previewContent = '';
|
||||||
|
let previewClickHandler = '';
|
||||||
|
|
||||||
|
// Check if the file is an image or PDF and set preview content accordingly
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||||
|
<img src="data:image/png;base64,${base64String}" style="max-width: 100%; max-height: 100%; object-fit: contain; cursor: pointer;" />
|
||||||
|
</div>`;
|
||||||
|
previewClickHandler = () => {
|
||||||
|
this.$('#modal_attachment_photo_preview').attr('src', `data:image/png;base64,${base64String}`);
|
||||||
|
this.$('#modal_attachment_photo_preview').show();
|
||||||
|
this.$('#modal_attachment_pdf_preview').hide(); // Hide PDF preview
|
||||||
|
this.$('#attachmentPreviewModal').modal('show');
|
||||||
|
};
|
||||||
|
} else if (file.type === 'application/pdf') {
|
||||||
|
previewContent = `<div class="file-preview-wrapper" style="width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; border: 1px solid #ccc; border-radius: 5px; overflow: hidden;">
|
||||||
|
<iframe src="data:application/pdf;base64,${base64String}" style="width: 100%; height: 100%; border: none; cursor: pointer;"></iframe>
|
||||||
|
</div>`;
|
||||||
|
previewClickHandler = () => {
|
||||||
|
this.$('#modal_attachment_pdf_preview').attr('src', `data:application/pdf;base64,${base64String}`);
|
||||||
|
this.$('#modal_attachment_pdf_preview').show();
|
||||||
|
this.$('#modal_attachment_photo_preview').hide(); // Hide image preview
|
||||||
|
this.$('#attachmentPreviewModal').modal('show');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append new row to the table with a preview and buttons
|
||||||
|
tableBody.append(`
|
||||||
|
<tr data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control file-name-input" id="${fileNameInputId}" value="${file.name}"/>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<div class="d-flex flex-column align-items-center justify-content-center gap-2">
|
||||||
|
${previewContent}
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-danger btn-sm delete-file-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-info btn-sm preview-btn" data-attachment-id="${attachmentId}" data-file-id="${fileId}">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
|
||||||
|
this.$(`#preview_table_container_${attachmentId}`).removeClass('d-none');
|
||||||
|
|
||||||
|
// Attach click handler for preview (image or PDF)
|
||||||
|
this.$(`#preview_wrapper_${fileId}`).on('click', previewClickHandler);
|
||||||
|
|
||||||
|
// Attach click handler for the preview button (to trigger modal)
|
||||||
|
this.$(`.preview-btn[data-attachment-id="${attachmentId}"][data-file-id="${fileId}"]`).on('click', previewClickHandler);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDeleteUploadedFile(ev) {
|
||||||
|
const button = ev.currentTarget;
|
||||||
|
const attachmentId = $(button).data('attachment-id');
|
||||||
|
const fileId = $(button).data('file-id');
|
||||||
|
|
||||||
|
// Find the index of the file to delete based on unique file ID
|
||||||
|
const fileIndex = this.uploadedFiles[attachmentId].findIndex(f => f.id === fileId);
|
||||||
|
|
||||||
|
if (fileIndex !== -1) {
|
||||||
|
this.uploadedFiles[attachmentId].splice(fileIndex, 1); // Remove from array
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the row from DOM
|
||||||
|
this.$(`tr[data-file-id="${fileId}"]`).remove();
|
||||||
|
|
||||||
|
// Hide table if no files left
|
||||||
|
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||||
|
this.$(`#preview_table_container_${attachmentId}`).addClass('d-none');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
handleAttachmentUpload(ev) {
|
||||||
|
const input = ev.target;
|
||||||
|
const attachmentId = $(input).data('attachment-id');
|
||||||
|
|
||||||
|
if (input.files.length > 0) {
|
||||||
|
Array.from(input.files).forEach((file) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const base64String = e.target.result.split(',')[1];
|
||||||
|
this.addUploadedFileRow(attachmentId, file, base64String);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUploadNewFile(ev) {
|
||||||
|
console.log("hello upload file");
|
||||||
|
const button = $(ev.currentTarget);
|
||||||
|
const attachmentId = button.data('attachment-id');
|
||||||
|
const index = button.data('index');
|
||||||
|
|
||||||
|
// Find the hidden file input specific to this attachmentId
|
||||||
|
const hiddenInput = this.$(`.upload-new-file-input[data-attachment-id='${attachmentId}']`);
|
||||||
|
|
||||||
|
// When file is selected, update preview and replace old file
|
||||||
|
hiddenInput.off('change').on('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return; // Do nothing if no file is selected
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const base64String = event.target.result.split(',')[1];
|
||||||
|
|
||||||
|
// Replace the existing file in uploadedFiles
|
||||||
|
this.uploadedFiles[attachmentId][index] = {
|
||||||
|
name: file.name,
|
||||||
|
base64: base64String,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the preview image in the table
|
||||||
|
const imageId = `preview_image_${attachmentId}_${index}`;
|
||||||
|
const filePreviewWrapperId = `preview_wrapper_${attachmentId}_${index}`;
|
||||||
|
|
||||||
|
// Check if the file is an image or PDF and update accordingly
|
||||||
|
const fileType = file.type;
|
||||||
|
|
||||||
|
if (fileType.startsWith('image/')) {
|
||||||
|
this.$(`#${filePreviewWrapperId}`).html(`
|
||||||
|
<img id="${imageId}" src="data:image/png;base64,${base64String}" class="img-thumbnail"
|
||||||
|
style="width: 80px; height: 80px; object-fit: cover; cursor: pointer;" />
|
||||||
|
`);
|
||||||
|
} else if (fileType === 'application/pdf') {
|
||||||
|
this.$(`#${filePreviewWrapperId}`).html(`
|
||||||
|
<iframe src="data:application/pdf;base64,${base64String}" width="80" height="80" class="img-thumbnail" style="border: none; cursor: pointer;"></iframe>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Update the file name in the input field
|
||||||
|
const fileNameInputId = `file_name_input_${attachmentId}_${index}`;
|
||||||
|
this.$(`#${fileNameInputId}`).val(file.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger the hidden file input to open the file selection dialog
|
||||||
|
console.log("Triggering file input...");
|
||||||
|
hiddenInput.trigger('click'); // Ensure this is working properly
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
handleFileNameChange(event) {
|
||||||
|
const attachmentId = $(event.target).closest('tr').data('attachment-id');
|
||||||
|
const fileId = $(event.target).closest('tr').data('file-id');
|
||||||
|
const newFileName = event.target.value;
|
||||||
|
|
||||||
|
if (!attachmentId || !fileId) {
|
||||||
|
console.error('Missing attachmentId or fileId');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileList = this.uploadedFiles[attachmentId];
|
||||||
|
|
||||||
|
if (!fileList) {
|
||||||
|
console.error(`No files found for attachmentId: ${attachmentId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileRecord = fileList.find(file => file.id === fileId);
|
||||||
|
|
||||||
|
if (!fileRecord) {
|
||||||
|
console.error(`File with ID ${fileId} not found under attachment ${attachmentId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileRecord.name = newFileName;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
renderFilePreview(attachmentId) {
|
||||||
|
const container = this.$(`#preview_container_${attachmentId}`);
|
||||||
|
container.empty();
|
||||||
|
|
||||||
|
if (this.uploadedFiles[attachmentId].length === 0) {
|
||||||
|
container.addClass("d-none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.removeClass("d-none");
|
||||||
|
|
||||||
|
this.uploadedFiles[attachmentId].forEach((file, index) => {
|
||||||
|
const fileHtml = $(`
|
||||||
|
<div class="d-flex flex-column align-items-center">
|
||||||
|
<div class="position-relative">
|
||||||
|
<img src="${file.base64}" class="rounded-circle shadow-sm" style="width: 80px; height: 80px; object-fit: cover; border: 1px solid #ddd; cursor: pointer;" data-index="${index}" data-attachment-id="${attachmentId}" />
|
||||||
|
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 remove-file" data-attachment-id="${attachmentId}" data-index="${index}" style="transform: translate(50%, -50%);"><i class="fa fa-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
<small>${file.name}</small>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
fileHtml.find("img").on("click", this.previewAttachmentImage.bind(this));
|
||||||
|
fileHtml.find(".remove-file").on("click", this.removeFile.bind(this));
|
||||||
|
|
||||||
|
container.append(fileHtml);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
previewAttachmentImage(ev) {
|
||||||
|
const attachmentId = $(ev.currentTarget).data("attachment-id");
|
||||||
|
const index = $(ev.currentTarget).data("index");
|
||||||
|
const fileData = this.uploadedFiles[attachmentId][index];
|
||||||
|
|
||||||
|
this.$("#attachment_modal_preview").attr("src", fileData.base64);
|
||||||
|
this.$("#attachmentPreviewModal").modal("show");
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFile(ev) {
|
||||||
|
const attachmentId = $(ev.currentTarget).data("attachment-id");
|
||||||
|
const index = $(ev.currentTarget).data("index");
|
||||||
|
|
||||||
|
this.uploadedFiles[attachmentId].splice(index, 1);
|
||||||
|
this.renderFilePreview(attachmentId);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
previewFile(ev) {
|
||||||
|
const fileUrl = ev.currentTarget.dataset.fileUrl;
|
||||||
|
const modal = this.$("#photoPreviewModal");
|
||||||
|
|
||||||
|
this.$("#modal_photo_preview").attr("src", fileUrl);
|
||||||
|
modal.modal("show");
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open modal and display uploaded files
|
||||||
|
*/
|
||||||
|
openAttachmentModal(event) {
|
||||||
|
console.log("openAttachmentModal");
|
||||||
|
const rowId = $(event.currentTarget).closest("tr").index(); // Get the row index
|
||||||
|
this.currentRowId = rowId; // Store rowId for reference
|
||||||
|
this.renderAttachmentModal(rowId); // Render the modal for the row
|
||||||
|
},
|
||||||
|
|
||||||
|
renderAttachmentModal(rowId) {
|
||||||
|
const fileList = this.uploadedFiles && this.uploadedFiles[rowId] ? this.uploadedFiles[rowId] : [];
|
||||||
|
|
||||||
|
let modalHtml = `
|
||||||
|
<div id="attachmentModal" class="modal fade show" tabindex="-1" style="display: block; background: rgba(0,0,0,0.5);" aria-modal="true">
|
||||||
|
<div class="modal-dialog modal-lg" style="max-width: 600px;">
|
||||||
|
<div class="modal-content" style="border-radius: 8px; box-shadow: 0px 5px 15px rgba(0, 0, 0, 0.2);">
|
||||||
|
<!-- Modal Header -->
|
||||||
|
<div class="modal-header" style="background: #143d5d; color: white; border-bottom: 2px solid #dee2e6; font-weight: bold; padding: 12px 15px;">
|
||||||
|
<h5 class="modal-title" style="margin: 0;">Uploaded Attachments</h5>
|
||||||
|
<button type="button" class="close close-modal-btn" data-dismiss="modal" aria-label="Close" style="background: none; border: none; font-size: 20px; color: white; cursor: pointer;">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Body -->
|
||||||
|
<div class="modal-body" style="padding: 15px;">
|
||||||
|
<ul class="attachment-list" style="list-style: none; padding: 0; margin: 0; max-height: 300px; overflow-y: auto;">
|
||||||
|
${fileList.length ? fileList.map((file, index) => `
|
||||||
|
<li style="display: flex; justify-content: space-between; align-items: center; padding: 10px 15px; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 8px; background: #f8f9fa;">
|
||||||
|
<span style="font-weight: 500; flex-grow: 1;">${file.name}</span>
|
||||||
|
<button type="button" class="remove-file" data-index="${index}"
|
||||||
|
style="background: #dc3545; color: white; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 14px;">
|
||||||
|
<i style="margin-right: 5px;">🗑️</i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
`).join("") : `<p style="color: #6c757d; text-align: center; font-size: 16px; padding: 10px;">No files uploaded.</p>`}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Footer -->
|
||||||
|
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 12px;">
|
||||||
|
<button type="button" class="btn close-modal-btn" data-dismiss="modal"
|
||||||
|
style="background: #6c757d; color: white; border: none; padding: 8px 15px; border-radius: 5px; cursor: pointer; font-size: 14px;">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Remove old modal and append new one
|
||||||
|
$("#attachmentModal").remove();
|
||||||
|
$("body").append(modalHtml);
|
||||||
|
|
||||||
|
// Attach remove event to new modal content
|
||||||
|
$("#attachmentModal").on("click", ".remove-file", this.removeFile.bind(this));
|
||||||
|
$("#attachmentModal").on("click", ".close-modal-btn", this.closeAttachmentModal.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close attachment modal
|
||||||
|
*/
|
||||||
|
closeAttachmentModal() {
|
||||||
|
console.log("Closing modal");
|
||||||
|
$("#attachmentModal").remove(); // Remove the modal from the DOM
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// Function to preview the uploaded image
|
||||||
|
previewApplicantPhoto(ev) {
|
||||||
|
const input = ev.currentTarget;
|
||||||
|
const preview = this.$("#photo_preview");
|
||||||
|
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const base64String = e.target.result.split(",")[1]; // Get only Base64 part
|
||||||
|
preview.attr("src", e.target.result);
|
||||||
|
|
||||||
|
// Store the base64 in a hidden input field
|
||||||
|
this.$("input[name='candidate_image_base64']").val(base64String);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to delete the uploaded image
|
||||||
|
deleteCandidatePhoto() {
|
||||||
|
const preview = this.$("#photo_preview");
|
||||||
|
const inputFile = this.$("input[name='candidate_image']");
|
||||||
|
|
||||||
|
preview.attr("src", "data:image/png;base64,"); // Reset preview
|
||||||
|
inputFile.val(""); // Reset file input
|
||||||
|
},
|
||||||
|
|
||||||
|
// Function to preview full image inside a modal
|
||||||
|
previewFullImage() {
|
||||||
|
const previewSrc = this.$("#photo_preview").attr("src");
|
||||||
|
if (previewSrc) {
|
||||||
|
this.$("#modal_photo_preview").attr("src", previewSrc);
|
||||||
|
this.$("#photoPreviewModal").modal("show"); // Use jQuery to show the modal
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
handleFormSubmit(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
let attachments = [];
|
||||||
|
let fileReadPromises = [];
|
||||||
|
let attachmentInputs = this.uploadedFiles; // your object {1: Array(1), 2: Array(2)}
|
||||||
|
|
||||||
|
Object.keys(attachmentInputs).forEach(key => {
|
||||||
|
let filesArray = attachmentInputs[key]; // This is an array
|
||||||
|
filesArray.forEach(file => {
|
||||||
|
attachments.push({
|
||||||
|
attachment_rec_id: file.attachment_rec_id,
|
||||||
|
file_name: file.name,
|
||||||
|
file_content: file.base64, // Assuming base64 is already present
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Promise.all(fileReadPromises).then(() => {
|
||||||
|
this.$("#attachments_data_json").val(JSON.stringify(attachments));
|
||||||
|
|
||||||
|
let formElement = this.$el.is("form") ? this.$el[0] : this.$el.find("form")[0];
|
||||||
|
if (formElement) {
|
||||||
|
formElement.submit();
|
||||||
|
} else {
|
||||||
|
console.error("Form element not found.");
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Error reading files:", error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.$el.on("submit", this.handleFormSubmit.bind(this));
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -50,6 +50,7 @@
|
||||||
|
|
||||||
<field name="send_post_onboarding_form"/>
|
<field name="send_post_onboarding_form"/>
|
||||||
<field name="post_onboarding_form_status" readonly="not send_post_onboarding_form"/>
|
<field name="post_onboarding_form_status" readonly="not send_post_onboarding_form"/>
|
||||||
|
<field name="doc_requests_form_status" readonly="1"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|
||||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||||
|
|
@ -69,6 +70,10 @@
|
||||||
<button string="Send Post Onboarding Form" name="send_post_onboarding_form_to_candidate" type="object"
|
<button string="Send Post Onboarding Form" name="send_post_onboarding_form_to_candidate" type="object"
|
||||||
groups="hr.group_hr_user"
|
groups="hr.group_hr_user"
|
||||||
invisible="not employee_id or not send_post_onboarding_form or post_onboarding_form_status in ['email_sent_to_candidate','done']"/>
|
invisible="not employee_id or not send_post_onboarding_form or post_onboarding_form_status in ['email_sent_to_candidate','done']"/>
|
||||||
|
<button string="Request Documents" name="send_pre_onboarding_doc_request_form_to_candidate" type="object"
|
||||||
|
groups="hr.group_hr_user"
|
||||||
|
invisible="doc_requests_form_status in ['email_sent_to_candidate'] or send_post_onboarding_form"/>
|
||||||
|
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//notebook" position="inside">
|
<xpath expr="//notebook" position="inside">
|
||||||
<page name="Attachments" id="attachment_ids_page">
|
<page name="Attachments" id="attachment_ids_page">
|
||||||
|
|
@ -108,11 +113,18 @@
|
||||||
<!-- Group for One2many field with Full Width -->
|
<!-- Group for One2many field with Full Width -->
|
||||||
<group string="Post Onboarding Attachments" colspan="2">
|
<group string="Post Onboarding Attachments" colspan="2">
|
||||||
<field name="joining_attachment_ids" nolabel="1">
|
<field name="joining_attachment_ids" nolabel="1">
|
||||||
<list editable="bottom" default_group_by="recruitment_attachment_id">
|
<list editable="bottom" default_group_by="recruitment_attachment_id" decoration-success="review_status == 'pass'" decoration-danger="review_status == 'fail'">
|
||||||
<field name="recruitment_attachment_id"/>
|
<field name="recruitment_attachment_id"/>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="recruitment_attachment_type"/>
|
<field name="recruitment_attachment_type"/>
|
||||||
<field name="file" widget="binary" options="{'download':true}"/>
|
<field name="file" widget="binary" options="{'download':true}"/>
|
||||||
|
<button name="action_preview_file"
|
||||||
|
type="object"
|
||||||
|
string="Preview"
|
||||||
|
class="oe_highlight"
|
||||||
|
icon="fa-eye"/>
|
||||||
|
<field name="review_status"/>
|
||||||
|
<field name="review_comments" optional="hide"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</group>
|
</group>
|
||||||
|
|
|
||||||
|
|
@ -404,15 +404,6 @@
|
||||||
<field name="employee_id" invisible="1"/>
|
<field name="employee_id" invisible="1"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<!-- <group>-->
|
|
||||||
<!-- <field name="attachments" widget="one2many_list">-->
|
|
||||||
<!-- <list editable="bottom">-->
|
|
||||||
<!-- <field name="name"/>-->
|
|
||||||
<!-- <field name="file"/>-->
|
|
||||||
<!-- </list>-->
|
|
||||||
<!-- </field>-->
|
|
||||||
|
|
||||||
<!-- </group>-->
|
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
<data>
|
<data>
|
||||||
|
|
||||||
<record id="view_hr_job_recruitment_tree" model="ir.ui.view">
|
<record id="view_hr_job_recruitment_tree" model="ir.ui.view">
|
||||||
<field name="name">hr.job.recruitment.form</field>
|
<field name="name">hr.job.recruitment.list</field>
|
||||||
<field name="model">hr.job.recruitment</field>
|
<field name="model">hr.job.recruitment</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list js_class="recruitment_list_view">
|
<list js_class="recruitment_list_view">
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
<field name="alias_id" invisible="not alias_name" column_invisible="True" optional="hide"/>
|
<field name="alias_id" invisible="not alias_name" column_invisible="True" optional="hide"/>
|
||||||
<field name="user_id" widget="many2one_avatar_user" optional="hide"/>
|
<field name="user_id" widget="many2one_avatar_user" optional="hide"/>
|
||||||
<field name="no_of_employee"/>
|
<field name="no_of_employee"/>
|
||||||
|
<field name="job_priority" optional="hide"/>
|
||||||
<field name="submission_status" optional="hide"/>
|
<field name="submission_status" optional="hide"/>
|
||||||
</list>
|
</list>
|
||||||
|
|
||||||
|
|
@ -51,7 +52,9 @@
|
||||||
<!-- Add the recruitment_sequence field into the form -->
|
<!-- Add the recruitment_sequence field into the form -->
|
||||||
|
|
||||||
<form string="Job" js_class="recruitment_form_view">
|
<form string="Job" js_class="recruitment_form_view">
|
||||||
<header/> <!-- inherited in other module -->
|
<header>
|
||||||
|
<button name="send_mail_to_recruiters" type="object" string="Send Recruiters Notification" class="oe_highlight" groups="hr_recruitment.group_hr_recruitment_user"/>
|
||||||
|
</header> <!-- inherited in other module -->
|
||||||
<field name="active" invisible="1"/>
|
<field name="active" invisible="1"/>
|
||||||
<field name="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/>
|
<field name="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/>
|
||||||
<sheet>
|
<sheet>
|
||||||
|
|
@ -108,8 +111,15 @@
|
||||||
<field name="recruitment_sequence" readonly="0" force_save="1"/>
|
<field name="recruitment_sequence" readonly="0" force_save="1"/>
|
||||||
<group>
|
<group>
|
||||||
<field name="job_id" string="Job Position"/>
|
<field name="job_id" string="Job Position"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
<field name="job_category" force_save="1"/>
|
<field name="job_category" force_save="1"/>
|
||||||
</group>
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="job_priority"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<notebook>
|
<notebook>
|
||||||
|
|
@ -339,6 +349,10 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
|
<div t-if="record.job_priority.value">
|
||||||
|
<strong>Priority : </strong> <field name="job_priority"/>
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
<div t-if="record.budget.value">
|
<div t-if="record.budget.value">
|
||||||
<strong>Budget : </strong> <field name="budget"/>
|
<strong>Budget : </strong> <field name="budget"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -310,96 +310,12 @@
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</xpath>
|
</xpath>
|
||||||
<!-- <t t-if="record.applicant_ids and record.applicant_ids.value">-->
|
|
||||||
<!-- <field name="applicant_ids" widget="kanban_one2many" options="{'display_field': ['job_id']}" />-->
|
|
||||||
|
|
||||||
<!--<!– <t t-foreach="record.applicant_ids.value" t-as="application">–>-->
|
|
||||||
<!--<!– <div class="d-flex align-items-center">–>-->
|
|
||||||
<!--<!– <span class="badge bg-info">–>-->
|
|
||||||
<!--<!– <t t-if="application.job_id">–>-->
|
|
||||||
<!--<!– <t t-esc="application.job_id.value"/>–>-->
|
|
||||||
<!--<!– </t>–>-->
|
|
||||||
<!--<!– <t t-else="">No Job</t>–>-->
|
|
||||||
<!--<!– </span>–>-->
|
|
||||||
<!--<!– <span class="ms-2 text-muted">–>-->
|
|
||||||
<!--<!– <t t-if="application.stage_id">–>-->
|
|
||||||
<!--<!– <t t-esc="application.stage_id.value"/>–>-->
|
|
||||||
<!--<!– </t>–>-->
|
|
||||||
<!--<!– <t t-else="">No Stage</t>–>-->
|
|
||||||
<!--<!– </span>–>-->
|
|
||||||
<!--<!– </div>–>-->
|
|
||||||
<!--<!– </t>–>-->
|
|
||||||
<!-- </t>-->
|
|
||||||
<!-- <t t-else="">-->
|
|
||||||
<!-- <span class="text-muted">No application history</span>-->
|
|
||||||
<!-- </t>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- </xpath>-->
|
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</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"/>
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
<record id="hr_recruitment.action_hr_candidate" model="ir.actions.act_window">
|
<record id="hr_recruitment.action_hr_candidate" model="ir.actions.act_window">
|
||||||
<field name="search_view_id" ref="hr_recruitment.hr_candidate_view_search"/>
|
<field name="search_view_id" ref="hr_recruitment.hr_candidate_view_search"/>
|
||||||
<field name="context">{'search_default_my_candidates': 1}</field>
|
<field name="context">{'search_default_my_candidates': 1,'active_test': False}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<menuitem
|
<menuitem
|
||||||
|
|
|
||||||
|
|
@ -1780,6 +1780,369 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<template id="doc_request_form_template" name="FTPROTECH Doc Request Form">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<section class="container mt-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div class="card shadow-lg p-4">
|
||||||
|
<form id="doc_request_form"
|
||||||
|
t-att-action="'/FTPROTECH/submit/%s/docRequest'%(applicant.id)" method="post"
|
||||||
|
enctype="multipart/form-data">
|
||||||
|
<div>
|
||||||
|
<!-- Upload or Capture Photo -->
|
||||||
|
<input type="hidden" name="applicant_id" t-att-value="applicant.id"/>
|
||||||
|
<input type="hidden" name="candidate_image_base64"/>
|
||||||
|
<input type="hidden" name="attachments_data_json" id="attachments_data_json"/>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Applicant Photo -->
|
||||||
|
<!-- Profile Picture Upload (Similar to res.users) -->
|
||||||
|
<div class="mb-3 text-center">
|
||||||
|
<!-- Image Preview with Label Click -->
|
||||||
|
<label for="candidate_image" style="cursor: pointer;">
|
||||||
|
<img id="photo_preview"
|
||||||
|
t-att-src="'data:image/png;base64,' + (applicant.candidate_image.decode() if applicant.candidate_image else '')"
|
||||||
|
class="rounded-circle shadow"
|
||||||
|
style="display: flex; align-items: center; justify-content: center; width: 150px; height: 150px; object-fit: cover; border: 2px solid #ddd; overflow: hidden;"/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Hidden File Input -->
|
||||||
|
<input type="file" class="d-none" name="candidate_image" id="candidate_image"
|
||||||
|
accept="image/*"/>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-primary"
|
||||||
|
onclick="document.getElementById('candidate_image').click();">
|
||||||
|
<i class="fa fa-upload"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" id="delete-photo-btn">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary"
|
||||||
|
id="preview-photo-btn">
|
||||||
|
<i class="fa fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Preview Modal -->
|
||||||
|
<div class="modal fade" id="photoPreviewModal" tabindex="-1"
|
||||||
|
aria-labelledby="photoPreviewModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Image Preview</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<img id="modal_photo_preview" src=""
|
||||||
|
class="img-fluid rounded shadow"
|
||||||
|
style="max-width: 100%;"/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary"
|
||||||
|
data-bs-dismiss="modal">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="step-indicator">
|
||||||
|
<div class="step-wrapper">
|
||||||
|
<span class="step-label">Required Documents</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="s_website_form_rows s_col_no_bgcolor steps-container">
|
||||||
|
|
||||||
|
<div class="form-step active">
|
||||||
|
<div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<h5>Upload Required Attachments</h5>
|
||||||
|
<div t-if="applicant.recruitment_attachments">
|
||||||
|
<div t-foreach="applicant.recruitment_attachments"
|
||||||
|
t-as="attachment">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label t-esc="attachment.name" class="form-label"/>
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="btn btn-outline-secondary custom-file-upload">
|
||||||
|
Choose Files
|
||||||
|
<input type="file"
|
||||||
|
class="form-control d-none attachment-input"
|
||||||
|
t-att-data-attachment-id="attachment.id"
|
||||||
|
multiple="multiple"
|
||||||
|
accept="image/*,application/pdf"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table to display uploaded files -->
|
||||||
|
<div t-att-id="'preview_table_container_%s' % attachment.id"
|
||||||
|
class="uploaded-files-preview d-none">
|
||||||
|
<table class="table table-bordered table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File Name</th>
|
||||||
|
<th>Image (Preview / Upload / Delete)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody t-att-id="'preview_body_%s' % attachment.id"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div t-else="">
|
||||||
|
<p>No attachments required.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Preview Modal -->
|
||||||
|
<div class="modal fade" id="attachmentPreviewModal" tabindex="-1"
|
||||||
|
aria-labelledby="attachmentPreviewModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Attachment Preview</h5>
|
||||||
|
<button type="button" class="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<!-- Image Preview -->
|
||||||
|
<img id="modal_attachment_photo_preview" src=""
|
||||||
|
class="img-fluid rounded shadow"
|
||||||
|
style="max-width: 100%; display: none;"/>
|
||||||
|
|
||||||
|
<!-- PDF Preview -->
|
||||||
|
<iframe id="modal_attachment_pdf_preview" src=""
|
||||||
|
width="100%" height="500px"
|
||||||
|
style="border: none; display: none;"></iframe>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary"
|
||||||
|
data-bs-dismiss="modal">Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-navigation">
|
||||||
|
<button type="submit" class="btn btn-primary" id="submit-btn">Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</t>
|
||||||
|
<style>
|
||||||
|
.steps-container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 500px; /* Adjust the height */
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
.form-step {
|
||||||
|
display: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.form-step.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.scrollable-content {
|
||||||
|
max-height: 400px; /* Ensures fixed height */
|
||||||
|
overflow-y: auto; /* Enables scrolling when needed */
|
||||||
|
overflow-x: hidden; /* Prevents horizontal scrolling */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between; /* Back button on left, Submit on right */
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
position: relative; /* Change from absolute to relative */
|
||||||
|
padding: 20px 0; /* Adds spacing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-step {
|
||||||
|
margin-left: auto; /* Pushes the Next button to the right end */
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-step {
|
||||||
|
margin-right: auto; /* Pushes the Back button to the left end */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.submit-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
position: relative;
|
||||||
|
padding: 20px 20px 20px 20px
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border: 2px solid #0260EB;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0260EB;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active {
|
||||||
|
background-color: #0260EB;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 5px;
|
||||||
|
color: #0260EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 3px;
|
||||||
|
background-color: #0260EB;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start; /* Align items to the left */
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 2px;
|
||||||
|
width: 46%; /* Adjust width as needed */
|
||||||
|
box-sizing: border-box; /* Ensures padding is included in the width */
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-info {
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0; /* Ensures it does not overflow */
|
||||||
|
max-width: 60%; /* Adjust the max width if needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-level-container {
|
||||||
|
flex-grow: 1,
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
align-items: flex-start; /* Align to the left */
|
||||||
|
justify-content: flex-start;
|
||||||
|
max-width: 40%; /* Ensures it does not overflow */
|
||||||
|
flex-shrink: 0; /* Prevent shrinking */
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-level-dropdown {
|
||||||
|
width: 100%; /* Make sure the dropdown fills available width */
|
||||||
|
max-width: 250px; /* Ensure it doesn't overflow */
|
||||||
|
min-width: 0; /* Minimum width for the dropdown */
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%; /* Ensure the progress container doesn't overflow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide Relation Header but maintain space */
|
||||||
|
.relation-header {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style Relation Column (Father, Mother, etc.) */
|
||||||
|
.relation-col {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #143d5d !important;
|
||||||
|
color: #FFFFFFE6 !important;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 120px; /* Fixed width */
|
||||||
|
}
|
||||||
|
|
||||||
|
.education-relation-col {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #143d5d !important;
|
||||||
|
color: #FFFFFFE6 !important;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 120px; /* Fixed width */
|
||||||
|
}
|
||||||
|
|
||||||
|
.employer-relation-col {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #143d5d !important;
|
||||||
|
color: #FFFFFFE6 !important;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 120px; /* Fixed width */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style Table Header */
|
||||||
|
.thead-light {
|
||||||
|
background-color: #143d5d !important; /* Blue */
|
||||||
|
color: #FFFFFFE6 !important;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template id="thank_you_template" name="Thank You Page">
|
<template id="thank_you_template" name="Thank You Page">
|
||||||
<t t-call="website.layout">
|
<t t-call="website.layout">
|
||||||
<div class="container mt-5 text-center">
|
<div class="container mt-5 text-center">
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
<filter string="Client Type" name="contact_type" context="{'group_by': 'contact_type'}"/>
|
<filter string="Client Type" name="contact_type" context="{'group_by': 'contact_type'}"/>
|
||||||
</filter>
|
</filter>
|
||||||
<xpath expr="//search" position="inside">
|
<xpath expr="//search" position="inside">
|
||||||
<filter name="internal_contact" string="Internal Contact" domain="[('contact_type', '=', 'internal')]"/>
|
<filter name="internal_contact" string="In-House Contact" domain="[('contact_type', '=', 'internal')]"/>
|
||||||
<filter name="external_contact" string="External Contact" domain="[('contact_type', '=', 'external')]"/>
|
<filter name="external_contact" string="Client-Side Contact" domain="[('contact_type', '=', 'external')]"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
from . import post_onboarding_attachment_wizard
|
from . import post_onboarding_attachment_wizard
|
||||||
from . import applicant_refuse_reason
|
from . import applicant_refuse_reason
|
||||||
|
from . import ats_invite_mail_template_wizard
|
||||||
|
from . import client_submission_mail_template_wizard
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
class AtsInviteMailTemplateWizard(models.TransientModel):
|
||||||
|
_name = 'ats.invite.mail.template.wizard'
|
||||||
|
_description = 'ATS Invite Mail Template Wizard'
|
||||||
|
|
||||||
|
template_id = fields.Many2one('mail.template', string='Email Template', required=True)
|
||||||
|
partner_ids = fields.Many2many('res.users', string='Recipients', help="Recipients of this email")
|
||||||
|
email_subject = fields.Char()
|
||||||
|
email_body = fields.Html(
|
||||||
|
'Body', render_engine='qweb', render_options={'post_process': True},
|
||||||
|
prefetch=True, translate=True, sanitize='email_outgoing',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.onchange('template_id')
|
||||||
|
def _onchange_template_id(self):
|
||||||
|
""" Update the email body and recipients based on the selected template. """
|
||||||
|
if self.template_id:
|
||||||
|
record_id = self.env.context.get('active_id')
|
||||||
|
if record_id:
|
||||||
|
record = self.env[self.template_id.model].browse(record_id)
|
||||||
|
|
||||||
|
if not record.exists():
|
||||||
|
raise UserError("The record does not exist or is not accessible.")
|
||||||
|
|
||||||
|
# Fetch email template
|
||||||
|
email_template = self.env['mail.template'].browse(self.template_id.id)
|
||||||
|
|
||||||
|
if not email_template:
|
||||||
|
raise UserError("Email template not found.")
|
||||||
|
|
||||||
|
self.email_body = email_template.body_html # Assign the rendered email bodyc
|
||||||
|
self.email_subject = email_template.subject
|
||||||
|
|
||||||
|
|
||||||
|
def action_send_email(self):
|
||||||
|
""" Send email to the selected partners """
|
||||||
|
if not self.partner_ids:
|
||||||
|
raise UserError("Please select at least one recipient.")
|
||||||
|
|
||||||
|
for partner in self.partner_ids:
|
||||||
|
template = self.env.ref('hr_recruitment_extended.email_template_recruiter_assignment_template')
|
||||||
|
values = {
|
||||||
|
'email_from': self.env.company.email,
|
||||||
|
'email_to': partner.email,
|
||||||
|
}
|
||||||
|
render_ctx = dict(recruiter_name=partner.employee_id.name)
|
||||||
|
# Use 'with_context' to override the email template fields dynamically
|
||||||
|
template.sudo().with_context(default_body_html=self.email_body, default_subject=self.email_subject,**render_ctx).send_mail(self.env.context.get('active_id'),email_values=values, force_send=True)
|
||||||
|
|
||||||
|
return {'type': 'ir.actions.act_window_close'} # Close wizard after sending
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="view_ats_invite_mail_template_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">ats.invite.mail.template.wizard.form</field>
|
||||||
|
<field name="model">ats.invite.mail.template.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="template_id" options="{'no_create': True}" required="1" readonly="1" force_save="1"/>
|
||||||
|
<field name="partner_ids" widget="many2many_tags" domain="[('email', '!=', False)]"/>
|
||||||
|
<field name="email_subject" options="{'dynamic_placeholder': true}" placeholder="e.g. "Welcome to MyCompany" or "Nice to meet you, {{ object.name }}""/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Body">
|
||||||
|
<field name="email_body" widget="html_mail" class="oe-bordered-editor"
|
||||||
|
options="{'codeview': true, 'dynamic_placeholder': true}"/>
|
||||||
|
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
<footer>
|
||||||
|
<button name="action_send_email" type="object" string="Send" class="btn-primary"/>
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_open_ats_invite_mail_template_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Send Email</field>
|
||||||
|
<field name="res_model">ats.invite.mail.template.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="view_ats_invite_mail_template_wizard_form"/>
|
||||||
|
<field name="target">new</field>
|
||||||
|
<field name="context">{'default_template_id':
|
||||||
|
ref('hr_recruitment_extended.email_template_recruiter_assignment_template')}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
class ClientSubmissionsMailTemplateWizard(models.TransientModel):
|
||||||
|
_name = 'client.submission.mails.template.wizard'
|
||||||
|
_description = 'Client Submission Mails Template Wizard'
|
||||||
|
|
||||||
|
template_id = fields.Many2one('mail.template', string='Email Template')
|
||||||
|
submit_date = fields.Date(string='Submission Date', required=True, default=fields.Date.today())
|
||||||
|
send_email_from_odoo = fields.Boolean(string="Send Email From Odoo", default=False)
|
||||||
|
email_from = fields.Char('Email From')
|
||||||
|
email_to = fields.Char('Email To')
|
||||||
|
email_cc = fields.Text('Email CC')
|
||||||
|
email_subject = fields.Char()
|
||||||
|
email_body = fields.Html(
|
||||||
|
'Body', render_engine='qweb', render_options={'post_process': True},
|
||||||
|
prefetch=True, translate=True, sanitize='email_outgoing',
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.onchange('template_id')
|
||||||
|
def _onchange_template_id(self):
|
||||||
|
""" Update the email body and recipients based on the selected template. """
|
||||||
|
if self.template_id:
|
||||||
|
record_id = self.env.context.get('active_id')
|
||||||
|
if record_id:
|
||||||
|
record = self.env[self.template_id.model].browse(record_id)
|
||||||
|
|
||||||
|
if not record.exists():
|
||||||
|
raise UserError("The record does not exist or is not accessible.")
|
||||||
|
|
||||||
|
# Fetch email template
|
||||||
|
email_template = self.env['mail.template'].browse(self.template_id.id)
|
||||||
|
|
||||||
|
if not email_template:
|
||||||
|
raise UserError("Email template not found.")
|
||||||
|
|
||||||
|
self.email_from = record.user_id.partner_id.email
|
||||||
|
self.email_to = record.hr_job_recruitment.requested_by.email
|
||||||
|
self.email_body = email_template.body_html # Assign the rendered email bodyc
|
||||||
|
self.email_subject = email_template.subject
|
||||||
|
|
||||||
|
|
||||||
|
def action_send_email(self):
|
||||||
|
""" Send email to the selected partners """
|
||||||
|
record_id = self.env.context.get('active_id')
|
||||||
|
for rec in self:
|
||||||
|
record = self.env[self.template_id.model].browse(record_id)
|
||||||
|
template = self.env.ref('hr_recruitment_extended.application_client_submission_email_template')
|
||||||
|
values = {
|
||||||
|
'email_from': rec.email_from,
|
||||||
|
'email_to': rec.email_to,
|
||||||
|
'email_cc': rec.email_cc,
|
||||||
|
}
|
||||||
|
render_ctx = dict(client_name=record.hr_job_recruitment.requested_by.name)
|
||||||
|
# Use 'with_context' to override the email template fields dynamically
|
||||||
|
template.sudo().with_context(default_body_html=self.email_body, default_subject=self.email_subject,**render_ctx).send_mail(self.env.context.get('active_id'),email_values=values, force_send=True)
|
||||||
|
record.sudo().write({
|
||||||
|
'submitted_to_client': True,
|
||||||
|
'client_submission_date': rec.submit_date,
|
||||||
|
'submitted_stage': record.recruitment_stage_id.id,
|
||||||
|
})
|
||||||
|
return {'type': 'ir.actions.act_window_close'} # Close wizard after sending
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<record id="view_client_submission_mails_template_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">client.submission.mails.template.wizard.form</field>
|
||||||
|
<field name="model">client.submission.mails.template.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="submit_date"/>
|
||||||
|
<field name="send_email_from_odoo"/>
|
||||||
|
<separator string="Section Title"/>
|
||||||
|
<field name="template_id" options="{'no_create': True}" invisible="not send_email_from_odoo" required="send_email_from_odoo" readonly="1" force_save="1"/>
|
||||||
|
<field name="email_from" required="send_email_from_odoo" invisible="not send_email_from_odoo" placeholder="Comma-separated recipient addresses"/>
|
||||||
|
<field name="email_to" required="send_email_from_odoo" invisible="not send_email_from_odoo"/>
|
||||||
|
<field name="email_cc" invisible="not send_email_from_odoo" placeholder="Comma-separated carbon copy recipients addresses"/>
|
||||||
|
<field name="email_subject" required="send_email_from_odoo" options="{'dynamic_placeholder': true}" placeholder="e.g. "Welcome to MyCompany" or "Nice to meet you, {{ object.name }}"" invisible="not send_email_from_odoo"/>
|
||||||
|
</group>
|
||||||
|
<notebook invisible="not send_email_from_odoo">
|
||||||
|
<page string="Body" invisible="not send_email_from_odoo">
|
||||||
|
<field name="email_body" required="send_email_from_odoo" widget="html_mail" class="oe-bordered-editor"
|
||||||
|
options="{'codeview': true, 'dynamic_placeholder': true}" invisible="not send_email_from_odoo"/>
|
||||||
|
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
<footer>
|
||||||
|
<button name="action_send_email" type="object" string="submit" class="btn-primary"/>
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_open_client_submission_mails_template_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Send Email</field>
|
||||||
|
<field name="res_model">client.submission.mails.template.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="view_ats_invite_mail_template_wizard_form"/>
|
||||||
|
<field name="target">new</field>
|
||||||
|
<field name="context">{'default_template_id':
|
||||||
|
ref('hr_recruitment_extended.application_client_submission_email_template')}
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -8,6 +8,7 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
'recruitment.attachments',
|
'recruitment.attachments',
|
||||||
string='Attachments to Request'
|
string='Attachments to Request'
|
||||||
)
|
)
|
||||||
|
is_pre_onboarding_attachment_request = fields.Boolean(default=False)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def default_get(self, fields_list):
|
def default_get(self, fields_list):
|
||||||
|
|
@ -27,8 +28,13 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
|
|
||||||
applicant.recruitment_attachments = [(4, attachment.id) for attachment in self.attachment_ids]
|
applicant.recruitment_attachments = [(4, attachment.id) for attachment in self.attachment_ids]
|
||||||
|
|
||||||
|
|
||||||
|
if self.is_pre_onboarding_attachment_request:
|
||||||
|
template = self.env.ref('hr_recruitment_extended.email_template_request_documents', raise_if_not_found=False)
|
||||||
|
else:
|
||||||
template = self.env.ref('hr_recruitment_extended.email_template_post_onboarding_form', raise_if_not_found=False)
|
template = self.env.ref('hr_recruitment_extended.email_template_post_onboarding_form', raise_if_not_found=False)
|
||||||
|
|
||||||
|
|
||||||
personal_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'personal').mapped('name')
|
personal_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'personal').mapped('name')
|
||||||
education_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'education').mapped('name')
|
education_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'education').mapped('name')
|
||||||
previous_employer_docs = self.attachment_ids.filtered(
|
previous_employer_docs = self.attachment_ids.filtered(
|
||||||
|
|
@ -53,6 +59,9 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
||||||
applicant.id, force_send=True, email_values=email_values
|
applicant.id, force_send=True, email_values=email_values
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.is_pre_onboarding_attachment_request:
|
||||||
|
applicant.doc_requests_form_status = 'email_sent_to_candidate'
|
||||||
|
else:
|
||||||
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
||||||
|
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,6 @@
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<menuitem id="menu_recruitment_requisition" name="Recruitment Requisition" parent="hr_recruitment.menu_hr_recruitment_root"/>
|
<menuitem id="menu_recruitment_requisition" name="Recruitment Requisition" active='0' parent="hr_recruitment.menu_hr_recruitment_root"/>
|
||||||
<menuitem id="menu_recruitment_requisition_main" name="Requisitions" parent="menu_recruitment_requisition" action="action_recruitment_requisition"/>
|
<menuitem id="menu_recruitment_requisition_main" name="Requisitions" parent="menu_recruitment_requisition" action="action_recruitment_requisition"/>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -480,13 +480,16 @@ class WebsiteJobHrRecruitment(WebsiteHrRecruitment):
|
||||||
values['partner_phone'] = partner_phone
|
values['partner_phone'] = partner_phone
|
||||||
if partner_email:
|
if partner_email:
|
||||||
values['email_from'] = partner_email
|
values['email_from'] = partner_email
|
||||||
|
notice_period_str = 'N/A'
|
||||||
|
if notice_period and notice_period_type:
|
||||||
|
notice_period_str = str(notice_period) + ' ' + str(notice_period_type)
|
||||||
data = super().extract_data(model, values)
|
data = super().extract_data(model, values)
|
||||||
data['record']['current_ctc'] = float(current_ctc if current_ctc else 0)
|
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']['salary_expected'] = float(expected_ctc if expected_ctc else 0)
|
||||||
data['record']['exp_type'] = exp_type if exp_type else 'fresher'
|
data['record']['exp_type'] = exp_type if exp_type else 'fresher'
|
||||||
data['record']['current_location'] = current_location if current_location else ''
|
data['record']['current_location'] = current_location if current_location else ''
|
||||||
data['record']['current_organization'] = current_organization if current_organization 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'] = notice_period_str if notice_period_str else 'N/A'
|
||||||
data['record']['notice_period_type'] = notice_period_type if notice_period_type else 'day'
|
data['record']['notice_period_type'] = notice_period_type if notice_period_type else 'day'
|
||||||
data['record']['hr_job_recruitment'] = int(hr_job_recruitment) if str(hr_job_recruitment).isdigit() else ''
|
data['record']['hr_job_recruitment'] = int(hr_job_recruitment) if str(hr_job_recruitment).isdigit() else ''
|
||||||
data['record']['department_id'] = int(department_id) if str(department_id).isdigit() else ''
|
data['record']['department_id'] = int(department_id) if str(department_id).isdigit() else ''
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue