Recruitment & Attendance module changes

This commit is contained in:
Pranay 2025-04-07 09:54:26 +05:30
parent 96e99017c8
commit be91a849f4
46 changed files with 2082 additions and 668 deletions

View File

@ -4,11 +4,12 @@
'category': 'Human Resources', 'category': 'Human Resources',
'summary': 'Manage and update weekly timesheets for CWF department', 'summary': 'Manage and update weekly timesheets for CWF department',
'author': 'Your Name or Company', 'author': 'Your Name or Company',
'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard'], 'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard','hr_employee_extended'],
'data': [ 'data': [
# 'views/timesheet_form.xml', # 'views/timesheet_form.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/timesheet_view.xml', 'views/timesheet_view.xml',
'views/timesheet_weekly_view.xml',
'data/email_template.xml', 'data/email_template.xml',
], ],
'assets': { 'assets': {

View File

@ -1,5 +1,5 @@
<odoo> <odoo>
<data noupdate="1"> <data noupdate="0">
<record id="email_template_timesheet_update" model="mail.template"> <record id="email_template_timesheet_update" model="mail.template">
<field name="name">Timesheet Update Reminder</field> <field name="name">Timesheet Update Reminder</field>
<field name="email_from">${(user.email or '')}</field> <field name="email_from">${(user.email or '')}</field>

View File

@ -1,25 +1,72 @@
from odoo import models, fields, api from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError from odoo.exceptions import ValidationError, UserError
from datetime import timedelta from datetime import datetime, timedelta
import datetime as dt
from odoo import _ from odoo import _
class CwfTimesheetYearly(models.Model):
_name = 'cwf.timesheet.calendar'
_description = "CWF Timesheet Calendar"
_rec_name = 'name'
name = fields.Char(string='Year Name', required=True)
week_period = fields.One2many('cwf.timesheet','cwf_calendar_id')
_sql_constraints = [
('unique_year', 'unique(name)', 'The year must be unique!')
]
@api.constrains('name')
def _check_year_format(self):
for record in self:
if not record.name.isdigit() or len(record.name) != 4:
raise ValidationError("Year Name must be a 4-digit number.")
def generate_week_period(self):
for record in self:
record.week_period.unlink()
year = int(record.name)
# Find the first Monday of the year
start_date = datetime(year, 1, 1)
while start_date.weekday() != 0: # Monday is 0 in weekday()
start_date += timedelta(days=1)
# Generate weeks from Monday to Sunday
while start_date.year == year or (start_date - timedelta(days=1)).year == year:
end_date = start_date + timedelta(days=6)
self.env['cwf.timesheet'].create({
'name': f'Week {start_date.strftime("%W")}, {year}',
'week_start_date': start_date.date(),
'week_end_date': end_date.date(),
'cwf_calendar_id': record.id,
})
start_date += timedelta(days=7)
def action_generate_weeks(self):
self.generate_week_period()
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
class CwfTimesheet(models.Model): class CwfTimesheet(models.Model):
_name = 'cwf.timesheet' _name = 'cwf.timesheet'
_description = 'CWF Weekly Timesheet' _description = 'CWF Weekly Timesheet'
_rec_name = 'name'
name = fields.Char(string='Week Name', required=True) name = fields.Char(string='Week Name', required=True)
department_id = fields.Many2one('hr.department', string='Department')
week_start_date = fields.Date(string='Week Start Date', required=True) week_start_date = fields.Date(string='Week Start Date', required=True)
week_end_date = fields.Date(string='Week End Date', required=True) week_end_date = fields.Date(string='Week End Date', required=True)
total_hours = fields.Float(string='Total Hours', required=True)
status = fields.Selection([ status = fields.Selection([
('draft', 'Draft'), ('draft', 'Draft'),
('submitted', 'Submitted') ('submitted', 'Submitted')
], default='draft', string='Status') ], default='draft', string='Status')
lines = fields.One2many('cwf.timesheet.line','week_id') lines = fields.One2many('cwf.timesheet.line','week_id')
cwf_calendar_id = fields.Many2one('cwf.timesheet.calendar')
def send_timesheet_update_email(self): def send_timesheet_update_email(self):
template = self.env.ref('cwf_timesheet.email_template_timesheet_update') template = self.env.ref('cwf_timesheet.email_template_timesheet_update')
@ -31,40 +78,219 @@ class CwfTimesheet(models.Model):
raise UserError('The start date cannot be after the end date.') raise UserError('The start date cannot be after the end date.')
# Get all employees in the department # Get all employees in the department
employees = self.env['hr.employee'].search([('department_id', '=', self.department_id.id)]) external_group_id = self.env.ref("hr_employee_extended.group_external_user")
users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)])
employees = self.env['hr.employee'].search([('user_id', 'in', users.ids),'|',('doj','=',False),('doj','>=', self.week_start_date)])
print(employees)
# Loop through each day of the week and create timesheet lines for each employee # Loop through each day of the week and create timesheet lines for each employee
while current_date <= end_date: while current_date <= end_date:
for employee in employees: for employee in employees:
self.env['cwf.timesheet.line'].create({ existing_record = self.env['cwf.weekly.timesheet'].search([
'week_id': self.id, ('week_id', '=', self.id),
'employee_id': employee.id, ('employee_id', '=', employee.id)
'week_day':current_date, ], 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) current_date += timedelta(days=1)
self.status = 'submitted' self.status = 'submitted'
for employee in employees: for employee in employees:
weekly_timesheet_exists = self.env['cwf.weekly.timesheet'].sudo().search([('week_id','=',self.id),('employee_id','=',employee.id)])
if not weekly_timesheet_exists:
weekly_timesheet = self.env['cwf.weekly.timesheet'].sudo().create({
'week_id': self.id,
'employee_id': employee.id,
'status': 'draft'
})
# Generate the URL for the newly created weekly_timesheet
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
record_url = f"{base_url}/web#id={weekly_timesheet.id}&view_type=form&model=cwf.weekly.timesheet"
weekly_timesheet.update_attendance()
if employee.work_email: if employee.work_email:
email_values = { email_values = {
'email_to': employee.work_email, 'email_to': employee.work_email,
'body_html': template.body_html, # Email body from template 'body_html': template.body_html.replace(
'https://ftprotech.in/odoo/action-261',
record_url
), # Email body from template
'subject': 'Timesheet Update Notification', 'subject': 'Timesheet Update Notification',
} }
template.send_mail(self.id, email_values=email_values, force_send=True) template.send_mail(self.id, email_values=email_values, force_send=True)
class CwfWeeklyTimesheet(models.Model):
_name = "cwf.weekly.timesheet"
_description = "CWF Weekly Timesheet"
_rec_name = 'employee_id'
week_id = fields.Many2one('cwf.timesheet', 'Week')
employee_id = fields.Many2one('hr.employee', default=lambda self: self.env.user.employee_id.id)
cwf_timesheet_lines = fields.One2many('cwf.timesheet.line' ,'weekly_timesheet')
status = fields.Selection([('draft','Draft'),('submitted','Submitted')], default='draft')
week_start_date = fields.Date(related='week_id.week_start_date')
week_end_date = fields.Date(related='week_id.week_end_date')
@api.constrains('week_id', 'employee_id')
def _check_unique_week_employee(self):
for record in self:
# Search for existing records with the same week_id and employee_id
existing_record = self.env['cwf.weekly.timesheet'].search([
('week_id', '=', record.week_id.id),
('employee_id', '=', record.employee_id.id)
], limit=1)
# If an existing record is found and it's not the current record (in case of update), raise an error
if existing_record and existing_record.id != record.id:
raise ValidationError("A timesheet for this employee already exists for the selected week.")
def update_attendance(self):
for rec in self:
# Get the week start and end date
week_start_date = rec.week_id.week_start_date
week_end_date = rec.week_id.week_end_date
# Convert start and end dates to datetime objects for proper filtering
week_start_datetime = datetime.combine(week_start_date, datetime.min.time())
week_end_datetime = datetime.combine(week_end_date, datetime.max.time())
# Delete timesheet lines that are outside the week range
rec.cwf_timesheet_lines.filtered(lambda line:
line.week_day < week_start_date or line.week_day > week_end_date
).unlink()
# Search for attendance records that fall within the week period and match the employee
hr_attendance_records = self.env['hr.attendance'].sudo().search([
('check_in', '>=', week_start_datetime),
('check_out', '<=', week_end_datetime),
('employee_id', '=', rec.employee_id.id)
])
# Group the attendance records by date
attendance_by_date = {}
for attendance in hr_attendance_records:
attendance_date = attendance.check_in.date()
if attendance_date not in attendance_by_date:
attendance_by_date[attendance_date] = []
attendance_by_date[attendance_date].append(attendance)
# Get all the dates within the week period
all_week_dates = [week_start_date + timedelta(days=i) for i in
range((week_end_date - week_start_date).days + 1)]
# Create or update timesheet lines for each day in the week
for date in all_week_dates:
# Check if there is attendance for this date
if date in attendance_by_date:
# If there are multiple attendance records, take the earliest check_in and latest check_out
earliest_check_in = min(attendance.check_in for attendance in attendance_by_date[date])
latest_check_out = max(attendance.check_out for attendance in attendance_by_date[date])
if (earliest_check_in + timedelta(hours=5, minutes=30)).date() > date:
earliest_check_in = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
if (latest_check_out + timedelta(hours=5, minutes=30)).date() > date:
latest_check_out = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
# Check if a timesheet line for this employee, week, and date already exists
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if existing_timesheet_line:
# If it exists, update the existing record
existing_timesheet_line.write({
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If it doesn't exist, create a new timesheet line with present state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If no attendance exists for this date, create a new timesheet line with time_off state_type
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if not existing_timesheet_line:
if date.weekday() != 5 and date.weekday() != 6:
# If no record exists for this date, create a new timesheet line with time_off state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'state_type': 'time_off',
})
def action_submit(self):
for rec in self:
for timesheet in rec.cwf_timesheet_lines:
timesheet.action_submit()
rec.status = 'submitted'
class CwfTimesheetLine(models.Model): class CwfTimesheetLine(models.Model):
_name = 'cwf.timesheet.line' _name = 'cwf.timesheet.line'
_description = 'CWF Weekly Timesheet Lines' _description = 'CWF Weekly Timesheet Lines'
_rec_name = 'employee_id'
employee_id = fields.Many2one('hr.employee', string='Employee') weekly_timesheet = fields.Many2one('cwf.weekly.timesheet')
week_id = fields.Many2one('cwf.timesheet', 'Week') employee_id = fields.Many2one('hr.employee', string='Employee', related='weekly_timesheet.employee_id')
week_id = fields.Many2one('cwf.timesheet', 'Week', related='weekly_timesheet.week_id')
week_day = fields.Date(string='Date') week_day = fields.Date(string='Date')
check_in_date = fields.Datetime(string='Checkin') check_in_date = fields.Datetime(string='Checkin')
check_out_date = fields.Datetime(string='Checkout ') check_out_date = fields.Datetime(string='Checkout ')
is_updated = fields.Boolean('Attendance Updated') is_updated = fields.Boolean('Attendance Updated')
state_type = fields.Selection([('draft','Draft'),('holiday', 'Holiday'),('time_off','Time Off'),('present','Present')], default='draft') state_type = fields.Selection([('draft','Draft'),('holiday', 'Holiday'),('time_off','Time Off'),('half_day','Half Day'),('present','Present')], default='draft', required=True)
@api.constrains('week_day', 'check_in_date', 'check_out_date')
def _check_week_day_and_times(self):
for record in self:
# Ensure week_day is within the week range
if record.week_id:
if record.week_day < record.week_id.week_start_date or record.week_day > record.week_id.week_end_date:
raise ValidationError(
"The selected 'week_day' must be within the range of the week from %s to %s." %
(record.week_id.week_start_date, record.week_id.week_end_date)
)
# Ensure check_in_date and check_out_date are on the selected week_day
if record.check_in_date:
if record.check_in_date.date() != record.week_day:
raise ValidationError(
"The 'check_in_date' must be on the selected Date."
)
if record.check_out_date:
if record.check_out_date.date() != record.week_day:
raise ValidationError(
"The 'check_out_date' must be on the selected Date."
)
def action_submit(self): def action_submit(self):
if self.state_type == 'draft' or not self.state_type: if self.state_type == 'draft' or not self.state_type:
@ -77,9 +303,23 @@ class CwfTimesheetLine(models.Model):
def _update_attendance(self): def _update_attendance(self):
attendance_obj = self.env['hr.attendance'] attendance_obj = self.env['hr.attendance']
for record in self: for record in self:
attendance_obj.sudo().create({ if record.check_in_date != False and record.check_out_date != False and record.employee_id:
'employee_id': record.employee_id.id, first_check_in = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
'check_in': record.check_in_date, ('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
'check_out': record.check_out_date, 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 record.is_updated = True

View File

@ -1,4 +1,12 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_cwf_timesheet_user,access.cwf.timesheet,model_cwf_timesheet,,1,1,1,1 access_cwf_timesheet_user,access.cwf.timesheet,model_cwf_timesheet,base.group_user,1,0,0,0
access_cwf_timesheet_manager,access.cwf.timesheet,model_cwf_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar,cwf_timesheet_calendar,model_cwf_timesheet_calendar,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar_user,cwf_timesheet_calendar_user,model_cwf_timesheet_calendar,base.group_user,1,0,0,0
access_cwf_timesheet_line_user,access.cwf.timesheet.line,model_cwf_timesheet_line,,1,1,1,1 access_cwf_timesheet_line_user,access.cwf.timesheet.line,model_cwf_timesheet_line,,1,1,1,1
access_cwf_weekly_timesheet_user,cwf.weekly.timesheet access,model_cwf_weekly_timesheet,,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_cwf_timesheet_user access.cwf.timesheet model_cwf_timesheet base.group_user 1 1 0 1 0 1 0
3 access_cwf_timesheet_manager access.cwf.timesheet model_cwf_timesheet hr_attendance.group_hr_attendance_manager 1 1 1 1
4 access_cwf_timesheet_calendar cwf_timesheet_calendar model_cwf_timesheet_calendar hr_attendance.group_hr_attendance_manager 1 1 1 1
5 access_cwf_timesheet_calendar_user cwf_timesheet_calendar_user model_cwf_timesheet_calendar base.group_user 1 0 0 0
6 access_cwf_timesheet_line_user access.cwf.timesheet.line model_cwf_timesheet_line 1 1 1 1
7 access_cwf_weekly_timesheet_user cwf.weekly.timesheet access model_cwf_weekly_timesheet 1 1 1 1
8
9
10
11
12

View File

@ -2,6 +2,7 @@
import { patch } from "@web/core/utils/patch"; import { patch } from "@web/core/utils/patch";
import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component"; import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
import { user } from "@web/core/user";
// Apply patch to NetflixProfileContainer prototype // Apply patch to NetflixProfileContainer prototype
patch(NetflixProfileContainer.prototype, { patch(NetflixProfileContainer.prototype, {
@ -24,9 +25,14 @@ patch(NetflixProfileContainer.prototype, {
/** /**
* Override the hr_timesheets method * Override the hr_timesheets method
*/ */
hr_timesheets() { async hr_timesheets() {
const isExternalUser = await user.hasGroup("hr_employee_extended.group_external_user");
// Check the department of the logged-in employee // Check the department of the logged-in employee
if (this.state.login_employee.department_id == 'CWF') { console.log(isExternalUser);
console.log("is external user");
debugger;
if (isExternalUser && this.state.login_employee.department_id) {
console.log("hello external");
// If the department is 'CWF', perform the action to open the timesheets // If the department is 'CWF', perform the action to open the timesheets
this.action.doAction({ this.action.doAction({
name: "Timesheets", name: "Timesheets",

View File

@ -1,10 +1,52 @@
<odoo> <odoo>
<record id="view_cwf_timesheet_calendar_form" model="ir.ui.view">
<field name="name">cwf.timesheet.calendar.form</field>
<field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<form string="CWF Timesheet Calendar">
<sheet>
<group>
<field name="name"/>
<button name="action_generate_weeks" string="Generate Week Periods" type="object" class="oe_highlight"/>
</group>
<notebook>
<page string="Weeks">
<field name="week_period" context="{'order': 'week_start_date asc'}">
<list editable="bottom" decoration-success="status == 'submitted'">
<field name="name"/>
<field name="week_start_date"/>
<field name="week_end_date"/>
<field name="status"/>
<button name="send_timesheet_update_email" string="Send Update Email" invisible="status == 'submitted'" type="object" confirm="You can't revert this action. Please check twice before Submitting?" class="oe_highlight"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_cwf_timesheet" model="ir.actions.act_window"> <record id="view_cwf_timesheet_calendar_list" model="ir.ui.view">
<field name="name">CWF Timesheet</field> <field name="name">cwf.timesheet.calendar.list</field>
<field name="res_model">cwf.timesheet</field> <field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<record id="action_cwf_timesheet_calendar" model="ir.actions.act_window">
<field name="name">CWF Timesheet Calendar</field>
<field name="res_model">cwf.timesheet.calendar</field>
<field name="view_mode">list,form</field> <field name="view_mode">list,form</field>
</record> </record>
<menuitem id="menu_timesheet_calendar_form" name="CWF Timesheet Calendar" parent="hr_attendance_extended.menu_attendance_attendance" action="cwf_timesheet.action_cwf_timesheet_calendar"/>
<record id="view_timesheet_form" model="ir.ui.view"> <record id="view_timesheet_form" model="ir.ui.view">
<field name="name">cwf.timesheet.form</field> <field name="name">cwf.timesheet.form</field>
<field name="model">cwf.timesheet</field> <field name="model">cwf.timesheet</field>
@ -21,63 +63,21 @@
<group> <group>
<!-- Section for Employee and Date Range --> <!-- Section for Employee and Date Range -->
<group> <group>
<field name="department_id" readonly="status != 'draft'"/>
<field name="week_start_date" readonly="status != 'draft'"/> <field name="week_start_date" readonly="status != 'draft'"/>
<field name="week_end_date" readonly="status != 'draft'"/> <field name="week_end_date" readonly="status != 'draft'"/>
</group> </group>
<!-- Section for Hours and Status -->
<group>
<field name="total_hours" readonly="status != 'draft'"/>
</group>
</group> </group>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<menuitem id="menu_timesheet_form" name="Timesheet Form" parent="hr_attendance_extended.menu_attendance_attendance" action="action_cwf_timesheet"/> <record id="action_cwf_timesheet" model="ir.actions.act_window">
<field name="name">CWF Timesheet</field>
<record id="action_cwf_timesheet_line" model="ir.actions.act_window"> <field name="res_model">cwf.timesheet</field>
<field name="name">CWF Timesheet Lines</field> <field name="view_mode">list,form</field>
<field name="res_model">cwf.timesheet.line</field>
<field name="view_mode">list</field>
<field name="context">{'search_default_group_by_employee_id':1}</field>
</record>
<record id="view_cwf_timesheet_line_list" model="ir.ui.view">
<field name="name">cwf.timesheet.line.list</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<list editable="bottom" create="0" delete="0" decoration-success="is_updated == True">
<field name="employee_id" readonly="1" force_save="1"/>
<field name="week_day" readonly="1" force_save="1"/>
<field name="check_in_date" readonly="is_updated == True"/>
<field name="check_out_date" readonly="is_updated == True"/>
<field name="state_type" readonly="is_updated == True"/>
<button name="action_submit" type="object" string="Submit" class="btn btn-outline-primary" invisible="is_updated == True"/>
</list>
</field>
</record> </record>
<menuitem id="menu_timesheet_form_line" name="Week Timesheet " parent="hr_attendance_extended.menu_attendance_attendance" action="action_cwf_timesheet_line"/>
<record id="view_cwf_timesheet_line_search" model="ir.ui.view">
<field name="name">cwf.timesheet.line.search</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<search string="Timesheets">
<field name="employee_id"/>
<field name="week_id"/>
<field name="week_day"/>
<field name="check_in_date"/>
<field name="check_out_date"/>
<field name="state_type"/>
<group expand="0" string="Group By">
<filter string="Employee" name="employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
<filter string="Week" name="week_id" domain="[]" context="{'group_by':'week_id'}"/>
<filter string="Status" name ="state_type" domain="[]" context="{'group_by': 'state_type'}"/>
</group>
</search>
</field>
</record>
</odoo> </odoo>

View File

@ -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>

View File

@ -12,10 +12,10 @@
'website': "https://www.ftprotech.com", 'website': "https://www.ftprotech.com",
# Categories can be used to filter modules in modules listing # Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list # for the full list
'category': 'Human Resources/Attendances', 'category': 'Human Resources/Attendances',
'version': '0.1', 'version': '0.1',
'license': 'LGPL-3',
# any module necessary for this one to work correctly # any module necessary for this one to work correctly
'depends': ['base','hr','hr_attendance','hr_holidays','hr_employee_extended'], 'depends': ['base','hr','hr_attendance','hr_holidays','hr_employee_extended'],
@ -23,21 +23,11 @@
# always loaded # always loaded
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'security/security.xml',
'data/cron.xml', 'data/cron.xml',
'data/sequence.xml',
'views/hr_attendance.xml', 'views/hr_attendance.xml',
'views/day_attendance_report.xml', 'views/on_duty_form.xml',
], ],
'assets': {
'web.assets_backend': [
'hr_attendance_extended/static/src/xml/attendance_report.xml',
'hr_attendance_extended/static/src/js/attendance_report.js',
],
'web.assets_frontend': [
'web/static/lib/jquery/jquery.js',
'hr_attendance_extended/static/src/js/jquery-ui.min.js',
'hr_attendance_extended/static/src/js/jquery-ui.min.css',
]
}
} }

View File

@ -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>

View File

@ -1,2 +1,2 @@
from . import hr_attendance from . import hr_attendance
from . import hr_attendance_report from . import on_duty_form

View File

@ -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),

View File

@ -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.")

View File

@ -1,3 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_attendance_attendance_user,attendance.attendance.access,model_attendance_attendance,base.group_user,1,1,1,1 access_attendance_attendance_user,attendance.attendance.access,model_attendance_attendance,base.group_user,1,1,1,1
access_attendance_data_user,attendance.data.access,model_attendance_data,base.group_user,1,1,1,1 access_attendance_data_user,attendance.data.access,model_attendance_data,base.group_user,1,1,1,1
access_on_duty_form_user,on_duty_form_user,model_on_duty_form,base.group_user,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_attendance_attendance_user attendance.attendance.access model_attendance_attendance base.group_user 1 1 1 1
3 access_attendance_data_user attendance.data.access model_attendance_data base.group_user 1 1 1 1
4 access_on_duty_form_user on_duty_form_user model_on_duty_form base.group_user 1 1 1 1

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -7,12 +7,12 @@
<field name="sequence">17</field> <field name="sequence">17</field>
</record> </record>
<record id="group_external_user" model="res.groups"> <record id="group_external_user" model="res.groups">
<field name="name">External User</field> <field name="name">Client-Side User</field>
<field name="category_id" ref="hr_employee_extended.module_internal_user_category"/> <field name="category_id" ref="hr_employee_extended.module_internal_user_category"/>
</record> </record>
<record id="group_internal_user" model="res.groups"> <record id="group_internal_user" model="res.groups">
<field name="name">Internal User</field> <field name="name">In-House User</field>
<field name="implied_ids" eval="[(4, ref('group_external_user'))]"/> <field name="implied_ids" eval="[(4, ref('group_external_user'))]"/>
<field name="category_id" ref="hr_employee_extended.module_internal_user_category"/> <field name="category_id" ref="hr_employee_extended.module_internal_user_category"/>
</record> </record>

View File

@ -25,6 +25,7 @@
'security/security.xml', 'security/security.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/cron.xml', 'data/cron.xml',
'data/data.xml',
'data/sequence.xml', 'data/sequence.xml',
'data/mail_template.xml', 'data/mail_template.xml',
'views/job_category.xml', 'views/job_category.xml',
@ -43,6 +44,8 @@
'views/skills.xml', 'views/skills.xml',
'wizards/post_onboarding_attachment_wizard.xml', 'wizards/post_onboarding_attachment_wizard.xml',
'wizards/applicant_refuse_reason.xml', 'wizards/applicant_refuse_reason.xml',
'wizards/ats_invite_mail_template_wizard.xml',
'wizards/client_submission_mail_template_wizard.xml',
# 'views/resume_pearser.xml', # 'views/resume_pearser.xml',
], ],
'assets': { 'assets': {
@ -51,6 +54,7 @@
], ],
'web.assets_frontend': [ 'web.assets_frontend': [
'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js', 'hr_recruitment_extended/static/src/js/website_hr_applicant_form.js',
'hr_recruitment_extended/static/src/js/pre_onboarding_attachment_requests.js',
'hr_recruitment_extended/static/src/js/post_onboarding_form.js', 'hr_recruitment_extended/static/src/js/post_onboarding_form.js',
], ],
} }

View File

@ -84,6 +84,64 @@ class website_hr_recruitment_applications(http.Controller):
@http.route(['/FTPROTECH/DocRequests/<int:applicant_id>'], type='http', auth="public",
website=True)
def doc_request_form(self, applicant_id, **kwargs):
"""Renders the website form for applicants to submit additional details."""
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return request.not_found()
if applicant:
if applicant.doc_requests_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template")
else:
return request.render("hr_recruitment_extended.doc_request_form_template", {
'applicant': applicant
})
else:
return request.not_found()
@http.route(['/FTPROTECH/submit/<int:applicant_id>/docRequest'], type='http', auth="public",
methods=['POST'], website=True, csrf=False)
def process_applicant_doc_submission_form(self, applicant_id, **post):
applicant = request.env['hr.applicant'].sudo().browse(applicant_id)
if not applicant.exists():
return request.not_found() # Return 404 if applicant doesn't exist
if applicant.doc_requests_form_status == 'done':
return request.render("hr_recruitment_extended.thank_you_template")
applicant_data = {
'applicant_id': int(post.get('applicant_id', 0)),
'candidate_image': post.get('candidate_image_base64', ''),
'doc_requests_form_status': 'done'
}
applicant_data = {k: v for k, v in applicant_data.items() if v != '' and v != 0}
# attachments
attachments_data_json = post.get('attachments_data_json', '[]')
attachments_data = json.loads(attachments_data_json) if attachments_data_json else []
if attachments_data:
applicant_data['joining_attachment_ids'] = [
(4, existing_id) for existing_id in
(applicant.joining_attachment_ids).ids
] + [
(0, 0, {
'name': attachment.get('file_name', ''),
'recruitment_attachment_id': attachment.get(
'attachment_rec_id', ''),
'file': attachment.get('file_content', '')
}) for attachment in attachments_data if
attachment.get('attachment_rec_id')
]
applicant.write(applicant_data)
return request.render("hr_recruitment_extended.thank_you_template")
@http.route(['/FTPROTECH/JoiningForm/<int:applicant_id>'], type='http', auth="public", @http.route(['/FTPROTECH/JoiningForm/<int:applicant_id>'], type='http', auth="public",
website=True) website=True)
@ -102,6 +160,7 @@ class website_hr_recruitment_applications(http.Controller):
else: else:
return request.not_found() return request.not_found()
@http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public", @http.route(['/FTPROTECH/submit/<int:applicant_id>/JoinForm'], type='http', auth="public",
methods=['POST'], website=True, csrf=False) methods=['POST'], website=True, csrf=False)
def process_employee_joining_form(self,applicant_id,**post): def process_employee_joining_form(self,applicant_id,**post):
@ -209,12 +268,17 @@ class website_hr_recruitment_applications(http.Controller):
if attachments_data: if attachments_data:
applicant_data['joining_attachment_ids'] = [ applicant_data['joining_attachment_ids'] = [
(0,0,{ (4, existing_id) for existing_id in
'name': attachment.get('file_name',''), (applicant.joining_attachment_ids).ids
'recruitment_attachment_id': attachment.get('attachment_rec_id',''), ] + [
'file': attachment.get('file_content','') (0, 0, {
}) for attachment in attachments_data if attachment.get('attachment_rec_id') 'name': attachment.get('file_name', ''),
] 'recruitment_attachment_id': attachment.get(
'attachment_rec_id', ''),
'file': attachment.get('file_content', '')
}) for attachment in attachments_data if
attachment.get('attachment_rec_id')
]
applicant.write(applicant_data) applicant.write(applicant_data)

View File

@ -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>

View File

@ -191,6 +191,104 @@
<field name="auto_delete" eval="True"/> <field name="auto_delete" eval="True"/>
</record> </record>
<record id="email_template_request_documents" model="mail.template">
<field name="name">Document Submission Request</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
<field name="email_from">{{ user.company_id.email or user.email_formatted }}</field>
<field name="email_to">{{ object.email_from }}</field>
<field name="subject">Important: Document Submission Request | FTPROTECH</field>
<field name="description">
Email requesting necessary documents from applicants.
</field>
<field name="body_html" type="html">
<t t-set="applicant_name" t-value="object.candidate_id.partner_name or 'Applicant'"/>
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #333; padding: 20px; line-height: 1.6;">
<p>Dear
<strong>
<t t-esc="applicant_name">Applicant</t>
</strong>
,
</p>
<p>We hope you are doing well! As part of our recruitment process, we kindly request you to submit
the following documents at the earliest.
</p>
<t t-if="ctx.get('personal_docs') or ctx.get('education_docs') or ctx.get('previous_employer_docs') or ctx.get('other_docs')">
<p>Please ensure to provide soft copies of the required documents:</p>
<!-- Personal Documents -->
<t t-if="ctx.get('personal_docs')">
<strong>Personal Documents:</strong>
<ul>
<t t-foreach="ctx.get('personal_docs')" t-as="doc">
<li t-esc="doc"/>
</t>
</ul>
</t>
<!-- Education Documents -->
<t t-if="ctx.get('education_docs')">
<strong>Education Documents:</strong>
<ul>
<t t-foreach="ctx.get('education_docs')" t-as="doc">
<li t-esc="doc"/>
</t>
</ul>
</t>
<!-- Previous Employer Documents -->
<t t-if="ctx.get('previous_employer_docs')">
<strong>Previous Employer Documents:</strong>
<ul>
<t t-foreach="ctx.get('previous_employer_docs')" t-as="doc">
<li t-esc="doc"/>
</t>
</ul>
</t>
<!-- Additional Documents -->
<t t-if="ctx.get('other_docs')">
<strong>Additional Documents:</strong>
<ul>
<t t-foreach="ctx.get('other_docs')" t-as="doc">
<li t-esc="doc"/>
</t>
</ul>
</t>
</t>
<p>Please upload your documents via the following link:</p>
<t t-set="base_url" t-value="object.env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
<t t-set="upload_url" t-value="base_url + '/FTPROTECH/DocRequests/%s' % object.id"/>
<p style="text-align: center; margin-top: 20px;">
<a t-att-href="upload_url" target="_blank"
style="background-color: #007bff; color: #fff; padding: 10px 20px; text-decoration: none;
font-weight: bold; border-radius: 5px; display: inline-block;">
Upload Your Documents
</a>
</p>
<p>If you have any questions or need assistance, please contact us at
<a href="mailto:hr@ftprotech.com" style="color: #007bff; text-decoration: none;">
hr@ftprotech.com</a>.
</p>
<p>We appreciate your cooperation and look forward to completing the process smoothly.</p>
<p>Best Regards,
<br/>
<strong>
<t t-esc="object.company_id.name or 'HR Team'">HR Team</t>
</strong>
</p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
<record id="email_template_post_onboarding_form" model="mail.template"> <record id="email_template_post_onboarding_form" model="mail.template">
<field name="name">Joining Formalities Notification</field> <field name="name">Joining Formalities Notification</field>
@ -421,5 +519,104 @@
<field name="auto_delete" eval="True"/> <field name="auto_delete" eval="True"/>
</record> </record>
<record id="email_template_recruiter_assignment_template" model="mail.template">
<field name="name">Recruitment Assignment Notification</field>
<field name="model_id" ref="model_hr_job_recruitment"/>
<field name="email_from">{{ user.email_formatted }}</field>
<field name="email_to">{{ object.user_id.email }}</field>
<field name="subject">Job Assignment - {{ object.job_id.name }}</field>
<field name="description">
Notification to recruiter regarding new job requisition assignment.
</field>
<field name="body_html" type="html">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="ctx['recruiter_name']">Recruiter</t>,
<br/>
<br/>
A new job requisition has been assigned to you.
<br/>
<br/>
<ul>
<t t-if="object.recruitment_sequence">
<li>
<strong>Job ID:</strong>
<t t-out="object.recruitment_sequence"/>
</li>
</t>
<t t-if="object.job_id">
<li>
<strong>Job Title:</strong>
<t t-out="object.job_id.name"/>
</li>
</t>
<t t-if="object.job_priority">
<li>
<strong>Priority Level:</strong>
<t t-out="object.job_priority"/>
</li>
</t>
<t t-if="object.locations and object.locations.mapped('location_name')">
<li>
<strong>Location:</strong>
<t t-out="', '.join(object.locations.mapped('location_name'))"/>
</li>
</t>
<t t-if="object.budget">
<li>
<strong>Budget:</strong>
<t t-out="object.budget"/>
</li>
</t>
<t t-if="object.id">
<li>
<strong>Job Description:</strong>
<a t-att-href="'%s/web#id=%d&amp;model=hr.job.recruitment&amp;action=%s&amp;view_type=form' %
(object.env['ir.config_parameter'].sudo().get_param('web.base.url'), object.id, object.env.ref('hr_recruitment_extended.action_hr_job_recruitment').id)"
target="_blank">
View Job Details
</a>
</li>
</t>
</ul>
<br/>
Kindly review the requisition and start sourcing suitable candidates in the ATS.
<br/>
<br/>
Regards,
<br/>
<t t-out="user.name or 'Hiring Manager'">Hiring Manager</t>
</p>
</field>
</record>
<record id="application_client_submission_email_template" model="mail.template">
<field name="name">Applicant Client Submissions</field>
<field name="model_id" ref="hr_recruitment.model_hr_applicant"/>
<field name="email_from">{{ user.email_formatted }}</field>
<field name="email_to">{{ object.hr_job_recruitment.requested_by.email }}</field>
<field name="subject">Applicant Submission</field>
<field name="description">
Submitting the Applicant Details to Client.
</field>
<field name="body_html" type="html">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="ctx['client_name']">Sir/Madam</t>,
<br/>
<br/>
Submitting new applicant.
<br/>
Kindly review the Applicant.
<br/>
<br/>
Regards,
<br/>
<t t-out="user.name or 'Hiring Manager'">Hiring Manager</t>
</p>
</field>
</record>
</data> </data>
</odoo> </odoo>

View File

@ -20,6 +20,12 @@ class HRApplicant(models.Model):
refused_stage = fields.Many2one('hr.recruitment.stage') refused_stage = fields.Many2one('hr.recruitment.stage')
refused_comments = fields.Text() refused_comments = fields.Text()
@api.constrains('candidate_id','hr_job_recruitment')
def hr_applicant_constrains(self):
for rec in self:
if rec.candidate_id and rec.hr_job_recruitment:
self.sudo().search([('candidate_id','=',rec.candidate_id.id),('hr_job_recruitment','=',rec.hr_job_recruitment.id),('id','!=',rec.id)])
@api.model @api.model
def _read_group_recruitment_stage_ids(self, stages, domain): def _read_group_recruitment_stage_ids(self, stages, domain):
# retrieve job_id from the context and write the domain: ids + contextual columns (job or default) # retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
@ -101,7 +107,7 @@ class HRApplicant(models.Model):
second_application_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft') second_application_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
send_post_onboarding_form = fields.Boolean(related='recruitment_stage_id.post_onboarding_form') send_post_onboarding_form = fields.Boolean(related='recruitment_stage_id.post_onboarding_form')
post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft') post_onboarding_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
doc_requests_form_status = fields.Selection([('draft','Draft'),('email_sent_to_candidate','Email Sent to Candidate'),('done','Done')], default='draft')
legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked') legend_blocked = fields.Char(related='recruitment_stage_id.legend_blocked', string='Kanban Blocked')
legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid') legend_done = fields.Char(related='recruitment_stage_id.legend_done', string='Kanban Valid')
legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing') legend_normal = fields.Char(related='recruitment_stage_id.legend_normal', string='Kanban Ongoing')
@ -129,11 +135,19 @@ class HRApplicant(models.Model):
warnings.warn( warnings.warn(
"Max no of submissions for this JD has been reached", "Max no of submissions for this JD has been reached",
DeprecationWarning, DeprecationWarning,
stacklevel=2,
) )
rec.submitted_to_client = True return {
rec.client_submission_date = fields.Datetime.now() 'type': 'ir.actions.act_window',
rec.submitted_stage = rec.recruitment_stage_id.id 'name': 'Submission',
'res_model': 'client.submission.mails.template.wizard',
'view_mode': 'form',
'view_id': self.env.ref('hr_recruitment_extended.view_client_submission_mails_template_wizard_form').id,
'target': 'new',
'context': {'default_template_id': self.env.ref(
"hr_recruitment_extended.application_client_submission_email_template").id,
},
}
def submit_for_approval(self): def submit_for_approval(self):
for rec in self: for rec in self:
@ -208,7 +222,16 @@ class HRApplicant(models.Model):
} }
def send_pre_onboarding_doc_request_form_to_candidate(self):
return {
'type': 'ir.actions.act_window',
'name': 'Select Attachments',
'res_model': 'post.onboarding.attachment.wizard',
'view_mode': 'form',
'view_type': 'form',
'target': 'new',
'context': {'default_attachment_ids': [],'default_is_pre_onboarding_attachment_request': True}
}
def _track_template(self, changes): def _track_template(self, changes):
res = super(HRApplicant, self)._track_template(changes) res = super(HRApplicant, self)._track_template(changes)
applicant = self[0] applicant = self[0]

View File

@ -158,7 +158,7 @@ class HRJobRecruitment(models.Model):
'res.partner', "Job Location", default=_default_address_id, 'res.partner', "Job Location", default=_default_address_id,
domain="[('is_company','=',True),('contact_type','=',recruitment_type)]", domain="[('is_company','=',True),('contact_type','=',recruitment_type)]",
help="Select the location where the applicant will work. Addresses listed here are defined on the company's contact information.", exportable=False) help="Select the location where the applicant will work. Addresses listed here are defined on the company's contact information.", exportable=False)
recruitment_type = fields.Selection([('internal','Internal'),('external','External')], required=True, default='internal') recruitment_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='internal')
requested_by = fields.Many2one('res.partner', string="Requested By", requested_by = fields.Many2one('res.partner', string="Requested By",
default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]") default=lambda self: self.env.user.partner_id, domain="[('contact_type','=',recruitment_type)]")
@ -167,6 +167,26 @@ class HRJobRecruitment(models.Model):
self.requested_by = False self.requested_by = False
self.address_id = False self.address_id = False
def send_mail_to_recruiters(self):
for rec in self:
""" Open the email wizard """
users = rec.interviewer_ids.ids
primary_user = rec.user_id.id
# template = self.env.ref('hr_recruitment_extended.email_template_recruiter_assignment_template')
# template.sudo().send_mail(rec.id, force_send=True)
users.append(primary_user)
return {
'type': 'ir.actions.act_window',
'name': 'Send Email',
'res_model': 'ats.invite.mail.template.wizard',
'view_mode': 'form',
'view_id': self.env.ref('hr_recruitment_extended.view_ats_invite_mail_template_wizard_form').id,
'target': 'new',
'context': {'default_partner_ids': [(6, 0, users)],'default_template_id': self.env.ref("hr_recruitment_extended.email_template_recruiter_assignment_template").id,
},
}
@api.onchange('requested_by') @api.onchange('requested_by')
def _onchange_requested_by(self): def _onchange_requested_by(self):
for rec in self: for rec in self:
@ -209,7 +229,7 @@ class HRJobRecruitment(models.Model):
job_category = fields.Many2one("job.category", string="Category") job_category = fields.Many2one("job.category", string="Category")
job_priority = fields.Selection([('low','Low'),('medium','Medium'),('high','High')], string="Pirority")
@api.onchange('job_id','job_category') @api.onchange('job_id','job_category')
def onchange_job_id(self): def onchange_job_id(self):

View File

@ -199,8 +199,8 @@ class HRApplicant(models.Model):
relevant_exp = fields.Float(string="Relevant Experience") relevant_exp = fields.Float(string="Relevant Experience")
total_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year') total_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year')
relevant_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year') relevant_exp_type = fields.Selection([('month',"Month's"),('year',"Year's")], default='year')
notice_period = fields.Integer(string="Notice Period") notice_period = fields.Char(string="Notice Period")
notice_period_type = fields.Selection([('day',"Day's"),('month',"Month's"),('year',"Year's")], string='Type', default='day') notice_period_type = fields.Selection([('day',"Day's"),('month',"Month's"),('year',"Year's")], string='Type', default='day', invisible=True)
current_ctc = fields.Float(string="Current CTC", aggregator="avg", help="Applicant Current Salary", tracking=True, groups="hr_recruitment.group_hr_recruitment_user") current_ctc = fields.Float(string="Current CTC", aggregator="avg", help="Applicant Current Salary", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")
salary_expected = fields.Float("Expected CTC", aggregator="avg", help="Salary Expected by Applicant", tracking=True, groups="hr_recruitment.group_hr_recruitment_user") salary_expected = fields.Float("Expected CTC", aggregator="avg", help="Salary Expected by Applicant", tracking=True, groups="hr_recruitment.group_hr_recruitment_user")

View File

@ -22,8 +22,21 @@ class EmployeeRecruitmentAttachments(models.Model):
recruitment_attachment_id = fields.Many2one('recruitment.attachments') recruitment_attachment_id = fields.Many2one('recruitment.attachments')
recruitment_attachment_type = fields.Selection([('personal','Personal Documents'),('education','Education Documents'),('previous_employer','Previous Employer'),('others','Others')],related='recruitment_attachment_id.attachment_type') recruitment_attachment_type = fields.Selection([('personal','Personal Documents'),('education','Education Documents'),('previous_employer','Previous Employer'),('others','Others')],related='recruitment_attachment_id.attachment_type')
file = fields.Binary(string='File', required=True) file = fields.Binary(string='File', required=True)
review_status = fields.Selection([('draft','Under Review'),('pass','PASS'),('fail','FAIL')], default='draft')
review_comments = fields.Char()
def action_preview_file(self):
""" Returns a URL to preview the attachment in a popup """
for record in self:
if record.file:
attachment = self.env.ref("hr_recruitment_extended.employee_recruitment_attachments_preview")
attachment.datas = record.file
return {
'name': "File Preview",
'type': 'ir.actions.act_url',
'url': f'/web/content/{attachment.id}?download=false',
'target': 'current', # Opens in a new tab
}
@api.model @api.model
def create(self, vals): def create(self, vals):

View File

@ -4,4 +4,4 @@ from odoo import models, fields, api, _
class ResPartner(models.Model): class ResPartner(models.Model):
_inherit = 'res.partner' _inherit = 'res.partner'
contact_type = fields.Selection([('internal','Internal'),('external','External')], required=True, default='external') contact_type = fields.Selection([('internal','In-House'),('external','Client-Side')], required=True, default='external')

View File

@ -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 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
25
26
27
28
29
30
31
32

View File

@ -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;">
&times;
</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));
},
});

