692 lines
21 KiB
Python
692 lines
21 KiB
Python
from odoo import api, fields, models, _
|
|
from datetime import date, timedelta
|
|
from odoo.exceptions import ValidationError, UserError
|
|
|
|
|
|
class WeeklyPeriodTimesheets(models.Model):
|
|
_name = 'weekly.periodtimesheets'
|
|
_description = 'Weekly Period Timesheets'
|
|
_rec_name = 'employee_id'
|
|
|
|
employee_id = fields.Many2one(
|
|
'hr.employee',
|
|
string="Employee",
|
|
required=True,
|
|
default=lambda self: self.env.user.employee_id.id,
|
|
)
|
|
|
|
year_id = fields.Many2one(
|
|
'week.timesheet',
|
|
string="Year",
|
|
required=True,
|
|
)
|
|
|
|
week_line_id = fields.Many2one(
|
|
'week.timesheet.line',
|
|
string="Week",
|
|
required=True,
|
|
domain="[('week_timesheet_id', '=', year_id)]"
|
|
)
|
|
|
|
timesheet_line_ids = fields.One2many(
|
|
'weekly.periodtimesheets.line',
|
|
'weekly_timesheet_id',
|
|
string="Timesheet Lines"
|
|
)
|
|
|
|
total_hours = fields.Float(
|
|
string="Total Hours",
|
|
compute="_compute_total_hours",
|
|
store=True
|
|
)
|
|
|
|
@api.depends('timesheet_line_ids.hours')
|
|
def _compute_total_hours(self):
|
|
|
|
for rec in self:
|
|
rec.total_hours = sum(
|
|
rec.timesheet_line_ids.mapped('hours')
|
|
)
|
|
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('submitted', 'Submitted'),
|
|
('approved', 'Approved'),
|
|
('rejected', 'Rejected'),
|
|
], string="Status", default='draft')
|
|
|
|
can_current_user_refresh_weekly = fields.Boolean(
|
|
compute='_compute_can_current_user_refresh_weekly',
|
|
)
|
|
|
|
def _compute_can_current_user_refresh_weekly(self):
|
|
for rec in self:
|
|
rec.can_current_user_refresh_weekly = (
|
|
rec.employee_id.user_id == self.env.user
|
|
and rec.state in ('draft', 'rejected')
|
|
)
|
|
|
|
@api.model
|
|
def _get_current_week_defaults(self):
|
|
today = date.today()
|
|
year_name = str(today.year)
|
|
year = self.env['week.timesheet'].sudo().search([
|
|
('year', '=', year_name),
|
|
], limit=1)
|
|
|
|
if not year:
|
|
year = self.env['week.timesheet'].sudo().create({
|
|
'year': year_name,
|
|
})
|
|
year.action_generate_weeks()
|
|
elif not year.week_line_ids:
|
|
year.action_generate_weeks()
|
|
|
|
week = self.env['week.timesheet.line'].sudo().search([
|
|
('week_timesheet_id', '=', year.id),
|
|
('date_from', '<=', today),
|
|
('date_to', '>=', today),
|
|
], limit=1)
|
|
|
|
return year, week
|
|
|
|
@api.model
|
|
def default_get(self, fields_list):
|
|
vals = super().default_get(fields_list)
|
|
|
|
if 'employee_id' in fields_list and not vals.get('employee_id'):
|
|
vals['employee_id'] = self.env.user.employee_id.id
|
|
|
|
year, week = self._get_current_week_defaults()
|
|
|
|
if 'year_id' in fields_list and not vals.get('year_id') and year:
|
|
vals['year_id'] = year.id
|
|
|
|
if 'week_line_id' in fields_list and not vals.get('week_line_id') and week:
|
|
vals['week_line_id'] = week.id
|
|
|
|
return vals
|
|
|
|
@api.onchange('year_id')
|
|
def _onchange_year_id(self):
|
|
if not self.year_id:
|
|
self.week_line_id = False
|
|
return
|
|
|
|
today = date.today()
|
|
week = self.env['week.timesheet.line'].search([
|
|
('week_timesheet_id', '=', self.year_id.id),
|
|
('date_from', '<=', today),
|
|
('date_to', '>=', today),
|
|
], limit=1)
|
|
|
|
if week:
|
|
self.week_line_id = week
|
|
|
|
def _is_weekly_admin(self):
|
|
return self.env.user.has_group('base.group_system')
|
|
|
|
def _is_employee_manager_for(self, employee):
|
|
return employee.parent_id.user_id == self.env.user
|
|
|
|
def _check_employee_can_create(self, vals_list):
|
|
if self._is_weekly_admin():
|
|
return
|
|
|
|
current_employee = self.env.user.employee_id
|
|
for vals in vals_list:
|
|
employee_id = vals.get('employee_id') or current_employee.id
|
|
if not current_employee or employee_id != current_employee.id:
|
|
raise UserError(_("Employees can create only their own weekly timesheets."))
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
self._check_employee_can_create(vals_list)
|
|
return super().create(vals_list)
|
|
|
|
def write(self, vals):
|
|
if not self._is_weekly_admin() and 'employee_id' in vals:
|
|
current_employee = self.env.user.employee_id
|
|
if vals['employee_id'] != current_employee.id:
|
|
raise UserError(_("Employees can update only their own weekly timesheets."))
|
|
return super().write(vals)
|
|
|
|
def action_submit(self):
|
|
|
|
for rec in self:
|
|
|
|
if rec.total_hours < 40:
|
|
raise ValidationError(
|
|
"Weekly timesheet hours cannot be less than 40 hours."
|
|
)
|
|
|
|
analytic_lines = self.env[
|
|
'account.analytic.line'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('date', '>=', rec.week_line_id.date_from),
|
|
('date', '<=', rec.week_line_id.date_to),
|
|
])
|
|
|
|
for line in analytic_lines:
|
|
|
|
vals = {
|
|
'weekly_submitted': True
|
|
}
|
|
|
|
if not line.pm_approval_required:
|
|
vals['approval_state'] = 'approved'
|
|
|
|
line.write(vals)
|
|
|
|
rec.state = 'submitted'
|
|
template = self.env.ref(
|
|
'weekly_timesheets.email_template_weekly_timesheet_submit'
|
|
)
|
|
|
|
if template:
|
|
template.send_mail(
|
|
rec.id,
|
|
force_send=True
|
|
)
|
|
|
|
def action_reject(self):
|
|
|
|
for rec in self:
|
|
if not rec.can_current_user_approve_weekly:
|
|
raise UserError(
|
|
_("Only Employee Manager or Administrator can reject.")
|
|
)
|
|
|
|
analytic_lines = self.env[
|
|
'account.analytic.line'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('date', '>=', rec.week_line_id.date_from),
|
|
('date', '<=', rec.week_line_id.date_to),
|
|
])
|
|
|
|
for line in analytic_lines:
|
|
|
|
vals = {
|
|
'weekly_submitted': True
|
|
}
|
|
|
|
if not line.pm_approval_required:
|
|
vals['approval_state'] = 'rejected'
|
|
|
|
line.write(vals)
|
|
|
|
rec.state = 'rejected'
|
|
|
|
template = self.env.ref(
|
|
'weekly_timesheets.email_template_weekly_timesheet_reject'
|
|
)
|
|
|
|
if template:
|
|
template.send_mail(
|
|
rec.id,
|
|
force_send=True
|
|
)
|
|
def action_approve(self):
|
|
|
|
for rec in self:
|
|
if rec.state != 'submitted':
|
|
raise UserError(
|
|
_("Only submitted timesheets can be approved.")
|
|
)
|
|
# Employee Manager
|
|
employee_manager = (
|
|
rec.employee_id.parent_id.user_id
|
|
)
|
|
# Administrator
|
|
is_admin = self._is_weekly_admin()
|
|
# Validation
|
|
if (
|
|
self.env.user != employee_manager
|
|
and not is_admin
|
|
):
|
|
raise UserError(
|
|
_("Only Employee Manager or Administrator can approve.")
|
|
)
|
|
|
|
# Ensure all PM approvals completed
|
|
pending_lines = self.env[
|
|
'account.analytic.line'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('date', '>=', rec.week_line_id.date_from),
|
|
('date', '<=', rec.week_line_id.date_to),
|
|
('pm_approval_required', '=', True),
|
|
('approval_state', '!=', 'approved'),
|
|
])
|
|
|
|
if pending_lines:
|
|
raise UserError(
|
|
_("All Project Manager approvals must be completed.")
|
|
)
|
|
|
|
rec.state = 'approved'
|
|
template = self.env.ref(
|
|
'weekly_timesheets.email_template_weekly_timesheet_approve'
|
|
)
|
|
|
|
if template:
|
|
template.send_mail(
|
|
rec.id,
|
|
force_send=True
|
|
)
|
|
|
|
def action_reset_to_draft(self):
|
|
for rec in self:
|
|
analytic_lines = self.env[
|
|
'account.analytic.line'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('date', '>=', rec.week_line_id.date_from),
|
|
('date', '<=', rec.week_line_id.date_to),
|
|
])
|
|
|
|
analytic_lines.write({
|
|
'weekly_submitted': False,
|
|
})
|
|
|
|
rec.state = 'draft'
|
|
|
|
def action_refresh_timesheets(self):
|
|
|
|
for rec in self:
|
|
if not rec.can_current_user_refresh_weekly:
|
|
raise UserError(
|
|
_("Only the employee can refresh their own draft or rejected weekly timesheet.")
|
|
)
|
|
|
|
if not rec.employee_id or not rec.week_line_id:
|
|
continue
|
|
|
|
lines_to_create = []
|
|
existing_lines_by_date = {
|
|
line.date: line
|
|
for line in rec.timesheet_line_ids
|
|
}
|
|
|
|
current_date = rec.week_line_id.date_from
|
|
|
|
while current_date <= rec.week_line_id.date_to:
|
|
analytic_lines = self.env[
|
|
'account.analytic.line'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('date', '=', current_date),
|
|
])
|
|
|
|
total_hours = sum(
|
|
analytic_lines.mapped('unit_amount')
|
|
)
|
|
|
|
existing_line = existing_lines_by_date.get(current_date)
|
|
values = {
|
|
'date': current_date,
|
|
'day_name': current_date.strftime("%A"),
|
|
'hours': total_hours,
|
|
}
|
|
|
|
if analytic_lines:
|
|
if existing_line:
|
|
existing_line.write(values)
|
|
else:
|
|
lines_to_create.append((0, 0, values))
|
|
elif not existing_line:
|
|
lines_to_create.append((0, 0, values))
|
|
|
|
current_date += timedelta(days=1)
|
|
|
|
if lines_to_create:
|
|
rec.timesheet_line_ids = lines_to_create
|
|
|
|
@api.onchange('employee_id', 'week_line_id')
|
|
def _onchange_employee_week(self):
|
|
|
|
self.timesheet_line_ids = [(5, 0, 0)]
|
|
|
|
if not self.employee_id or not self.week_line_id:
|
|
return
|
|
|
|
lines = []
|
|
|
|
current_date = self.week_line_id.date_from
|
|
|
|
while current_date <= self.week_line_id.date_to:
|
|
analytic_lines = self.env[
|
|
'account.analytic.line'
|
|
].search([
|
|
('employee_id', '=', self.employee_id.id),
|
|
('date', '=', current_date),
|
|
])
|
|
|
|
total_hours = sum(
|
|
analytic_lines.mapped('unit_amount')
|
|
)
|
|
|
|
lines.append((0, 0, {
|
|
'date': current_date,
|
|
'day_name': current_date.strftime("%A"),
|
|
'hours': total_hours,
|
|
}))
|
|
|
|
current_date += timedelta(days=1)
|
|
|
|
self.timesheet_line_ids = lines
|
|
|
|
analytic_line_ids = fields.One2many(
|
|
'account.analytic.line',
|
|
compute='_compute_analytic_lines',
|
|
inverse='_inverse_analytic_lines',
|
|
string="Original Timesheets"
|
|
)
|
|
|
|
def _inverse_analytic_lines(self):
|
|
pass
|
|
|
|
@api.depends('employee_id', 'week_line_id')
|
|
def _compute_analytic_lines(self):
|
|
for rec in self:
|
|
rec.analytic_line_ids = False
|
|
if not rec.employee_id or not rec.week_line_id:
|
|
continue
|
|
analytic_lines = self.env[
|
|
'account.analytic.line'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('date', '>=', rec.week_line_id.date_from),
|
|
('date', '<=', rec.week_line_id.date_to),
|
|
])
|
|
rec.analytic_line_ids = analytic_lines
|
|
all_pm_approved = fields.Boolean(
|
|
compute='_compute_all_pm_approved'
|
|
)
|
|
|
|
can_current_user_approve_weekly = fields.Boolean(
|
|
compute='_compute_can_current_user_approve_weekly',
|
|
search='_search_can_current_user_approve_weekly',
|
|
)
|
|
|
|
def _compute_can_current_user_approve_weekly(self):
|
|
is_admin = self._is_weekly_admin()
|
|
for rec in self:
|
|
rec.can_current_user_approve_weekly = (
|
|
is_admin
|
|
or rec._is_employee_manager_for(rec.employee_id)
|
|
)
|
|
|
|
def _search_can_current_user_approve_weekly(self, operator, value):
|
|
if operator not in ('=', '!='):
|
|
return []
|
|
|
|
match_true = (operator == '=' and value) or (operator == '!=' and not value)
|
|
if self._is_weekly_admin():
|
|
return [] if match_true else [('id', '=', 0)]
|
|
|
|
domain = [('employee_id.parent_id.user_id', '=', self.env.uid)]
|
|
if match_true:
|
|
return domain
|
|
return ['!', domain[0]]
|
|
|
|
def _compute_all_pm_approved(self):
|
|
|
|
for rec in self:
|
|
pending_lines = self.env[
|
|
'account.analytic.line'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('date', '>=', rec.week_line_id.date_from),
|
|
('date', '<=', rec.week_line_id.date_to),
|
|
('pm_approval_required', '=', True),
|
|
('approval_state', '!=', 'approved'),
|
|
])
|
|
|
|
rec.all_pm_approved = not pending_lines
|
|
|
|
|
|
class WeeklyPeriodTimesheetsLine(models.Model):
|
|
_name = 'weekly.periodtimesheets.line'
|
|
_description = 'Weekly Period Timesheet Line'
|
|
|
|
weekly_timesheet_id = fields.Many2one(
|
|
'weekly.periodtimesheets',
|
|
string="Weekly Timesheet"
|
|
)
|
|
|
|
date = fields.Date(
|
|
string="Date"
|
|
)
|
|
|
|
day_name = fields.Char(
|
|
string="Day"
|
|
)
|
|
|
|
hours = fields.Float(
|
|
string="Hours"
|
|
)
|
|
|
|
|
|
class AccountAnalyticLine(models.Model):
|
|
_inherit = 'account.analytic.line'
|
|
|
|
approval_state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('approved', 'Approved'),
|
|
('rejected', 'Rejected'),
|
|
], string="Approval Status", default='draft')
|
|
weekly_submitted = fields.Boolean(
|
|
string="Weekly Submitted",
|
|
default=False
|
|
)
|
|
pm_approval_required = fields.Boolean(
|
|
compute='_compute_pm_approval_required',
|
|
store=True
|
|
)
|
|
task_stage_id = fields.Many2one(
|
|
'project.task.type',
|
|
string="Stage",
|
|
related='task_id.stage_id',
|
|
store=True,
|
|
readonly=True,
|
|
)
|
|
can_current_user_approve_line = fields.Boolean(
|
|
compute='_compute_can_current_user_approve_line',
|
|
search='_search_can_current_user_approve_line',
|
|
)
|
|
|
|
@api.depends(
|
|
'task_id.is_generic',
|
|
'project_id.privacy_visibility'
|
|
)
|
|
def _compute_pm_approval_required(self):
|
|
|
|
for rec in self:
|
|
rec.pm_approval_required = not (
|
|
rec.task_id.is_generic
|
|
or rec.project_id.privacy_visibility != 'followers'
|
|
)
|
|
|
|
def _compute_can_current_user_approve_line(self):
|
|
is_admin = (
|
|
self.env.user.has_group('project.group_project_manager')
|
|
or self.env.user.has_group('base.group_system')
|
|
)
|
|
for rec in self:
|
|
rec.can_current_user_approve_line = (
|
|
is_admin
|
|
or rec.project_id.user_id == self.env.user
|
|
or (
|
|
'project_lead' in rec.project_id._fields
|
|
and rec.project_id.project_lead == self.env.user
|
|
)
|
|
)
|
|
|
|
def _search_can_current_user_approve_line(self, operator, value):
|
|
if operator not in ('=', '!='):
|
|
return []
|
|
|
|
match_true = (operator == '=' and value) or (operator == '!=' and not value)
|
|
if (
|
|
self.env.user.has_group('project.group_project_manager')
|
|
or self.env.user.has_group('base.group_system')
|
|
):
|
|
return [] if match_true else [('id', '=', 0)]
|
|
|
|
if 'project_lead' in self.env['project.project']._fields:
|
|
domain = [
|
|
'|',
|
|
('project_id.user_id', '=', self.env.uid),
|
|
('project_id.project_lead', '=', self.env.uid),
|
|
]
|
|
else:
|
|
domain = [('project_id.user_id', '=', self.env.uid)]
|
|
if match_true:
|
|
return domain
|
|
return ['!'] + domain
|
|
|
|
def action_pm_approve(self):
|
|
|
|
for rec in self:
|
|
|
|
weekly_sheet = self.env[
|
|
'weekly.periodtimesheets'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('week_line_id.date_from', '<=', rec.date),
|
|
('week_line_id.date_to', '>=', rec.date),
|
|
], limit=1)
|
|
|
|
# Final approval completed
|
|
|
|
if weekly_sheet.state == 'approved':
|
|
raise UserError(
|
|
_("Weekly Timesheet already approved.")
|
|
)
|
|
|
|
# Weekly not submitted
|
|
|
|
if not rec.weekly_submitted:
|
|
raise UserError(
|
|
_("Weekly Timesheet is not submitted yet.")
|
|
)
|
|
|
|
is_project_manager = (
|
|
rec.project_id.user_id == self.env.user
|
|
)
|
|
|
|
is_project_lead = (
|
|
'project_lead' in rec.project_id._fields
|
|
and rec.project_id.project_lead == self.env.user
|
|
)
|
|
|
|
is_admin = (
|
|
self.env.user.has_group('project.group_project_manager')
|
|
or self.env.user.has_group('base.group_system')
|
|
)
|
|
|
|
if not (is_project_manager or is_project_lead or is_admin):
|
|
raise UserError(
|
|
_("Only Project Manager, Project Lead or Administrator can approve.")
|
|
)
|
|
|
|
rec.approval_state = 'approved'
|
|
|
|
def action_pm_reject(self):
|
|
|
|
for rec in self:
|
|
|
|
weekly_sheet = self.env[
|
|
'weekly.periodtimesheets'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('week_line_id.date_from', '<=', rec.date),
|
|
('week_line_id.date_to', '>=', rec.date),
|
|
], limit=1)
|
|
|
|
if weekly_sheet.state == 'approved':
|
|
raise UserError(
|
|
_("Weekly Timesheet already approved.")
|
|
)
|
|
|
|
if not rec.weekly_submitted:
|
|
raise UserError(
|
|
_("Weekly Timesheet is not submitted yet.")
|
|
)
|
|
|
|
is_project_manager = (
|
|
rec.project_id.user_id == self.env.user
|
|
)
|
|
|
|
is_project_lead = (
|
|
'project_lead' in rec.project_id._fields
|
|
and rec.project_id.project_lead == self.env.user
|
|
)
|
|
|
|
is_admin = (
|
|
self.env.user.has_group('project.group_project_manager')
|
|
or self.env.user.has_group('base.group_system')
|
|
)
|
|
|
|
if not (is_project_manager or is_project_lead or is_admin):
|
|
raise UserError(
|
|
_("Only Project Manager, Project Lead or Administrator can reject.")
|
|
)
|
|
|
|
rec.approval_state = 'rejected'
|
|
|
|
def _check_weekly_submission(self):
|
|
|
|
for rec in self:
|
|
|
|
if not rec.employee_id or not rec.date:
|
|
continue
|
|
|
|
weekly_sheet = self.env[
|
|
'weekly.periodtimesheets'
|
|
].search([
|
|
('employee_id', '=', rec.employee_id.id),
|
|
('state', '=', 'submitted'),
|
|
('week_line_id.date_from', '<=', rec.date),
|
|
('week_line_id.date_to', '>=', rec.date),
|
|
], limit=1)
|
|
|
|
if weekly_sheet:
|
|
raise ValidationError(
|
|
"Weekly Timesheet already submitted. "
|
|
"You cannot create, edit or delete "
|
|
"timesheets for this week."
|
|
)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
|
|
records = super().create(vals_list)
|
|
|
|
records._check_weekly_submission()
|
|
|
|
return records
|
|
|
|
def write(self, vals):
|
|
|
|
# Skip validation for system approval updates
|
|
|
|
skip_fields = [
|
|
'approval_state',
|
|
'weekly_submitted',
|
|
]
|
|
|
|
if not all(field in skip_fields for field in vals.keys()):
|
|
self._check_weekly_submission()
|
|
|
|
return super().write(vals)
|
|
|
|
def unlink(self):
|
|
|
|
self._check_weekly_submission()
|
|
|
|
return super().unlink()
|