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

423 lines
15 KiB
Python

# -*- coding: utf-8 -*-
"""
Cost Reports
============
- Total Pay
- Total Payroll Cost
- Deductions and Contributions
- Workers' Compensation
"""
from collections import defaultdict
from odoo import api, fields, models, _
class PayrollReportTotalPay(models.AbstractModel):
"""
Total Pay Report
Breakdown of pay by type per employee.
"""
_name = 'payroll.report.total.pay'
_inherit = 'payroll.report'
_description = 'Total Pay Report'
report_name = 'Total Pay'
report_code = 'total_pay'
def _get_columns(self):
return [
{'name': _('Name'), 'field': 'name', 'type': 'char', 'sortable': True},
{'name': _('Regular Pay'), 'field': 'regular_pay', 'type': 'monetary', 'sortable': True},
{'name': _('Stat Holiday Pay'), 'field': 'stat_holiday', 'type': 'monetary', 'sortable': True},
{'name': _('Vacation Pay'), 'field': 'vacation_pay', 'type': 'monetary', 'sortable': True},
{'name': _('Total'), 'field': 'total', 'type': 'monetary', 'sortable': True},
]
def _get_lines(self, options):
domain = self._get_domain(options)
domain.append(('state', 'in', ['done', 'paid']))
payslips = self.env['hr.payslip'].search(domain)
# Aggregate by employee
emp_data = defaultdict(lambda: {
'regular_pay': 0,
'stat_holiday': 0,
'vacation_pay': 0,
'total': 0,
})
for slip in payslips:
if not slip.employee_id:
continue
emp_key = slip.employee_id.id
emp_data[emp_key]['name'] = slip.employee_id.name or 'Unknown'
if hasattr(slip, 'line_ids') and slip.line_ids:
for line in slip.line_ids:
if hasattr(line, 'category_id') and line.category_id and hasattr(line.category_id, 'code'):
if line.category_id.code == 'BASIC':
emp_data[emp_key]['regular_pay'] += line.total or 0
if hasattr(line, 'code') and line.code:
if line.code == 'STAT_HOLIDAY':
emp_data[emp_key]['stat_holiday'] += line.total or 0
elif line.code == 'VACATION':
emp_data[emp_key]['vacation_pay'] += line.total or 0
emp_data[emp_key]['total'] += getattr(slip, 'gross_wage', 0) or 0
lines = []
totals = defaultdict(float)
for emp_id, data in emp_data.items():
for key in ['regular_pay', 'stat_holiday', 'vacation_pay', 'total']:
totals[key] += data[key]
lines.append({
'id': f'emp_{emp_id}',
'name': data['name'],
'values': {
'name': data['name'],
'regular_pay': data['regular_pay'],
'stat_holiday': data['stat_holiday'],
'vacation_pay': data['vacation_pay'],
'total': data['total'],
},
'level': 0,
})
# Sort by name
lines.sort(key=lambda x: x['name'])
# Total line
if lines:
lines.append({
'id': 'total',
'name': _('Total Pay'),
'values': {
'name': _('Total Pay'),
'regular_pay': totals['regular_pay'],
'stat_holiday': totals['stat_holiday'],
'vacation_pay': totals['vacation_pay'],
'total': totals['total'],
},
'level': -1,
'class': 'o_payroll_report_total fw-bold bg-success text-white',
})
return lines
class PayrollReportTotalCost(models.AbstractModel):
"""
Total Payroll Cost Report
Summary of all payroll costs.
"""
_name = 'payroll.report.total.cost'
_inherit = 'payroll.report'
_description = 'Total Payroll Cost Report'
report_name = 'Total Payroll Cost'
report_code = 'total_cost'
filter_employee = False
def _get_columns(self):
return [
{'name': _('Item'), 'field': 'item', 'type': 'char', 'sortable': False},
{'name': _('Amount'), 'field': 'amount', 'type': 'monetary', 'sortable': True},
]
def _get_lines(self, options):
domain = [
('state', 'in', ['done', 'paid']),
('company_id', '=', self.env.company.id),
]
date_from = options.get('date', {}).get('date_from')
date_to = options.get('date', {}).get('date_to')
if date_from:
domain.append(('date_from', '>=', date_from))
if date_to:
domain.append(('date_to', '<=', date_to))
payslips = self.env['hr.payslip'].search(domain)
# Calculate totals - safely handle missing fields
paycheque_wages = 0
non_paycheque = 0 # Reimbursements, etc.
reimbursements = 0
ei_employer = 0
cpp_employer = 0
cpp2_employer = 0
for slip in payslips:
paycheque_wages += getattr(slip, 'gross_wage', 0) or 0
ei_employer += getattr(slip, 'employer_ei', 0) or 0
cpp_employer += getattr(slip, 'employer_cpp', 0) or 0
cpp2_employer += getattr(slip, 'employer_cpp2', 0) or 0
total_employer_taxes = ei_employer + cpp_employer + cpp2_employer
total_pay = paycheque_wages + non_paycheque + reimbursements
total_cost = total_pay + total_employer_taxes
lines = [
# Total Pay Section
{
'id': 'total_pay_header',
'name': _('Total Pay'),
'values': {'item': _('Total Pay'), 'amount': ''},
'level': -1,
'class': 'fw-bold',
},
{
'id': 'paycheque_wages',
'name': _('Paycheque Wages'),
'values': {'item': _(' Paycheque Wages'), 'amount': paycheque_wages},
'level': 0,
},
{
'id': 'non_paycheque',
'name': _('Non-paycheque Wages'),
'values': {'item': _(' Non-paycheque Wages'), 'amount': non_paycheque},
'level': 0,
},
{
'id': 'reimbursements',
'name': _('Reimbursements'),
'values': {'item': _(' Reimbursements'), 'amount': reimbursements},
'level': 0,
},
{
'id': 'subtotal_pay',
'name': _('Subtotal'),
'values': {'item': _('Subtotal'), 'amount': total_pay},
'level': 0,
'class': 'fw-bold',
},
# Company Contributions Section
{
'id': 'contributions_header',
'name': _('Company Contributions'),
'values': {'item': _('Company Contributions'), 'amount': ''},
'level': -1,
'class': 'fw-bold',
},
{
'id': 'subtotal_contributions',
'name': _('Subtotal'),
'values': {'item': _('Subtotal'), 'amount': 0},
'level': 0,
'class': 'fw-bold',
},
# Employer Taxes Section
{
'id': 'employer_taxes_header',
'name': _('Employer Taxes'),
'values': {'item': _('Employer Taxes'), 'amount': ''},
'level': -1,
'class': 'fw-bold',
},
{
'id': 'ei_employer',
'name': _('Employment Insurance Employer'),
'values': {'item': _(' Employment Insurance Employer'), 'amount': ei_employer},
'level': 0,
},
{
'id': 'cpp_employer',
'name': _('Canada Pension Plan Employer'),
'values': {'item': _(' Canada Pension Plan Employer'), 'amount': cpp_employer},
'level': 0,
},
{
'id': 'cpp2_employer',
'name': _('Second Canada Pension Plan Employer'),
'values': {'item': _(' Second Canada Pension Plan Employer'), 'amount': cpp2_employer},
'level': 0,
},
{
'id': 'subtotal_employer',
'name': _('Subtotal'),
'values': {'item': _('Subtotal'), 'amount': total_employer_taxes},
'level': 0,
'class': 'fw-bold',
},
# Grand Total
{
'id': 'total_cost',
'name': _('Total Payroll Cost'),
'values': {'item': _('Total Payroll Cost'), 'amount': total_cost},
'level': -1,
'class': 'o_payroll_report_total fw-bold bg-dark text-white',
},
]
return lines
class PayrollReportDeductions(models.AbstractModel):
"""
Deductions and Contributions Report
"""
_name = 'payroll.report.deductions'
_inherit = 'payroll.report'
_description = 'Deductions and Contributions Report'
report_name = 'Deductions and Contributions'
report_code = 'deductions'
filter_employee = False
def _get_columns(self):
return [
{'name': _('Description'), 'field': 'description', 'type': 'char', 'sortable': True},
{'name': _('Type'), 'field': 'type', 'type': 'char', 'sortable': True},
{'name': _('Employee Deductions'), 'field': 'employee_deductions', 'type': 'monetary', 'sortable': True},
{'name': _('Company Contributions'), 'field': 'company_contributions', 'type': 'monetary', 'sortable': True},
{'name': _('Plan Total'), 'field': 'plan_total', 'type': 'monetary', 'sortable': True},
]
def _get_lines(self, options):
domain = [
('state', 'in', ['done', 'paid']),
('company_id', '=', self.env.company.id),
]
date_from = options.get('date', {}).get('date_from')
date_to = options.get('date', {}).get('date_to')
if date_from:
domain.append(('date_from', '>=', date_from))
if date_to:
domain.append(('date_to', '<=', date_to))
payslips = self.env['hr.payslip'].search(domain)
# Aggregate deductions
deduction_data = defaultdict(lambda: {'employee': 0, 'company': 0})
deduction_codes = {
'CPP': {'name': 'Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP_ER'},
'CPP2': {'name': 'Second Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP2_ER'},
'EI': {'name': 'Employment Insurance', 'type': 'Tax', 'employer_code': 'EI_ER'},
}
for slip in payslips:
if not hasattr(slip, 'line_ids') or not slip.line_ids:
continue
for line in slip.line_ids:
if not hasattr(line, 'code') or not line.code:
continue
if line.code in deduction_codes:
deduction_data[line.code]['employee'] += abs(line.total or 0)
elif line.code.endswith('_ER'):
base_code = line.code[:-3]
if base_code in deduction_codes:
deduction_data[base_code]['company'] += abs(line.total or 0)
lines = []
for code, info in deduction_codes.items():
data = deduction_data[code]
total = data['employee'] + data['company']
lines.append({
'id': f'ded_{code}',
'name': info['name'],
'values': {
'description': info['name'],
'type': info['type'],
'employee_deductions': data['employee'],
'company_contributions': data['company'],
'plan_total': total,
},
'level': 0,
})
return lines
class PayrollReportWorkersComp(models.AbstractModel):
"""
Workers' Compensation Report
"""
_name = 'payroll.report.workers.comp'
_inherit = 'payroll.report'
_description = 'Workers Compensation Report'
report_name = "Workers' Compensation"
report_code = 'workers_comp'
filter_employee = False
def _get_columns(self):
return [
{'name': _('Province'), 'field': 'province', 'type': 'char', 'sortable': True},
{'name': _("Workers' Comp Class"), 'field': 'wc_class', 'type': 'char', 'sortable': True},
{'name': _('Premium Wage Paid'), 'field': 'premium_wage', 'type': 'monetary', 'sortable': True},
{'name': _('Tips Paid'), 'field': 'tips_paid', 'type': 'monetary', 'sortable': True},
{'name': _('Employee Taxes Paid by Employer'), 'field': 'emp_taxes_employer', 'type': 'monetary', 'sortable': True},
{'name': _('Wages Paid'), 'field': 'wages_paid', 'type': 'monetary', 'sortable': True},
]
def _get_lines(self, options):
domain = [
('state', 'in', ['done', 'paid']),
('company_id', '=', self.env.company.id),
]
date_from = options.get('date', {}).get('date_from')
date_to = options.get('date', {}).get('date_to')
if date_from:
domain.append(('date_from', '>=', date_from))
if date_to:
domain.append(('date_to', '<=', date_to))
payslips = self.env['hr.payslip'].search(domain)
# Group by province
province_data = defaultdict(lambda: {'wages': 0, 'premium': 0, 'tips': 0, 'emp_taxes': 0})
for slip in payslips:
if not slip.employee_id:
continue
province = 'ON' # Default, would get from employee address
if hasattr(slip.employee_id, 'province_of_employment'):
province = getattr(slip.employee_id, 'province_of_employment', None) or 'ON'
province_data[province]['wages'] += getattr(slip, 'gross_wage', 0) or 0
lines = []
total_wages = 0
for province, data in province_data.items():
total_wages += data['wages']
lines.append({
'id': f'prov_{province}',
'name': province,
'values': {
'province': province,
'wc_class': 'No Name Specified',
'premium_wage': data['premium'],
'tips_paid': data['tips'],
'emp_taxes_employer': data['emp_taxes'],
'wages_paid': data['wages'],
},
'level': 0,
})
# Total
if lines:
lines.append({
'id': 'total',
'name': _('Total'),
'values': {
'province': _('Total'),
'wc_class': '',
'premium_wage': 0,
'tips_paid': 0,
'emp_taxes_employer': 0,
'wages_paid': total_wages,
},
'level': -1,
'class': 'o_payroll_report_total fw-bold bg-success text-white',
})
return lines