View File

@ -50,6 +50,7 @@
<field name="send_post_onboarding_form"/> <field name="send_post_onboarding_form"/>
<field name="post_onboarding_form_status" readonly="not send_post_onboarding_form"/> <field name="post_onboarding_form_status" readonly="not send_post_onboarding_form"/>
<field name="doc_requests_form_status" readonly="1"/>
</xpath> </xpath>
<xpath expr="//field[@name='stage_id']" position="attributes"> <xpath expr="//field[@name='stage_id']" position="attributes">
@ -69,6 +70,10 @@
<button string="Send Post Onboarding Form" name="send_post_onboarding_form_to_candidate" type="object" <button string="Send Post Onboarding Form" name="send_post_onboarding_form_to_candidate" type="object"
groups="hr.group_hr_user" groups="hr.group_hr_user"
invisible="not employee_id or not send_post_onboarding_form or post_onboarding_form_status in ['email_sent_to_candidate','done']"/> invisible="not employee_id or not send_post_onboarding_form or post_onboarding_form_status in ['email_sent_to_candidate','done']"/>
<button string="Request Documents" name="send_pre_onboarding_doc_request_form_to_candidate" type="object"
groups="hr.group_hr_user"
invisible="doc_requests_form_status in ['email_sent_to_candidate'] or send_post_onboarding_form"/>
</xpath> </xpath>
<xpath expr="//notebook" position="inside"> <xpath expr="//notebook" position="inside">
<page name="Attachments" id="attachment_ids_page"> <page name="Attachments" id="attachment_ids_page">
@ -108,11 +113,18 @@
<!-- Group for One2many field with Full Width --> <!-- Group for One2many field with Full Width -->
<group string="Post Onboarding Attachments" colspan="2"> <group string="Post Onboarding Attachments" colspan="2">
<field name="joining_attachment_ids" nolabel="1"> <field name="joining_attachment_ids" nolabel="1">
<list editable="bottom" default_group_by="recruitment_attachment_id"> <list editable="bottom" default_group_by="recruitment_attachment_id" decoration-success="review_status == 'pass'" decoration-danger="review_status == 'fail'">
<field name="recruitment_attachment_id"/> <field name="recruitment_attachment_id"/>
<field name="name"/> <field name="name"/>
<field name="recruitment_attachment_type"/> <field name="recruitment_attachment_type"/>
<field name="file" widget="binary" options="{'download':true}"/> <field name="file" widget="binary" options="{'download':true}"/>
<button name="action_preview_file"
type="object"
string="Preview"
class="oe_highlight"
icon="fa-eye"/>
<field name="review_status"/>
<field name="review_comments" optional="hide"/>
</list> </list>
</field> </field>
</group> </group>

