Initial commit
This commit is contained in:
350
fusion_payroll/models/payroll_report_tax.py
Normal file
350
fusion_payroll/models/payroll_report_tax.py
Normal file
@@ -0,0 +1,350 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Tax Reports
|
||||
===========
|
||||
- Payroll Tax Liability
|
||||
- Payroll Tax Payments
|
||||
- Payroll Tax and Wage Summary
|
||||
"""
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class PayrollReportTaxLiability(models.AbstractModel):
|
||||
"""
|
||||
Payroll Tax Liability Report
|
||||
Shows tax amounts owed vs paid.
|
||||
"""
|
||||
_name = 'payroll.report.tax.liability'
|
||||
_inherit = 'payroll.report'
|
||||
_description = 'Payroll Tax Liability Report'
|
||||
|
||||
report_name = 'Payroll Tax Liability'
|
||||
report_code = 'tax_liability'
|
||||
filter_employee = False
|
||||
|
||||
def _get_columns(self):
|
||||
return [
|
||||
{'name': _('Tax Type'), 'field': 'tax_type', 'type': 'char', 'sortable': True},
|
||||
{'name': _('Tax Amount'), 'field': 'tax_amount', 'type': 'monetary', 'sortable': True},
|
||||
{'name': _('Tax Paid'), 'field': 'tax_paid', 'type': 'monetary', 'sortable': True},
|
||||
{'name': _('Tax Owed'), 'field': 'tax_owed', 'type': 'monetary', 'sortable': True},
|
||||
]
|
||||
|
||||
def _get_lines(self, options):
|
||||
date_from = options.get('date', {}).get('date_from')
|
||||
date_to = options.get('date', {}).get('date_to')
|
||||
|
||||
# Build domain for payslips
|
||||
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)
|
||||
|
||||
# Calculate totals by tax type
|
||||
tax_totals = {
|
||||
'income_tax': {'name': _('Income Tax'), 'amount': 0, 'codes': ['FED_TAX', 'PROV_TAX']},
|
||||
'ei_employee': {'name': _('Employment Insurance'), 'amount': 0, 'codes': ['EI']},
|
||||
'ei_employer': {'name': _('Employment Insurance Employer'), 'amount': 0, 'codes': ['EI_ER']},
|
||||
'cpp_employee': {'name': _('Canada Pension Plan'), 'amount': 0, 'codes': ['CPP']},
|
||||
'cpp_employer': {'name': _('Canada Pension Plan Employer'), 'amount': 0, 'codes': ['CPP_ER']},
|
||||
'cpp2_employee': {'name': _('Second Canada Pension Plan'), 'amount': 0, 'codes': ['CPP2']},
|
||||
'cpp2_employer': {'name': _('Second Canada Pension Plan Employer'), 'amount': 0, 'codes': ['CPP2_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
|
||||
for key, data in tax_totals.items():
|
||||
if line.code in data['codes']:
|
||||
tax_totals[key]['amount'] += abs(line.total or 0)
|
||||
|
||||
# Get paid amounts from remittances
|
||||
remittance_domain = [
|
||||
('state', '=', 'paid'),
|
||||
('company_id', '=', self.env.company.id),
|
||||
]
|
||||
if date_from:
|
||||
remittance_domain.append(('period_start', '>=', date_from))
|
||||
if date_to:
|
||||
remittance_domain.append(('period_end', '<=', date_to))
|
||||
|
||||
remittances = self.env['hr.tax.remittance'].search(remittance_domain)
|
||||
|
||||
# Safely get remittance fields
|
||||
def safe_sum(records, field_name):
|
||||
if not records:
|
||||
return 0
|
||||
try:
|
||||
return sum(records.mapped(field_name)) or 0
|
||||
except:
|
||||
return 0
|
||||
|
||||
paid_totals = {
|
||||
'income_tax': safe_sum(remittances, 'income_tax'),
|
||||
'ei_employee': safe_sum(remittances, 'ei_employee'),
|
||||
'ei_employer': safe_sum(remittances, 'ei_employer'),
|
||||
'cpp_employee': safe_sum(remittances, 'cpp_employee'),
|
||||
'cpp2_employee': safe_sum(remittances, 'cpp2_employee'),
|
||||
'cpp_employer': safe_sum(remittances, 'cpp_employer'),
|
||||
'cpp2_employer': safe_sum(remittances, 'cpp2_employer'),
|
||||
}
|
||||
|
||||
lines = []
|
||||
grand_total = {'amount': 0, 'paid': 0, 'owed': 0}
|
||||
|
||||
# Federal Taxes header
|
||||
lines.append({
|
||||
'id': 'federal_header',
|
||||
'name': _('Federal Taxes'),
|
||||
'values': {
|
||||
'tax_type': _('Federal Taxes'),
|
||||
'tax_amount': '',
|
||||
'tax_paid': '',
|
||||
'tax_owed': '',
|
||||
},
|
||||
'level': -1,
|
||||
'class': 'fw-bold',
|
||||
})
|
||||
|
||||
for key, data in tax_totals.items():
|
||||
paid = paid_totals.get(key, 0)
|
||||
owed = data['amount'] - paid
|
||||
|
||||
grand_total['amount'] += data['amount']
|
||||
grand_total['paid'] += paid
|
||||
grand_total['owed'] += owed
|
||||
|
||||
lines.append({
|
||||
'id': f'tax_{key}',
|
||||
'name': data['name'],
|
||||
'values': {
|
||||
'tax_type': f" {data['name']}",
|
||||
'tax_amount': data['amount'],
|
||||
'tax_paid': paid,
|
||||
'tax_owed': owed,
|
||||
},
|
||||
'level': 0,
|
||||
})
|
||||
|
||||
# Grand total
|
||||
lines.append({
|
||||
'id': 'grand_total',
|
||||
'name': _('Total'),
|
||||
'values': {
|
||||
'tax_type': _('Total'),
|
||||
'tax_amount': grand_total['amount'],
|
||||
'tax_paid': grand_total['paid'],
|
||||
'tax_owed': grand_total['owed'],
|
||||
},
|
||||
'level': -1,
|
||||
'class': 'o_payroll_report_total fw-bold',
|
||||
})
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
class PayrollReportTaxPayments(models.AbstractModel):
|
||||
"""
|
||||
Payroll Tax Payments Report
|
||||
Shows history of tax remittance payments.
|
||||
"""
|
||||
_name = 'payroll.report.tax.payments'
|
||||
_inherit = 'payroll.report'
|
||||
_description = 'Payroll Tax Payments Report'
|
||||
|
||||
report_name = 'Payroll Tax Payments'
|
||||
report_code = 'tax_payments'
|
||||
filter_employee = False
|
||||
|
||||
def _get_columns(self):
|
||||
return [
|
||||
{'name': _('Payment Date'), 'field': 'payment_date', 'type': 'date', 'sortable': True},
|
||||
{'name': _('Tax Type'), 'field': 'tax_type', 'type': 'char', 'sortable': True},
|
||||
{'name': _('Amount'), 'field': 'amount', 'type': 'monetary', 'sortable': True},
|
||||
{'name': _('Payment Method'), 'field': 'payment_method', 'type': 'char', 'sortable': False},
|
||||
{'name': _('Notes'), 'field': 'notes', 'type': 'char', 'sortable': False},
|
||||
]
|
||||
|
||||
def _get_lines(self, options):
|
||||
date_from = options.get('date', {}).get('date_from')
|
||||
date_to = options.get('date', {}).get('date_to')
|
||||
|
||||
domain = [
|
||||
('state', '=', 'paid'),
|
||||
('company_id', '=', self.env.company.id),
|
||||
]
|
||||
if date_from:
|
||||
domain.append(('payment_date', '>=', date_from))
|
||||
if date_to:
|
||||
domain.append(('payment_date', '<=', date_to))
|
||||
|
||||
remittances = self.env['hr.tax.remittance'].search(domain, order='payment_date desc')
|
||||
|
||||
lines = []
|
||||
for rem in remittances:
|
||||
period_start = getattr(rem, 'period_start', '') or ''
|
||||
period_end = getattr(rem, 'period_end', '') or ''
|
||||
period_str = f"{period_start} - {period_end}" if period_start and period_end else ''
|
||||
|
||||
lines.append({
|
||||
'id': f'remit_{rem.id}',
|
||||
'name': rem.name or 'Unknown',
|
||||
'values': {
|
||||
'payment_date': getattr(rem, 'payment_date', '') or '',
|
||||
'tax_type': f"Federal Taxes\n{period_str}" if period_str else 'Federal Taxes',
|
||||
'amount': getattr(rem, 'total', 0) or 0,
|
||||
'payment_method': getattr(rem, 'payment_method', None) or 'Manual',
|
||||
'notes': getattr(rem, 'payment_reference', None) or '',
|
||||
},
|
||||
'level': 0,
|
||||
'model': 'hr.tax.remittance',
|
||||
'res_id': rem.id,
|
||||
})
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
class PayrollReportTaxWageSummary(models.AbstractModel):
|
||||
"""
|
||||
Payroll Tax and Wage Summary Report
|
||||
Shows total wages, excess wages, taxable wages, and tax amounts.
|
||||
"""
|
||||
_name = 'payroll.report.tax.wage.summary'
|
||||
_inherit = 'payroll.report'
|
||||
_description = 'Payroll Tax and Wage Summary Report'
|
||||
|
||||
report_name = 'Payroll Tax and Wage Summary'
|
||||
report_code = 'tax_wage_summary'
|
||||
filter_employee = False
|
||||
|
||||
def _get_columns(self):
|
||||
return [
|
||||
{'name': _('Tax Type'), 'field': 'tax_type', 'type': 'char', 'sortable': True},
|
||||
{'name': _('Total Wages'), 'field': 'total_wages', 'type': 'monetary', 'sortable': True},
|
||||
{'name': _('Excess Wages'), 'field': 'excess_wages', 'type': 'monetary', 'sortable': True},
|
||||
{'name': _('Taxable Wages'), 'field': 'taxable_wages', 'type': 'monetary', 'sortable': True},
|
||||
{'name': _('Tax Amount'), 'field': 'tax_amount', 'type': 'monetary', 'sortable': True},
|
||||
]
|
||||
|
||||
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)
|
||||
|
||||
# Calculate totals - safely handle missing gross_wage field
|
||||
total_wages = 0
|
||||
for slip in payslips:
|
||||
total_wages += getattr(slip, 'gross_wage', 0) or 0
|
||||
|
||||
# Tax calculations
|
||||
tax_data = [
|
||||
{
|
||||
'name': _('Income Tax'),
|
||||
'codes': ['FED_TAX', 'PROV_TAX'],
|
||||
'total_wages': total_wages,
|
||||
'excess_wages': 0, # No excess for income tax
|
||||
},
|
||||
{
|
||||
'name': _('Employment Insurance'),
|
||||
'codes': ['EI'],
|
||||
'total_wages': total_wages,
|
||||
'excess_wages': 0, # Would need to calculate based on max
|
||||
},
|
||||
{
|
||||
'name': _('Employment Insurance Employer'),
|
||||
'codes': ['EI_ER'],
|
||||
'total_wages': total_wages,
|
||||
'excess_wages': 0,
|
||||
},
|
||||
{
|
||||
'name': _('Canada Pension Plan'),
|
||||
'codes': ['CPP'],
|
||||
'total_wages': total_wages,
|
||||
'excess_wages': 0,
|
||||
},
|
||||
{
|
||||
'name': _('Canada Pension Plan Employer'),
|
||||
'codes': ['CPP_ER'],
|
||||
'total_wages': total_wages,
|
||||
'excess_wages': 0,
|
||||
},
|
||||
{
|
||||
'name': _('Second Canada Pension Plan'),
|
||||
'codes': ['CPP2'],
|
||||
'total_wages': total_wages,
|
||||
'excess_wages': 0,
|
||||
},
|
||||
{
|
||||
'name': _('Second Canada Pension Plan Employer'),
|
||||
'codes': ['CPP2_ER'],
|
||||
'total_wages': total_wages,
|
||||
'excess_wages': 0,
|
||||
},
|
||||
]
|
||||
|
||||
lines = []
|
||||
|
||||
# Federal header
|
||||
lines.append({
|
||||
'id': 'federal_header',
|
||||
'name': _('Federal Taxes'),
|
||||
'values': {
|
||||
'tax_type': _('Federal Taxes'),
|
||||
'total_wages': '',
|
||||
'excess_wages': '',
|
||||
'taxable_wages': '',
|
||||
'tax_amount': '',
|
||||
},
|
||||
'level': -1,
|
||||
'class': 'fw-bold',
|
||||
})
|
||||
|
||||
grand_tax = 0
|
||||
for data in tax_data:
|
||||
tax_amount = 0
|
||||
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 data['codes']:
|
||||
tax_amount += abs(line.total or 0)
|
||||
|
||||
taxable = data['total_wages'] - data['excess_wages']
|
||||
grand_tax += tax_amount
|
||||
|
||||
lines.append({
|
||||
'id': f"tax_{data['name']}",
|
||||
'name': data['name'],
|
||||
'values': {
|
||||
'tax_type': f" {data['name']}",
|
||||
'total_wages': data['total_wages'],
|
||||
'excess_wages': data['excess_wages'],
|
||||
'taxable_wages': taxable,
|
||||
'tax_amount': tax_amount,
|
||||
},
|
||||
'level': 0,
|
||||
})
|
||||
|
||||
return lines
|
||||
Reference in New Issue
Block a user