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