View File

@ -404,15 +404,6 @@
<field name="employee_id" invisible="1"/> <field name="employee_id" invisible="1"/>
</group> </group>
</group> </group>
<!-- <group>-->
<!-- <field name="attachments" widget="one2many_list">-->
<!-- <list editable="bottom">-->
<!-- <field name="name"/>-->
<!-- <field name="file"/>-->
<!-- </list>-->
<!-- </field>-->
<!-- </group>-->
</sheet> </sheet>
</form> </form>
</field> </field>

View File

@ -3,7 +3,7 @@
<data> <data>
<record id="view_hr_job_recruitment_tree" model="ir.ui.view"> <record id="view_hr_job_recruitment_tree" model="ir.ui.view">
<field name="name">hr.job.recruitment.form</field> <field name="name">hr.job.recruitment.list</field>
<field name="model">hr.job.recruitment</field> <field name="model">hr.job.recruitment</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<list js_class="recruitment_list_view"> <list js_class="recruitment_list_view">
@ -24,6 +24,7 @@
<field name="alias_id" invisible="not alias_name" column_invisible="True" optional="hide"/> <field name="alias_id" invisible="not alias_name" column_invisible="True" optional="hide"/>
<field name="user_id" widget="many2one_avatar_user" optional="hide"/> <field name="user_id" widget="many2one_avatar_user" optional="hide"/>
<field name="no_of_employee"/> <field name="no_of_employee"/>
<field name="job_priority" optional="hide"/>
<field name="submission_status" optional="hide"/> <field name="submission_status" optional="hide"/>
</list> </list>
@ -51,7 +52,9 @@
<!-- Add the recruitment_sequence field into the form --> <!-- Add the recruitment_sequence field into the form -->
<form string="Job" js_class="recruitment_form_view"> <form string="Job" js_class="recruitment_form_view">
<header/> <!-- inherited in other module --> <header>
<button name="send_mail_to_recruiters" type="object" string="Send Recruiters Notification" class="oe_highlight" groups="hr_recruitment.group_hr_recruitment_user"/>
</header> <!-- inherited in other module -->
<field name="active" invisible="1"/> <field name="active" invisible="1"/>
<field name="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/> <field name="company_id" invisible="1" on_change="1" can_create="True" can_write="True"/>
<sheet> <sheet>
@ -108,7 +111,14 @@
<field name="recruitment_sequence" readonly="0" force_save="1"/> <field name="recruitment_sequence" readonly="0" force_save="1"/>
<group> <group>
<field name="job_id" string="Job Position"/> <field name="job_id" string="Job Position"/>
<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> </group>
</div> </div>
@ -339,6 +349,10 @@
</button> </button>
</div> </div>
<br/><br/> <br/><br/>
<div t-if="record.job_priority.value">
<strong>Priority : </strong> <field name="job_priority"/>
</div>
<br/>
<div t-if="record.budget.value"> <div t-if="record.budget.value">
<strong>Budget : </strong> <field name="budget"/> <strong>Budget : </strong> <field name="budget"/>
</div> </div>

