# -*- coding: utf-8 -*- """ Summary Reports =============== - Payroll Summary - Payroll Summary by Employee """ from collections import defaultdict from odoo import api, fields, models, _ class PayrollReportSummary(models.AbstractModel): """ Payroll Summary Report Per pay date summary with all payroll components. """ _name = 'payroll.report.summary' _inherit = 'payroll.report' _description = 'Payroll Summary Report' report_name = 'Payroll Summary' report_code = 'payroll_summary' def _get_columns(self): return [ {'name': _('Pay Date'), 'field': 'pay_date', 'type': 'date', 'sortable': True}, {'name': _('Name'), 'field': 'name', 'type': 'char', 'sortable': True}, {'name': _('Hours'), 'field': 'hours', 'type': 'float', 'sortable': True}, {'name': _('Gross Pay'), 'field': 'gross_pay', 'type': 'monetary', 'sortable': True}, {'name': _('Pretax Deductions'), 'field': 'pretax_deductions', 'type': 'monetary', 'sortable': False}, {'name': _('Other Pay'), 'field': 'other_pay', 'type': 'monetary', 'sortable': False}, {'name': _('Employee Taxes'), 'field': 'employee_taxes', 'type': 'monetary', 'sortable': True}, {'name': _('Aftertax Deductions'), 'field': 'aftertax_deductions', 'type': 'monetary', 'sortable': False}, {'name': _('Net Pay'), 'field': 'net_pay', 'type': 'monetary', 'sortable': True}, {'name': _('Employer Taxes'), 'field': 'employer_taxes', 'type': 'monetary', 'sortable': True}, {'name': _('Company Contributions'), 'field': 'company_contributions', 'type': 'monetary', 'sortable': False}, {'name': _('Total Payroll Cost'), 'field': 'total_cost', '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, order='date_to desc, employee_id') lines = [] totals = defaultdict(float) for slip in payslips: if not slip.employee_id: continue hours = 0 if hasattr(slip, 'worked_days_line_ids') and slip.worked_days_line_ids: hours = sum(slip.worked_days_line_ids.mapped('number_of_hours')) or 0 employee_taxes = getattr(slip, 'total_employee_deductions', 0) or 0 employer_taxes = getattr(slip, 'total_employer_cost', 0) or 0 gross_wage = getattr(slip, 'gross_wage', 0) or 0 net_wage = getattr(slip, 'net_wage', 0) or 0 total_cost = gross_wage + employer_taxes values = { 'pay_date': slip.date_to or '', 'name': slip.employee_id.name or 'Unknown', 'hours': hours, 'gross_pay': gross_wage, 'pretax_deductions': 0, 'other_pay': 0, 'employee_taxes': employee_taxes, 'aftertax_deductions': 0, 'net_pay': net_wage, 'employer_taxes': employer_taxes, 'company_contributions': 0, 'total_cost': total_cost, } # Accumulate totals for key in ['hours', 'gross_pay', 'employee_taxes', 'net_pay', 'employer_taxes', 'total_cost']: totals[key] += values[key] lines.append({ 'id': f'summary_{slip.id}', 'name': slip.employee_id.name or 'Unknown', 'values': values, 'level': 0, 'model': 'hr.payslip', 'res_id': slip.id, }) # Total line if lines: lines.insert(0, { 'id': 'total', 'name': _('Total'), 'values': { 'pay_date': '', 'name': _('Total'), 'hours': totals['hours'], 'gross_pay': totals['gross_pay'], 'pretax_deductions': 0, 'other_pay': 0, 'employee_taxes': totals['employee_taxes'], 'aftertax_deductions': 0, 'net_pay': totals['net_pay'], 'employer_taxes': totals['employer_taxes'], 'company_contributions': 0, 'total_cost': totals['total_cost'], }, 'level': -1, 'class': 'o_payroll_report_total fw-bold bg-light', }) return lines class PayrollReportSummaryByEmployee(models.AbstractModel): """ Payroll Summary by Employee Report Pivot-style with employees as columns, pay types as rows. """ _name = 'payroll.report.summary.by.employee' _inherit = 'payroll.report' _description = 'Payroll Summary by Employee Report' report_name = 'Payroll Summary by Employee' report_code = 'payroll_summary_employee' def _get_columns(self): # Dynamic columns based on employees in date range # Base columns first return [ {'name': _('Payroll'), 'field': 'payroll_item', 'type': 'char', 'sortable': False}, {'name': _('Total'), 'field': 'total', 'type': 'monetary', 'sortable': True}, # Employee columns will be added dynamically ] def _get_dynamic_columns(self, options): """Get columns including employee names.""" date_from = options.get('date', {}).get('date_from') date_to = options.get('date', {}).get('date_to') domain = [ ('state', 'in', ['done', 'paid']), ('company_id', '=', self.env.company.id), ] 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) employees = payslips.mapped('employee_id') columns = [ {'name': _('Payroll'), 'field': 'payroll_item', 'type': 'char', 'sortable': False}, {'name': _('Total'), 'field': 'total', 'type': 'monetary', 'sortable': True}, ] for emp in employees.sorted('name'): columns.append({ 'name': emp.name, 'field': f'emp_{emp.id}', 'type': 'monetary', 'sortable': True, }) return columns, employees def _get_lines(self, options): date_from = options.get('date', {}).get('date_from') date_to = options.get('date', {}).get('date_to') domain = [ ('state', 'in', ['done', 'paid']), ('company_id', '=', self.env.company.id), ] 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) employees = payslips.mapped('employee_id').sorted('name') # Initialize data structure rows = { 'hours': {'name': _('Hours'), 'is_header': True, 'totals': defaultdict(float)}, 'regular_pay_hrs': {'name': _('Regular Pay'), 'parent': 'hours', 'totals': defaultdict(float)}, 'stat_holiday_hrs': {'name': _('Stat Holiday Pay'), 'parent': 'hours', 'totals': defaultdict(float)}, 'gross_pay': {'name': _('Gross Pay'), 'is_header': True, 'totals': defaultdict(float)}, 'regular_pay': {'name': _('Regular Pay'), 'parent': 'gross_pay', 'totals': defaultdict(float)}, 'stat_holiday_pay': {'name': _('Stat Holiday Pay'), 'parent': 'gross_pay', 'totals': defaultdict(float)}, 'vacation_pay': {'name': _('Vacation Pay'), 'parent': 'gross_pay', 'totals': defaultdict(float)}, 'adjusted_gross': {'name': _('Adjusted Gross'), 'is_subtotal': True, 'totals': defaultdict(float)}, 'employee_taxes': {'name': _('Employee Taxes & Deductions'), 'is_header': True, 'totals': defaultdict(float)}, 'income_tax': {'name': _('Income Tax'), 'parent': 'employee_taxes', 'totals': defaultdict(float)}, 'ei': {'name': _('Employment Insurance'), 'parent': 'employee_taxes', 'totals': defaultdict(float)}, 'cpp': {'name': _('Canada Pension Plan'), 'parent': 'employee_taxes', 'totals': defaultdict(float)}, 'net_pay': {'name': _('Net Pay'), 'is_header': True, 'totals': defaultdict(float)}, 'employer_taxes': {'name': _('Employer Taxes & Contributions'), 'is_header': True, 'totals': defaultdict(float)}, 'ei_employer': {'name': _('Employment Insurance Employer'), 'parent': 'employer_taxes', 'totals': defaultdict(float)}, 'cpp_employer': {'name': _('Canada Pension Plan Employer'), 'parent': 'employer_taxes', 'totals': defaultdict(float)}, 'total_cost': {'name': _('Total Payroll Cost'), 'is_total': True, 'totals': defaultdict(float)}, } # Aggregate data by employee for slip in payslips: if not slip.employee_id: continue emp_key = f'emp_{slip.employee_id.id}' # Hours hours = 0 if hasattr(slip, 'worked_days_line_ids') and slip.worked_days_line_ids: hours = sum(slip.worked_days_line_ids.mapped('number_of_hours')) or 0 rows['hours']['totals'][emp_key] += hours rows['regular_pay_hrs']['totals'][emp_key] += hours # Gross gross_wage = getattr(slip, 'gross_wage', 0) or 0 rows['gross_pay']['totals'][emp_key] += gross_wage rows['regular_pay']['totals'][emp_key] += gross_wage rows['adjusted_gross']['totals'][emp_key] += gross_wage # Employee taxes rows['employee_taxes']['totals'][emp_key] += getattr(slip, 'total_employee_deductions', 0) or 0 rows['income_tax']['totals'][emp_key] += getattr(slip, 'employee_income_tax', 0) or 0 rows['ei']['totals'][emp_key] += getattr(slip, 'employee_ei', 0) or 0 rows['cpp']['totals'][emp_key] += getattr(slip, 'employee_cpp', 0) or 0 # Net rows['net_pay']['totals'][emp_key] += getattr(slip, 'net_wage', 0) or 0 # Employer total_employer_cost = getattr(slip, 'total_employer_cost', 0) or 0 rows['employer_taxes']['totals'][emp_key] += total_employer_cost rows['ei_employer']['totals'][emp_key] += getattr(slip, 'employer_ei', 0) or 0 rows['cpp_employer']['totals'][emp_key] += getattr(slip, 'employer_cpp', 0) or 0 # Total cost rows['total_cost']['totals'][emp_key] += gross_wage + total_employer_cost # Build lines lines = [] for row_key, row_data in rows.items(): values = {'payroll_item': row_data['name']} # Calculate total total = sum(row_data['totals'].values()) values['total'] = total # Add employee columns for emp in employees: emp_field = f'emp_{emp.id}' values[emp_field] = row_data['totals'].get(emp_field, 0) level = 0 css_class = '' if row_data.get('is_header'): level = -1 css_class = 'fw-bold' elif row_data.get('parent'): level = 1 values['payroll_item'] = f" {row_data['name']}" elif row_data.get('is_subtotal'): css_class = 'fw-bold' elif row_data.get('is_total'): level = -1 css_class = 'fw-bold bg-primary text-white' lines.append({ 'id': row_key, 'name': row_data['name'], 'values': values, 'level': level, 'class': css_class, }) return lines