Files
Odoo-Modules/fusion_payroll/models/payroll_dashboard.py
2026-02-22 01:22:18 -05:00

380 lines
13 KiB
Python

# -*- 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