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