feature/odoo18 #2
|
|
@ -4,11 +4,12 @@
|
|||
'category': 'Human Resources',
|
||||
'summary': 'Manage and update weekly timesheets for CWF department',
|
||||
'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': [
|
||||
# 'views/timesheet_form.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/timesheet_view.xml',
|
||||
'views/timesheet_weekly_view.xml',
|
||||
'data/email_template.xml',
|
||||
],
|
||||
'assets': {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<odoo>
|
||||
<data noupdate="1">
|
||||
<data noupdate="0">
|
||||
<record id="email_template_timesheet_update" model="mail.template">
|
||||
<field name="name">Timesheet Update Reminder</field>
|
||||
<field name="email_from">${(user.email or '')}</field>
|
||||
|
|
|
|||
|
|
@ -1,25 +1,72 @@
|
|||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from datetime import timedelta
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import datetime as dt
|
||||
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):
|
||||
_name = 'cwf.timesheet'
|
||||
_description = 'CWF Weekly Timesheet'
|
||||
_rec_name = 'name'
|
||||
|
||||
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_end_date = fields.Date(string='Week End Date', required=True)
|
||||
total_hours = fields.Float(string='Total Hours', required=True)
|
||||
status = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('submitted', 'Submitted')
|
||||
], default='draft', string='Status')
|
||||
lines = fields.One2many('cwf.timesheet.line','week_id')
|
||||
|
||||
|
||||
cwf_calendar_id = fields.Many2one('cwf.timesheet.calendar')
|
||||
|
||||
def send_timesheet_update_email(self):
|
||||
template = self.env.ref('cwf_timesheet.email_template_timesheet_update')
|
||||
|
|
@ -31,40 +78,219 @@ class CwfTimesheet(models.Model):
|
|||
raise UserError('The start date cannot be after the end date.')
|
||||
|
||||
# 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
|
||||
while current_date <= end_date:
|
||||
for employee in employees:
|
||||
self.env['cwf.timesheet.line'].create({
|
||||
'week_id': self.id,
|
||||
'employee_id': employee.id,
|
||||
'week_day':current_date,
|
||||
})
|
||||
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({
|
||||
'week_id': self.id,
|
||||
'employee_id': employee.id,
|
||||
'week_day':current_date,
|
||||
})
|
||||
current_date += timedelta(days=1)
|
||||
self.status = 'submitted'
|
||||
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:
|
||||
email_values = {
|
||||
'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',
|
||||
}
|
||||
|
||||
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):
|
||||
_name = 'cwf.timesheet.line'
|
||||
_description = 'CWF Weekly Timesheet Lines'
|
||||
_rec_name = 'employee_id'
|
||||
|
||||
employee_id = fields.Many2one('hr.employee', string='Employee')
|
||||
week_id = fields.Many2one('cwf.timesheet', 'Week')
|
||||
weekly_timesheet = fields.Many2one('cwf.weekly.timesheet')
|
||||
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')
|
||||
check_in_date = fields.Datetime(string='Checkin')
|
||||
check_out_date = fields.Datetime(string='Checkout ')
|
||||
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):
|
||||
if self.state_type == 'draft' or not self.state_type:
|
||||
|
|
@ -77,9 +303,23 @@ class CwfTimesheetLine(models.Model):
|
|||
def _update_attendance(self):
|
||||
attendance_obj = self.env['hr.attendance']
|
||||
for record in self:
|
||||
attendance_obj.sudo().create({
|
||||
'employee_id': record.employee_id.id,
|
||||
'check_in': record.check_in_date,
|
||||
'check_out': record.check_out_date,
|
||||
})
|
||||
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({
|
||||
'employee_id': record.employee_id.id,
|
||||
'check_in': record.check_in_date - timedelta(hours=5, minutes=30),
|
||||
'check_out': record.check_out_date - timedelta(hours=5, minutes=30),
|
||||
})
|
||||
record.is_updated = True
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
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_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 { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
// Apply patch to NetflixProfileContainer prototype
|
||||
patch(NetflixProfileContainer.prototype, {
|
||||
|
|
@ -24,9 +25,14 @@ patch(NetflixProfileContainer.prototype, {
|
|||
/**
|
||||
* 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
|
||||
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
|
||||
this.action.doAction({
|
||||
name: "Timesheets",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,52 @@
|
|||
<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">
|
||||
<field name="name">CWF Timesheet</field>
|
||||
<field name="res_model">cwf.timesheet</field>
|
||||
<record id="view_cwf_timesheet_calendar_list" model="ir.ui.view">
|
||||
<field name="name">cwf.timesheet.calendar.list</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>
|
||||
</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">
|
||||
<field name="name">cwf.timesheet.form</field>
|
||||
<field name="model">cwf.timesheet</field>
|
||||
|
|
@ -21,63 +63,21 @@
|
|||
<group>
|
||||
<!-- Section for Employee and Date Range -->
|
||||
<group>
|
||||
<field name="department_id" readonly="status != 'draft'"/>
|
||||
<field name="week_start_date" readonly="status != 'draft'"/>
|
||||
<field name="week_end_date" readonly="status != 'draft'"/>
|
||||
</group>
|
||||
<!-- Section for Hours and Status -->
|
||||
<group>
|
||||
<field name="total_hours" readonly="status != 'draft'"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</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_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_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 id="action_cwf_timesheet" model="ir.actions.act_window">
|
||||
<field name="name">CWF Timesheet</field>
|
||||
<field name="res_model">cwf.timesheet</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
||||
# Categories can be used to filter modules in modules listing
|
||||
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||
# for the full list
|
||||
'category': 'Human Resources/Attendances',
|
||||
'version': '0.1',
|
||||
'license': 'LGPL-3',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['base','hr','hr_attendance','hr_holidays','hr_employee_extended'],
|
||||
|
|
@ -23,21 +23,11 @@
|
|||
# always loaded
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/security.xml',
|
||||
'data/cron.xml',
|
||||
'data/sequence.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_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
|
||||
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_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>
|
||||
</record>
|
||||
<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"/>
|
||||
</record>
|
||||
|
||||
<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="category_id" ref="hr_employee_extended.module_internal_user_category"/>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
'security/security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/cron.xml',
|
||||
'data/data.xml',
|
||||
'data/sequence.xml',
|
||||
'data/mail_template.xml',
|
||||
'views/job_category.xml',
|
||||
|
|
@ -43,6 +44,8 @@
|
|||
'views/skills.xml',
|
||||
'wizards/post_onboarding_attachment_wizard.xml',
|
||||
'wizards/applicant_refuse_reason.xml',
|
||||
'wizards/ats_invite_mail_template_wizard.xml',
|
||||
'wizards/client_submission_mail_template_wizard.xml',
|
||||
# 'views/resume_pearser.xml',
|
||||
],
|
||||
'assets': {
|
||||
|
|
@ -51,6 +54,7 @@
|
|||
],
|
||||
'web.assets_frontend': [
|
||||
'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',
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
website=True)
|
||||
|
|
@ -102,6 +160,7 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
else:
|
||||
return request.not_found()
|
||||
|
||||
|
||||
@http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
|
||||
methods=['POST'], website=True, csrf=False)
|
||||
def process_employee_joining_form(self,applicant_id,**post):
|
||||
|
|
@ -209,12 +268,17 @@ class website_hr_recruitment_applications(http.Controller):
|
|||
|
||||
if attachments_data:
|
||||
applicant_data['joining_attachment_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')
|
||||
]
|
||||
(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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
</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">
|
||||
<field name="name">Joining Formalities Notification</field>
|
||||
|
|
@ -421,5 +519,104 @@
|
|||
<field name="auto_delete" eval="True"/>
|
||||
</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>
|
||||
</odoo>
|
||||
|
|
@ -20,6 +20,12 @@ class HRApplicant(models.Model):
|
|||
refused_stage = fields.Many2one('hr.recruitment.stage')
|
||||
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
|
||||
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)
|
||||
|
|
@ -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')
|
||||
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')
|
||||
|
||||
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_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')
|
||||
|
|
@ -129,11 +135,19 @@ class HRApplicant(models.Model):
|
|||
warnings.warn(
|
||||
"Max no of submissions for this JD has been reached",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
rec.submitted_to_client = True
|
||||
rec.client_submission_date = fields.Datetime.now()
|
||||
rec.submitted_stage = rec.recruitment_stage_id.id
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'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):
|
||||
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):
|
||||
res = super(HRApplicant, self)._track_template(changes)
|
||||
applicant = self[0]
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ class HRJobRecruitment(models.Model):
|
|||
'res.partner', "Job Location", default=_default_address_id,
|
||||
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)
|
||||
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",
|
||||
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.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')
|
||||
def _onchange_requested_by(self):
|
||||
for rec in self:
|
||||
|
|
@ -209,7 +229,7 @@ class HRJobRecruitment(models.Model):
|
|||
|
||||
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')
|
||||
def onchange_job_id(self):
|
||||
|
|
|
|||
|
|
@ -199,8 +199,8 @@ class HRApplicant(models.Model):
|
|||
relevant_exp = fields.Float(string="Relevant Experience")
|
||||
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')
|
||||
notice_period = fields.Integer(string="Notice Period")
|
||||
notice_period_type = fields.Selection([('day',"Day's"),('month',"Month's"),('year',"Year's")], string='Type', default='day')
|
||||
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', invisible=True)
|
||||
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -22,8 +22,21 @@ class EmployeeRecruitmentAttachments(models.Model):
|
|||
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')
|
||||
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
|
||||
def create(self, vals):
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@ from odoo import models, fields, api, _
|
|||
class ResPartner(models.Model):
|
||||
_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')
|
||||
|
|
@ -25,4 +25,8 @@ hr_recruitment.access_hr_applicant_interviewer,hr.applicant.interviewer,hr_recru
|
|||
hr_recruitment.access_hr_recruitment_stage_user,hr.recruitment.stage.user,hr_recruitment.model_hr_recruitment_stage,hr_recruitment.group_hr_recruitment_user,1,1,1,0
|
||||
|
||||
|
||||
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="post_onboarding_form_status" readonly="not send_post_onboarding_form"/>
|
||||
<field name="doc_requests_form_status" readonly="1"/>
|
||||
</xpath>
|
||||
|
||||
<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"
|
||||
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']"/>
|
||||
<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 expr="//notebook" position="inside">
|
||||
<page name="Attachments" id="attachment_ids_page">
|
||||
|
|
@ -108,11 +113,18 @@
|
|||
<!-- Group for One2many field with Full Width -->
|
||||
<group string="Post Onboarding Attachments" colspan="2">
|
||||
<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="name"/>
|
||||
<field name="recruitment_attachment_type"/>
|
||||
<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>
|
||||
</field>
|
||||
</group>
|
||||
|
|
|
|||
|
|
@ -404,15 +404,6 @@
|
|||
<field name="employee_id" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<!-- <group>-->
|
||||
<!-- <field name="attachments" widget="one2many_list">-->
|
||||
<!-- <list editable="bottom">-->
|
||||
<!-- <field name="name"/>-->
|
||||
<!-- <field name="file"/>-->
|
||||
<!-- </list>-->
|
||||
<!-- </field>-->
|
||||
|
||||
<!-- </group>-->
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<data>
|
||||
|
||||
<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="arch" type="xml">
|
||||
<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="user_id" widget="many2one_avatar_user" optional="hide"/>
|
||||
<field name="no_of_employee"/>
|
||||
<field name="job_priority" optional="hide"/>
|
||||
<field name="submission_status" optional="hide"/>
|
||||
</list>
|
||||
|
||||
|
|
@ -51,7 +52,9 @@
|
|||
<!-- Add the recruitment_sequence field into the form -->
|
||||
|
||||
<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="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/>
|
||||
<sheet>
|
||||
|
|
@ -108,7 +111,14 @@
|
|||
<field name="recruitment_sequence" readonly="0" force_save="1"/>
|
||||
<group>
|
||||
<field name="job_id" string="Job Position"/>
|
||||
<field name="job_category" force_save="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="job_category" force_save="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="job_priority"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
</div>
|
||||
|
|
@ -339,6 +349,10 @@
|
|||
</button>
|
||||
</div>
|
||||
<br/><br/>
|
||||
<div t-if="record.job_priority.value">
|
||||
<strong>Priority : </strong> <field name="job_priority"/>
|
||||
</div>
|
||||
<br/>
|
||||
<div t-if="record.budget.value">
|
||||
<strong>Budget : </strong> <field name="budget"/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -310,96 +310,12 @@
|
|||
</div>
|
||||
</t>
|
||||
</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>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
<menuitem
|
||||
|
|
|
|||
|
|
@ -1780,6 +1780,369 @@
|
|||
</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">
|
||||
<t t-call="website.layout">
|
||||
<div class="container mt-5 text-center">
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
<filter string="Client Type" name="contact_type" context="{'group_by': 'contact_type'}"/>
|
||||
</filter>
|
||||
<xpath expr="//search" position="inside">
|
||||
<filter name="internal_contact" string="Internal Contact" domain="[('contact_type', '=', 'internal')]"/>
|
||||
<filter name="external_contact" string="External Contact" domain="[('contact_type', '=', 'external')]"/>
|
||||
<filter name="internal_contact" string="In-House Contact" domain="[('contact_type', '=', 'internal')]"/>
|
||||
<filter name="external_contact" string="Client-Side Contact" domain="[('contact_type', '=', 'external')]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
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',
|
||||
string='Attachments to Request'
|
||||
)
|
||||
is_pre_onboarding_attachment_request = fields.Boolean(default=False)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
|
|
@ -27,7 +28,12 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
|
||||
applicant.recruitment_attachments = [(4, attachment.id) for attachment in self.attachment_ids]
|
||||
|
||||
template = self.env.ref('hr_recruitment_extended.email_template_post_onboarding_form', raise_if_not_found=False)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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')
|
||||
|
|
@ -53,6 +59,9 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
|
|||
applicant.id, force_send=True, email_values=email_values
|
||||
)
|
||||
|
||||
applicant.post_onboarding_form_status = 'email_sent_to_candidate'
|
||||
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'
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,6 @@
|
|||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_recruitment_requisition" name="Recruitment Requisition" parent="hr_recruitment.menu_hr_recruitment_root"/>
|
||||
<menuitem id="menu_recruitment_requisition" 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"/>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -480,13 +480,16 @@ class WebsiteJobHrRecruitment(WebsiteHrRecruitment):
|
|||
values['partner_phone'] = partner_phone
|
||||
if 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['record']['current_ctc'] = float(current_ctc if current_ctc else 0)
|
||||
data['record']['salary_expected'] = float(expected_ctc if expected_ctc else 0)
|
||||
data['record']['exp_type'] = exp_type if exp_type else 'fresher'
|
||||
data['record']['current_location'] = current_location if current_location else ''
|
||||
data['record']['current_organization'] = current_organization if current_organization else ''
|
||||
data['record']['notice_period'] = notice_period if notice_period else 0
|
||||
data['record']['notice_period'] = 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']['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 ''
|
||||
|
|
|
|||
Loading…
Reference in New Issue