diff --git a/fusion_payroll/__init__.py b/fusion_payroll/__init__.py index 5c681d07..e5e231d7 100644 --- a/fusion_payroll/__init__.py +++ b/fusion_payroll/__init__.py @@ -3,3 +3,44 @@ from . import models from . import wizards from . import controllers + + +def _fusion_payroll_post_init(env): + """Archive default salary rules auto-added to Canadian structure. + + When hr_payroll creates a new structure, it automatically generates + default rules (BASIC, GROSS, NET, etc.). Since this module defines + its own custom rules, the defaults must be archived to avoid duplicates. + """ + structure = env.ref( + 'fusion_payroll.hr_payroll_structure_canada', + raise_if_not_found=False, + ) + if not structure: + return + + default_codes = [ + 'BASIC', 'GROSS', 'NET', + 'ATTACH_SALARY', 'ASSIG_SALARY', + 'CHILD_SUPPORT', 'DEDUCTION', 'REIMBURSEMENT', + ] + + own_rule_xmlids = [ + 'fusion_payroll.hr_rule_basic', + 'fusion_payroll.hr_rule_gross', + 'fusion_payroll.hr_rule_net', + ] + own_rule_ids = set() + for xmlid in own_rule_xmlids: + rule = env.ref(xmlid, raise_if_not_found=False) + if rule: + own_rule_ids.add(rule.id) + + default_rules = env['hr.salary.rule'].search([ + ('struct_id', '=', structure.id), + ('code', 'in', default_codes), + ]) + + rules_to_archive = default_rules.filtered(lambda r: r.id not in own_rule_ids) + if rules_to_archive: + rules_to_archive.active = False diff --git a/fusion_payroll/__manifest__.py b/fusion_payroll/__manifest__.py index e067dcba..cbc011dd 100644 --- a/fusion_payroll/__manifest__.py +++ b/fusion_payroll/__manifest__.py @@ -49,9 +49,9 @@ Comprehensive Canadian payroll functionality inspired by QuickBooks Online Payro Built for Odoo Enterprise Payroll (hr_payroll). """, - 'author': 'Your Company', + 'author': 'Nexa Systems Inc.', 'website': '', - 'license': 'LGPL-3', + 'license': 'OPL-1', 'depends': [ 'hr_payroll', # Core payroll functionality 'hr_work_entry_enterprise', # For payroll menu structure (Odoo 19) @@ -59,10 +59,12 @@ Built for Odoo Enterprise Payroll (hr_payroll). 'hr_attendance', # For punch-in/out time tracking 'mail', # For ROE chatter/tracking ], + 'external_dependencies': { + 'python': ['lxml'], + }, 'data': [ # Views 'views/pay_period_views.xml', - 'views/tax_yearly_rates_views.xml', 'views/hr_employee_views.xml', 'views/hr_roe_views.xml', 'views/hr_payslip_views.xml', @@ -75,7 +77,6 @@ Built for Odoo Enterprise Payroll (hr_payroll). 'views/payroll_tax_payment_schedule_views.xml', 'views/payroll_config_settings_views.xml', 'views/payroll_work_location_views.xml', - 'views/payroll_tax_payment_schedule_views.xml', 'views/payroll_dashboard_views.xml', 'views/payroll_cheque_views.xml', 'views/payroll_cheque_print_wizard_views.xml', @@ -95,18 +96,18 @@ Built for Odoo Enterprise Payroll (hr_payroll). 'data/hr_rule_parameter_data.xml', # 2. Input types for additional pay (OT, Stat, Bonus) 'data/hr_payslip_input_type_data.xml', - # 3. Legacy tax rates data - 'data/tax_yearly_rates_data.xml', - # 4. Payroll structure (creates structure and category) + # 3. Payroll structure (creates structure and category) 'data/hr_payroll_structure.xml', # 5. Canadian salary rules (references structure and parameters) 'data/hr_salary_rules.xml', - # 6. Demo/Sample data (loads on install) - 'demo/demo_data.xml', + # 6. Sequences + 'data/ir_sequence_data.xml', # Security (load last to ensure all models are registered) 'security/ir.model.access.csv', ], - 'demo': [], + 'demo': [ + 'demo/demo_data.xml', + ], 'images': ['static/description/icon.png'], 'assets': { 'web.assets_backend': [ @@ -123,4 +124,5 @@ Built for Odoo Enterprise Payroll (hr_payroll). 'installable': True, 'application': True, 'auto_install': False, + 'post_init_hook': '_fusion_payroll_post_init', } diff --git a/fusion_payroll/data/hr_payroll_structure.xml b/fusion_payroll/data/hr_payroll_structure.xml index b71579b6..59eade86 100644 --- a/fusion_payroll/data/hr_payroll_structure.xml +++ b/fusion_payroll/data/hr_payroll_structure.xml @@ -2,133 +2,55 @@ - - - - - + - Canada + Canadian Employee - + - Canada salary structure + Canadian Employee Salary Canada + - - - - - - Deduction - CANADA - - - + + + CPP + CPP - - - - - - - Basic Salary - BASIC - 1 - - - none - code - result = payslip.paid_amount + + EI + EI - - - House Rent Allowance - HRA - 5 - - - none - fix - 0.0 + + Federal Tax + FED_TAX - - - Dearness Allowance - DA - 6 - - - none - fix - 0.0 + + Provincial Tax + PROV_TAX - - - Travel Allowance - Travel - 7 - - - none - fix - 0.0 + + Ontario Health Premium + OHP - - - Meal Allowance - Meal - 8 - - - none - fix - 0.0 + + Employer Contributions + EMPLOYER - - - Medical Allowance - Medical - 9 - - - none - fix - 0.0 - - - - - Gross - GROSS - 100 - - - none - code - result = categories.BASIC + categories.ALW - - - - - Net Salary - NET - 200 - - - none - code - result = categories.GROSS + categories.DED + + Earnings + EARN + diff --git a/fusion_payroll/data/hr_payslip_input_type_data.xml b/fusion_payroll/data/hr_payslip_input_type_data.xml index ef5bd452..b01e8e70 100644 --- a/fusion_payroll/data/hr_payslip_input_type_data.xml +++ b/fusion_payroll/data/hr_payslip_input_type_data.xml @@ -2,37 +2,28 @@ - - - - + + + Overtime + OT + + + Overtime Hours OT_HOURS + - - - Stat Holiday Hours - STAT_HOURS - - - - + Bonus BONUS - - - - - Vacation Payout - VACATION_PAYOUT - + @@ -40,6 +31,39 @@ Commission COMMISSION + + + + + + RRSP Deduction + RRSP + + + + + + + Union Dues + UNION + + + + + + + Vacation Pay + VAC_PAY + + + + + + + Stat Holiday Hours + STAT_HOURS + + @@ -47,6 +71,7 @@ Retroactive Pay RETRO_PAY + @@ -54,6 +79,7 @@ Shift Premium SHIFT_PREMIUM + diff --git a/fusion_payroll/data/hr_rule_parameter_data.xml b/fusion_payroll/data/hr_rule_parameter_data.xml index 21ab2d6e..6be9af03 100644 --- a/fusion_payroll/data/hr_rule_parameter_data.xml +++ b/fusion_payroll/data/hr_rule_parameter_data.xml @@ -3,18 +3,18 @@ - + - + Canada - CPP Rate ca_cpp_rate CPP employee/employer contribution rate - + - 2025-01-01 + 2026-01-01 0.0595 @@ -24,10 +24,10 @@ CPP basic exemption amount per year - + - 2025-01-01 - 3500.00 + 2026-01-01 + 3500 @@ -36,14 +36,38 @@ Maximum CPP employee contribution per year - + - 2025-01-01 - 4034.10 + 2026-01-01 + 4230.45 + + + + Canada - YMPE (CPP Ceiling 1) + ca_ympe + + Year's Maximum Pensionable Earnings - CPP first ceiling + + + + 2026-01-01 + 74600 + + + + Canada - YAMPE (CPP Ceiling 2) + ca_yampe + + Year's Additional Maximum Pensionable Earnings - CPP second ceiling + + + + 2026-01-01 + 85000 - + @@ -52,9 +76,9 @@ CPP2 contribution rate (on earnings above YMPE) - + - 2025-01-01 + 2026-01-01 0.04 @@ -64,38 +88,14 @@ Maximum CPP2 employee contribution per year - + - 2025-01-01 - 396.00 - - - - Canada - YMPE (CPP Ceiling 1) - ca_ympe - - Year's Maximum Pensionable Earnings - CPP first ceiling - - - - 2025-01-01 - 71300.00 - - - - Canada - YAMPE (CPP Ceiling 2) - ca_yampe - - Year's Additional Maximum Pensionable Earnings - CPP second ceiling - - - - 2025-01-01 - 81200.00 + 2026-01-01 + 416.00 - + @@ -104,10 +104,22 @@ EI employee contribution rate - + - 2025-01-01 - 0.0164 + 2026-01-01 + 0.0163 + + + + Canada - EI Maximum Insurable Earnings + ca_ei_max_insurable + + Maximum annual insurable earnings for EI + + + + 2026-01-01 + 68900 @@ -116,10 +128,10 @@ Maximum EI employee premium per year - + - 2025-01-01 - 1077.48 + 2026-01-01 + 1123.07 @@ -128,26 +140,150 @@ EI employer contribution multiplier (1.4x employee) - + - 2025-01-01 + 2026-01-01 1.4 - + + + + + Canada - Federal Tax Bracket 1 Threshold + ca_fed_bracket_1 + + Upper limit of the first federal tax bracket + + + + 2026-01-01 + 58523 + + + + Canada - Federal Tax Rate 1 + ca_fed_rate_1 + + Federal tax rate for the first bracket + + + + 2026-01-01 + 0.14 + + + + Canada - Federal Tax Bracket 2 Threshold + ca_fed_bracket_2 + + Upper limit of the second federal tax bracket + + + + 2026-01-01 + 117045 + + + + Canada - Federal Tax Rate 2 + ca_fed_rate_2 + + Federal tax rate for the second bracket + + + + 2026-01-01 + 0.205 + + + + Canada - Federal Tax Bracket 3 Threshold + ca_fed_bracket_3 + + Upper limit of the third federal tax bracket + + + + 2026-01-01 + 181440 + + + + Canada - Federal Tax Rate 3 + ca_fed_rate_3 + + Federal tax rate for the third bracket + + + + 2026-01-01 + 0.26 + + + + Canada - Federal Tax Bracket 4 Threshold + ca_fed_bracket_4 + + Upper limit of the fourth federal tax bracket + + + + 2026-01-01 + 258482 + + + + Canada - Federal Tax Rate 4 + ca_fed_rate_4 + + Federal tax rate for the fourth bracket + + + + 2026-01-01 + 0.29 + + + + Canada - Federal Tax Rate 5 + ca_fed_rate_5 + + Federal tax rate for the fifth bracket (above bracket 4) + + + + 2026-01-01 + 0.33 + + + + - Canada - Federal Basic Personal Amount + Canada - Federal Basic Personal Amount (Max) ca_fed_bpa - Federal basic personal amount (TD1 default) + Federal basic personal amount - maximum (TD1 default) - + - 2025-01-01 - 16129 + 2026-01-01 + 16452 + + + + Canada - Federal Basic Personal Amount (Min) + ca_fed_bpa_min + + Federal basic personal amount - minimum for high-income phase-out + + + + 2026-01-01 + 14829 @@ -156,54 +292,14 @@ Canada Employment Amount for federal tax credit - + - 2025-01-01 + 2026-01-01 1433 - - Canada - Federal Tax Brackets - ca_fed_brackets - - Federal income tax brackets: [(threshold, rate), ...] - - - - 2025-01-01 - [(57375, 0.15), (114750, 0.205), (177882, 0.26), (253414, 0.29), (float('inf'), 0.33)] - - - - - - - Canada Ontario - Basic Personal Amount - ca_on_bpa - - Ontario basic personal amount - - - - 2025-01-01 - 12399 - - - - Canada Ontario - Tax Brackets - ca_on_brackets - - Ontario income tax brackets: [(threshold, rate), ...] - - - - 2025-01-01 - [(52886, 0.0505), (105775, 0.0915), (150000, 0.1116), (220000, 0.1216), (float('inf'), 0.1316)] - - - - + @@ -212,11 +308,39 @@ Vacation pay percentage (Ontario minimum 4%) - + - 2025-01-01 + 2026-01-01 0.04 + + + + + + Canada - Overtime Pay Multiplier + ca_overtime_multiplier + + Overtime pay multiplier (1.5x regular rate) + + + + 2026-01-01 + 1.5 + + + + Canada - Standard Hours Per Pay Period + ca_standard_hours_per_period + + Standard hours per bi-weekly pay period + + + + 2026-01-01 + 80 + + diff --git a/fusion_payroll/data/hr_salary_rules.xml b/fusion_payroll/data/hr_salary_rules.xml index f491c677..cd29967d 100644 --- a/fusion_payroll/data/hr_salary_rules.xml +++ b/fusion_payroll/data/hr_salary_rules.xml @@ -1,58 +1,101 @@ - + - + - - Overtime Pay - OT_PAY - 101 + + Basic Pay + BASIC + 1 - - python - result = 'OT_HOURS' in inputs + + none code True -# Overtime Pay - 1.5x regular hourly rate -OT_MULTIPLIER = 1.5 -ot_hours = inputs['OT_HOURS'].amount if 'OT_HOURS' in inputs else 0 -# Calculate hourly rate from paid amount (assuming semi-monthly ~86.67 hours) -hourly_rate = payslip.paid_amount / 86.67 if payslip.paid_amount else 0 -result = ot_hours * hourly_rate * OT_MULTIPLIER +result = payslip.paid_amount - + + + + Overtime Pay + OT_PAY + 3 + + + python + result = (inputs.get('OT_HOURS') and inputs['OT_HOURS'].amount > 0) or (inputs.get('OT') and inputs['OT'].amount > 0) + code + True + +if inputs.get('OT_HOURS') and inputs['OT_HOURS'].amount > 0: + basic = result_rules.get('BASIC', {}).get('total', 0) + hours_per_period = payslip._rule_parameter('ca_standard_hours_per_period') + ot_multiplier = payslip._rule_parameter('ca_overtime_multiplier') + hourly_rate = basic / hours_per_period if hours_per_period else 0 + result = inputs['OT_HOURS'].amount * hourly_rate * ot_multiplier +elif inputs.get('OT') and inputs['OT'].amount > 0: + result = inputs['OT'].amount +else: + result = 0 + + + + + Stat Holiday Pay STAT_PAY - 102 + 4 python - result = 'STAT_HOURS' in inputs + result = inputs.get('STAT_HOURS') and inputs['STAT_HOURS'].amount > 0 code True -# Stat Holiday Pay -stat_hours = inputs['STAT_HOURS'].amount if 'STAT_HOURS' in inputs else 0 -hourly_rate = payslip.paid_amount / 86.67 if payslip.paid_amount else 0 +stat_hours = inputs['STAT_HOURS'].amount if inputs.get('STAT_HOURS') else 0 +basic = result_rules.get('BASIC', {}).get('total', 0) +hours_per_period = payslip._rule_parameter('ca_standard_hours_per_period') +hourly_rate = basic / hours_per_period if hours_per_period else 0 result = stat_hours * hourly_rate - + + + + Vacation Pay + VAC_PAY + 5 + + + python + result = inputs.get('VAC_PAY') and inputs['VAC_PAY'].amount > 0 + code + True + +if inputs.get('VAC_PAY') and inputs['VAC_PAY'].amount > 0: + result = round(float(inputs['VAC_PAY'].amount), 2) +else: + result = 0 + + + + + Bonus BONUS_PAY - 103 + 6 python @@ -60,297 +103,468 @@ result = stat_hours * hourly_rate code True -# Bonus Pay - direct amount from input -result = inputs['BONUS'].amount if 'BONUS' in inputs else 0 +result = inputs['BONUS'].amount if inputs.get('BONUS') else 0 - - + + + + Commission + COMMISSION + 7 + + + python + result = inputs.get('COMMISSION') and inputs['COMMISSION'].amount > 0 + code + True + +result = inputs['COMMISSION'].amount if inputs.get('COMMISSION') else 0 + + + + + + + + Retroactive Pay + RETRO_PAY + 8 + + + python + result = inputs.get('RETRO_PAY') and inputs['RETRO_PAY'].amount > 0 + code + True + +result = inputs['RETRO_PAY'].amount if inputs.get('RETRO_PAY') else 0 + + + + + + + + Shift Premium + SHIFT_PREMIUM + 9 + + + python + result = inputs.get('SHIFT_PREMIUM') and inputs['SHIFT_PREMIUM'].amount > 0 + code + True + +result = inputs['SHIFT_PREMIUM'].amount if inputs.get('SHIFT_PREMIUM') else 0 + + + + + + + + Gross + GROSS + 10 + + + none + code + True + +result = ( + result_rules.get('BASIC', {}).get('total', 0) + + result_rules.get('OT_PAY', {}).get('total', 0) + + result_rules.get('STAT_PAY', {}).get('total', 0) + + result_rules.get('VAC_PAY', {}).get('total', 0) + + result_rules.get('BONUS_PAY', {}).get('total', 0) + + result_rules.get('COMMISSION', {}).get('total', 0) + + result_rules.get('RETRO_PAY', {}).get('total', 0) + + result_rules.get('SHIFT_PREMIUM', {}).get('total', 0) +) + + + + + + + + RRSP Deduction + RRSP + 15 + + + python + result = inputs.get('RRSP') and inputs['RRSP'].amount > 0 + code + True + +result = -(inputs['RRSP'].amount if inputs.get('RRSP') else 0) + + + + + + + + Union Dues + UNION_DUES + 16 + + + python + result = inputs.get('UNION') and inputs['UNION'].amount > 0 + code + True + +result = -(inputs['UNION'].amount if inputs.get('UNION') else 0) + + + + + CPP Employee CPP_EE - 150 + 20 - + none code True -# CPP Employee Deduction - Using Rule Parameters -CPP_RATE = payslip._rule_parameter('ca_cpp_rate') -CPP_EXEMPTION = payslip._rule_parameter('ca_cpp_exemption') -CPP_MAX = payslip._rule_parameter('ca_cpp_max') -PAY_PERIODS = 24 # Semi-monthly - -exemption_per_period = CPP_EXEMPTION / PAY_PERIODS -gross = categories['GROSS'] -pensionable = max(0, gross - exemption_per_period) -cpp = pensionable * CPP_RATE - -# YTD check - get year start -from datetime import date -year_start = date(payslip.date_from.year, 1, 1) -ytd = payslip._sum('CPP_EE', year_start, payslip.date_from) or 0 -remaining = CPP_MAX + ytd # ytd is negative - -if remaining <= 0: +gross_amount = result_rules.get('GROSS', {}).get('total', 0) +if employee.exempt_cpp: result = 0 -elif cpp > remaining: - result = -remaining else: - result = -cpp + cpp_rate = payslip._rule_parameter('ca_cpp_rate') + cpp_exemption = payslip._rule_parameter('ca_cpp_exemption') + cpp_max = payslip._rule_parameter('ca_cpp_max') + ympe = payslip._rule_parameter('ca_ympe') + period_exemption = cpp_exemption / 26 + period_max = cpp_max / 26 + period_ympe = ympe / 26 + pensionable = min(gross_amount, period_ympe) + pensionable = max(pensionable - period_exemption, 0) + result = -min(pensionable * cpp_rate, period_max) - + CPP Employer CPP_ER - 151 + 21 - + none code True -# CPP Employer - 1:1 match with employee -result = abs(CPP_EE) if CPP_EE else 0 +result = -result_rules.get('CPP_EE', {}).get('total', 0) - - + CPP2 Employee CPP2_EE - 152 + 22 - + none code True -# CPP2 (Second CPP) - Using Rule Parameters -CPP2_RATE = payslip._rule_parameter('ca_cpp2_rate') -YMPE = payslip._rule_parameter('ca_ympe') -YAMPE = payslip._rule_parameter('ca_yampe') -CPP2_MAX = payslip._rule_parameter('ca_cpp2_max') -PAY_PERIODS = 24 - -gross = categories['GROSS'] -annual_equiv = gross * PAY_PERIODS - -result = 0 -# CPP2 only on earnings between YMPE and YAMPE -if annual_equiv > YMPE: - cpp2_base = min(annual_equiv, YAMPE) - YMPE - cpp2_per_period = (cpp2_base * CPP2_RATE) / PAY_PERIODS - - # YTD check - from datetime import date - year_start = date(payslip.date_from.year, 1, 1) - ytd = abs(payslip._sum('CPP2_EE', year_start, payslip.date_from) or 0) - remaining = CPP2_MAX - ytd - - if remaining > 0: - result = -min(cpp2_per_period, remaining) +gross_amount = result_rules.get('GROSS', {}).get('total', 0) +if employee.exempt_cpp: + result = 0 +else: + ympe = payslip._rule_parameter('ca_ympe') + cpp2_rate = payslip._rule_parameter('ca_cpp2_rate') + yampe = payslip._rule_parameter('ca_yampe') + cpp2_max = payslip._rule_parameter('ca_cpp2_max') + period_ympe = ympe / 26 + period_ceiling = yampe / 26 + period_max = cpp2_max / 26 + if gross_amount > period_ympe: + cpp2_pensionable = min(gross_amount, period_ceiling) - period_ympe + result = -min(cpp2_pensionable * cpp2_rate, period_max) + else: + result = 0 - + CPP2 Employer CPP2_ER - 153 + 23 - + none code True -# CPP2 Employer - 1:1 match -result = abs(CPP2_EE) if CPP2_EE else 0 +result = -result_rules.get('CPP2_EE', {}).get('total', 0) - - + EI Employee EI_EE - 154 + 25 - + none code True -# EI Employee - Using Rule Parameters -EI_RATE = payslip._rule_parameter('ca_ei_rate') -EI_MAX = payslip._rule_parameter('ca_ei_max') - -gross = categories['GROSS'] -ei = gross * EI_RATE - -# YTD check -from datetime import date -year_start = date(payslip.date_from.year, 1, 1) -ytd = abs(payslip._sum('EI_EE', year_start, payslip.date_from) or 0) -remaining = EI_MAX - ytd - -if remaining <= 0: +gross_amount = result_rules.get('GROSS', {}).get('total', 0) +if employee.exempt_ei: result = 0 -elif ei > remaining: - result = -remaining else: - result = -ei + ei_rate = payslip._rule_parameter('ca_ei_rate') + ei_max_insurable = payslip._rule_parameter('ca_ei_max_insurable') + ei_max = payslip._rule_parameter('ca_ei_max') + period_max_insurable = ei_max_insurable / 26 + period_max_premium = ei_max / 26 + insurable = min(gross_amount, period_max_insurable) + result = -min(insurable * ei_rate, period_max_premium) - + EI Employer EI_ER - 155 + 26 - + none code True -# EI Employer - Using Rule Parameter for multiplier -EI_ER_MULT = payslip._rule_parameter('ca_ei_employer_mult') -result = abs(EI_EE) * EI_ER_MULT if EI_EE else 0 +ei_employer_mult = payslip._rule_parameter('ca_ei_employer_mult') +result = -result_rules.get('EI_EE', {}).get('total', 0) * ei_employer_mult - - + Federal Income Tax FED_TAX - 160 + 30 - + none code True -# Federal Income Tax - Using Rule Parameters -PAY_PERIODS = 24 -brackets = payslip._rule_parameter('ca_fed_brackets') -BPA = payslip._rule_parameter('ca_fed_bpa') -CEA = payslip._rule_parameter('ca_fed_cea') -CPP_MAX = payslip._rule_parameter('ca_cpp_max') -EI_MAX = payslip._rule_parameter('ca_ei_max') +if hasattr(employee, 'exempt_federal_tax') and employee.exempt_federal_tax: + result = 0 +else: + gross_amount = result_rules.get('GROSS', {}).get('total', 0) + rrsp = abs(result_rules.get('RRSP', {}).get('total', 0)) + union = abs(result_rules.get('UNION_DUES', {}).get('total', 0)) + taxable_per_period = gross_amount - rrsp - union + annual_income = taxable_per_period * 26 -gross = categories['GROSS'] -annual = gross * PAY_PERIODS + fed_brackets = [ + (payslip._rule_parameter('ca_fed_bracket_1'), payslip._rule_parameter('ca_fed_rate_1')), + (payslip._rule_parameter('ca_fed_bracket_2'), payslip._rule_parameter('ca_fed_rate_2')), + (payslip._rule_parameter('ca_fed_bracket_3'), payslip._rule_parameter('ca_fed_rate_3')), + (payslip._rule_parameter('ca_fed_bracket_4'), payslip._rule_parameter('ca_fed_rate_4')), + (float('inf'), payslip._rule_parameter('ca_fed_rate_5')), + ] -# Calculate tax using brackets -tax = 0 -prev_threshold = 0 -for threshold, rate in brackets: - if annual <= threshold: - tax += (annual - prev_threshold) * rate - break + bpa_max = payslip._rule_parameter('ca_fed_bpa') + bpa_min = payslip._rule_parameter('ca_fed_bpa_min') + cea = payslip._rule_parameter('ca_fed_cea') + phase_out_start = payslip._rule_parameter('ca_fed_bracket_3') + phase_out_end = payslip._rule_parameter('ca_fed_bracket_4') + + td1_override = employee.federal_td1_amount if hasattr(employee, 'federal_td1_amount') and employee.federal_td1_amount > 0 else 0 + if td1_override: + fed_bpa = td1_override + elif annual_income <= phase_out_start: + fed_bpa = bpa_max + elif annual_income >= phase_out_end: + fed_bpa = bpa_min else: - tax += (threshold - prev_threshold) * rate - prev_threshold = threshold + fed_bpa = bpa_max - (bpa_max - bpa_min) * (annual_income - phase_out_start) / (phase_out_end - phase_out_start) -# Basic personal amount credit -tax_credit = BPA * brackets[0][1] # Lowest rate + tax = 0 + prev_bracket = 0 + for bracket, rate in fed_brackets: + taxable_in_bracket = min(annual_income, bracket) - prev_bracket + if taxable_in_bracket > 0: + tax += taxable_in_bracket * rate + prev_bracket = bracket + if annual_income <= bracket: + break -# CPP/EI credits -cpp_credit = min(abs(CPP_EE) * PAY_PERIODS if CPP_EE else 0, CPP_MAX) * brackets[0][1] -ei_credit = min(abs(EI_EE) * PAY_PERIODS if EI_EE else 0, EI_MAX) * brackets[0][1] - -# Canada Employment Amount credit -cea_credit = CEA * brackets[0][1] - -annual_tax = max(0, tax - tax_credit - cpp_credit - ei_credit - cea_credit) -result = -(annual_tax / PAY_PERIODS) + credit = fed_bpa * fed_brackets[0][1] + cea_credit = cea * fed_brackets[0][1] + annual_tax = max(tax - credit - cea_credit, 0) + additional = employee.federal_additional_tax or 0 + result = -(annual_tax / 26 + additional) - - + + Provincial Income Tax PROV_TAX - 161 + 35 - + none code True -# Ontario Provincial Tax - Using Rule Parameters -PAY_PERIODS = 24 -brackets = payslip._rule_parameter('ca_on_brackets') -BPA_ON = payslip._rule_parameter('ca_on_bpa') -CPP_MAX = payslip._rule_parameter('ca_cpp_max') -EI_MAX = payslip._rule_parameter('ca_ei_max') +gross_amount = result_rules.get('GROSS', {}).get('total', 0) +rrsp = abs(result_rules.get('RRSP', {}).get('total', 0)) +union = abs(result_rules.get('UNION_DUES', {}).get('total', 0)) +taxable_per_period = gross_amount - rrsp - union +annual_income = taxable_per_period * 26 -gross = categories['GROSS'] -annual = gross * PAY_PERIODS +province = employee.home_province or 'ON' -# Calculate tax using brackets -tax = 0 -prev_threshold = 0 -for threshold, rate in brackets: - if annual <= threshold: - tax += (annual - prev_threshold) * rate - break - else: - tax += (threshold - prev_threshold) * rate - prev_threshold = threshold +if province == 'QC': + result = 0 +else: + PROV = { + 'ON': {'b': [[53891, 0.0505], [107785, 0.0915], [150000, 0.1116], [220000, 0.1216], [0, 0.1316]], 'bpa': 12989, 'st': [[5818, 0.20], [7446, 0.36]]}, + 'AB': {'b': [[61200, 0.08], [154259, 0.10], [185111, 0.12], [246813, 0.13], [370220, 0.14], [0, 0.15]], 'bpa': 21885, 'st': []}, + 'BC': {'b': [[50363, 0.0506], [100728, 0.077], [115648, 0.105], [140430, 0.1229], [190405, 0.147], [265545, 0.168], [0, 0.205]], 'bpa': 12273, 'st': []}, + 'SK': {'b': [[54532, 0.105], [155805, 0.125], [0, 0.145]], 'bpa': 18635, 'st': []}, + 'MB': {'b': [[47000, 0.108], [100000, 0.1275], [0, 0.174]], 'bpa': 15780, 'st': []}, + 'NB': {'b': [[52333, 0.094], [104666, 0.14], [193861, 0.16], [0, 0.195]], 'bpa': 12458, 'st': []}, + 'NS': {'b': [[30995, 0.0879], [61991, 0.1495], [97417, 0.1667], [157124, 0.175], [0, 0.21]], 'bpa': 11481, 'st': []}, + 'PE': {'b': [[33928, 0.095], [65820, 0.1347], [106890, 0.166], [142250, 0.1762], [0, 0.19]], 'bpa': 12750, 'st': []}, + 'NL': {'b': [[44678, 0.087], [89354, 0.145], [159528, 0.158], [223340, 0.178], [285319, 0.198], [570638, 0.208], [1141275, 0.213], [0, 0.218]], 'bpa': 10382, 'st': []}, + 'NT': {'b': [[53003, 0.059], [106009, 0.086], [172346, 0.122], [0, 0.1405]], 'bpa': 16442, 'st': []}, + 'YT': {'b': [[58523, 0.064], [117045, 0.09], [181440, 0.109], [500000, 0.128], [0, 0.15]], 'bpa': 16729, 'st': []}, + 'NU': {'b': [[55801, 0.04], [111602, 0.07], [181439, 0.09], [0, 0.115]], 'bpa': 17091, 'st': []}, + } -# Ontario Basic Personal Amount credit -tax_credit = BPA_ON * brackets[0][1] # Lowest rate + cfg = PROV.get(province, PROV['ON']) + prov_brackets = [] + for br in cfg['b']: + t = br[0] if br[0] != 0 else float('inf') + prov_brackets.append((t, br[1])) -# CPP/EI credits at lowest rate -cpp_credit = min(abs(CPP_EE) * PAY_PERIODS if CPP_EE else 0, CPP_MAX) * brackets[0][1] -ei_credit = min(abs(EI_EE) * PAY_PERIODS if EI_EE else 0, EI_MAX) * brackets[0][1] + tax = 0 + prev_bracket = 0 + for bracket, rate in prov_brackets: + taxable_in_bracket = min(annual_income, bracket) - prev_bracket + if taxable_in_bracket > 0: + tax += taxable_in_bracket * rate + prev_bracket = bracket + if annual_income <= bracket: + break -annual_tax = max(0, tax - tax_credit - cpp_credit - ei_credit) -result = -(annual_tax / PAY_PERIODS) + prov_bpa = cfg['bpa'] + if employee.provincial_claim_amount and employee.provincial_claim_amount > 0: + prov_bpa = employee.provincial_claim_amount + prov_credit = prov_bpa * prov_brackets[0][1] + basic_provincial_tax = max(tax - prov_credit, 0) + + surtax = 0 + for s in cfg['st']: + if basic_provincial_tax > s[0]: + surtax += (basic_provincial_tax - s[0]) * s[1] + + total_provincial_tax = basic_provincial_tax + surtax + result = -(total_provincial_tax / 26) - + - - Vacation Pay - VAC_PAY - 170 + + Ontario Health Premium + OHP + 36 - + + python + +province = employee.home_province or 'ON' +result = (province == 'ON') + + code + True + +gross_amount = result_rules.get('GROSS', {}).get('total', 0) +rrsp = abs(result_rules.get('RRSP', {}).get('total', 0)) +union = abs(result_rules.get('UNION_DUES', {}).get('total', 0)) +taxable_per_period = gross_amount - rrsp - union +annual_income = taxable_per_period * 26 + +ohp = 0 +if annual_income <= 20000: + ohp = 0 +elif annual_income <= 36000: + ohp = min((annual_income - 20000) * 0.06, 300) +elif annual_income <= 48000: + ohp = 300 + min((annual_income - 36000) * 0.06, 150) +elif annual_income <= 72000: + ohp = 450 + min((annual_income - 48000) * 0.0025, 150) +elif annual_income <= 200000: + ohp = 600 + min((annual_income - 72000) * 0.0025, 300) +else: + ohp = 900 + +result = -(ohp / 26) + + + + + + + + Net Pay + NET + 100 + + none code True -# Vacation Pay - Using Rule Parameter -VAC_RATE = payslip._rule_parameter('ca_vacation_rate') -result = categories['BASIC'] * VAC_RATE +result = ( + result_rules.get('GROSS', {}).get('total', 0) + + result_rules.get('RRSP', {}).get('total', 0) + + result_rules.get('UNION_DUES', {}).get('total', 0) + + result_rules.get('CPP_EE', {}).get('total', 0) + + result_rules.get('CPP2_EE', {}).get('total', 0) + + result_rules.get('EI_EE', {}).get('total', 0) + + result_rules.get('FED_TAX', {}).get('total', 0) + + result_rules.get('PROV_TAX', {}).get('total', 0) + + result_rules.get('OHP', {}).get('total', 0) +) diff --git a/fusion_payroll/data/ir_sequence_data.xml b/fusion_payroll/data/ir_sequence_data.xml new file mode 100644 index 00000000..edd821ab --- /dev/null +++ b/fusion_payroll/data/ir_sequence_data.xml @@ -0,0 +1,30 @@ + + + + + + Payroll Cheque + payroll.cheque + CHQ + 6 + + + + + Record of Employment + hr.roe + ROE + 6 + + + + + Tax Remittance + hr.tax.remittance + REM + 6 + + + + + diff --git a/fusion_payroll/data/tax_yearly_rates_data.xml b/fusion_payroll/data/tax_yearly_rates_data.xml deleted file mode 100644 index b12d4afa..00000000 --- a/fusion_payroll/data/tax_yearly_rates_data.xml +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - federal - 1433.00 - - - - - - - 55867.00 - 15.00 - 0.00 - - - - - - 111733.00 - 20.50 - 0.00 - - - - - - 173205.00 - 26.00 - 0.00 - - - - - - 246752.00 - 29.00 - 0.00 - - - - - - 246752.01 - 33.00 - 0.00 - - - - - - - - provincial - - - - - - - 52886.00 - 5.05 - 0.00 - - - - - - 105775.00 - 9.15 - 0.00 - - - - - - 150000.00 - 11.16 - 0.00 - - - - - - 220000.00 - 12.16 - 0.00 - - - - - - 220000.01 - 13.16 - 0.00 - - - - - - - cpp - 5.95 - 5.95 - 134.61 - 4034.10 - - - - - - - - ei - 1.64 - 65700.00 - 1077.48 - 1508.47 - - - - - diff --git a/fusion_payroll/models/__init__.py b/fusion_payroll/models/__init__.py index cd73d08a..f6767d1a 100644 --- a/fusion_payroll/models/__init__.py +++ b/fusion_payroll/models/__init__.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- -from . import tax_yearly_rates -from . import tax_yearly_rate_line from . import hr_employee +from . import hr_contract from . import hr_payslip from . import hr_roe from . import hr_tax_remittance diff --git a/fusion_payroll/models/hr_payroll_t4.py b/fusion_payroll/models/hr_payroll_t4.py index 23a7a5da..6fef2ff0 100644 --- a/fusion_payroll/models/hr_payroll_t4.py +++ b/fusion_payroll/models/hr_payroll_t4.py @@ -4,6 +4,7 @@ import base64 import os import io from datetime import date +from lxml import etree from odoo import models, fields, api, tools from odoo.exceptions import UserError @@ -15,16 +16,6 @@ class HrT4Summary(models.Model): _order = 'tax_year desc' _inherit = ['mail.thread', 'mail.activity.mixin'] - def _get_pdf_text_coordinates(self): - """Get text overlay coordinates for flattened PDF - Returns dict mapping field names to (x, y, font_size, font_name) tuples - Coordinates are in points (1/72 inch), origin at bottom-left - Reads from pdf.field.position model based on template type - """ - # Query configured positions from database for T4 Summary - position_model = self.env['pdf.field.position'] - return position_model.get_coordinates_dict('T4 Summary') - STATE_SELECTION = [ ('draft', 'Draft'), ('generated', 'Generated'), @@ -205,6 +196,9 @@ class HrT4Summary(models.Model): xml_filename = fields.Char( string='XML Filename', ) + transmitter_bn = fields.Char(string='Business Number (BN)') + transmitter_name = fields.Char(string='Transmitter Name') + contact_email = fields.Char(string='Contact Email') # === Box 74: SIN of Proprietor === proprietor_sin = fields.Char( @@ -270,7 +264,7 @@ class HrT4Summary(models.Model): payslips = self.env['hr.payslip'].search([ ('company_id', '=', self.company_id.id), - ('state', 'in', ['validated', 'paid']), + ('state', 'in', ['done', 'paid']), ('date_from', '>=', year_start), ('date_to', '<=', year_end), ]) @@ -305,6 +299,8 @@ class HrT4Summary(models.Model): box_40_allowances = 0 box_42_commissions = 0 box_44_union_dues = 0 + rrsp = 0 + union_dues = 0 for ps in emp_payslips: # Process each payslip line @@ -357,8 +353,12 @@ class HrT4Summary(models.Model): ei_ee += amount elif code == 'EI_ER': ei_er += amount - elif code in ('FED_TAX', 'PROV_TAX'): + elif code in ('FED_TAX', 'PROV_TAX', 'OHP'): income_tax += amount + elif code == 'RRSP': + rrsp += amount + elif code == 'UNION_DUES': + union_dues += amount # Add GROSS to employment income (Box 14) # GROSS already includes all taxable income (salary, overtime, bonus, allowances, commissions, etc.) @@ -387,7 +387,8 @@ class HrT4Summary(models.Model): # New boxes 'box_40_taxable_benefits': box_40_allowances, 'box_42_commissions': box_42_commissions, - 'box_44_union_dues': box_44_union_dues, + 'box_44_union_dues': box_44_union_dues + union_dues, + 'box_20_rpp': rrsp, }) self.state = 'generated' @@ -410,6 +411,166 @@ class HrT4Summary(models.Model): 'filing_date': date.today(), }) + def action_export_xml(self): + """Generate CRA T4 XML file (T619 format) for electronic filing""" + self.ensure_one() + nsmap = { + None: 'http://www.cra-arc.gc.ca/enov/ol/interfaces/efile/partnership/t4' + } + root = etree.Element('Submission', nsmap=nsmap) + + # T619 header + t619 = etree.SubElement(root, 'T619') + self._add_xml_element(t619, 'sbmt_ref_id', 'T4-%s-%s' % (self.tax_year, self.id)) + self._add_xml_element(t619, 'rpt_tcd', 'O') + bn = self.transmitter_bn or self.cra_business_number or '' + self._add_xml_element(t619, 'trnmtr_nbr', 'MM' + bn[:7].ljust(7, '0') if bn else 'MM0000000') + self._add_xml_element(t619, 'trnmtr_tcd', '4') + self._add_xml_element(t619, 'summ_cnt', '1') + self._add_xml_element(t619, 'lang_cd', 'E') + + trnmtr_nm = etree.SubElement(t619, 'TRNMTR_NM') + self._add_xml_element(trnmtr_nm, 'l1_nm', self.transmitter_name or self.company_id.name or '') + + company = self.company_id + trnmtr_addr = etree.SubElement(t619, 'TRNMTR_ADDR') + if company.street: + self._add_xml_element(trnmtr_addr, 'addr_l1_txt', company.street) + if company.city: + self._add_xml_element(trnmtr_addr, 'cty_nm', company.city) + if company.state_id: + self._add_xml_element(trnmtr_addr, 'prov_cd', company.state_id.code) + if company.zip: + self._add_xml_element(trnmtr_addr, 'pstl_cd', company.zip) + self._add_xml_element(trnmtr_addr, 'cntry_cd', 'CAN') + + if self.contact_name or self.contact_phone or self.contact_email: + cntc = etree.SubElement(t619, 'CNTC') + self._add_xml_element(cntc, 'cntc_nm', self.contact_name) + if self.contact_phone: + phone = ''.join(filter(str.isdigit, self.contact_phone)) + if len(phone) >= 10: + self._add_xml_element(cntc, 'cntc_area_cd', phone[:3]) + self._add_xml_element(cntc, 'cntc_phn_nbr', phone[3:10]) + self._add_xml_element(cntc, 'cntc_email_area', self.contact_email) + + # T4Return + t4_return = etree.SubElement(root, 'T4Return') + + # T4Summary + t4_summary = etree.SubElement(t4_return, 'T4Summary') + bn15 = (self.transmitter_bn or self.cra_business_number or '')[:15] + self._add_xml_element(t4_summary, 'bn', bn15) + self._add_xml_element(t4_summary, 'tx_yr', str(self.tax_year)) + self._add_xml_element(t4_summary, 'slp_cnt', str(self.slip_count)) + + payr_nm = etree.SubElement(t4_summary, 'PAYR_NM') + self._add_xml_element(payr_nm, 'l1_nm', company.name or '') + + payr_addr = etree.SubElement(t4_summary, 'PAYR_ADDR') + if company.street: + self._add_xml_element(payr_addr, 'addr_l1_txt', company.street) + if company.city: + self._add_xml_element(payr_addr, 'cty_nm', company.city) + if company.state_id: + self._add_xml_element(payr_addr, 'prov_cd', company.state_id.code) + if company.zip: + self._add_xml_element(payr_addr, 'pstl_cd', company.zip) + self._add_xml_element(payr_addr, 'cntry_cd', 'CAN') + + t4_tamt = etree.SubElement(t4_summary, 'T4_TAMT') + self._add_xml_amount(t4_tamt, 'tot_empt_incm_amt', self.total_employment_income) + self._add_xml_amount(t4_tamt, 'tot_empe_cpp_amt', self.total_cpp_employee) + self._add_xml_amount(t4_tamt, 'tot_empe_eip_amt', self.total_ei_employee) + self._add_xml_amount(t4_tamt, 'tot_itx_ddct_amt', self.total_income_tax) + self._add_xml_amount(t4_tamt, 'tot_empr_cpp_amt', self.total_cpp_employer) + self._add_xml_amount(t4_tamt, 'tot_empr_eip_amt', self.total_ei_employer) + + # T4Slips + for slip in self.slip_ids: + slip_elem = etree.SubElement(t4_return, 'T4Slip') + emp = slip.employee_id + + empe_nm = etree.SubElement(slip_elem, 'EMPE_NM') + name_parts = (emp.name or '').split(' ', 1) + self._add_xml_element(empe_nm, 'snm', name_parts[-1] if len(name_parts) > 1 else emp.name or '') + self._add_xml_element(empe_nm, 'gvn_nm', name_parts[0] if len(name_parts) > 1 else '') + + if hasattr(emp, 'private_street') and (emp.private_street or emp.private_city): + empe_addr = etree.SubElement(slip_elem, 'EMPE_ADDR') + if emp.private_street: + self._add_xml_element(empe_addr, 'addr_l1_txt', emp.private_street) + if emp.private_city: + self._add_xml_element(empe_addr, 'cty_nm', emp.private_city) + if emp.private_state_id: + self._add_xml_element(empe_addr, 'prov_cd', emp.private_state_id.code) + if emp.private_zip: + self._add_xml_element(empe_addr, 'pstl_cd', emp.private_zip) + self._add_xml_element(empe_addr, 'cntry_cd', 'CAN') + elif emp.home_street or emp.home_city: + empe_addr = etree.SubElement(slip_elem, 'EMPE_ADDR') + if emp.home_street: + self._add_xml_element(empe_addr, 'addr_l1_txt', emp.home_street) + if emp.home_city: + self._add_xml_element(empe_addr, 'cty_nm', emp.home_city) + if emp.home_province: + self._add_xml_element(empe_addr, 'prov_cd', emp.home_province) + if emp.home_postal_code: + self._add_xml_element(empe_addr, 'pstl_cd', emp.home_postal_code) + self._add_xml_element(empe_addr, 'cntry_cd', 'CAN') + + self._add_xml_element(slip_elem, 'sin', slip.sin_number or '') + self._add_xml_element(slip_elem, 'empe_nbr', str(emp.employee_number or emp.id)) + province = emp.home_province or '' + self._add_xml_element(slip_elem, 'prov_cd', province) + + t4_amt = etree.SubElement(slip_elem, 'T4_AMT') + self._add_xml_amount(t4_amt, 'empt_incm_amt', slip.employment_income) + self._add_xml_amount(t4_amt, 'cpp_cntrb_amt', slip.cpp_employee) + self._add_xml_amount(t4_amt, 'empe_eip_amt', slip.ei_employee) + self._add_xml_amount(t4_amt, 'itx_ddct_amt', slip.income_tax) + self._add_xml_amount(t4_amt, 'ei_insu_earn_amt', slip.ei_insurable_earnings) + self._add_xml_amount(t4_amt, 'cpp_qpp_pnsn_amt', slip.cpp_pensionable_earnings) + self._add_xml_amount(t4_amt, 'unn_dues_amt', slip.box_44_union_dues) + + xml_bytes = etree.tostring(root, xml_declaration=True, encoding='UTF-8', pretty_print=True) + filename = 'T4_%s_%s.xml' % (self.tax_year, (company.name or 'Company').replace(' ', '_')) + + self.write({ + 'xml_file': base64.b64encode(xml_bytes), + 'xml_filename': filename, + }) + + attachment = self.env['ir.attachment'].create({ + 'name': filename, + 'type': 'binary', + 'datas': base64.b64encode(xml_bytes), + 'res_model': self._name, + 'res_id': self.id, + 'mimetype': 'application/xml', + }) + + self.message_post( + body='T4 XML generated: %s' % filename, + attachment_ids=[attachment.id], + ) + + return { + 'type': 'ir.actions.act_url', + 'url': '/web/content/%s?download=true' % attachment.id, + 'target': 'self', + } + + def _add_xml_element(self, parent, tag, value): + if value: + elem = etree.SubElement(parent, tag) + elem.text = str(value) + + def _add_xml_amount(self, parent, tag, amount): + if amount: + elem = etree.SubElement(parent, tag) + elem.text = '%.2f' % amount + def _get_pdf_text_coordinates(self): """Get text overlay coordinates for flattened PDF Returns dict mapping field names to (x, y, font_size, font_name) tuples @@ -1005,6 +1166,13 @@ class HrT4Slip(models.Model): currency_field='currency_id', ) + # === Box 20: RPP/RRSP Contributions === + box_20_rpp = fields.Monetary( + string='Box 20: RPP/RRSP', + currency_field='currency_id', + help='Registered Pension Plan or RRSP contributions', + ) + # === T4 Dental Benefits Code === t4_dental_code = fields.Selection( related='employee_id.t4_dental_code', diff --git a/fusion_payroll/models/hr_payroll_t4a.py b/fusion_payroll/models/hr_payroll_t4a.py index 7377a71c..1707322e 100644 --- a/fusion_payroll/models/hr_payroll_t4a.py +++ b/fusion_payroll/models/hr_payroll_t4a.py @@ -4,6 +4,7 @@ import base64 import os import io from datetime import date +from lxml import etree from odoo import models, fields, api from odoo.exceptions import UserError from odoo import tools @@ -128,6 +129,15 @@ class HrT4ASummary(models.Model): string='Telephone', ) + # === Transmitter Information === + transmitter_bn = fields.Char(string='Business Number (BN)') + transmitter_name = fields.Char(string='Transmitter Name') + contact_email = fields.Char(string='Contact Email') + + # === XML Export === + xml_file = fields.Binary(string='XML File', attachment=True) + xml_filename = fields.Char(string='XML Filename') + # === Filing Information === filing_date = fields.Date( string='Filing Date', @@ -158,6 +168,123 @@ class HrT4ASummary(models.Model): 'filing_date': date.today(), }) + def action_export_xml(self): + """Generate CRA T4A XML file (T619 format) for electronic filing""" + self.ensure_one() + nsmap = { + None: 'http://www.cra-arc.gc.ca/enov/ol/interfaces/efile/partnership/t4a' + } + root = etree.Element('Submission', nsmap=nsmap) + + t619 = etree.SubElement(root, 'T619') + self._add_xml_element(t619, 'sbmt_ref_id', 'T4A-%s-%s' % (self.tax_year, self.id)) + self._add_xml_element(t619, 'rpt_tcd', 'O') + bn = self.transmitter_bn or self.cra_business_number or '' + self._add_xml_element(t619, 'trnmtr_nbr', 'MM' + bn[:7].ljust(7, '0') if bn else 'MM0000000') + self._add_xml_element(t619, 'trnmtr_tcd', '4') + self._add_xml_element(t619, 'summ_cnt', '1') + self._add_xml_element(t619, 'lang_cd', 'E') + + trnmtr_nm = etree.SubElement(t619, 'TRNMTR_NM') + self._add_xml_element(trnmtr_nm, 'l1_nm', self.transmitter_name or self.company_id.name or '') + + company = self.company_id + trnmtr_addr = etree.SubElement(t619, 'TRNMTR_ADDR') + if company.street: + self._add_xml_element(trnmtr_addr, 'addr_l1_txt', company.street) + if company.city: + self._add_xml_element(trnmtr_addr, 'cty_nm', company.city) + if company.state_id: + self._add_xml_element(trnmtr_addr, 'prov_cd', company.state_id.code) + if company.zip: + self._add_xml_element(trnmtr_addr, 'pstl_cd', company.zip) + self._add_xml_element(trnmtr_addr, 'cntry_cd', 'CAN') + + if self.contact_name or self.contact_phone or self.contact_email: + cntc = etree.SubElement(t619, 'CNTC') + self._add_xml_element(cntc, 'cntc_nm', self.contact_name) + if self.contact_phone: + phone = ''.join(filter(str.isdigit, self.contact_phone)) + if len(phone) >= 10: + self._add_xml_element(cntc, 'cntc_area_cd', phone[:3]) + self._add_xml_element(cntc, 'cntc_phn_nbr', phone[3:10]) + self._add_xml_element(cntc, 'cntc_email_area', self.contact_email) + + t4a_return = etree.SubElement(root, 'T4AReturn') + + t4a_summary = etree.SubElement(t4a_return, 'T4ASummary') + bn15 = (self.transmitter_bn or self.cra_business_number or '')[:15] + self._add_xml_element(t4a_summary, 'bn', bn15) + self._add_xml_element(t4a_summary, 'tx_yr', str(self.tax_year)) + self._add_xml_element(t4a_summary, 'slp_cnt', str(self.slip_count)) + + payr_nm = etree.SubElement(t4a_summary, 'PAYR_NM') + self._add_xml_element(payr_nm, 'l1_nm', company.name or '') + + payr_addr = etree.SubElement(t4a_summary, 'PAYR_ADDR') + if company.street: + self._add_xml_element(payr_addr, 'addr_l1_txt', company.street) + if company.city: + self._add_xml_element(payr_addr, 'cty_nm', company.city) + if company.state_id: + self._add_xml_element(payr_addr, 'prov_cd', company.state_id.code) + if company.zip: + self._add_xml_element(payr_addr, 'pstl_cd', company.zip) + self._add_xml_element(payr_addr, 'cntry_cd', 'CAN') + + t4a_tamt = etree.SubElement(t4a_summary, 'T4A_TAMT') + self._add_xml_amount(t4a_tamt, 'tot_pens_spran_amt', self.total_box_016) + self._add_xml_amount(t4a_tamt, 'tot_lsp_amt', self.total_box_018) + self._add_xml_amount(t4a_tamt, 'tot_self_empl_cmsn_amt', self.total_box_020) + self._add_xml_amount(t4a_tamt, 'tot_annty_amt', self.total_box_024) + + for t4a in self.slip_ids: + slip_elem = etree.SubElement(t4a_return, 'T4ASlip') + self._add_xml_element(slip_elem, 'rcpnt_nm', t4a.recipient_name or '') + self._add_xml_element(slip_elem, 'sin', (t4a.recipient_sin or '').replace('-', '').replace(' ', '')) + if t4a.recipient_account_number: + self._add_xml_element(slip_elem, 'rcpnt_bn', t4a.recipient_account_number) + + t4a_amt = etree.SubElement(slip_elem, 'T4A_AMT') + self._add_xml_amount(t4a_amt, 'pens_spran_amt', t4a.box_016_pension) + self._add_xml_amount(t4a_amt, 'lsp_amt', t4a.box_018_lump_sum) + self._add_xml_amount(t4a_amt, 'self_empl_cmsn_amt', t4a.box_020_commissions) + self._add_xml_amount(t4a_amt, 'annty_amt', t4a.box_024_annuities) + self._add_xml_amount(t4a_amt, 'fees_svc_amt', t4a.box_048_fees) + + xml_bytes = etree.tostring(root, xml_declaration=True, encoding='UTF-8', pretty_print=True) + filename = 'T4A_%s_%s.xml' % (self.tax_year, (company.name or 'Company').replace(' ', '_')) + + self.write({ + 'xml_file': base64.b64encode(xml_bytes), + 'xml_filename': filename, + }) + + attachment = self.env['ir.attachment'].create({ + 'name': filename, + 'type': 'binary', + 'datas': base64.b64encode(xml_bytes), + 'res_model': self._name, + 'res_id': self.id, + 'mimetype': 'application/xml', + }) + + return { + 'type': 'ir.actions.act_url', + 'url': '/web/content/%s?download=true' % attachment.id, + 'target': 'self', + } + + def _add_xml_element(self, parent, tag, value): + if value: + elem = etree.SubElement(parent, tag) + elem.text = str(value) + + def _add_xml_amount(self, parent, tag, amount): + if amount: + elem = etree.SubElement(parent, tag) + elem.text = '%.2f' % amount + class HrT4ASlip(models.Model): """T4A Slip - One per recipient per tax year""" @@ -486,16 +613,17 @@ class HrT4ASlip(models.Model): }) # Post to chatter + attachment = self.env['ir.attachment'].create({ + 'name': filename, + 'type': 'binary', + 'datas': pdf_data, + 'res_model': self._name, + 'res_id': self.id, + 'mimetype': 'application/pdf', + }) self.message_post( body=f'T4A PDF generated: {filename}', - attachment_ids=[(0, 0, { - 'name': filename, - 'type': 'binary', - 'datas': pdf_data, - 'res_model': self._name, - 'res_id': self.id, - 'mimetype': 'application/pdf', - })], + attachment_ids=[attachment.id], ) return { diff --git a/fusion_payroll/models/hr_payslip.py b/fusion_payroll/models/hr_payslip.py index 13d5ef6a..ffc33f51 100644 --- a/fusion_payroll/models/hr_payslip.py +++ b/fusion_payroll/models/hr_payslip.py @@ -208,9 +208,16 @@ class HrPayslip(models.Model): ('date_to', '<=', payslip.date_to), ('state', 'in', ['done', 'paid']), ] - # Include current payslip if it's in draft/verify state if payslip.state in ['draft', 'verify']: - domain = ['|', ('id', '=', payslip.id)] + domain + domain = [ + '|', + ('id', '=', payslip.id), + '&', '&', '&', + ('employee_id', '=', payslip.employee_id.id), + ('date_from', '>=', year_start), + ('date_to', '<=', payslip.date_to), + ('state', 'in', ['done', 'paid']), + ] ytd_payslips = self.search(domain) @@ -229,13 +236,13 @@ class HrPayslip(models.Model): # Sum up specific rule amounts for line in slip.line_ids: code = line.code or '' - if code == 'CPP': + if code == 'CPP_EE': ytd_cpp += abs(line.total or 0) - elif code == 'CPP2': + elif code == 'CPP2_EE': ytd_cpp2 += abs(line.total or 0) - elif code == 'EI': + elif code == 'EI_EE': ytd_ei += abs(line.total or 0) - elif code in ['FED_TAX', 'PROV_TAX', 'INCOME_TAX']: + elif code in ['FED_TAX', 'PROV_TAX', 'OHP']: ytd_income_tax += abs(line.total or 0) payslip.ytd_gross = ytd_gross @@ -278,13 +285,13 @@ class HrPayslip(models.Model): for line in payslip.line_ids: code = line.code or '' - if code == 'CPP': + if code == 'CPP_EE': employee_cpp = abs(line.total or 0) - elif code == 'CPP2': + elif code == 'CPP2_EE': employee_cpp2 = abs(line.total or 0) - elif code == 'EI': + elif code == 'EI_EE': employee_ei = abs(line.total or 0) - elif code in ['FED_TAX', 'PROV_TAX', 'INCOME_TAX']: + elif code in ['FED_TAX', 'PROV_TAX', 'OHP']: employee_income_tax += abs(line.total or 0) payslip.employee_cpp = employee_cpp diff --git a/fusion_payroll/models/hr_salary_rule_category.py b/fusion_payroll/models/hr_salary_rule_category.py deleted file mode 100644 index 16a9e5f7..00000000 --- a/fusion_payroll/models/hr_salary_rule_category.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- - -from odoo import models, fields, api - - -class HrSalaryRuleCategory(models.Model): - _inherit = 'hr.salary.rule.category' - - # === Canadian Pension Plan (CPP) Configuration === - cpp_deduction_id = fields.Many2one( - 'tax.yearly.rates', - string='Canadian Configuration Values', - domain="[('ded_type', '=', 'cpp')]", - help='Link to CPP yearly rates configuration', - ) - cpp_date = fields.Date( - string='CPP Date', - related='cpp_deduction_id.cpp_date', - readonly=True, - ) - max_cpp = fields.Float( - string='Maximum CPP', - related='cpp_deduction_id.max_cpp', - readonly=True, - ) - emp_contribution_rate = fields.Float( - string='Employee Contribution Rate', - related='cpp_deduction_id.emp_contribution_rate', - readonly=True, - ) - employer_contribution_rate = fields.Float( - string='Employer Contribution Rate', - related='cpp_deduction_id.employer_contribution_rate', - readonly=True, - ) - exemption = fields.Float( - string='Exemption Amount', - related='cpp_deduction_id.exemption', - readonly=True, - ) - - # === Employment Insurance (EI) Configuration === - ei_deduction_id = fields.Many2one( - 'tax.yearly.rates', - string='Employment Insurance', - domain="[('ded_type', '=', 'ei')]", - help='Link to EI yearly rates configuration', - ) - ei_date = fields.Date( - string='EI Date', - related='ei_deduction_id.ei_date', - readonly=True, - ) - ei_rate = fields.Float( - string='EI Rate', - related='ei_deduction_id.ei_rate', - readonly=True, - ) - ei_earnings = fields.Float( - string='Maximum EI Earnings', - related='ei_deduction_id.ei_earnings', - readonly=True, - ) - emp_ei_amount = fields.Float( - string='Employee EI Amount', - related='ei_deduction_id.emp_ei_amount', - readonly=True, - ) - employer_ei_amount = fields.Float( - string='Employer EI Amount', - related='ei_deduction_id.employer_ei_amount', - readonly=True, - ) - - # === Federal Tax Configuration === - fed_tax_id = fields.Many2one( - 'tax.yearly.rates', - string='Federal Tax', - domain="[('tax_type', '=', 'federal')]", - help='Link to Federal tax yearly rates configuration', - ) - - # === Provincial Tax Configuration === - provincial_tax_id = fields.Many2one( - 'tax.yearly.rates', - string='Provincial Tax', - domain="[('tax_type', '=', 'provincial')]", - help='Link to Provincial tax yearly rates configuration', - ) diff --git a/fusion_payroll/models/hr_tax_remittance.py b/fusion_payroll/models/hr_tax_remittance.py index b1f35908..cc32c0ea 100644 --- a/fusion_payroll/models/hr_tax_remittance.py +++ b/fusion_payroll/models/hr_tax_remittance.py @@ -182,7 +182,7 @@ class HrTaxRemittance(models.Model): # Find all confirmed payslips in the period payslips = self.env['hr.payslip'].search([ ('company_id', '=', self.company_id.id), - ('state', 'in', ['validated', 'paid']), + ('state', 'in', ['done', 'paid']), ('date_from', '>=', self.period_start), ('date_to', '<=', self.period_end), ]) @@ -212,7 +212,7 @@ class HrTaxRemittance(models.Model): ei_ee += amount elif code == 'EI_ER': ei_er += amount - elif code in ('FED_TAX', 'PROV_TAX'): + elif code in ('FED_TAX', 'PROV_TAX', 'OHP'): income_tax += amount self.write({ diff --git a/fusion_payroll/models/payroll_cheque.py b/fusion_payroll/models/payroll_cheque.py index eca7e068..3ce85b8a 100644 --- a/fusion_payroll/models/payroll_cheque.py +++ b/fusion_payroll/models/payroll_cheque.py @@ -358,38 +358,40 @@ class PayrollCheque(models.Model): get_ytd_amount('GROSS') or 0) # Get vacation pay - vacation_pay_current = get_line_amount('VAC') or get_line_amount('VACATION') or 0 - vacation_pay_ytd = get_ytd_amount('VAC') or get_ytd_amount('VACATION') or 0 + vacation_pay_current = get_line_amount('VAC_PAY') or 0 + vacation_pay_ytd = get_ytd_amount('VAC_PAY') or 0 # Get stat holiday pay - stat_pay_current = get_line_amount('STAT') or get_line_amount('STATHOLIDAY') or 0 - stat_pay_ytd = get_ytd_amount('STAT') or get_ytd_amount('STATHOLIDAY') or 0 + stat_pay_current = get_line_amount('STAT_PAY') or 0 + stat_pay_ytd = get_ytd_amount('STAT_PAY') or 0 # Get taxes - these are negative in payslip, so use abs() - # First try to get from payslip lines - income_tax_current = abs(get_line_amount('FIT') or get_line_amount('INCOMETAX') or 0) - ei_current = abs(get_line_amount('EI_EMP') or get_line_amount('EI') or 0) - cpp_current = abs(get_line_amount('CPP_EMP') or get_line_amount('CPP') or 0) - cpp2_current = abs(get_line_amount('CPP2_EMP') or get_line_amount('CPP2') or 0) + fed_tax_current = abs(get_line_amount('FED_TAX') or 0) + prov_tax_current = abs(get_line_amount('PROV_TAX') or 0) + ohp_current = abs(get_line_amount('OHP') or 0) + income_tax_current = fed_tax_current + prov_tax_current + ohp_current + ei_current = abs(get_line_amount('EI_EE') or 0) + cpp_current = abs(get_line_amount('CPP_EE') or 0) + cpp2_current = abs(get_line_amount('CPP2_EE') or 0) # If individual line values are 0, calculate from payslip totals total_taxes_from_lines = income_tax_current + ei_current + cpp_current + cpp2_current if total_taxes_from_lines == 0 and payslip.basic_wage > 0 and payslip.net_wage > 0: - # Calculate total taxes as difference between basic and net total_taxes_calculated = payslip.basic_wage - payslip.net_wage if total_taxes_calculated > 0: - # Approximate breakdown based on typical Canadian tax rates - # CPP ~5.95%, EI ~1.63%, Income Tax = remainder gross = payslip.basic_wage - cpp_current = min(gross * 0.0595, 3867.50) # 2025 CPP max - ei_current = min(gross * 0.0163, 1049.12) # 2025 EI max + cpp_current = min(gross * 0.0595, 4230.45 / 26) + ei_current = min(gross * 0.0163, 1123.07 / 26) income_tax_current = max(0, total_taxes_calculated - cpp_current - ei_current) - cpp2_current = 0 # Usually 0 unless over threshold + cpp2_current = 0 - income_tax_ytd = abs(get_ytd_amount('FIT') or get_ytd_amount('INCOMETAX') or 0) - ei_ytd = abs(get_ytd_amount('EI_EMP') or get_ytd_amount('EI') or 0) - cpp_ytd = abs(get_ytd_amount('CPP_EMP') or get_ytd_amount('CPP') or 0) - cpp2_ytd = abs(get_ytd_amount('CPP2_EMP') or get_ytd_amount('CPP2') or 0) + fed_tax_ytd = abs(get_ytd_amount('FED_TAX') or 0) + prov_tax_ytd = abs(get_ytd_amount('PROV_TAX') or 0) + ohp_ytd = abs(get_ytd_amount('OHP') or 0) + income_tax_ytd = fed_tax_ytd + prov_tax_ytd + ohp_ytd + ei_ytd = abs(get_ytd_amount('EI_EE') or 0) + cpp_ytd = abs(get_ytd_amount('CPP_EE') or 0) + cpp2_ytd = abs(get_ytd_amount('CPP2_EE') or 0) # Calculate totals total_taxes_current = income_tax_current + ei_current + cpp_current + cpp2_current diff --git a/fusion_payroll/models/payroll_entry.py b/fusion_payroll/models/payroll_entry.py index 9df89520..a097018c 100644 --- a/fusion_payroll/models/payroll_entry.py +++ b/fusion_payroll/models/payroll_entry.py @@ -291,7 +291,8 @@ class PayrollEntry(models.TransientModel): @api.depends('gross_pay', 'employee_id') def _compute_taxes(self): - """Calculate employee tax deductions.""" + """Calculate employee tax deductions (preview estimate for bi-weekly).""" + PAY_PERIODS = 26 for entry in self: if entry.gross_pay <= 0: entry.income_tax = 0 @@ -300,26 +301,81 @@ class PayrollEntry(models.TransientModel): entry.cpp2 = 0 entry.total_employee_tax = 0 continue - - # Get tax rates from parameters or use defaults - # These are simplified calculations - actual payroll uses full tax rules + gross = entry.gross_pay - - # Simplified tax calculations (bi-weekly) - # Income tax: ~15-20% average for Canadian employees - entry.income_tax = round(gross * 0.128, 2) # Approximate federal + provincial - - # EI: 1.64% of gross (2025 rate) up to maximum - entry.employment_insurance = round(min(gross * 0.0164, 1049.12 / 26), 2) - - # CPP: 5.95% of pensionable earnings above basic exemption (2025) - cpp_exempt = 3500 / 26 # Annual exemption / 26 pay periods - pensionable = max(0, gross - cpp_exempt) - entry.cpp = round(min(pensionable * 0.0595, 4034.10 / 26), 2) - - # CPP2: 4% on earnings above first ceiling (2025) - entry.cpp2 = 0 # Only applies if earnings exceed $71,300/year - + annual = gross * PAY_PERIODS + emp = entry.employee_id + + is_cpp_exempt = getattr(emp, 'exempt_cpp', False) + is_ei_exempt = getattr(emp, 'exempt_ei', False) + is_fed_exempt = getattr(emp, 'exempt_federal_tax', False) + + # Federal tax estimate using 2026 brackets + BPA phase-out + fed_brackets = [ + (58523, 0.14), (117045, 0.205), (181440, 0.26), + (258482, 0.29), (float('inf'), 0.33), + ] + bpa_max, bpa_min = 16452, 14829 + if annual <= 181440: + fed_bpa = bpa_max + elif annual >= 258482: + fed_bpa = bpa_min + else: + fed_bpa = bpa_max - (bpa_max - bpa_min) * (annual - 181440) / (258482 - 181440) + + fed_tax = 0 + prev = 0 + for threshold, rate in fed_brackets: + taxable_in = min(annual, threshold) - prev + if taxable_in > 0: + fed_tax += taxable_in * rate + prev = threshold + if annual <= threshold: + break + fed_tax = max(fed_tax - fed_bpa * 0.14 - 1433 * 0.14, 0) + + # Ontario provincial estimate (default province) + on_brackets = [ + (53891, 0.0505), (107785, 0.0915), (150000, 0.1116), + (220000, 0.1216), (float('inf'), 0.1316), + ] + prov_tax = 0 + prev = 0 + for threshold, rate in on_brackets: + taxable_in = min(annual, threshold) - prev + if taxable_in > 0: + prov_tax += taxable_in * rate + prev = threshold + if annual <= threshold: + break + prov_tax = max(prov_tax - 12989 * 0.0505, 0) + + entry.income_tax = 0 if is_fed_exempt else round((fed_tax + prov_tax) / PAY_PERIODS, 2) + + if is_ei_exempt: + entry.employment_insurance = 0 + else: + period_max_insurable = 68900 / PAY_PERIODS + insurable = min(gross, period_max_insurable) + entry.employment_insurance = round(min(insurable * 0.0163, 1123.07 / PAY_PERIODS), 2) + + if is_cpp_exempt: + entry.cpp = 0 + entry.cpp2 = 0 + else: + period_ympe = 74600 / PAY_PERIODS + cpp_exempt_amt = 3500 / PAY_PERIODS + pensionable = min(gross, period_ympe) + pensionable = max(0, pensionable - cpp_exempt_amt) + entry.cpp = round(min(pensionable * 0.0595, 4230.45 / PAY_PERIODS), 2) + + if not is_cpp_exempt and gross > 74600 / PAY_PERIODS: + period_ceiling = 85000 / PAY_PERIODS + cpp2_base = min(gross, period_ceiling) - period_ympe + entry.cpp2 = round(min(cpp2_base * 0.04, 416.00 / PAY_PERIODS), 2) + else: + entry.cpp2 = 0 + entry.total_employee_tax = entry.income_tax + entry.employment_insurance + entry.cpp + entry.cpp2 @api.depends('employment_insurance', 'cpp', 'cpp2') diff --git a/fusion_payroll/models/payroll_report_cost.py b/fusion_payroll/models/payroll_report_cost.py index c09ed168..5e723e1c 100644 --- a/fusion_payroll/models/payroll_report_cost.py +++ b/fusion_payroll/models/payroll_report_cost.py @@ -60,9 +60,9 @@ class PayrollReportTotalPay(models.AbstractModel): if line.category_id.code == 'BASIC': emp_data[emp_key]['regular_pay'] += line.total or 0 if hasattr(line, 'code') and line.code: - if line.code == 'STAT_HOLIDAY': + if line.code == 'STAT_PAY': emp_data[emp_key]['stat_holiday'] += line.total or 0 - elif line.code == 'VACATION': + elif line.code == 'VAC_PAY': emp_data[emp_key]['vacation_pay'] += line.total or 0 emp_data[emp_key]['total'] += getattr(slip, 'gross_wage', 0) or 0 @@ -295,9 +295,9 @@ class PayrollReportDeductions(models.AbstractModel): deduction_data = defaultdict(lambda: {'employee': 0, 'company': 0}) deduction_codes = { - 'CPP': {'name': 'Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP_ER'}, - 'CPP2': {'name': 'Second Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP2_ER'}, - 'EI': {'name': 'Employment Insurance', 'type': 'Tax', 'employer_code': 'EI_ER'}, + 'CPP_EE': {'name': 'Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP_ER'}, + 'CPP2_EE': {'name': 'Second Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP2_ER'}, + 'EI_EE': {'name': 'Employment Insurance', 'type': 'Tax', 'employer_code': 'EI_ER'}, } for slip in payslips: @@ -309,9 +309,9 @@ class PayrollReportDeductions(models.AbstractModel): if line.code in deduction_codes: deduction_data[line.code]['employee'] += abs(line.total or 0) elif line.code.endswith('_ER'): - base_code = line.code[:-3] - if base_code in deduction_codes: - deduction_data[base_code]['company'] += abs(line.total or 0) + ee_code = line.code.replace('_ER', '_EE') + if ee_code in deduction_codes: + deduction_data[ee_code]['company'] += abs(line.total or 0) lines = [] for code, info in deduction_codes.items(): @@ -378,8 +378,8 @@ class PayrollReportWorkersComp(models.AbstractModel): continue province = 'ON' # Default, would get from employee address - if hasattr(slip.employee_id, 'province_of_employment'): - province = getattr(slip.employee_id, 'province_of_employment', None) or 'ON' + if hasattr(slip.employee_id, 'home_province'): + province = getattr(slip.employee_id, 'home_province', None) or 'ON' province_data[province]['wages'] += getattr(slip, 'gross_wage', 0) or 0 diff --git a/fusion_payroll/models/payroll_report_paycheque.py b/fusion_payroll/models/payroll_report_paycheque.py index bb41a4c6..50d7008e 100644 --- a/fusion_payroll/models/payroll_report_paycheque.py +++ b/fusion_payroll/models/payroll_report_paycheque.py @@ -208,7 +208,7 @@ class PayrollReportPayrollDetails(models.AbstractModel): # Tax breakdown tax_lines = payslip.line_ids.filtered( - lambda l: hasattr(l, 'code') and l.code in ['CPP', 'CPP2', 'EI', 'FED_TAX', 'PROV_TAX'] + lambda l: hasattr(l, 'code') and l.code in ['CPP_EE', 'CPP2_EE', 'EI_EE', 'FED_TAX', 'PROV_TAX', 'OHP'] ) for line in tax_lines: lines.append({ diff --git a/fusion_payroll/models/payroll_report_tax.py b/fusion_payroll/models/payroll_report_tax.py index 15d0e733..df51bdc7 100644 --- a/fusion_payroll/models/payroll_report_tax.py +++ b/fusion_payroll/models/payroll_report_tax.py @@ -49,12 +49,12 @@ class PayrollReportTaxLiability(models.AbstractModel): # 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']}, + 'income_tax': {'name': _('Income Tax'), 'amount': 0, 'codes': ['FED_TAX', 'PROV_TAX', 'OHP']}, + 'ei_employee': {'name': _('Employment Insurance'), 'amount': 0, 'codes': ['EI_EE']}, 'ei_employer': {'name': _('Employment Insurance Employer'), 'amount': 0, 'codes': ['EI_ER']}, - 'cpp_employee': {'name': _('Canada Pension Plan'), 'amount': 0, 'codes': ['CPP']}, + 'cpp_employee': {'name': _('Canada Pension Plan'), 'amount': 0, 'codes': ['CPP_EE']}, 'cpp_employer': {'name': _('Canada Pension Plan Employer'), 'amount': 0, 'codes': ['CPP_ER']}, - 'cpp2_employee': {'name': _('Second Canada Pension Plan'), 'amount': 0, 'codes': ['CPP2']}, + 'cpp2_employee': {'name': _('Second Canada Pension Plan'), 'amount': 0, 'codes': ['CPP2_EE']}, 'cpp2_employer': {'name': _('Second Canada Pension Plan Employer'), 'amount': 0, 'codes': ['CPP2_ER']}, } @@ -260,15 +260,15 @@ class PayrollReportTaxWageSummary(models.AbstractModel): tax_data = [ { 'name': _('Income Tax'), - 'codes': ['FED_TAX', 'PROV_TAX'], + 'codes': ['FED_TAX', 'PROV_TAX', 'OHP'], 'total_wages': total_wages, - 'excess_wages': 0, # No excess for income tax + 'excess_wages': 0, }, { 'name': _('Employment Insurance'), - 'codes': ['EI'], + 'codes': ['EI_EE'], 'total_wages': total_wages, - 'excess_wages': 0, # Would need to calculate based on max + 'excess_wages': 0, }, { 'name': _('Employment Insurance Employer'), @@ -278,7 +278,7 @@ class PayrollReportTaxWageSummary(models.AbstractModel): }, { 'name': _('Canada Pension Plan'), - 'codes': ['CPP'], + 'codes': ['CPP_EE'], 'total_wages': total_wages, 'excess_wages': 0, }, @@ -290,7 +290,7 @@ class PayrollReportTaxWageSummary(models.AbstractModel): }, { 'name': _('Second Canada Pension Plan'), - 'codes': ['CPP2'], + 'codes': ['CPP2_EE'], 'total_wages': total_wages, 'excess_wages': 0, }, diff --git a/fusion_payroll/models/tax_yearly_rate_line.py b/fusion_payroll/models/tax_yearly_rate_line.py deleted file mode 100644 index aef24fb4..00000000 --- a/fusion_payroll/models/tax_yearly_rate_line.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - -from odoo import models, fields, api - - -class TaxYearlyRateLine(models.Model): - _name = 'tax.yearly.rate.line' - _description = 'Tax Yearly Rate Line' - _order = 'id' - - # === Relational Fields === - tax_id = fields.Many2one( - 'tax.yearly.rates', - string='Tax', - ondelete='cascade', - ) - - # === Tax Bracket Fields === - tax_bracket = fields.Float(string='Tax Bracket') - tax_rate = fields.Float(string='Tax Rate') - tax_constant = fields.Float(string='Tax Constant') diff --git a/fusion_payroll/models/tax_yearly_rates.py b/fusion_payroll/models/tax_yearly_rates.py deleted file mode 100644 index 454cbfc4..00000000 --- a/fusion_payroll/models/tax_yearly_rates.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- - -from odoo import models, fields, api - - -class TaxYearlyRates(models.Model): - _name = 'tax.yearly.rates' - _description = 'Yearly Tax Rates' - _order = 'id' - - # === Selection Options === - TAX_TYPE_SELECTION = [ - ('federal', 'Federal Taxes'), - ('provincial', 'Provincial Taxes'), - ] - - DEDUCTION_TYPE_SELECTION = [ - ('cpp', 'Canada Pension Plan'), - ('ei', 'Employment Insurance'), - ] - - # === Core Fields === - fiscal_year = fields.Many2one( - 'account.fiscal.year', - string='Fiscal Year', - ) - tax_type = fields.Selection( - selection=TAX_TYPE_SELECTION, - string='Tax Type', - ) - ded_type = fields.Selection( - selection=DEDUCTION_TYPE_SELECTION, - string='Deduction Type', - ) - - # === Tax Bracket Lines === - tax_yearly_rate_ids = fields.One2many( - 'tax.yearly.rate.line', - 'tax_id', - string='Tax Lines', - ) - - # === Federal/Provincial Tax Fields === - fed_tax_credit = fields.Float(string='Federal Tax Credit') - provincial_tax_credit = fields.Float(string='Provincial Tax Credit') - canada_emp_amount = fields.Float(string='Canada Employment Amount') - exemption = fields.Float(string='Exemption Amount') - - # === CPP (Canada Pension Plan) Fields === - cpp_date = fields.Date(string='CPP Date') - max_cpp = fields.Float(string='Maximum CPP') - emp_contribution_rate = fields.Float(string='Employee Contribution Rate') - employer_contribution_rate = fields.Float(string='Employer Contribution Rate') - - # === EI (Employment Insurance) Fields === - ei_date = fields.Date(string='EI Date') - ei_rate = fields.Float(string='EI Rate') - ei_earnings = fields.Float(string='Maximum EI Earnings') - emp_ei_amount = fields.Float(string='Employee EI Amount') - employer_ei_amount = fields.Float(string='Employer EI Amount') diff --git a/fusion_payroll/security/ir.model.access.csv b/fusion_payroll/security/ir.model.access.csv index 5e5655b7..34464c49 100644 --- a/fusion_payroll/security/ir.model.access.csv +++ b/fusion_payroll/security/ir.model.access.csv @@ -1,6 +1,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_tax_yearly_rates,access_tax_yearly_rates,model_tax_yearly_rates,base.group_user,1,1,1,1 -access_tax_yearly_rate_line,access_tax_yearly_rate_line,model_tax_yearly_rate_line,base.group_user,1,1,1,1 access_hr_employee_terminate_wizard,access_hr_employee_terminate_wizard,model_hr_employee_terminate_wizard,hr.group_hr_user,1,1,1,1 access_hr_roe_user,access_hr_roe_user,model_hr_roe,hr.group_hr_user,1,1,1,0 access_hr_roe_manager,access_hr_roe_manager,model_hr_roe,hr.group_hr_manager,1,1,1,1 @@ -50,4 +48,5 @@ access_payroll_cheque_print_wizard_user,payroll.cheque.print.wizard user,model_p access_cheque_layout_settings_user,cheque.layout.settings user,model_cheque_layout_settings,hr.group_hr_user,1,0,0,0 access_cheque_layout_settings_manager,cheque.layout.settings manager,model_cheque_layout_settings,hr.group_hr_manager,1,1,1,1 access_cheque_layout_preview_wizard_user,cheque.layout.preview.wizard user,model_cheque_layout_preview_wizard,hr.group_hr_user,1,1,1,1 -access_payroll_cheque_number_wizard_user,payroll.cheque.number.wizard user,model_payroll_cheque_number_wizard,hr.group_hr_user,1,1,1,1 \ No newline at end of file +access_payroll_cheque_number_wizard_user,payroll.cheque.number.wizard user,model_payroll_cheque_number_wizard,hr.group_hr_user,1,1,1,1 +access_hr_tax_remittance_sequence,hr.tax.remittance.sequence,model_hr_tax_remittance_sequence,hr.group_hr_manager,1,1,1,1 \ No newline at end of file diff --git a/fusion_payroll/views/hr_t4_views.xml b/fusion_payroll/views/hr_t4_views.xml index 693b3b91..684e4d23 100644 --- a/fusion_payroll/views/hr_t4_views.xml +++ b/fusion_payroll/views/hr_t4_views.xml @@ -37,6 +37,10 @@ string="Generate T4 Slips" type="object" class="btn-primary"/> +