489 lines
14 KiB
Python
489 lines
14 KiB
Python
from odoo import api, fields, models, _
|
|
from datetime import 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,
|
|
)
|
|
|
|
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')
|
|
|
|
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:
|
|
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
|
|
)
|
|
# Project Administrator
|
|
is_admin = self.env.user.has_group(
|
|
'project.group_project_manager'
|
|
)
|
|
# Validation
|
|
if (
|
|
self.env.user != employee_manager
|
|
and not is_admin
|
|
):
|
|
raise UserError(
|
|
_("Only Employee Manager or Project 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:
|
|
|
|
rec.timesheet_line_ids = [(5, 0, 0)]
|
|
|
|
if not rec.employee_id or not rec.week_line_id:
|
|
return
|
|
|
|
lines = []
|
|
|
|
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')
|
|
)
|
|
|
|
lines.append((0, 0, {
|
|
'date': current_date,
|
|
'day_name': current_date.strftime("%A"),
|
|
'hours': total_hours,
|
|
}))
|
|
|
|
current_date += timedelta(days=1)
|
|
|
|
rec.timesheet_line_ids = lines
|
|
|
|
@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
|
|
self.action_refresh_timesheets()
|
|
|
|
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'
|
|
)
|
|
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
|
|
)
|
|
|
|
@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 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_admin = self.env.user.has_group(
|
|
'project.group_project_manager'
|
|
)
|
|
|
|
if not (is_project_manager or is_admin):
|
|
raise UserError(
|
|
_("Only Project Manager 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_admin = self.env.user.has_group(
|
|
'project.group_project_manager'
|
|
)
|
|
|
|
if not (is_project_manager or is_admin):
|
|
raise UserError(
|
|
_("Only Project Manager 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()
|