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