View File

@ -310,96 +310,12 @@
</div> </div>
</t> </t>
</xpath> </xpath>
<!-- <t t-if="record.applicant_ids and record.applicant_ids.value">-->
<!-- <field name="applicant_ids" widget="kanban_one2many" options="{'display_field': ['job_id']}" />-->
<!--&lt;!&ndash; <t t-foreach="record.applicant_ids.value" t-as="application">&ndash;&gt;-->
<!--&lt;!&ndash; <div class="d-flex align-items-center">&ndash;&gt;-->
<!--&lt;!&ndash; <span class="badge bg-info">&ndash;&gt;-->
<!--&lt;!&ndash; <t t-if="application.job_id">&ndash;&gt;-->
<!--&lt;!&ndash; <t t-esc="application.job_id.value"/>&ndash;&gt;-->
<!--&lt;!&ndash; </t>&ndash;&gt;-->
<!--&lt;!&ndash; <t t-else="">No Job</t>&ndash;&gt;-->
<!--&lt;!&ndash; </span>&ndash;&gt;-->
<!--&lt;!&ndash; <span class="ms-2 text-muted">&ndash;&gt;-->
<!--&lt;!&ndash; <t t-if="application.stage_id">&ndash;&gt;-->
<!--&lt;!&ndash; <t t-esc="application.stage_id.value"/>&ndash;&gt;-->
<!--&lt;!&ndash; </t>&ndash;&gt;-->
<!--&lt;!&ndash; <t t-else="">No Stage</t>&ndash;&gt;-->
<!--&lt;!&ndash; </span>&ndash;&gt;-->
<!--&lt;!&ndash; </div>&ndash;&gt;-->
<!--&lt;!&ndash; </t>&ndash;&gt;-->
<!-- </t>-->
<!-- <t t-else="">-->
<!-- <span class="text-muted">No application history</span>-->
<!-- </t>-->
<!-- </div>-->
<!-- </div>-->
<!-- </xpath>-->
</field> </field>
</record> </record>
<!-- explicit list view definition -->
<!--
<record model="ir.ui.view" id="hr_recruitment_extended.list">
<field name="name">hr_recruitment_extended list</field>
<field name="model">hr_recruitment_extended.hr_recruitment_extended</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="value"/>
<field name="value2"/>
</tree>
</field>
</record>
-->
<!-- actions opening views on models -->
<!--
<record model="ir.actions.act_window" id="hr_recruitment_extended.action_window">
<field name="name">hr_recruitment_extended window</field>
<field name="res_model">hr_recruitment_extended.hr_recruitment_extended</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="hr_recruitment_extended.action_server">
<field name="name">hr_recruitment_extended server</field>
<field name="model_id" ref="model_hr_recruitment_extended_hr_recruitment_extended"/>
<field name="state">code</field>
<field name="code">
action = {
"type": "ir.actions.act_window",
"view_mode": "tree,form",
"res_model": model._name,
}
</field>
</record>
-->
<!-- Top menu item -->
<!--
<menuitem name="hr_recruitment_extended" id="hr_recruitment_extended.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="hr_recruitment_extended.menu_1" parent="hr_recruitment_extended.menu_root"/>
<menuitem name="Menu 2" id="hr_recruitment_extended.menu_2" parent="hr_recruitment_extended.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="hr_recruitment_extended.menu_1_list" parent="hr_recruitment_extended.menu_1"
action="hr_recruitment_extended.action_window"/>
<menuitem name="Server to list" id="hr_recruitment_extended" parent="hr_recruitment_extended.menu_2"
action="hr_recruitment_extended.action_server"/>
-->
<record id="hr_recruitment.action_hr_candidate" model="ir.actions.act_window"> <record id="hr_recruitment.action_hr_candidate" model="ir.actions.act_window">
<field name="search_view_id" ref="hr_recruitment.hr_candidate_view_search"/> <field name="search_view_id" ref="hr_recruitment.hr_candidate_view_search"/>
<field name="context">{'search_default_my_candidates': 1}</field> <field name="context">{'search_default_my_candidates': 1,'active_test': False}</field>
</record> </record>
<menuitem <menuitem

