Initial commit
This commit is contained in:
282
fusion_payroll/models/payroll_report_summary.py
Normal file
282
fusion_payroll/models/payroll_report_summary.py
Normal file
@@ -0,0 +1,282 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user