423 lines
15 KiB
Python
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
|