View File

@ -1780,6 +1780,369 @@
</template> </template>
<template id="doc_request_form_template" name="FTPROTECH Doc Request Form">
<t t-call="website.layout">
<section class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card shadow-lg p-4">
<form id="doc_request_form"
t-att-action="'/FTPROTECH/submit/%s/docRequest'%(applicant.id)" method="post"
enctype="multipart/form-data">
<div>
<!-- Upload or Capture Photo -->
<input type="hidden" name="applicant_id" t-att-value="applicant.id"/>
<input type="hidden" name="candidate_image_base64"/>
<input type="hidden" name="attachments_data_json" id="attachments_data_json"/>
<!-- Applicant Photo -->
<!-- Profile Picture Upload (Similar to res.users) -->
<div class="mb-3 text-center">
<!-- Image Preview with Label Click -->
<label for="candidate_image" style="cursor: pointer;">
<img id="photo_preview"
t-att-src="'data:image/png;base64,' + (applicant.candidate_image.decode() if applicant.candidate_image else '')"
class="rounded-circle shadow"
style="display: flex; align-items: center; justify-content: center; width: 150px; height: 150px; object-fit: cover; border: 2px solid #ddd; overflow: hidden;"/>
</label>
<!-- Hidden File Input -->
<input type="file" class="d-none" name="candidate_image" id="candidate_image"
accept="image/*"/>
<!-- Action Buttons -->
<div class="mt-2">
<button type="button" class="btn btn-sm btn-primary"
onclick="document.getElementById('candidate_image').click();">
<i class="fa fa-upload"></i>
</button>
<button type="button" class="btn btn-sm btn-danger" id="delete-photo-btn">
<i class="fa fa-trash"></i>
</button>
<button type="button" class="btn btn-sm btn-secondary"
id="preview-photo-btn">
<i class="fa fa-eye"></i>
</button>
</div>
</div>
<!-- Image Preview Modal -->
<div class="modal fade" id="photoPreviewModal" tabindex="-1"
aria-labelledby="photoPreviewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Image Preview</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img id="modal_photo_preview" src=""
class="img-fluid rounded shadow"
style="max-width: 100%;"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">
Close
</button>
</div>
</div>
</div>
</div>
</div>
<div class="step-indicator">
<div class="step-wrapper">
<span class="step-label">Required Documents</span>
</div>
</div>
<div class="s_website_form_rows s_col_no_bgcolor steps-container">
<div class="form-step active">
<div>
<div class="mt-3">
<h5>Upload Required Attachments</h5>
<div t-if="applicant.recruitment_attachments">
<div t-foreach="applicant.recruitment_attachments"
t-as="attachment">
<div class="mb-3">
<label t-esc="attachment.name" class="form-label"/>
<div class="input-group">
<label class="btn btn-outline-secondary custom-file-upload">
Choose Files
<input type="file"
class="form-control d-none attachment-input"
t-att-data-attachment-id="attachment.id"
multiple="multiple"
accept="image/*,application/pdf"/>
</label>
</div>
</div>
<!-- Table to display uploaded files -->
<div t-att-id="'preview_table_container_%s' % attachment.id"
class="uploaded-files-preview d-none">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>File Name</th>
<th>Image (Preview / Upload / Delete)</th>
</tr>
</thead>
<tbody t-att-id="'preview_body_%s' % attachment.id"></tbody>
</table>
</div>
</div>
</div>
<div t-else="">
<p>No attachments required.</p>
</div>
<!-- Image Preview Modal -->
<div class="modal fade" id="attachmentPreviewModal" tabindex="-1"
aria-labelledby="attachmentPreviewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Attachment Preview</h5>
<button type="button" class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<!-- Image Preview -->
<img id="modal_attachment_photo_preview" src=""
class="img-fluid rounded shadow"
style="max-width: 100%; display: none;"/>
<!-- PDF Preview -->
<iframe id="modal_attachment_pdf_preview" src=""
width="100%" height="500px"
style="border: none; display: none;"></iframe>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary"
data-bs-dismiss="modal">Close
</button>
</div>
</div>
</div>
</div>
</div>
<div class="form-navigation">
<button type="submit" class="btn btn-primary" id="submit-btn">Submit
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</section>
</t>
<style>
.steps-container {
width: 100%;
min-height: 500px; /* Adjust the height */
padding-top: 30px;
}
.form-step {
display: none;
width: 100%;
}
.form-step.active {
display: block;
}
.scrollable-content {
max-height: 400px; /* Ensures fixed height */
overflow-y: auto; /* Enables scrolling when needed */
overflow-x: hidden; /* Prevents horizontal scrolling */
}
.form-navigation {
display: flex;
justify-content: space-between; /* Back button on left, Submit on right */
align-items: center;
width: 100%;
position: relative; /* Change from absolute to relative */
padding: 20px 0; /* Adds spacing */
}
.next-step {
margin-left: auto; /* Pushes the Next button to the right end */
}
.prev-step {
margin-right: auto; /* Pushes the Back button to the left end */
}
.submit-container {
display: flex;
align-items: center;
}
.step-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
position: relative;
padding: 20px 20px 20px 20px
}
.step-wrapper {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.step {
width: 35px;
height: 35px;
border: 2px solid #0260EB;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #0260EB;
background-color: transparent;
transition: all 0.3s ease;
}
.step.active {
background-color: #0260EB;
color: white;
}
.step-label {
font-size: 14px;
margin-top: 5px;
color: #0260EB;
}
.step-line {
flex: 1;
height: 3px;
background-color: #0260EB;
margin: 0 10px;
}
.skill-card {
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
display: flex;
flex-direction: column;
align-items: flex-start; /* Align items to the left */
justify-content: space-between;
gap: 2px;
width: 46%; /* Adjust width as needed */
box-sizing: border-box; /* Ensures padding is included in the width */
}
.skill-info {
flex-grow: 1;
min-width: 0; /* Ensures it does not overflow */
max-width: 60%; /* Adjust the max width if needed */
}
.skill-level-container {
flex-grow: 1,
display: flex;
flex-direction: column;
gap: 5px;
align-items: flex-start; /* Align to the left */
justify-content: flex-start;
max-width: 40%; /* Ensures it does not overflow */
flex-shrink: 0; /* Prevent shrinking */
}
.skill-level-dropdown {
width: 100%; /* Make sure the dropdown fills available width */
max-width: 250px; /* Ensure it doesn't overflow */
min-width: 0; /* Minimum width for the dropdown */
}
.progress-container {
position: relative;
width: 100%; /* Ensure the progress container doesn't overflow */
}
.progress-bar {
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
position: absolute;
top: 0;
left: 0;
height: 100%;
}
.progress-text {
font-size: 12px;
font-weight: 600;
}
.progress {
background-color: #e0e0e0;
border-radius: 5px;
width: 100%;
height: 100%;
margin-top: 0;
}
/* Hide Relation Header but maintain space */
.relation-header {
visibility: hidden;
width: 120px;
}
/* Style Relation Column (Father, Mother, etc.) */
.relation-col {
font-weight: bold;
background-color: #143d5d !important;
color: #FFFFFFE6 !important;
text-align: center;
vertical-align: middle;
width: 120px; /* Fixed width */
}
.education-relation-col {
font-weight: bold;
background-color: #143d5d !important;
color: #FFFFFFE6 !important;
text-align: center;
vertical-align: middle;
width: 120px; /* Fixed width */
}
.employer-relation-col {
font-weight: bold;
background-color: #143d5d !important;
color: #FFFFFFE6 !important;
text-align: center;
vertical-align: middle;
width: 120px; /* Fixed width */
}
/* Style Table Header */
.thead-light {
background-color: #143d5d !important; /* Blue */
color: #FFFFFFE6 !important;
text-align: center;
}
</style>
</template>
<template id="thank_you_template" name="Thank You Page"> <template id="thank_you_template" name="Thank You Page">
<t t-call="website.layout"> <t t-call="website.layout">
<div class="container mt-5 text-center"> <div class="container mt-5 text-center">

View File

@ -14,8 +14,8 @@
<filter string="Client Type" name="contact_type" context="{'group_by': 'contact_type'}"/> <filter string="Client Type" name="contact_type" context="{'group_by': 'contact_type'}"/>
</filter> </filter>
<xpath expr="//search" position="inside"> <xpath expr="//search" position="inside">
<filter name="internal_contact" string="Internal Contact" domain="[('contact_type', '=', 'internal')]"/> <filter name="internal_contact" string="In-House Contact" domain="[('contact_type', '=', 'internal')]"/>
<filter name="external_contact" string="External Contact" domain="[('contact_type', '=', 'external')]"/> <filter name="external_contact" string="Client-Side Contact" domain="[('contact_type', '=', 'external')]"/>
</xpath> </xpath>
</field> </field>
</record> </record>

View File

@ -1,2 +1,4 @@
from . import post_onboarding_attachment_wizard from . import post_onboarding_attachment_wizard
from . import applicant_refuse_reason from . import applicant_refuse_reason
from . import ats_invite_mail_template_wizard
from . import client_submission_mail_template_wizard

View File

@ -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

View File

@ -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. &quot;Welcome to MyCompany&quot; or &quot;Nice to meet you, {{ object.name }}&quot;"/>
</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>

View File

@ -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

View File

@ -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. &quot;Welcome to MyCompany&quot; or &quot;Nice to meet you, {{ object.name }}&quot;" 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>

View File

@ -8,6 +8,7 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
'recruitment.attachments', 'recruitment.attachments',
string='Attachments to Request' string='Attachments to Request'
) )
is_pre_onboarding_attachment_request = fields.Boolean(default=False)
@api.model @api.model
def default_get(self, fields_list): def default_get(self, fields_list):
@ -27,7 +28,12 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
applicant.recruitment_attachments = [(4, attachment.id) for attachment in self.attachment_ids] applicant.recruitment_attachments = [(4, attachment.id) for attachment in self.attachment_ids]
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') personal_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'personal').mapped('name')
education_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'education').mapped('name') education_docs = self.attachment_ids.filtered(lambda a: a.attachment_type == 'education').mapped('name')
@ -53,6 +59,9 @@ class PostOnboardingAttachmentWizard(models.TransientModel):
applicant.id, force_send=True, email_values=email_values applicant.id, force_send=True, email_values=email_values
) )
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'} return {'type': 'ir.actions.act_window_close'}

