odoo18/addons_extensions/weekly_timesheets/models/weekly_period_timesheets.py

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()