# -*- coding: utf-8 -*- """ Fusion Payroll Dashboard ======================== Modern dashboard with statistics, quick actions, and charts. """ from odoo import api, fields, models, _ from datetime import date from dateutil.relativedelta import relativedelta class PayrollDashboard(models.Model): """ Dashboard model for Fusion Payroll. This is a singleton model that provides computed statistics. """ _name = 'fusion.payroll.dashboard' _description = 'Fusion Payroll Dashboard' name = fields.Char(default='Fusion Payroll Dashboard') company_id = fields.Many2one( 'res.company', default=lambda self: self.env.company, ) # ==================== STATISTICS ==================== # Employee Stats total_employees = fields.Integer( string='Total Employees', compute='_compute_employee_stats', ) active_employees = fields.Integer( string='Active Employees', compute='_compute_employee_stats', ) on_leave_employees = fields.Integer( string='On Leave', compute='_compute_employee_stats', ) terminated_employees = fields.Integer( string='Terminated', compute='_compute_employee_stats', ) # Payroll Stats payroll_this_month = fields.Monetary( string='Payroll This Month', currency_field='currency_id', compute='_compute_payroll_stats', ) payroll_this_year = fields.Monetary( string='Payroll This Year', currency_field='currency_id', compute='_compute_payroll_stats', ) payroll_last_month = fields.Monetary( string='Payroll Last Month', currency_field='currency_id', compute='_compute_payroll_stats', ) payroll_count_this_month = fields.Integer( string='Payslips This Month', compute='_compute_payroll_stats', ) avg_payroll_per_employee = fields.Monetary( string='Avg Pay Per Employee', currency_field='currency_id', compute='_compute_payroll_stats', ) # Trend indicators payroll_trend = fields.Float( string='Payroll Trend (%)', compute='_compute_payroll_stats', help='Percentage change from last month', ) currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.company.currency_id, ) # ==================== COMPUTED METHODS ==================== @api.depends('company_id') def _compute_employee_stats(self): for dashboard in self: Employee = self.env['hr.employee'] company_domain = [('company_id', '=', dashboard.company_id.id)] dashboard.total_employees = Employee.search_count(company_domain) # Check if employment_status field exists if 'employment_status' in Employee._fields: dashboard.active_employees = Employee.search_count( company_domain + [('employment_status', '=', 'active')] ) dashboard.on_leave_employees = Employee.search_count( company_domain + [('employment_status', '=', 'on_leave')] ) dashboard.terminated_employees = Employee.search_count( company_domain + [('employment_status', '=', 'terminated')] ) else: dashboard.active_employees = Employee.search_count( company_domain + [('active', '=', True)] ) dashboard.on_leave_employees = 0 dashboard.terminated_employees = Employee.search_count( company_domain + [('active', '=', False)] ) @api.depends('company_id') def _compute_payroll_stats(self): today = date.today() month_start = today.replace(day=1) year_start = today.replace(month=1, day=1) last_month_start = month_start - relativedelta(months=1) last_month_end = month_start - relativedelta(days=1) for dashboard in self: Payslip = self.env['hr.payslip'] company_domain = [ ('company_id', '=', dashboard.company_id.id), ('state', 'in', ['done', 'paid']), ] # This month month_payslips = Payslip.search( company_domain + [ ('date_from', '>=', month_start), ('date_to', '<=', today), ] ) dashboard.payroll_this_month = sum(month_payslips.mapped('net_wage')) dashboard.payroll_count_this_month = len(month_payslips) # This year year_payslips = Payslip.search( company_domain + [ ('date_from', '>=', year_start), ] ) dashboard.payroll_this_year = sum(year_payslips.mapped('net_wage')) # Last month (for trend) last_month_payslips = Payslip.search( company_domain + [ ('date_from', '>=', last_month_start), ('date_to', '<=', last_month_end), ] ) dashboard.payroll_last_month = sum(last_month_payslips.mapped('net_wage')) # Trend calculation if dashboard.payroll_last_month > 0: dashboard.payroll_trend = ( (dashboard.payroll_this_month - dashboard.payroll_last_month) / dashboard.payroll_last_month * 100 ) else: dashboard.payroll_trend = 0 # Average per employee if dashboard.active_employees > 0 and dashboard.payroll_this_month > 0: dashboard.avg_payroll_per_employee = ( dashboard.payroll_this_month / dashboard.active_employees ) else: dashboard.avg_payroll_per_employee = 0 # ==================== ACTION METHODS ==================== def action_run_payroll(self): """Open Run Payroll wizard.""" action = self.env['run.payroll.wizard'].action_open_run_payroll() # Ensure edit is enabled (override dashboard context) if action.get('context'): ctx = action['context'] if isinstance(ctx, str): try: ctx = eval(ctx, {'uid': self.env.uid}) except: ctx = {} if not isinstance(ctx, dict): ctx = {} else: ctx = {} ctx.update({'create': True, 'edit': True, 'delete': True}) action['context'] = ctx return action def _get_action_context(self, action): """Parse action context and add create/edit/delete permissions.""" ctx = action.get('context', {}) if isinstance(ctx, str): try: ctx = eval(ctx, {'uid': self.env.uid, 'active_id': self.id}) except: ctx = {} if not isinstance(ctx, dict): ctx = {} ctx.update({'create': True, 'edit': True, 'delete': True}) return ctx def action_view_employees(self): """Open All Employees view using existing action.""" action = self.env['ir.actions.act_window']._for_xml_id('fusion_payroll.action_fusion_employees') action['context'] = self._get_action_context(action) return action def action_cra_remittance(self): """Open CRA Remittance view using existing action.""" action = self.env['ir.actions.act_window']._for_xml_id('fusion_payroll.action_fusion_tax_remittances') action['context'] = self._get_action_context(action) return action def action_t4_slips(self): """Open T4 Slips view using existing action.""" action = self.env['ir.actions.act_window']._for_xml_id('fusion_payroll.action_fusion_t4_slips') action['context'] = self._get_action_context(action) return action def action_roe(self): """Open Record of Employment view using existing action.""" action = self.env['ir.actions.act_window']._for_xml_id('fusion_payroll.action_fusion_roe') action['context'] = self._get_action_context(action) return action def action_reports(self): """Open Reports hub using existing action.""" try: action = self.env['ir.actions.act_window']._for_xml_id('fusion_payroll.action_fusion_report_hub_page') return action except ValueError: # Fallback if action doesn't exist return { 'type': 'ir.actions.act_window', 'name': _('Payroll Reports'), 'res_model': 'ir.actions.report', 'view_mode': 'kanban,list', 'domain': [('model', 'like', 'hr.payslip')], } def action_settings(self): """Open Fusion Payroll Settings.""" # Get or create settings record settings = self.env['payroll.config.settings'].get_settings() return { 'type': 'ir.actions.act_window', 'name': _('Fusion Payroll Settings'), 'res_model': 'payroll.config.settings', 'res_id': settings.id, 'view_mode': 'form', 'target': 'current', 'context': {'form_view_initial_mode': 'edit'}, } # ==================== SINGLETON PATTERN ==================== @api.model def get_dashboard(self): """Get or create the dashboard singleton.""" dashboard = self.search([ ('company_id', '=', self.env.company.id) ], limit=1) if not dashboard: dashboard = self.create({ 'name': 'Fusion Payroll Dashboard', 'company_id': self.env.company.id, }) return dashboard @api.model def action_open_dashboard(self): """Open the dashboard.""" dashboard = self.get_dashboard() return { 'type': 'ir.actions.act_window', 'name': _('Fusion Payroll'), 'res_model': 'fusion.payroll.dashboard', 'res_id': dashboard.id, 'view_mode': 'form', 'target': 'current', 'flags': {'mode': 'readonly'}, } class PayrollDashboardStats(models.Model): """ Monthly payroll statistics for graphing. This model stores aggregated monthly data for trend charts. """ _name = 'fusion.payroll.stats' _description = 'Payroll Statistics' _order = 'year desc, month desc' name = fields.Char(compute='_compute_name', store=True) company_id = fields.Many2one('res.company', required=True) year = fields.Integer(required=True) month = fields.Integer(required=True) month_name = fields.Char(compute='_compute_name', store=True) total_payroll = fields.Monetary(currency_field='currency_id') total_gross = fields.Monetary(currency_field='currency_id') total_taxes = fields.Monetary(currency_field='currency_id') employee_count = fields.Integer() payslip_count = fields.Integer() currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.company.currency_id, ) @api.depends('year', 'month') def _compute_name(self): month_names = [ '', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ] for stat in self: if stat.month and stat.year: stat.month_name = month_names[stat.month] stat.name = f"{month_names[stat.month]} {stat.year}" else: stat.month_name = '' stat.name = 'New' @api.model def refresh_stats(self): """Refresh statistics from payslip data.""" today = date.today() company = self.env.company # Get data for last 12 months for i in range(12): target_date = today - relativedelta(months=i) year = target_date.year month = target_date.month month_start = target_date.replace(day=1) month_end = (month_start + relativedelta(months=1)) - relativedelta(days=1) # Find payslips for this month payslips = self.env['hr.payslip'].search([ ('company_id', '=', company.id), ('state', 'in', ['done', 'paid']), ('date_from', '>=', month_start), ('date_to', '<=', month_end), ]) # Find or create stat record stat = self.search([ ('company_id', '=', company.id), ('year', '=', year), ('month', '=', month), ], limit=1) vals = { 'company_id': company.id, 'year': year, 'month': month, 'total_payroll': sum(payslips.mapped('net_wage')), 'total_gross': sum(payslips.mapped('basic_wage')) if 'basic_wage' in payslips._fields else 0, 'total_taxes': 0, # Would need to sum tax lines 'employee_count': len(set(payslips.mapped('employee_id.id'))), 'payslip_count': len(payslips), } if stat: stat.write(vals) else: self.create(vals) return True