View File

@ -77,6 +77,6 @@
<field name="view_mode">list,form</field> <field name="view_mode">list,form</field>
</record> </record>
<menuitem id="menu_recruitment_requisition" name="Recruitment Requisition" parent="hr_recruitment.menu_hr_recruitment_root"/> <menuitem id="menu_recruitment_requisition" name="Recruitment Requisition" active='0' parent="hr_recruitment.menu_hr_recruitment_root"/>
<menuitem id="menu_recruitment_requisition_main" name="Requisitions" parent="menu_recruitment_requisition" action="action_recruitment_requisition"/> <menuitem id="menu_recruitment_requisition_main" name="Requisitions" parent="menu_recruitment_requisition" action="action_recruitment_requisition"/>
</odoo> </odoo>

View File

@ -480,13 +480,16 @@ class WebsiteJobHrRecruitment(WebsiteHrRecruitment):
values['partner_phone'] = partner_phone values['partner_phone'] = partner_phone
if partner_email: if partner_email:
values['email_from'] = partner_email values['email_from'] = partner_email
notice_period_str = 'N/A'
if notice_period and notice_period_type:
notice_period_str = str(notice_period) + ' ' + str(notice_period_type)
data = super().extract_data(model, values) data = super().extract_data(model, values)
data['record']['current_ctc'] = float(current_ctc if current_ctc else 0) data['record']['current_ctc'] = float(current_ctc if current_ctc else 0)
data['record']['salary_expected'] = float(expected_ctc if expected_ctc else 0) data['record']['salary_expected'] = float(expected_ctc if expected_ctc else 0)
data['record']['exp_type'] = exp_type if exp_type else 'fresher' data['record']['exp_type'] = exp_type if exp_type else 'fresher'
data['record']['current_location'] = current_location if current_location else '' data['record']['current_location'] = current_location if current_location else ''
data['record']['current_organization'] = current_organization if current_organization else '' data['record']['current_organization'] = current_organization if current_organization else ''
data['record']['notice_period'] = notice_period if notice_period else 0 data['record']['notice_period'] = notice_period_str if notice_period_str else 'N/A'
data['record']['notice_period_type'] = notice_period_type if notice_period_type else 'day' data['record']['notice_period_type'] = notice_period_type if notice_period_type else 'day'
data['record']['hr_job_recruitment'] = int(hr_job_recruitment) if str(hr_job_recruitment).isdigit() else '' data['record']['hr_job_recruitment'] = int(hr_job_recruitment) if str(hr_job_recruitment).isdigit() else ''
data['record']['department_id'] = int(department_id) if str(department_id).isdigit() else '' data['record']['department_id'] = int(department_id) if str(department_id).isdigit() else ''