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