380 lines
13 KiB
Python
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
|