Roster management and grace period changes
This commit is contained in:
parent
5c6341d8b7
commit
adfe801d8e
|
|
@ -12,6 +12,7 @@
|
|||
'hr_holidays',
|
||||
'resource',
|
||||
'hr_attendance_extended',
|
||||
'roster_management',
|
||||
],
|
||||
|
||||
'data': [
|
||||
|
|
@ -23,7 +24,8 @@
|
|||
'views/late_coming_request.xml',
|
||||
'views/hr_employee_inherit.xml',
|
||||
'views/late_coming_mail_template.xml',
|
||||
# 'security/record_rules.xml',
|
||||
'views/ot_request.xml',
|
||||
'views/ot_mail_template.xml',
|
||||
],
|
||||
|
||||
'installable': True,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from odoo import api, fields, models, tools
|
||||
from odoo import api, fields, models, tools, _
|
||||
|
||||
|
||||
class AttendanceAnalytics(models.Model):
|
||||
|
|
@ -12,86 +12,73 @@ class AttendanceAnalytics(models.Model):
|
|||
'hr.employee',
|
||||
string='Employee'
|
||||
)
|
||||
|
||||
department_id = fields.Many2one(
|
||||
'hr.department',
|
||||
string='Department'
|
||||
)
|
||||
|
||||
date = fields.Date()
|
||||
|
||||
date_end = fields.Date(
|
||||
string="End Date"
|
||||
)
|
||||
|
||||
min_check_in = fields.Datetime()
|
||||
|
||||
max_check_out = fields.Datetime()
|
||||
|
||||
worked_hours = fields.Float()
|
||||
|
||||
out_time = fields.Float()
|
||||
|
||||
expected_check_in = fields.Float()
|
||||
|
||||
expected_check_out = fields.Float()
|
||||
|
||||
required_checkout_time = fields.Float()
|
||||
|
||||
compensated_time = fields.Float()
|
||||
|
||||
department_grace_period = fields.Integer()
|
||||
|
||||
late_minutes = fields.Float()
|
||||
|
||||
early_out_minutes = fields.Float()
|
||||
|
||||
is_late = fields.Boolean(
|
||||
string="Late"
|
||||
)
|
||||
|
||||
is_early_out = fields.Boolean(
|
||||
string="Early Out"
|
||||
)
|
||||
|
||||
is_compensation_pending = fields.Boolean(
|
||||
string="Compensation Pending"
|
||||
)
|
||||
|
||||
late_time = fields.Char(
|
||||
string="Late Time",
|
||||
compute="_compute_late_time"
|
||||
)
|
||||
|
||||
early_out_time = fields.Char(
|
||||
string="Early Out Time",
|
||||
compute="_compute_early_out_time"
|
||||
)
|
||||
|
||||
holiday_name = fields.Char()
|
||||
|
||||
is_holiday = fields.Boolean()
|
||||
|
||||
is_week_off = fields.Boolean()
|
||||
|
||||
status_message = fields.Char(
|
||||
string="Status"
|
||||
)
|
||||
|
||||
display_label = fields.Char(
|
||||
compute="_compute_display_label",
|
||||
store=False
|
||||
)
|
||||
|
||||
color = fields.Integer(
|
||||
compute="_compute_color"
|
||||
)
|
||||
|
||||
status = fields.Selection([
|
||||
|
||||
('present', 'Present'),
|
||||
('leave', 'Leave'),
|
||||
('no_info', 'No Information')
|
||||
])
|
||||
|
||||
('absent', 'Absent'),
|
||||
|
||||
('half_day', 'Half Day'),
|
||||
|
||||
('late_in', 'Late In'),
|
||||
|
||||
('early_out', 'Early Out'),
|
||||
|
||||
('on_duty', 'On Duty'),
|
||||
|
||||
('work_from_home', 'Work From Home'),
|
||||
|
||||
('holiday', 'Holiday'),
|
||||
|
||||
('week_off', 'Week Off'),
|
||||
|
||||
], string="Attendance Status")
|
||||
|
||||
late_request_id = fields.Many2one(
|
||||
'late.coming.request',
|
||||
|
|
@ -102,11 +89,26 @@ class AttendanceAnalytics(models.Model):
|
|||
string="Late Approved"
|
||||
)
|
||||
|
||||
hours_per_day = fields.Float()
|
||||
|
||||
allowed_ot_limit = fields.Float()
|
||||
|
||||
ot_eligible = fields.Boolean()
|
||||
|
||||
overtime_hours = fields.Float()
|
||||
shift_id = fields.Many2one(
|
||||
'resource.calendar',
|
||||
string='Shift'
|
||||
)
|
||||
|
||||
shift_name = fields.Char(
|
||||
string='Shift'
|
||||
)
|
||||
|
||||
@api.depends('late_minutes')
|
||||
def _compute_late_time(self):
|
||||
|
||||
for rec in self:
|
||||
|
||||
total_minutes = int(rec.late_minutes or 0)
|
||||
|
||||
hours = total_minutes // 60
|
||||
|
|
@ -121,7 +123,6 @@ class AttendanceAnalytics(models.Model):
|
|||
def _compute_early_out_time(self):
|
||||
|
||||
for rec in self:
|
||||
|
||||
total_minutes = int(
|
||||
rec.early_out_minutes or 0
|
||||
)
|
||||
|
|
@ -135,12 +136,16 @@ class AttendanceAnalytics(models.Model):
|
|||
)
|
||||
|
||||
@api.depends(
|
||||
'status_message',
|
||||
'status',
|
||||
'late_time',
|
||||
'early_out_time'
|
||||
)
|
||||
def _compute_display_label(self):
|
||||
|
||||
status_dict = dict(
|
||||
self._fields['status'].selection
|
||||
)
|
||||
|
||||
for rec in self:
|
||||
|
||||
lines = []
|
||||
|
|
@ -148,8 +153,13 @@ class AttendanceAnalytics(models.Model):
|
|||
if rec.employee_id:
|
||||
lines.append(rec.employee_id.name)
|
||||
|
||||
if rec.status_message:
|
||||
lines.append(rec.status_message)
|
||||
if rec.status:
|
||||
lines.append(
|
||||
status_dict.get(
|
||||
rec.status,
|
||||
rec.status
|
||||
)
|
||||
)
|
||||
|
||||
if rec.late_minutes:
|
||||
lines.append(
|
||||
|
|
@ -163,36 +173,30 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
rec.display_label = "\n".join(lines)
|
||||
|
||||
@api.depends(
|
||||
'is_holiday',
|
||||
'is_week_off',
|
||||
'status',
|
||||
'is_compensation_pending',
|
||||
'is_late'
|
||||
)
|
||||
|
||||
@api.depends('status')
|
||||
def _compute_color(self):
|
||||
for rec in self:
|
||||
|
||||
if rec.is_holiday:
|
||||
rec.color = 4
|
||||
|
||||
elif rec.is_week_off:
|
||||
rec.color = 7
|
||||
|
||||
elif rec.status == 'leave':
|
||||
rec.color = 3
|
||||
|
||||
elif rec.is_compensation_pending:
|
||||
rec.color = 2
|
||||
|
||||
elif rec.is_late:
|
||||
rec.color = 1
|
||||
|
||||
elif rec.status == 'present':
|
||||
if rec.status == 'present':
|
||||
rec.color = 10
|
||||
|
||||
else:
|
||||
elif rec.status == 'absent':
|
||||
rec.color = 1
|
||||
elif rec.status == 'half_day':
|
||||
rec.color = 2
|
||||
elif rec.status == 'late_in':
|
||||
rec.color = 3
|
||||
elif rec.status == 'early_out':
|
||||
rec.color = 4
|
||||
elif rec.status == 'holiday':
|
||||
rec.color = 7
|
||||
elif rec.status == 'week_off':
|
||||
rec.color = 8
|
||||
elif rec.status == 'work_from_home':
|
||||
rec.color = 5
|
||||
elif rec.status == 'on_duty':
|
||||
rec.color = 6
|
||||
else:
|
||||
rec.color = 0
|
||||
|
||||
def action_create_late_request(self):
|
||||
|
||||
|
|
@ -216,8 +220,6 @@ class AttendanceAnalytics(models.Model):
|
|||
self.is_compensation_pending,
|
||||
'compensation_minutes':
|
||||
self.early_out_minutes,
|
||||
'status_message':
|
||||
self.status_message,
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
|
|
@ -228,6 +230,71 @@ class AttendanceAnalytics(models.Model):
|
|||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_create_ot_request(self):
|
||||
self.ensure_one()
|
||||
request = self.env['overtime.request'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('attendance_date', '=', self.date)
|
||||
], limit=1)
|
||||
if not request:
|
||||
request = self.env['overtime.request'].create({
|
||||
'employee_id':
|
||||
self.employee_id.id,
|
||||
'attendance_date':
|
||||
self.date,
|
||||
'check_in':
|
||||
self.min_check_in,
|
||||
'check_out':
|
||||
self.max_check_out,
|
||||
'worked_hours':
|
||||
self.worked_hours,
|
||||
'hours_per_day':
|
||||
self.hours_per_day,
|
||||
'allowed_ot_limit':
|
||||
self.allowed_ot_limit,
|
||||
'overtime_hours':
|
||||
self.overtime_hours,
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'OT Request',
|
||||
'res_model': 'overtime.request',
|
||||
'res_id': request.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_create_shiftswap_request(self):
|
||||
self.ensure_one()
|
||||
|
||||
request = self.env['shift.swap.request'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('roster_date', '=', self.date),
|
||||
('state', '!=', 'approved')
|
||||
], limit=1)
|
||||
|
||||
|
||||
self.ensure_one()
|
||||
roster_form = self.env.ref('roster_management.view_shift_swap_form')
|
||||
if not request:
|
||||
request = self.env['shift.swap.request'].create({
|
||||
'employee_id': self.employee_id.id,
|
||||
'roster_date': self.date,
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Shift Swap Request',
|
||||
'res_model': 'shift.swap.request',
|
||||
'res_id': request.id,
|
||||
'view_mode': 'form',
|
||||
'view_id': roster_form.id,
|
||||
'target': 'current',
|
||||
'context': {
|
||||
'edit': True,
|
||||
},
|
||||
}
|
||||
|
||||
def init(self):
|
||||
|
||||
tools.drop_view_if_exists(
|
||||
|
|
@ -318,6 +385,23 @@ class AttendanceAnalytics(models.Model):
|
|||
|
||||
WHERE state = 'approved'
|
||||
),
|
||||
|
||||
ot_requests AS (
|
||||
|
||||
SELECT
|
||||
|
||||
id,
|
||||
|
||||
employee_id,
|
||||
|
||||
attendance_date,
|
||||
|
||||
state
|
||||
|
||||
FROM overtime_request
|
||||
|
||||
WHERE state = 'approved'
|
||||
),
|
||||
|
||||
holiday_data AS (
|
||||
|
||||
|
|
@ -343,45 +427,73 @@ class AttendanceAnalytics(models.Model):
|
|||
)
|
||||
|
||||
SELECT
|
||||
|
||||
row_number() OVER() AS id,
|
||||
|
||||
ed.employee_id,
|
||||
|
||||
ed.department_id,
|
||||
|
||||
rc.id AS shift_id,
|
||||
rc.name AS shift_name,
|
||||
ed.date,
|
||||
|
||||
ed.date AS date_end,
|
||||
|
||||
ats.min_check_in,
|
||||
|
||||
ats.max_check_out,
|
||||
|
||||
COALESCE(
|
||||
ats.worked_hours,
|
||||
0
|
||||
) AS worked_hours,
|
||||
CASE
|
||||
WHEN ats.min_check_in IS NOT NULL
|
||||
AND ats.max_check_out IS NOT NULL
|
||||
THEN (
|
||||
EXTRACT(
|
||||
EPOCH FROM (
|
||||
ats.max_check_out -
|
||||
ats.min_check_in
|
||||
)
|
||||
) / 3600
|
||||
) - ats.worked_hours
|
||||
ELSE 0
|
||||
END AS out_time,
|
||||
|
||||
CASE
|
||||
WHEN ats.worked_hours > 9
|
||||
THEN ats.worked_hours - 9
|
||||
ELSE 0
|
||||
END AS compensated_time,
|
||||
rc.hours_per_day AS hours_per_day,
|
||||
|
||||
(
|
||||
rc.hours_per_day
|
||||
+
|
||||
COALESCE(rc.over_time_hrs, 0)
|
||||
) AS allowed_ot_limit,
|
||||
|
||||
CASE
|
||||
|
||||
WHEN ats.worked_hours > rc.hours_per_day
|
||||
|
||||
THEN
|
||||
ats.worked_hours - rc.hours_per_day
|
||||
|
||||
ELSE 0
|
||||
|
||||
END AS overtime_hours,
|
||||
|
||||
CASE
|
||||
|
||||
WHEN ats.worked_hours >
|
||||
|
||||
(
|
||||
rc.hours_per_day
|
||||
+
|
||||
COALESCE(rc.over_time_hrs, 0)
|
||||
)
|
||||
|
||||
THEN TRUE
|
||||
|
||||
ELSE FALSE
|
||||
|
||||
END AS ot_eligible,
|
||||
CASE
|
||||
WHEN ats.min_check_in IS NOT NULL
|
||||
AND ats.max_check_out IS NOT NULL
|
||||
THEN (
|
||||
EXTRACT(
|
||||
EPOCH FROM (
|
||||
ats.max_check_out -
|
||||
ats.min_check_in
|
||||
)
|
||||
) / 3600
|
||||
) - ats.worked_hours
|
||||
ELSE 0
|
||||
END AS out_time,
|
||||
|
||||
CASE
|
||||
WHEN ats.worked_hours > 9
|
||||
THEN ats.worked_hours - 9
|
||||
ELSE 0
|
||||
END AS compensated_time,
|
||||
|
||||
lr.id AS late_request_id,
|
||||
|
||||
hd.holiday_name,
|
||||
|
|
@ -796,104 +908,28 @@ END AS compensated_time,
|
|||
THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS late_approved,
|
||||
|
||||
|
||||
CASE
|
||||
|
||||
|
||||
WHEN hd.id IS NOT NULL
|
||||
THEN 'Holiday'
|
||||
|
||||
THEN 'holiday'
|
||||
|
||||
WHEN EXTRACT(
|
||||
DOW FROM ed.date
|
||||
) IN (0,6)
|
||||
|
||||
THEN 'Week Off'
|
||||
|
||||
WHEN leave.id IS NOT NULL
|
||||
THEN 'Leave'
|
||||
|
||||
THEN 'week_off'
|
||||
|
||||
WHEN ats.min_check_in IS NULL
|
||||
THEN 'Absent'
|
||||
|
||||
WHEN
|
||||
(
|
||||
(
|
||||
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
)
|
||||
|
||||
+
|
||||
|
||||
CASE
|
||||
|
||||
WHEN ats.min_check_in
|
||||
IS NOT NULL
|
||||
|
||||
THEN GREATEST(
|
||||
|
||||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.min_check_in
|
||||
)
|
||||
)
|
||||
|
||||
-
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time
|
||||
* 60
|
||||
)
|
||||
+
|
||||
COALESCE(
|
||||
dg.grace_period,
|
||||
rc.late_grace_period,
|
||||
0
|
||||
)
|
||||
),
|
||||
|
||||
0
|
||||
)
|
||||
|
||||
ELSE 0
|
||||
|
||||
END
|
||||
|
||||
)
|
||||
|
||||
>
|
||||
|
||||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.max_check_out
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
THEN 'Compensation Pending'
|
||||
|
||||
THEN 'absent'
|
||||
|
||||
WHEN ats.worked_hours < (rc.hours_per_day / 2.0)
|
||||
THEN 'half_day'
|
||||
|
||||
WHEN
|
||||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.min_check_in
|
||||
HOUR FROM ats.min_check_in
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
|
|
@ -902,9 +938,9 @@ END AS compensated_time,
|
|||
FROM ats.min_check_in
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
>
|
||||
|
||||
|
||||
(
|
||||
(
|
||||
rc.shift_start_time * 60
|
||||
|
|
@ -916,23 +952,36 @@ END AS compensated_time,
|
|||
0
|
||||
)
|
||||
)
|
||||
|
||||
THEN 'Late'
|
||||
|
||||
ELSE 'Present'
|
||||
|
||||
END AS status_message,
|
||||
|
||||
CASE
|
||||
WHEN leave.id IS NOT NULL
|
||||
THEN 'leave'
|
||||
|
||||
WHEN ats.min_check_in IS NOT NULL
|
||||
THEN 'present'
|
||||
|
||||
ELSE 'no_info'
|
||||
|
||||
THEN 'late_in'
|
||||
|
||||
WHEN
|
||||
(
|
||||
(
|
||||
EXTRACT(
|
||||
HOUR
|
||||
FROM ats.max_check_out
|
||||
) * 60
|
||||
)
|
||||
+
|
||||
EXTRACT(
|
||||
MINUTE
|
||||
FROM ats.max_check_out
|
||||
)
|
||||
)
|
||||
|
||||
<
|
||||
|
||||
(
|
||||
rc.shift_end_time * 60
|
||||
)
|
||||
|
||||
THEN 'early_out'
|
||||
|
||||
ELSE 'present'
|
||||
|
||||
END AS status
|
||||
|
||||
|
||||
FROM employee_dates ed
|
||||
|
||||
LEFT JOIN attendance_summary ats
|
||||
|
|
@ -949,6 +998,10 @@ END AS compensated_time,
|
|||
LEFT JOIN late_requests lr
|
||||
ON lr.employee_id = ed.employee_id
|
||||
AND lr.attendance_date = ed.date
|
||||
|
||||
LEFT JOIN ot_requests otr
|
||||
ON otr.employee_id = ed.employee_id
|
||||
AND otr.attendance_date = ed.date
|
||||
|
||||
LEFT JOIN holiday_data hd
|
||||
ON ed.date BETWEEN
|
||||
|
|
@ -981,7 +1034,7 @@ END AS compensated_time,
|
|||
|
||||
('date', '=', today),
|
||||
|
||||
('status', '=', 'no_info'),
|
||||
('status', '=', 'absent'),
|
||||
|
||||
('is_holiday', '=', False),
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,17 @@ class HREmployee(models.Model):
|
|||
string="Attendance Count",
|
||||
compute="_compute_attendance_analytics_count"
|
||||
)
|
||||
attendance_mode = fields.Selection(
|
||||
[
|
||||
('office', 'Office Based'),
|
||||
('remote', 'Remote'),
|
||||
('hybrid', 'Hybrid'),
|
||||
('shift', 'Shift Based'),
|
||||
],
|
||||
string='Attendance Mode',
|
||||
default='office',
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
def _compute_attendance_analytics_count(self):
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
from odoo import fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class OvertimeRequest(models.Model):
|
||||
_name = 'overtime.request'
|
||||
_description = 'Overtime Request'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_rec_name = 'employee_id'
|
||||
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
required=True,
|
||||
tracking=True
|
||||
)
|
||||
|
||||
department_id = fields.Many2one(
|
||||
'hr.department',
|
||||
related='employee_id.department_id',
|
||||
store=True
|
||||
)
|
||||
|
||||
manager_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
related='employee_id.parent_id',
|
||||
store=True
|
||||
)
|
||||
|
||||
attendance_date = fields.Date(
|
||||
tracking=True
|
||||
)
|
||||
|
||||
check_in = fields.Datetime()
|
||||
|
||||
check_out = fields.Datetime()
|
||||
|
||||
worked_hours = fields.Float()
|
||||
|
||||
hours_per_day = fields.Float()
|
||||
|
||||
allowed_ot_limit = fields.Float()
|
||||
|
||||
overtime_hours = fields.Float(
|
||||
tracking=True
|
||||
)
|
||||
|
||||
reason = fields.Text(
|
||||
tracking=True
|
||||
)
|
||||
|
||||
state = fields.Selection([
|
||||
|
||||
('draft', 'Draft'),
|
||||
|
||||
('submitted', 'Submitted'),
|
||||
|
||||
('approved', 'Approved'),
|
||||
|
||||
('rejected', 'Rejected')
|
||||
|
||||
], default='draft', tracking=True)
|
||||
|
||||
def action_submit(self):
|
||||
|
||||
for rec in self:
|
||||
|
||||
if not rec.reason:
|
||||
raise UserError(
|
||||
_("Please enter reason.")
|
||||
)
|
||||
|
||||
if not rec.manager_id:
|
||||
raise UserError(
|
||||
_("Manager not configured.")
|
||||
)
|
||||
|
||||
if not rec.manager_id.work_email:
|
||||
raise UserError(
|
||||
_("Manager email not configured.")
|
||||
)
|
||||
|
||||
rec.state = 'submitted'
|
||||
|
||||
template = self.env.ref(
|
||||
'grace_period.email_template_ot_request'
|
||||
)
|
||||
|
||||
template.send_mail(
|
||||
rec.id,
|
||||
force_send=True
|
||||
)
|
||||
|
||||
rec.message_post(
|
||||
body=_(
|
||||
"OT Request Submitted"
|
||||
)
|
||||
)
|
||||
|
||||
def action_approve(self):
|
||||
|
||||
for rec in self:
|
||||
|
||||
if (
|
||||
rec.manager_id.user_id != self.env.user
|
||||
and not self.env.user.has_group(
|
||||
'hr.group_hr_manager'
|
||||
)
|
||||
):
|
||||
|
||||
raise UserError(
|
||||
_("Only Manager or HR can approve.")
|
||||
)
|
||||
|
||||
rec.state = 'approved'
|
||||
|
||||
rec.message_post(
|
||||
body=_("Overtime Request Approved.")
|
||||
)
|
||||
|
||||
def action_reject(self):
|
||||
|
||||
for rec in self:
|
||||
|
||||
if (
|
||||
rec.manager_id.user_id != self.env.user
|
||||
and not self.env.user.has_group(
|
||||
'hr.group_hr_manager'
|
||||
)
|
||||
):
|
||||
|
||||
raise UserError(
|
||||
_("Only Manager or HR can reject.")
|
||||
)
|
||||
|
||||
rec.state = 'rejected'
|
||||
|
||||
template = self.env.ref(
|
||||
'grace_period.email_template_ot_rejected'
|
||||
)
|
||||
|
||||
template.send_mail(
|
||||
rec.id,
|
||||
force_send=True
|
||||
)
|
||||
|
||||
rec.message_post(
|
||||
body=_("Overtime Request Rejected.")
|
||||
)
|
||||
|
||||
def action_reset_to_draft(self):
|
||||
|
||||
self.state = 'draft'
|
||||
|
||||
self.message_post(
|
||||
body=_(
|
||||
"Reset to Draft"
|
||||
)
|
||||
)
|
||||
|
|
@ -24,6 +24,7 @@ class ResourceCalendar(models.Model):
|
|||
shift_end_time = fields.Float(string="End Time")
|
||||
late_grace_period = fields.Integer(default=15)
|
||||
early_out_grace_period = fields.Integer(default=0)
|
||||
over_time_hrs = fields.Float(default=2)
|
||||
department_grace_ids = fields.One2many(
|
||||
'resource.calendar.department.grace',
|
||||
'calendar_id',
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ access_attendance_analytics_manager,attendance.analytics.manager,model_attendanc
|
|||
access_attendance_analytics_admin,attendance.analytics.admin,model_attendance_analytics,base.group_system,1,1,1,1
|
||||
access_late_coming_request,attendance_late_coming_request,model_late_coming_request,base.group_user,1,1,1,1
|
||||
access_resource_calendar_department_grace,resource_calendar_department_grace,model_resource_calendar_department_grace,base.group_user,1,1,1,1
|
||||
access_overtime_request,overtime_request,model_overtime_request,base.group_user,1,1,1,1
|
||||
|
|
|
|||
|
|
|
@ -1,11 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
|
||||
<!-- =============================================== -->
|
||||
<!-- TREE VIEW -->
|
||||
<!-- =============================================== -->
|
||||
|
||||
<record id="view_attendance_analytics_list" model="ir.ui.view">
|
||||
<field name="name">attendance.analytics.list</field>
|
||||
<field name="model">attendance.analytics</field>
|
||||
|
|
@ -25,7 +22,15 @@
|
|||
<field name="is_early_out"/>
|
||||
<field name="is_compensation_pending"/>
|
||||
<field name="late_approved"/>
|
||||
<field name="status_message"/>
|
||||
<field name="shift_id"/>
|
||||
<!-- <field name="shift_name"/>-->
|
||||
<field name="status"
|
||||
widget="badge"
|
||||
decoration-success="status == 'present'"
|
||||
decoration-danger="status == 'absent'"
|
||||
decoration-warning="status == 'late_in'"
|
||||
decoration-info="status == 'half_day'"
|
||||
decoration-primary="status == 'holiday'"/>
|
||||
<button name="action_create_late_request"
|
||||
type="object"
|
||||
string="Request Approval"
|
||||
|
|
@ -39,6 +44,15 @@
|
|||
and early_out_minutes == 0
|
||||
)
|
||||
"/>
|
||||
<button name="action_create_ot_request"
|
||||
string="Apply OT"
|
||||
type="object"
|
||||
class="btn-warning"
|
||||
invisible="ot_eligible == False"/>
|
||||
<button name="action_create_shiftswap_request"
|
||||
string="Swift Swap Request"
|
||||
type="object"
|
||||
class="btn-primary"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
|
@ -60,7 +74,6 @@
|
|||
event_open_popup="true">
|
||||
<field name="display_label"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="status_message"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
|
|
@ -104,6 +117,9 @@
|
|||
<filter string="Date"
|
||||
name="group_date"
|
||||
context="{'group_by':'date'}"/>
|
||||
<filter string="Status"
|
||||
name="group_status"
|
||||
context="{'group_by':'status'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
|
|
@ -114,12 +130,13 @@
|
|||
<field name="name">Attendance Analytics</field>
|
||||
<field name="res_model">attendance.analytics</field>
|
||||
<field name="view_mode">list,calendar</field>
|
||||
<field name="domain"> [ '|', ('employee_id.user_id', '=', uid), ('employee_id.parent_id.user_id', '=', uid) ] </field>
|
||||
<field name="domain">[ '|', ('employee_id.user_id', '=', uid), ('employee_id.parent_id.user_id', '=', uid) ]
|
||||
</field>
|
||||
<field name="search_view_id" ref="view_attendance_analytics_search"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_attendance_analytics"
|
||||
name="Attendance Report"
|
||||
name="Attendance Data"
|
||||
parent="hr_attendance.menu_hr_attendance_root"
|
||||
action="action_attendance_analytics"
|
||||
sequence="50"/>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,54 @@
|
|||
</xpath>
|
||||
|
||||
</field>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//field[@name='department_id']"
|
||||
position="after">
|
||||
|
||||
<field name="attendance_mode"/>
|
||||
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
|
||||
|
||||
</record>
|
||||
<record id="view_employee_filter_inherit_attendance_mode"
|
||||
model="ir.ui.view">
|
||||
|
||||
<field name="name">
|
||||
hr.employee.search.attendance.mode
|
||||
</field>
|
||||
|
||||
<field name="model">hr.employee</field>
|
||||
|
||||
<field name="inherit_id"
|
||||
ref="hr.view_employee_filter"/>
|
||||
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//search" position="inside">
|
||||
|
||||
<filter string="Office"
|
||||
name="office_employees"
|
||||
domain="[('attendance_mode','=','office')]"/>
|
||||
|
||||
<filter string="Remote"
|
||||
name="remote_employees"
|
||||
domain="[('attendance_mode','=','remote')]"/>
|
||||
|
||||
<filter string="Hybrid"
|
||||
name="hybrid_employees"
|
||||
domain="[('attendance_mode','=','hybrid')]"/>
|
||||
|
||||
<filter string="Shift"
|
||||
name="shift_employees"
|
||||
domain="[('attendance_mode','=','shift')]"/>
|
||||
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
|
||||
</record>
|
||||
|
||||
|
|
|
|||
|
|
@ -54,57 +54,29 @@
|
|||
type="object"
|
||||
invisible="state != 'submitted'"
|
||||
class="btn-danger"/>
|
||||
|
||||
<field name="state"
|
||||
widget="statusbar"/>
|
||||
|
||||
</header>
|
||||
|
||||
<sheet>
|
||||
|
||||
<group>
|
||||
|
||||
<group>
|
||||
|
||||
<field name="employee_id"
|
||||
readonly="1"/>
|
||||
|
||||
<field name="manager_id"
|
||||
readonly="1"/>
|
||||
|
||||
<field name="attendance_date"
|
||||
readonly="1"/>
|
||||
|
||||
<field name="worked_hours"
|
||||
readonly="1"/>
|
||||
|
||||
<field name="employee_id"/>
|
||||
<field name="manager_id"/>
|
||||
<field name="attendance_date"/>
|
||||
<field name="worked_hours"/>
|
||||
</group>
|
||||
|
||||
<group>
|
||||
|
||||
<field name="late_minutes"
|
||||
readonly="1"/>
|
||||
|
||||
<field name="compensation_minutes"
|
||||
readonly="1"/>
|
||||
|
||||
<field name="department_grace_period"
|
||||
readonly="1"/>
|
||||
|
||||
<field name="required_checkout_time"
|
||||
readonly="1"/>
|
||||
|
||||
<field name="late_minutes"/>
|
||||
<field name="compensation_minutes"/>
|
||||
<field name="department_grace_period"/>
|
||||
<field name="required_checkout_time"/>
|
||||
</group>
|
||||
|
||||
</group>
|
||||
|
||||
<group>
|
||||
|
||||
<field name="status_message"
|
||||
readonly="1"/>
|
||||
|
||||
<field name="reason"
|
||||
placeholder="Enter reason here..."
|
||||
placeholder="Enter reason here...Forgot punch/Client meeting/System issue/Travel/on-duty"
|
||||
readonly="state in ('approved','rejected')"/>
|
||||
</group>
|
||||
</sheet>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
<odoo>
|
||||
|
||||
<record id="email_template_ot_request" model="mail.template">
|
||||
<field name="name">Overtime Request Submitted</field>
|
||||
<field name="model_id" ref="model_overtime_request"/>
|
||||
<field name="subject">Overtime Request - {{ object.employee_id.name }}</field>
|
||||
<field name="email_from">{{ user.email }}</field>
|
||||
<field name="email_to">{{ object.manager_id.work_email }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>
|
||||
Hello,
|
||||
</p>
|
||||
<p>
|
||||
Employee
|
||||
<strong>
|
||||
<t t-out="object.employee_id.name"/>
|
||||
</strong>
|
||||
submitted an Overtime Request.
|
||||
</p>
|
||||
<table border="1"
|
||||
cellpadding="5"
|
||||
cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Date</strong>
|
||||
</td>
|
||||
<td>
|
||||
<t t-out="object.attendance_date"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Worked Hours</strong>
|
||||
</td>
|
||||
<td>
|
||||
<t t-out="object.worked_hours"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>OT Hours</strong>
|
||||
</td>
|
||||
<td>
|
||||
<t t-out="object.overtime_hours"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
<p>
|
||||
<strong>Reason:</strong>
|
||||
</p>
|
||||
<p>
|
||||
<t t-out="object.reason"/>
|
||||
</p>
|
||||
<br/>
|
||||
<p>
|
||||
Please review the request.
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
<record id="email_template_ot_rejected" model="mail.template">
|
||||
<field name="name">Overtime Request Rejected</field>
|
||||
<field name="model_id" ref="model_overtime_request"/>
|
||||
<field name="subject">Overtime Request Rejected</field>
|
||||
<field name="email_from">{{ user.email }}</field>
|
||||
<field name="email_to">{{ object.employee_id.work_email }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>
|
||||
Hello
|
||||
<strong>
|
||||
<t t-out="object.employee_id.name"/>
|
||||
</strong>
|
||||
,
|
||||
</p>
|
||||
<p>
|
||||
Your Overtime Request for
|
||||
<strong>
|
||||
<t t-out="object.attendance_date"/>
|
||||
</strong>
|
||||
has been
|
||||
<strong style="color:red;">
|
||||
Rejected
|
||||
</strong>.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Manager:</strong>
|
||||
<t t-out="object.manager_id.name"/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>OT Hours:</strong>
|
||||
|
||||
<t t-out="object.overtime_hours"/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Reason Submitted:</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<t t-out="object.reason"/>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>
|
||||
Please contact your manager.
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>
|
||||
Thank You
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</field>
|
||||
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<odoo>
|
||||
|
||||
<record id="view_overtime_request_form" model="ir.ui.view">
|
||||
<field name="name">overtime.request.form</field>
|
||||
<field name="model">overtime.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="OT Request">
|
||||
<header>
|
||||
<button name="action_submit"
|
||||
string="Submit"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="state != 'draft'"/>
|
||||
|
||||
<button name="action_approve"
|
||||
string="Approve"
|
||||
type="object"
|
||||
class="btn-success"
|
||||
invisible="state != 'submitted'"/>
|
||||
|
||||
<button name="action_reject"
|
||||
string="Reject"
|
||||
type="object"
|
||||
class="btn-danger"
|
||||
invisible="state != 'submitted'"/>
|
||||
|
||||
<button name="action_reset_to_draft"
|
||||
string="Reset to Draft"
|
||||
type="object"
|
||||
invisible="state == 'draft'"/>
|
||||
|
||||
<field name="state"
|
||||
widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="attendance_date"/>
|
||||
<field name="worked_hours"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="hours_per_day"/>
|
||||
<field name="allowed_ot_limit"/>
|
||||
<field name="overtime_hours"/>
|
||||
<field name="check_in"/>
|
||||
<field name="check_out"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="reason"
|
||||
placeholder="Enter OT Reason..."
|
||||
readonly="state in ('approved','rejected')"/>
|
||||
</group>
|
||||
</sheet>
|
||||
|
||||
<chatter/>
|
||||
|
||||
</form>
|
||||
|
||||
</field>
|
||||
|
||||
</record>
|
||||
<record id="view_overtime_request_list" model="ir.ui.view">
|
||||
<field name="name">overtime.request.list</field>
|
||||
<field name="model">overtime.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="OT Request">
|
||||
<field name="employee_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="attendance_date"/>
|
||||
<field name="worked_hours"/>
|
||||
<field name="hours_per_day"/>
|
||||
<field name="overtime_hours"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_overtime_request" model="ir.actions.act_window">
|
||||
<field name="name">Overtime Requests</field>
|
||||
<field name="res_model">overtime.request</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">
|
||||
[
|
||||
'|',
|
||||
('employee_id.user_id', '=', uid),
|
||||
('employee_id.parent_id.user_id', '=', uid)
|
||||
]
|
||||
</field>
|
||||
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_overtime_request"
|
||||
name="Overtime Requests"
|
||||
parent="hr_attendance.menu_hr_attendance_root"
|
||||
action="action_overtime_request"
|
||||
sequence="35"/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -46,6 +46,7 @@
|
|||
<field name="late_grace_period"/>
|
||||
|
||||
<field name="early_out_grace_period"/>
|
||||
<field name="over_time_hrs"/>
|
||||
|
||||
</group>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
'name': 'Roster Management',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Human Resources',
|
||||
'summary': 'Employee Shift and Roster Management',
|
||||
'author': 'Srivyn Platforms',
|
||||
'license': 'LGPL-3',
|
||||
'depends': [
|
||||
'hr',
|
||||
'mail',
|
||||
'resource',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/shift_swap.xml',
|
||||
'views/team_roster.xml',
|
||||
'views/shift_swap_mail_template.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import shift_swap
|
||||
from . import team_roster
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ShiftSwapRequest(models.Model):
|
||||
_name = 'shift.swap.request'
|
||||
_inherit = [
|
||||
'mail.thread',
|
||||
'mail.activity.mixin'
|
||||
]
|
||||
|
||||
employee_id = fields.Many2one('hr.employee')
|
||||
shift_id = fields.Many2one(
|
||||
'resource.calendar',
|
||||
related='employee_id.resource_calendar_id',
|
||||
string='Assigned Shift',
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
swap_employee_id = fields.Many2one(
|
||||
'hr.employee'
|
||||
)
|
||||
roster_date = fields.Date()
|
||||
reason = fields.Text()
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('submitted', 'Submitted'),
|
||||
('approved', 'Approved'),
|
||||
('rejected', 'Rejected')
|
||||
], default='draft')
|
||||
manager_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
related='employee_id.parent_id',
|
||||
store=True
|
||||
)
|
||||
|
||||
def action_submit(self):
|
||||
for rec in self:
|
||||
|
||||
if not rec.reason:
|
||||
raise UserError(
|
||||
_("Please enter reason.")
|
||||
)
|
||||
|
||||
if not rec.manager_id:
|
||||
raise UserError(
|
||||
_("Manager not configured.")
|
||||
)
|
||||
|
||||
if not rec.manager_id.work_email:
|
||||
raise UserError(
|
||||
_("Manager email not configured.")
|
||||
)
|
||||
|
||||
rec.state = 'submitted'
|
||||
|
||||
template = self.env.ref(
|
||||
'roster_management.email_template_shift_swap_request'
|
||||
)
|
||||
|
||||
template.send_mail(
|
||||
rec.id,
|
||||
force_send=True
|
||||
)
|
||||
|
||||
rec.message_post(
|
||||
body=_(
|
||||
"Shift swapping Request Submitted"
|
||||
)
|
||||
)
|
||||
|
||||
def action_reject(self):
|
||||
for rec in self:
|
||||
|
||||
if (
|
||||
rec.manager_id.user_id != self.env.user
|
||||
and not self.env.user.has_group(
|
||||
'hr.group_hr_manager'
|
||||
)
|
||||
):
|
||||
raise UserError(
|
||||
_("Only Manager or HR can reject.")
|
||||
)
|
||||
|
||||
rec.state = 'rejected'
|
||||
|
||||
template = self.env.ref(
|
||||
'roster_management.email_template_shiftswap_rejected'
|
||||
)
|
||||
|
||||
template.send_mail(
|
||||
rec.id,
|
||||
force_send=True
|
||||
)
|
||||
|
||||
rec.message_post(
|
||||
body=_("ShiftSwap Request Rejected.")
|
||||
)
|
||||
|
||||
def action_approve(self):
|
||||
|
||||
for rec in self:
|
||||
|
||||
if (
|
||||
rec.manager_id.user_id != self.env.user
|
||||
and not self.env.user.has_group(
|
||||
'hr.group_hr_manager'
|
||||
)
|
||||
):
|
||||
raise UserError(
|
||||
_("Only Manager or HR can approve.")
|
||||
)
|
||||
|
||||
roster1 = self.env[
|
||||
'team.roster.line'
|
||||
].search([
|
||||
('employee_id', '=', rec.employee_id.id),
|
||||
('roster_date', '=', rec.roster_date)
|
||||
], limit=1)
|
||||
|
||||
roster2 = self.env[
|
||||
'team.roster.line'
|
||||
].search([
|
||||
('employee_id', '=', rec.swap_employee_id.id),
|
||||
('roster_date', '=', rec.roster_date)
|
||||
], limit=1)
|
||||
|
||||
if not roster1 or not roster2:
|
||||
raise UserError(
|
||||
_("Roster records not found for the selected date.")
|
||||
)
|
||||
|
||||
shift = roster1.shift_id
|
||||
|
||||
roster1.shift_id = roster2.shift_id
|
||||
|
||||
roster2.shift_id = shift
|
||||
|
||||
rec.state = 'approved'
|
||||
|
||||
rec.message_post(
|
||||
body=_("Shift Swap Request Approved.")
|
||||
)
|
||||
def action_reset_to_draft(self):
|
||||
|
||||
self.state = 'draft'
|
||||
|
||||
self.message_post(
|
||||
body=_(
|
||||
"Reset to Draft"
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class TeamRoster(models.Model):
|
||||
_name = 'team.roster'
|
||||
_description = 'Team Roster'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
name = fields.Char(
|
||||
required=True
|
||||
)
|
||||
|
||||
start_date = fields.Date(
|
||||
required=True
|
||||
)
|
||||
|
||||
end_date = fields.Date(
|
||||
required=True
|
||||
)
|
||||
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('generated', 'Generated'),
|
||||
('approved', 'Approved')
|
||||
], default='draft')
|
||||
|
||||
line_ids = fields.One2many(
|
||||
'team.roster.line',
|
||||
'roster_id'
|
||||
)
|
||||
|
||||
|
||||
|
||||
def action_generate_roster(self):
|
||||
|
||||
shifts = self.env[
|
||||
'resource.calendar'
|
||||
].search([])
|
||||
|
||||
employees = self.env[
|
||||
'hr.employee'
|
||||
].search([
|
||||
('active', '=', True)
|
||||
])
|
||||
|
||||
if not shifts:
|
||||
return
|
||||
|
||||
self.line_ids.unlink()
|
||||
|
||||
shift_count = len(shifts)
|
||||
|
||||
current_date = self.start_date
|
||||
|
||||
counter = 0
|
||||
|
||||
while current_date <= self.end_date:
|
||||
|
||||
for emp in employees:
|
||||
|
||||
self.env[
|
||||
'team.roster.line'
|
||||
].create({
|
||||
|
||||
'roster_id': self.id,
|
||||
|
||||
'employee_id': emp.id,
|
||||
|
||||
'shift_id':
|
||||
shifts[
|
||||
counter %
|
||||
shift_count
|
||||
].id,
|
||||
|
||||
'roster_date':
|
||||
current_date
|
||||
|
||||
})
|
||||
|
||||
counter += 1
|
||||
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
self.state = 'generated'
|
||||
|
||||
|
||||
|
||||
|
||||
class TeamRosterLine(models.Model):
|
||||
_name = 'team.roster.line'
|
||||
_description = 'Team Roster Line'
|
||||
|
||||
roster_id = fields.Many2one(
|
||||
'team.roster'
|
||||
)
|
||||
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee',
|
||||
required=True
|
||||
)
|
||||
|
||||
shift_id = fields.Many2one(
|
||||
'resource.calendar',
|
||||
required=True,
|
||||
string="Shift"
|
||||
)
|
||||
|
||||
roster_date = fields.Date(
|
||||
required=True
|
||||
)
|
||||
|
||||
department_id = fields.Many2one(
|
||||
related='employee_id.department_id',
|
||||
store=True
|
||||
)
|
||||
|
||||
@api.constrains(
|
||||
'employee_id',
|
||||
'roster_date'
|
||||
)
|
||||
def _check_duplicate(self):
|
||||
|
||||
for rec in self:
|
||||
|
||||
duplicate = self.search([
|
||||
('employee_id', '=', rec.employee_id.id),
|
||||
('roster_date', '=', rec.roster_date),
|
||||
('id', '!=', rec.id)
|
||||
])
|
||||
|
||||
if duplicate:
|
||||
|
||||
raise ValidationError(
|
||||
"Employee already has a shift assigned."
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_team_roster,team.roster,model_team_roster,,1,1,1,1
|
||||
access_team_roster_line,team.roster.line,model_team_roster_line,,1,1,1,1
|
||||
access_shift_swap_request,shift.swap.request,model_shift_swap_request,,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<odoo>
|
||||
|
||||
<record id="view_shift_swap_tree" model="ir.ui.view">
|
||||
<field name="name">shift.swap.request.tree</field>
|
||||
<field name="model">shift.swap.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="employee_id"/>
|
||||
<field name="swap_employee_id"/>
|
||||
<field name="roster_date"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_shift_swap_form" model="ir.ui.view">
|
||||
<field name="name">shift.swap.request.form</field>
|
||||
<field name="model">shift.swap.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_submit"
|
||||
string="Submit"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="state != 'draft'"/>
|
||||
<button name="action_approve"
|
||||
string="Approve"
|
||||
type="object"
|
||||
class="btn-success"
|
||||
invisible="state != 'submitted'"/>
|
||||
<button name="action_reject"
|
||||
string="Reject"
|
||||
type="object"
|
||||
class="btn-danger"
|
||||
invisible="state != 'submitted'"/>
|
||||
<field name="state"
|
||||
widget="statusbar" force_save="1"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="employee_id"/>
|
||||
<field name="shift_id" readonly="1"/>
|
||||
<field name="swap_employee_id"
|
||||
required="1" force_save="1"/>
|
||||
<field name="roster_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="reason"
|
||||
readonly="state in ('approved','rejected')"
|
||||
required="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_shift_swap" model="ir.actions.act_window">
|
||||
<field name="name">Shift Swap Request</field>
|
||||
<field name="res_model">shift.swap.request</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">
|
||||
[
|
||||
'|',
|
||||
('employee_id.user_id', '=', uid),
|
||||
('employee_id.parent_id.user_id', '=', uid)
|
||||
]
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
<odoo>
|
||||
|
||||
<record id="email_template_shift_swap_request" model="mail.template">
|
||||
<field name="name">ShiftSwap Request Submitted</field>
|
||||
<field name="model_id" ref="model_shift_swap_request"/>
|
||||
<field name="subject">ShiftSwap Request - {{ object.employee_id.name }}</field>
|
||||
<field name="email_from">{{ user.email }}</field>
|
||||
<field name="email_to">{{ object.manager_id.work_email }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>
|
||||
Hello,
|
||||
</p>
|
||||
<p>
|
||||
Employee
|
||||
<strong>
|
||||
<t t-out="object.employee_id.name"/>
|
||||
</strong>
|
||||
submitted an ShiftSwap Request.
|
||||
</p>
|
||||
<p>
|
||||
The Swap Employee is
|
||||
<strong>
|
||||
<t t-out="object.swap_employee_id.name"/>
|
||||
</strong>
|
||||
|
||||
</p>
|
||||
<table border="1"
|
||||
cellpadding="5"
|
||||
cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Date</strong>
|
||||
</td>
|
||||
<td>
|
||||
<t t-out="object.roster_date"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br/>
|
||||
<p>
|
||||
<strong>Reason:</strong>
|
||||
</p>
|
||||
<p>
|
||||
<t t-out="object.reason"/>
|
||||
</p>
|
||||
<br/>
|
||||
<p>
|
||||
Please review the request.
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
<record id="email_template_shiftswap_rejected" model="mail.template">
|
||||
<field name="name">ShiftSwap Request Rejected</field>
|
||||
<field name="model_id" ref="model_shift_swap_request"/>
|
||||
<field name="subject">ShiftSwap Request Rejected</field>
|
||||
<field name="email_from">{{ user.email }}</field>
|
||||
<field name="email_to">{{ object.employee_id.work_email }}</field>
|
||||
<field name="body_html" type="html">
|
||||
<div>
|
||||
<p>
|
||||
Hello
|
||||
<strong>
|
||||
<t t-out="object.employee_id.name"/>
|
||||
</strong>
|
||||
,
|
||||
</p>
|
||||
<p>
|
||||
Your ShiftSwap Request for
|
||||
<strong>
|
||||
<t t-out="object.roster_date"/>
|
||||
</strong>
|
||||
has been
|
||||
<strong style="color:red;">
|
||||
Rejected
|
||||
</strong>.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Manager:</strong>
|
||||
<t t-out="object.manager_id.name"/>
|
||||
</p>
|
||||
|
||||
|
||||
<p>
|
||||
<strong>Reason Submitted:</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<t t-out="object.reason"/>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>
|
||||
Please contact your manager.
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
|
||||
<p>
|
||||
Thank You
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</field>
|
||||
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<odoo>
|
||||
|
||||
<record id="view_team_roster_tree" model="ir.ui.view">
|
||||
<field name="name">team.roster.tree</field>
|
||||
<field name="model">team.roster</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_team_roster_form" model="ir.ui.view">
|
||||
<field name="name">team.roster.form</field>
|
||||
<field name="model">team.roster</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_generate_roster"
|
||||
string="Generate Roster"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="state != 'draft'"/>
|
||||
<!-- <button name="action_approve"-->
|
||||
<!-- string="Approve"-->
|
||||
<!-- type="object"-->
|
||||
<!-- class="btn-success"-->
|
||||
<!-- invisible="state != 'generated'"/>-->
|
||||
<field name="state"
|
||||
widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Roster Lines">
|
||||
<field name="line_ids">
|
||||
<list editable="bottom">
|
||||
<field name="roster_date"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="shift_id"/>
|
||||
<field name="department_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_team_roster_calendar" model="ir.ui.view">
|
||||
<field name="name">team.roster.line.calendar</field>
|
||||
<field name="model">team.roster.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<calendar
|
||||
string="Roster Calendar"
|
||||
date_start="roster_date"
|
||||
color="employee_id">
|
||||
<field name="employee_id"/>
|
||||
<field name="shift_id"/>
|
||||
</calendar>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_team_roster" model="ir.actions.act_window">
|
||||
<field name="name">Team Rosters</field>
|
||||
<field name="res_model">team.roster</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
<menuitem
|
||||
id="menu_roster_root"
|
||||
name="Roster Management"
|
||||
sequence="50"/>
|
||||
<menuitem
|
||||
id="menu_team_roster"
|
||||
name="Team Rosters"
|
||||
parent="menu_roster_root"
|
||||
action="action_team_roster"/>
|
||||
<menuitem
|
||||
id="menu_shift_swap"
|
||||
name="Shift Swap Requests"
|
||||
parent="menu_roster_root"
|
||||
action="action_shift_swap"/>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue