283 lines
12 KiB
Python
283 lines
12 KiB
Python
# -*- 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
|