diff --git a/l10n_ca_hr_payroll/__init__.py b/l10n_ca_hr_payroll/__init__.py index 1f5fb7fa..013f4e73 100644 --- a/l10n_ca_hr_payroll/__init__.py +++ b/l10n_ca_hr_payroll/__init__.py @@ -1,3 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + from . import models def _post_install_hook(cr, registry): diff --git a/l10n_ca_hr_payroll/__manifest__.py b/l10n_ca_hr_payroll/__manifest__.py index dbc83a1a..bea6a892 100644 --- a/l10n_ca_hr_payroll/__manifest__.py +++ b/l10n_ca_hr_payroll/__manifest__.py @@ -3,24 +3,24 @@ { 'name': 'Canada - Payroll', 'author': 'Hibou Corp. ', - 'version': '14.0.2020.0.0', + 'version': '14.0.2021.0.0', 'category': 'Payroll Localization', 'depends': [ 'hr_payroll_hibou', ], 'description': """ Canada - Payroll Rules. -========================================= +======================= """, 'data': [ + 'security/ir.model.access.csv', 'data/base.xml', 'data/federal.xml', 'data/ca_cpp.xml', - 'security/ir.model.access.csv', # 'views/hr_contract_views.xml', - # 'views/us_payroll_config_views.xml', + # 'views/ca_payroll_config_views.xml', ], 'demo': [ ], diff --git a/l10n_ca_hr_payroll/data/base.xml b/l10n_ca_hr_payroll/data/base.xml index 652f1b63..e2af112a 100644 --- a/l10n_ca_hr_payroll/data/base.xml +++ b/l10n_ca_hr_payroll/data/base.xml @@ -20,120 +20,125 @@ + + + + + - - Wages: Salary - SALARY - - + + + + + - - Wages: Overtime - OVERTIME - - + + + + + - - Wages: Pension Income - PENSION - - + + + + + - - Wages: Taxable Benefits - TAXED_BENEFITS - - + + + + + - - - Wages: Bonus - BONUS - - + + + + + + - - Wages: Retroactive Pay Increase - RETRO_PAY - - + + + + + - - Wages: Non-Periodic Payments - NON-PERIOD - - + + + + + - - - Deduction: Registerd Pension Plan - DED_RPP - - + + + + + + - - Deduction: Registerd Retirement Savings Plan - DED_RRSP - - + + + + + - - Deduction: Pooled Registered Pension Plan - DED_RPP - - + + + + + - - Deduction: Retirement Compensation Arrangement - DED_RCA - - + + + + + - - Deduction: Alimony Before May 5th, 1997 - DED_ALIMONY_PRE_1997 - - + + + + + - - Deduction: Maintenance Before May 5th, 1997 - DED_ALIMONY_PRE_1997 - - + + + + + - - Deduction: Union Dues - DED_UNION_DUES - - + + + + + - - Deduction: Living In Prescribed Zone - DED_PRESCRIBED_ZONE - - + + + + + - - Deduction: Employee Requested - DED_EMPLOYEE_REQUESTED - - + + + + + - - Deduction: TD1 Deductions - DED_TD1 - - + + + + + - - Deduction: TD1 Deductions - DED_TD1 - - + + + + + diff --git a/l10n_ca_hr_payroll/data/ca_cpp.xml b/l10n_ca_hr_payroll/data/ca_cpp.xml index c61945d2..b29cb513 100644 --- a/l10n_ca_hr_payroll/data/ca_cpp.xml +++ b/l10n_ca_hr_payroll/data/ca_cpp.xml @@ -1,47 +1,47 @@ - - CA Canada Pension Plan - ca_cpp - - + + + + + - - - { - 'annually': ( - ( 0, 0.1500, 0.00), - ( 49029, 0.2050, 2696.00), - ( 98040, 0.2600, 8088.00), - ( 151978, 0.2900, 12648.00), - ( 216511, 0.3300, 21308.00), - ( 'inf', 0.3300, 21308.00), - ), - } - - - - + + + + + + + + + + + + + + + + - - CA Federal - Canada Revenue Agency - Canada Pension Plan - + + + - - - - - - EE: CA Canada Pension Plan - EE_CA_CPP - python - result, _ = ca_cpp_canada_pension_plan_withholding(payslip, categories) - code - result, result_rate = ca_cpp_canada_pension_plan_withholding(payslip, categories) - - - + + + + + + + + + + + + + + diff --git a/l10n_ca_hr_payroll/data/federal.xml b/l10n_ca_hr_payroll/data/federal.xml index 5476f60c..9d400310 100644 --- a/l10n_ca_hr_payroll/data/federal.xml +++ b/l10n_ca_hr_payroll/data/federal.xml @@ -9,16 +9,14 @@ - { - 'annually': ( - ( 0, 0.1500, 0.00), - ( 49029, 0.2050, 2696.00), - ( 98040, 0.2600, 8088.00), - ( 151978, 0.2900, 12648.00), - ( 216511, 0.3300, 21308.00), - ( 'inf', 0.3300, 21308.00), - ), - } + [ + ( 0, 0.1500, 0.00), + ( 49020, 0.2050, 2696.00), + ( 98040, 0.2600, 8088.00), + ( 151978, 0.2900, 12648.00), + ( 216511, 0.3300, 21308.00), + ( 'inf', 0.3300, 21308.00), + ] @@ -28,17 +26,36 @@ CA Federal - Canada Revenue Agency - Federal Income Tax + + + EE: Federal Income Tax Withholding + EE_CA_FIT + + + + + EE: Canada Pension Plan + EE_CA_CPP + + + + + EE: CA Employment Insurance + EE_CA_EI + + + - + - + EE: CA Federal Income Tax EE_CA_FIT python - result, _ = ca_fit_federal_income_tax_withholding(payslip) + result, _ = ca_fit_federal_income_tax_withholding(payslip, categories, worked_days, inputs) code - result, result_rate = ca_fit_federal_income_tax_withholding(payslip) + result, result_rate = ca_fit_federal_income_tax_withholding(payslip, categories, worked_days, inputs) diff --git a/l10n_ca_hr_payroll/models/__init__.py b/l10n_ca_hr_payroll/models/__init__.py index 4cc120cd..61ea9a9f 100644 --- a/l10n_ca_hr_payroll/models/__init__.py +++ b/l10n_ca_hr_payroll/models/__init__.py @@ -1,4 +1,6 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + from . import ca_payroll_config -from . import hr_ca_contract +from . import hr_contract from . import hr_payslip from .federal import ca_fit diff --git a/l10n_ca_hr_payroll/models/ca_payroll_config.py b/l10n_ca_hr_payroll/models/ca_payroll_config.py index aecf520a..e9b43db5 100644 --- a/l10n_ca_hr_payroll/models/ca_payroll_config.py +++ b/l10n_ca_hr_payroll/models/ca_payroll_config.py @@ -14,53 +14,68 @@ class HRContractCanadaPayrollConfig(models.Model): employee_id = fields.Many2one('hr.employee', string="Employee", required=True) state_id = fields.Many2one('res.country.state', string="Applied State") state_code = fields.Char(related='state_id.code') + # + # contributes_to_rpp = fields.Boolean( + # string='Employee Contributes to a registered pension plan (RPP)?', + # help='For tax deduction purposes, employers can deduct amounts contributed to an RPP, RRSP, PRPP, or RCA by or on behalf of an employee to determine the employee\'s taxable income', + # ) + # rpp_withdrawal_per_check = fields.Float( + # string='RPP to Withdrawal Per Paycheck', + # help='Enter the dollar amount to be withdrawn per paycheck for a registered pension plan' + # ) + # + # contributes_to_rrsp = fields.Boolean( + # string='Contributes to a registered retirement savings plan (RRSP)', + # help='For tax deduction purposes, employers can deduct amounts contributed to an RPP, RRSP, PRPP, or RCA by or on behalf of an employee to determine the employee\'s taxable income', + # ) + # rrsp_withdrawal_per_check = fields.Float( + # string='RRSP to Withdrawal Per Paycheck', + # help='Enter the dollar amount to be withdrawn per paycheck for a registered retirement savings plan (RRSP)' + # ) + # + # contributes_to_prpp = fields.Boolean( + # string='Contributes to a pooled registered pension plan (PRPP)?', + # help='For tax deduction purposes, employers can deduct amounts contributed to an RPP, RRSP, PRPP, or RCA by or on behalf of an employee to determine the employee\'s taxable income', + # ) + # contributes_to_rca = fields.Boolean( + # string='Contributes to a retirement compensation arrangement (RCA)?', + # help='For tax deduction purposes, employers can deduct amounts contributed to an RPP, RRSP, PRPP, or RCA by or on behalf of an employee to determine the employee\'s taxable income', + # ) + # alimony_or_maintenance_deduction_required = fields.Boolean( + # string='Alimony or maintenance payments required?', + # help='Annual deductions such as child care expenses and support payments, requested by an employee or pensioner and authorized by a tax services office or tax centre', + # ) + # union_dues_deducted = fields.Boolean( + # string='Dues deducted?', + # help='Union dues for the pay period paid to a trade union, an association of public servants, or dues required under the law of a province to a parity or advisory committee or similar body', + # ) + # lives_in_prescribed_zone = fields.Boolean( + # string='Perscribed zone deduction?', + # help='Annual deduction for living in a prescribed zone, as shown on Form TD1' + # ) + # other_anual_deductions = fields.Boolean( + # string='Other annual deductions?', + # help='Annual deductions such as child care expenses and support payments, requested by an employee or pensioner and authorized by a tax services office or tax centre' + # ) + # paid_commission = fields.Boolean( + # string='Paid a commission?', + # help='Does the employee receive any commissions?', + # ) - contributes_to_rpp = fields.Boolean( - string='Employee Contributes to a registered pension plan (RPP)?', - help='For tax deduction purposes, employers can deduct amounts contributed to an RPP, RRSP, PRPP, or RCA by or on behalf of an employee to determine the employee\'s taxable income', - ) - rpp_withdrawal_per_check = fields.Float( - string='RPP to Withdrawal Per Paycheck', - help='Enter the dollar amount to be withdrawn per paycheck for a registered pension plan' - ) + # fed_fit_exempt = fields.Boolean() + # td1_fit_additional = fields.Float() + # td1_fit_tc = fields.Float() - contributes_to_rrsp = fields.Boolean( - string='Contributes to a registered retirement savings plan (RRSP)', - help='For tax deduction purposes, employers can deduct amounts contributed to an RPP, RRSP, PRPP, or RCA by or on behalf of an employee to determine the employee\'s taxable income', - ) - rrsp_withdrawal_per_check = fields.Float( - string='RRSP to Withdrawal Per Paycheck', - help='Enter the dollar amount to be withdrawn per paycheck for a registered retirement savings plan (RRSP)' - ) - - contributes_to_prpp = fields.Boolean( - string='Contributes to a pooled registered pension plan (PRPP)?', - help='For tax deduction purposes, employers can deduct amounts contributed to an RPP, RRSP, PRPP, or RCA by or on behalf of an employee to determine the employee\'s taxable income', - ) - contributes_to_rca = fields.Boolean( - string='Contributes to a retirement compensation arrangement (RCA)?', - help='For tax deduction purposes, employers can deduct amounts contributed to an RPP, RRSP, PRPP, or RCA by or on behalf of an employee to determine the employee\'s taxable income', - ) - alimony_or_maintenance_deduction_required = fields.Boolean( - string='Alimony or maintenance payments required?', - help='Annual deductions such as child care expenses and support payments, requested by an employee or pensioner and authorized by a tax services office or tax centre', - ) - union_dues_deducted = fields.Boolean( - string='Dues deducted?', - help='Union dues for the pay period paid to a trade union, an association of public servants, or dues required under the law of a province to a parity or advisory committee or similar body', - ) - lives_in_prescribed_zone = fields.Boolean( - string='Perscribed zone deduction?', - help='Annual deduction for living in a prescribed zone, as shown on Form TD1' - ) - other_anual_deductions = fields.Boolean( - string='Other annual deductions?', - help='Annual deductions such as child care expenses and support payments, requested by an employee or pensioner and authorized by a tax services office or tax centre' - ) - paid_commission = fields.Boolean( - string='Paid a commission?', - help='Does the employee receive any commissions?', - ) - - def ca_payroll_config_value(self, name): - return self.ca_payroll_config_id[name] \ No newline at end of file + # fed_td1_1_basic_personal_amount = fields.Float() + # fed_td1_2_caregiver_amount = fields.Float() + # fed_td1_3_age_amount = fields.Float() + # fed_td1_4_pension_income_amount = fields.Float() + # fed_td1_5_tuition = fields.Float() + # fed_td1_6_disability_amount = fields.Float() + # fed_td1_7_spouse_amount = fields.Float() + # fed_td1_8_dependant_amount = fields.Float() + fed_td1_total_claim_amount = fields.Float() + fed_td1_deduction_prescribed_zone = fields.Float() + fed_td1_additional = fields.Float() + is_cpp_exempt = fields.Boolean() + is_ei_exempt = fields.Boolean() diff --git a/l10n_ca_hr_payroll/models/common.py b/l10n_ca_hr_payroll/models/common.py index 5a35556b..29982d35 100644 --- a/l10n_ca_hr_payroll/models/common.py +++ b/l10n_ca_hr_payroll/models/common.py @@ -1,25 +1,26 @@ - -def _compute_employee_contribution_deductions(payslip): - # todo: _compute_employee_contribution_deductions - return 0.0 - -def _compute_annual_taxable_income(payslip): - # A = Annual taxable income = [P × (I – F – F2 – U1 )] – HD – F1 - # # If the result is negative, T = L. - # annual_taxable_income = ( - # annual_pay_periods_p - # *( - # gross_remuneration_i - # - employee_contribution_deductions_f - # - required_deductions_f2 - # - union_dues_u1 - # ) - # - prescribed_zone_hd - # - employee_requested_deduction_f1 - # ) - pay_periods = payslip.dict.get_pay_periods_in_year() - annual_pay_periods_p = pay_periods[payslip.contract_id.schedule_pay] - gross_remuneration_i = annual_pay_periods_p * payslip.contract_id.wage - employee_contribution_deductions_f = _compute_employee_contribution_deductions(payslip) - required_deductions_f2 = _compute_employee_contribution_deductions(payslip) - pass \ No newline at end of file +# # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. +# +# def _compute_employee_contribution_deductions(payslip): +# # todo: _compute_employee_contribution_deductions +# return 0.0 +# +# def _compute_annual_taxable_income(payslip): +# # A = Annual taxable income = [P × (I – F – F2 – U1 )] – HD – F1 +# # # If the result is negative, T = L. +# # annual_taxable_income = ( +# # annual_pay_periods_p +# # *( +# # gross_remuneration_i +# # - employee_contribution_deductions_f +# # - required_deductions_f2 +# # - union_dues_u1 +# # ) +# # - prescribed_zone_hd +# # - employee_requested_deduction_f1 +# # ) +# pay_periods = payslip.dict.get_pay_periods_in_year() +# annual_pay_periods_p = pay_periods[payslip.contract_id.schedule_pay] +# gross_remuneration_i = annual_pay_periods_p * payslip.contract_id.wage +# employee_contribution_deductions_f = _compute_employee_contribution_deductions(payslip) +# required_deductions_f2 = _compute_employee_contribution_deductions(payslip) +# pass \ No newline at end of file diff --git a/l10n_ca_hr_payroll/models/federal/__init__.py b/l10n_ca_hr_payroll/models/federal/__init__.py index e69de29b..0358305d 100644 --- a/l10n_ca_hr_payroll/models/federal/__init__.py +++ b/l10n_ca_hr_payroll/models/federal/__init__.py @@ -0,0 +1 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. diff --git a/l10n_ca_hr_payroll/models/federal/ca_cpp.py b/l10n_ca_hr_payroll/models/federal/ca_cpp.py index 2205dedf..0870d892 100644 --- a/l10n_ca_hr_payroll/models/federal/ca_cpp.py +++ b/l10n_ca_hr_payroll/models/federal/ca_cpp.py @@ -1,113 +1,111 @@ -from odoo import fields -from datetime import datetime, timedelta -import logging - - -_logger = logging.getLogger("__name__") - -def ca_cpp_canada_pension_plan_withholding(payslip, categories): - #K2 = [(0.15 × ((0.0545 × ((S1 × PI) + B1 – $3,500)*, maximum $3,166.45)))) + (0.15 × ((0.0158 × ((S1 × IE) + B1), maximum $889.54))] - - payperiods_s1 = _compute_payperiod_ratio_s1(payslip) - pensionable_income_pi = _compute_pensionable_income_pi(payslip, categories) - #todo: remove - import pydevd_pycharm - pydevd_pycharm.settrace('192.168.1.27', port=6900, stdoutToServer=True, stderrToServer=True) - - return 0.0, 0.0 - -def _compute_payperiod_ratio_s1(payslip): - wage_type = payslip.wage_type - pay_periods = payslip.dict.PAY_PERIODS_IN_YEAR[wage_type] - if wage_type == 'annually': - return 1 - elif wage_type == 'semi_annually': - if payslip.date_to.month < 7: - return 1/pay_periods - else: - return 2/pay_periods - elif wage_type == 'quarterly': - quarters = { - 1:1, - 2:1, - 3:1, - 4:2, - 5:2, - 6:2, - 7:3, - 8:3, - 9:3, - 10:4, - 11:4, - 12:4, - } - quarter = quarters[payslip.date_to.month] - return quarter/pay_periods - elif wage_type == 'bi-monthly': - bi_monthly_int = { - 1:1, - 2:1, - 3:2, - 4:2, - 5:3, - 6:3, - 7:4, - 8:4, - 9:5, - 10:5, - 11:6, - 12:6, - } - bi_monthly = bi_monthly_int[payslip.date_to.month] - return bi_monthly/pay_periods - elif wage_type == 'monthly': - return payslip.date_to.month/pay_periods - elif wage_type == 'semi-monthly': - pay_period = payslip.date_to.month * 2 - if payslip.date_to.day <= 15: - return pay_period/pay_periods - else: - pay_period += 1 - return pay_period/pay_periods - elif wage_type == 'bi-weekly': - week_num = payslip.date_to.isocalendar()[1] - if week_num == 53: - return 1 - else: - return week_num/pay_periods - elif wage_type == 'weekly': - return payslip.date_to.isocalendar()[1]/pay_periods - elif wage_type == 'daily': - day_of_year = payslip.date_to.timetuple().tm_yday - return day_of_year/pay_periods - else: - raise Exception(f'Payslip does not have a valid wage_type. The wagetype presented is "{wage_type}".') - -def _compute_pensionable_income_of_slip(slip): - pensionable_income = 0.0 - for line in slip.line_ids: - if line.category_id.code == 'BASIC': - pensionable_income += line.amount - return pensionable_income - -def _compute_pensionable_income_year_to_date_piytd(payslip, categories): - employee_payslips = payslip.dict.env['hr.payslip'].search([ - ('employee_id', '=', payslip.dict.employee_id.id), - ('id', '!=', payslip.dict.id), - ]) - piytd = 0.0 - for slip in employee_payslips: - piytd += _compute_pensionable_income_of_slip(slip) - return piytd - -def _compute_pensionable_income_pi(payslip, categories): - """ - PI = Pensionable income for the pay period, or the gross income plus any taxable benefits for the pay period, plus PIYTD - """ - pensionable_income_year_to_date_piytd = _compute_pensionable_income_year_to_date_piytd(payslip, categories) - pensionable_income_for_current_payslip = _compute_pensionable_income_of_slip(payslip) - return pensionable_income_year_to_date_piytd + pensionable_income_for_current_payslip +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. +# from odoo import fields +# from datetime import datetime, timedelta +# import logging +# +# +# _logger = logging.getLogger("__name__") +# +# def ca_cpp_canada_pension_plan_withholding(payslip, categories): +# #K2 = [(0.15 × ((0.0545 × ((S1 × PI) + B1 – $3,500)*, maximum $3,166.45)))) + (0.15 × ((0.0158 × ((S1 × IE) + B1), maximum $889.54))] +# +# payperiods_s1 = _compute_payperiod_ratio_s1(payslip) +# pensionable_income_pi = _compute_pensionable_income_pi(payslip, categories) +# return 0.0, 0.0 +# +# def _compute_payperiod_ratio_s1(payslip): +# wage_type = payslip.wage_type +# pay_periods = payslip.dict.PAY_PERIODS_IN_YEAR[wage_type] +# if wage_type == 'annually': +# return 1 +# elif wage_type == 'semi_annually': +# if payslip.date_to.month < 7: +# return 1/pay_periods +# else: +# return 2/pay_periods +# elif wage_type == 'quarterly': +# quarters = { +# 1:1, +# 2:1, +# 3:1, +# 4:2, +# 5:2, +# 6:2, +# 7:3, +# 8:3, +# 9:3, +# 10:4, +# 11:4, +# 12:4, +# } +# quarter = quarters[payslip.date_to.month] +# return quarter/pay_periods +# elif wage_type == 'bi-monthly': +# bi_monthly_int = { +# 1:1, +# 2:1, +# 3:2, +# 4:2, +# 5:3, +# 6:3, +# 7:4, +# 8:4, +# 9:5, +# 10:5, +# 11:6, +# 12:6, +# } +# bi_monthly = bi_monthly_int[payslip.date_to.month] +# return bi_monthly/pay_periods +# elif wage_type == 'monthly': +# return payslip.date_to.month/pay_periods +# elif wage_type == 'semi-monthly': +# pay_period = payslip.date_to.month * 2 +# if payslip.date_to.day <= 15: +# return pay_period/pay_periods +# else: +# pay_period += 1 +# return pay_period/pay_periods +# elif wage_type == 'bi-weekly': +# week_num = payslip.date_to.isocalendar()[1] +# if week_num == 53: +# return 1 +# else: +# return week_num/pay_periods +# elif wage_type == 'weekly': +# return payslip.date_to.isocalendar()[1]/pay_periods +# elif wage_type == 'daily': +# day_of_year = payslip.date_to.timetuple().tm_yday +# return day_of_year/pay_periods +# else: +# raise Exception(f'Payslip does not have a valid wage_type. The wagetype presented is "{wage_type}".') +# +# def _compute_pensionable_income_of_slip(slip): +# pensionable_income = 0.0 +# for line in slip.line_ids: +# if line.category_id.code == 'BASIC': +# pensionable_income += line.amount +# return pensionable_income +# +# def _compute_pensionable_income_year_to_date_piytd(payslip, categories): +# employee_payslips = payslip.dict.env['hr.payslip'].search([ +# ('employee_id', '=', payslip.dict.employee_id.id), +# ('id', '!=', payslip.dict.id), +# ]) +# piytd = 0.0 +# for slip in employee_payslips: +# piytd += _compute_pensionable_income_of_slip(slip) +# return piytd +# +# def _compute_pensionable_income_pi(payslip, categories): +# """ +# PI = Pensionable income for the pay period, or the gross income plus any taxable benefits for the pay period, plus PIYTD +# """ +# pensionable_income_year_to_date_piytd = _compute_pensionable_income_year_to_date_piytd(payslip, categories) +# pensionable_income_for_current_payslip = _compute_pensionable_income_of_slip(payslip) +# return pensionable_income_year_to_date_piytd + pensionable_income_for_current_payslip +# diff --git a/l10n_ca_hr_payroll/models/federal/ca_ei.py b/l10n_ca_hr_payroll/models/federal/ca_ei.py index e69de29b..f8616a60 100644 --- a/l10n_ca_hr_payroll/models/federal/ca_ei.py +++ b/l10n_ca_hr_payroll/models/federal/ca_ei.py @@ -0,0 +1,2 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + diff --git a/l10n_ca_hr_payroll/models/federal/ca_fit.py b/l10n_ca_hr_payroll/models/federal/ca_fit.py index 69b70f5e..a1b06c2d 100644 --- a/l10n_ca_hr_payroll/models/federal/ca_fit.py +++ b/l10n_ca_hr_payroll/models/federal/ca_fit.py @@ -1,50 +1,66 @@ -import logging - -_logger = logging.getLogger("__name__") - -def ca_fit_federal_income_tax_withholding(payslip): - # annual_taxable_income = _compute_annual_taxable_income(payslip) +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. - # _logger.warning('payslip.contract_id************************************') - # _logger.warning(str(payslip.contract_id.read())) - # _logger.warning('payslip.contract_id.structure_type_id.read()************************************') - # _logger.warning(str(payslip.contract_id.structure_type_id.read())) - # _logger.warning('payslip.contract_id.structure_type_id.struct_ids[0].read()************************************') - # _logger.warning(str(payslip.contract_id.structure_type_id.struct_ids[0].read())) - # _logger.warning('payslip.contract_id.structure_type_id.struct_ids[0].rule_ids[0].read()************************************') - # _logger.warning(str(payslip.contract_id.structure_type_id.struct_ids[0].rule_ids[0].read())) - _logger.warning('payslip.rule_parameter(rule_parameter_ca_fed_tax_rate)************************************') - _logger.warning(str(payslip.rule_parameter)) - rates = payslip.rule_parameter('ca_fed_tax_rate')['annually'] #this is the hr.rule.parameter code - # _logger.warning(str(rates)) - wage = payslip.contract_id.wage - _logger.warning(f'wage = {str(wage)}') - i = 0 - _logger.warning(f'rates ================================== {str(rates)}') +def _compute_annual_taxable_income(payslip, categories): + """ + A = Annual taxable income + = [P × (I – F – F2 – U1 )] – HD – F1 + """ + P = payslip.dict.get_pay_periods_in_year() + I = categories.GROSS \ + - categories.ALW_FIT_EXEMPT \ + + categories.DED_FIT_EXEMPT + F = 0.0 # Payroll deductions for RPP or RRSP ... + F2 = 0.0 + U1 = 0.0 # Union Dues + HD = 0.0 # Annual deduction for living in a prescribed zone Form TD1 + F1 = 0.0 # Annual deductions such as child care and authorized + A = (P * (I - F - F2 - U1)) - HD - F1 + return A + + +def ca_fit_federal_income_tax_withholding(payslip, categories, worked_days, inputs): + L = payslip.contract_id.ca_payroll_config_value('fed_td1_additional') + A = _compute_annual_taxable_income(payslip, categories) + # If the result is negative, T = L. + if A <= 0.0 and L: + return -L, 1.0 + elif A <= 0.0: + return 0.0, 0.0 + + TC = payslip.contract_id.ca_payroll_config_value('fed_td1_total_claim_amount') + P = payslip.dict.get_pay_periods_in_year() + + """ + T3 = Annual basic federal tax + = (R × A) – K – K1 – K2 – K3 – K4 + If the result is negative, T3 = $0. + """ + rates = payslip.rule_parameter('ca_fed_tax_rate') for annual_taxable_income, rate, federal_constant in rates: - if isinstance(annual_taxable_income, str): - _logger.warning(f'annual_taxable_income is str {annual_taxable_income}') - _logger.warning(f'wage*rate = {str(wage*rate)}, and rate is {str(rate)}') - return wage, -rate - if annual_taxable_income/12 >= wage: - if i != 0: - _logger.warning(f'if i != 0') - rate = rates[i-1][1]*100 - _logger.warning(f'rate = **************************************** {rate}') - _logger.warning(f' wage*rate = *************************************** {wage*rate}') - return wage, -rate - else: - _logger.warning(f'return 0.0, 0.0') - return 0.0, 0.0 - else: - _logger.warning(f' annual_taxable_income/12 = {str(annual_taxable_income/12)} which is below wage = {str(str(wage))}*****************************') - i +=1 - continue + annual_taxable_income = float(annual_taxable_income) + if A < annual_taxable_income: + break + R, K = rate, federal_constant + K1 = 0.15 * TC + K2 = 0.0 + if not payslip.contract_id.ca_payroll_config_value('is_cpp_exempt'): + C = categories.EE_CA_CPP + K2 += 0.15 * min(P * C, 3166.45) # min because we can only have up to + if not payslip.contract_id.ca_payroll_config_value('is_ei_exempt'): + EI = categories.EE_CA_EI + K2 += 0.15 * min(P * EI, 889.54) + K3 = 0.0 # medical + CEA = 1257.0 # TODO this is an indexed parameter + K4 = min(0.15 * A, 0.15 * CEA) + T3 = (R * A) - K - K1 - K2 - K3 - K4 - - - return 0.0, 0.0 \ No newline at end of file + LCF = min(750.0, 0.15 * 0.0) # 0.0 => amount deducted or withheld during the year for the acquisition by the employee of approved shares of the capital stock of a prescribed labour-sponsored venture capital corporation + T1 = T3 - LCF + T = (T1 / P) + L + if T > 0.0: + return A, -(T / A * 100.0) + return 0.0, 0.0 diff --git a/l10n_ca_hr_payroll/models/hr_ca_contract.py b/l10n_ca_hr_payroll/models/hr_ca_contract.py deleted file mode 100644 index 76dcfd16..00000000 --- a/l10n_ca_hr_payroll/models/hr_ca_contract.py +++ /dev/null @@ -1,10 +0,0 @@ -from odoo import api, fields, models - - -class CAHRContract(models.Model): - _inherit = 'hr.contract' - - ca_payroll_config_id = fields.Many2one('hr.contract.ca_payroll_config', 'Canada Payroll Forms') - - - diff --git a/l10n_ca_hr_payroll/models/hr_contract.py b/l10n_ca_hr_payroll/models/hr_contract.py new file mode 100644 index 00000000..797c3b4b --- /dev/null +++ b/l10n_ca_hr_payroll/models/hr_contract.py @@ -0,0 +1,12 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + + +class HRContract(models.Model): + _inherit = 'hr.contract' + + ca_payroll_config_id = fields.Many2one('hr.contract.ca_payroll_config', 'Canada Payroll Forms') + + def ca_payroll_config_value(self, name): + return self.ca_payroll_config_id[name] diff --git a/l10n_ca_hr_payroll/tests/__init__.py b/l10n_ca_hr_payroll/tests/__init__.py index 9a13c238..e2386e0c 100644 --- a/l10n_ca_hr_payroll/tests/__init__.py +++ b/l10n_ca_hr_payroll/tests/__init__.py @@ -1,2 +1,5 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + from . import common -from . import test_ca_federal_payslip \ No newline at end of file +from . import test_ca_fed_2021_1 +# from . import test_ca_province_payslip diff --git a/l10n_ca_hr_payroll/tests/common.py b/l10n_ca_hr_payroll/tests/common.py index ecf49b9e..90756dc0 100644 --- a/l10n_ca_hr_payroll/tests/common.py +++ b/l10n_ca_hr_payroll/tests/common.py @@ -1,10 +1,12 @@ -from . import common +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. -import logging from odoo.addons.hr_payroll_hibou.tests import common -_logger = logging.getLogger("__name__") -#todo need to work in currency + +import logging +_logger = logging.getLogger(__name__) + +STANDARD_TOTAL_CLAIM_AMOUNT = 13808.0 class TestCAPayslip(common.TestPayslip): @@ -14,9 +16,6 @@ class TestCAPayslip(common.TestPayslip): self.structure_type = self.env.ref('l10n_ca_hr_payroll.ca_structure_type_employee') self.structure = self.env.ref('l10n_ca_hr_payroll.hr_ca_payroll_structure') self.structure_type.default_struct_id = self.structure - self._log('US structue_type %s and structure %s' % (self.structure_type, self.structure)) - _logger.warning(str(self.structure_type)) - def _createEmployee(self): return self.env['hr.employee'].create({ @@ -27,23 +26,64 @@ class TestCAPayslip(common.TestPayslip): 'name': 'Jared' }) - def _createCAContract(self, employee, wage=7000, pay_schedule='monthly'): - country_id = self.env['res.country'].search([('code', '=', 'CA')]) - self.assertEqual(employee.country_id, country_id, 'The employee\'s country_id is not for Canada') + def _createContract(self, employee, **kwargs): + # Override + if not 'schedule_pay' in kwargs: + kwargs['schedule_pay'] = 'monthly' + schedule_pay = kwargs['schedule_pay'] + config_model = self.env['hr.contract.ca_payroll_config'] + contract_model = self.env['hr.contract'] + config_values = { + 'name': 'Test Config Values', + 'employee_id': employee.id, + } + contract_values = { + 'name': 'Test Contract', + 'employee_id': employee.id, + } + if 'fed_td1_total_claim_amount' not in kwargs: + kwargs['fed_td1_total_claim_amount'] = STANDARD_TOTAL_CLAIM_AMOUNT + if 'state_id' not in kwargs: + kwargs['state_id'] = self.get_ca_state('AB') - contract = self._createContract(employee, - wage=wage, - structure_type_id=self.env.ref( - 'l10n_ca_hr_payroll.ca_structure_type_employee'), - pay_schedule=pay_schedule) - self.assertEqual(contract.wage, wage, - 'The contract salary of "%s" does not equal the test salary of "%s".' % ( - contract.wage, wage)) - _logger.warning('Created Contract &&&&&&&&&&&&&&&&&&&&&&&') + for key, val in kwargs.items(): + # Assume any Odoo object is in a Many2one + if hasattr(val, 'id'): + val = val.id + found = False + if hasattr(contract_model, key): + contract_values[key] = val + found = True + if hasattr(config_model, key): + config_values[key] = val + found = True + if not found: + self._logger.warn('cannot locate attribute names "%s" on contract or payroll config' % (key, )) + + # US Payroll Config Defaults Should be set on the Model + config = config_model.create(config_values) + contract_values['ca_payroll_config_id'] = config.id + self._get_contract_defaults(contract_values) + self._log('creating contract with finial values: %s' % (contract_values, )) + contract = contract_model.create(contract_values) + + # Compatibility with Odoo 13/14 + contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay return contract - def get_providence(self): - pass + def get_ca_state(self, code, cache={}): + country_key = 'CA_COUNTRY' + if code in cache: + return cache[code] + if country_key not in cache: + cache[country_key] = self.env.ref('base.ca') + ca_country = cache[country_key] + ca_state = self.env['res.country.state'].search([ + ('country_id', '=', ca_country.id), + ('code', '=', code), + ], limit=1) + cache[code] = ca_state + return ca_state diff --git a/l10n_ca_hr_payroll/tests/test_ca_fed_2021_1.py b/l10n_ca_hr_payroll/tests/test_ca_fed_2021_1.py new file mode 100644 index 00000000..43bec6ff --- /dev/null +++ b/l10n_ca_hr_payroll/tests/test_ca_fed_2021_1.py @@ -0,0 +1,81 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import fields +from .common import TestCAPayslip +import logging + +_logger = logging.getLogger("__name__") + + +class TestPayslip(TestCAPayslip): + + def test_basic_federal_tax_monthly(self): + salary = 7000.0 + date_from = '2021-01-01' + date_to = '2021-01-31' + + employee = self._createEmployee() + + # not this would make + # Monthly + # Alberta + # Federal Claim 13808 + # Provincial Claim 19369 + contract = self._createContract(employee, + wage=salary, + is_cpp_exempt=True, + if_ei_exempt=True, + ) + + self._log('2021 tax first payslip:') + + payslip = self._createPayslip(employee, date_from, date_to) + self.assertEqual(payslip.contract_id, contract) + self.assertEqual(payslip.struct_id, self.structure) + self.assertEqual(payslip.date_from, fields.Date.from_string(date_from)) + self.assertEqual(payslip.date_to, fields.Date.from_string(date_to)) + + cats = self._getCategories(payslip) + self.assertEqual(cats['GROSS'], 7000.0) + self.assertEqual(cats.get('EE_CA_CPP', 0.0), 0.0) + self.assertEqual(cats.get('EE_CA_EI', 0.0), 0.0) + self.assertPayrollAlmostEqual(cats['EE_CA_FIT'], -1022.02) # amount from apps.cra-arc.gc.ca + + def test_basic_federal_tax_monthly_2(self): + salary = 2000.0 + date_from = '2021-01-01' + date_to = '2021-01-31' + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + is_cpp_exempt=True, + if_ei_exempt=True, + ) + payslip = self._createPayslip(employee, date_from, date_to) + cats = self._getCategories(payslip) + self.assertEqual(cats['GROSS'], 2000.0) + self.assertEqual(cats.get('EE_CA_CPP', 0.0), 0.0) + self.assertEqual(cats.get('EE_CA_EI', 0.0), 0.0) + self.assertPayrollAlmostEqual(cats['EE_CA_FIT'], -111.69) + + def test_basic_federal_tax_weekly(self): + salary = 3000.0 + date_from = '2021-01-25' + date_to = '2021-01-31' + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + is_cpp_exempt=True, + if_ei_exempt=True, + schedule_pay='weekly' + ) + payslip = self._createPayslip(employee, date_from, date_to) + cats = self._getCategories(payslip) + self.assertEqual(cats['GROSS'], 3000.0) + self.assertEqual(cats.get('EE_CA_CPP', 0.0), 0.0) + self.assertEqual(cats.get('EE_CA_EI', 0.0), 0.0) + # TODO why is this one off by ~0.30? + self.assertPayrollAlmostEqual(cats['EE_CA_FIT'], -583.28) # FAKE + self.assertPayrollAlmostEqual(cats['EE_CA_FIT'], -583.56) diff --git a/l10n_ca_hr_payroll/tests/test_ca_federal_payslip.py b/l10n_ca_hr_payroll/tests/test_ca_federal_payslip.py deleted file mode 100644 index f14fa0b8..00000000 --- a/l10n_ca_hr_payroll/tests/test_ca_federal_payslip.py +++ /dev/null @@ -1,98 +0,0 @@ -from odoo import fields -from .common import TestCAPayslip -import logging - -_logger = logging.getLogger("__name__") - - -class TestPayslip(TestCAPayslip): - - def test_basic_federal_tax(self, - salary=7000.0, - date_from='2021-01-01', - date_to='2021-01-31', - state_code=None, - **extra_contract): - annual_pay_periods_p = 12 - employee = self._createEmployee() - contract = self._createCAContract(employee=employee) - - - self._log('2021 tax first payslip:') - payslip = self._createPayslip(employee, date_from, date_to) - # self.assertEqual(payslip.struct_type_id, ) - self.assertEqual(payslip.contract_id, contract, f'Payslip contract {str(payslip.contract_id)} does not equal {str(contract)}') - self.assertEqual(payslip.struct_id.name, 'Canada Employee Standard', - f'payroll structure {payslip.struct_id.name} is not correct') - self.assertEqual(payslip.date_from, fields.Date.from_string(date_from), - f'payslip date_from {payslip.date_from} is not correct ') - self.assertEqual(payslip.date_to, fields.Date.from_string(date_to), - f'payslip date_to {payslip.date_to} is not correct ') - self.assertEqual(payslip.employee_id.name, 'Jared', - f'payslip employee {payslip.employee_id.name} is not correct') - - # _logger.warning(str(payslip.read())) - # for line in payslip.line_ids: - # _logger.warning(f'payslip line read {str(line)}************************************') - # _logger.warning(line.read()) - - # if line.name == 'EE: CA Federal Income Tax': - # _logger.warning(f'payslip line read {str(line)}************************************') - # _logger.warning(line.read()) - # _logger.warning('payslip.contract_id************************************') - # _logger.warning(str(payslip.contract_id.read())) - # _logger.warning('payslip.contract_id.structure_type_id.read()************************************') - # _logger.warning(str(payslip.contract_id.structure_type_id.read())) - # _logger.warning('payslip.contract_id.structure_type_id.struct_ids[0].read()************************************') - # _logger.warning(str(payslip.contract_id.structure_type_id.struct_ids[0].read())) - # _logger.warning('payslip.contract_id.structure_type_id.struct_ids[0].rule_ids[0].read()************************************') - # _logger.warning(str(payslip.contract_id.structure_type_id.struct_ids[0].rule_ids[0].read())) - # _logger.warning('payslip.rule_parameter(rule_parameter_ca_fed_tax_rate)************************************') - - - - # import pydevd_pycharm - # pydevd_pycharm.settrace('192.168.1.27', port=6900, stdoutToServer=True, stderrToServer=True) - # self.assertPayrollAlmostEqual(payslip.net_wage, 5565) - - - - - # self.assertEqual(payslip.net_wage, 5565, 'total tax is off') - - # schedule_pay = payslip.contract_id.schedule_pay - # additional = payslip.contract_id.us_payroll_config_value('state_income_tax_additional_withholding') - # sit_allowances = payslip.contract_id.us_payroll_config_value('ca_de4_sit_allowances') - # additional_allowances = payslip.contract_id.us_payroll_config_value('ca_de4_sit_additional_allowances') - # low_income_exemption = payslip.rule_parameter('us_ca_sit_income_exemption_rate')[schedule_pay] - # estimated_deduction = payslip.rule_parameter('us_ca_sit_estimated_deduction_rate')[schedule_pay] - # tax_table = payslip.rule_parameter('us_ca_sit_tax_rate')[filing_status].get(schedule_pay) - # standard_deduction = payslip.rule_parameter('us_ca_sit_standard_deduction_rate')[schedule_pay] - # exemption_allowances = payslip.rule_parameter('us_ca_sit_exemption_allowance_rate')[schedule_pay] - - #Determine the taxable income for the pay period (pay minus allowable deductions) and multiply it by the number of pay periods in the year to get an estimated annual taxable income amount. This annual taxable income amount is factor A. - #assert(gross_remuneration_for_period_i = gross payslip_pay - non_gross_remuneration) - - # Calculate the basic federal tax on the estimated annual taxable income, after allowable federal non-refundable tax credits. The basic federal tax is factor T3. - # T3 = Annual basic federal tax - # = (R × A) – K – K1 – K2 – K3 – K4 - - # (R federal rate for income based on table - # X A Annual Income) - # - K2 Federal Canada Pension Plan contributions and employment insurance premiums tax credits for the year (the lowest federal tax rate is used to calculate this credit). - # Note: If an employee has already contributed the maximum CPP and EI, for the year with the employer, use the maximum CPP and EI deduction to determine the credit for the rest of the year. If, during the pay period in which the employee reaches the maximum, the CPP and EI, when annualized, is less than the annual maximum, use the maximum annual deduction(s) in that pay period - # - K3 Other federal non-refundable tax credits (such as medical expenses and charitable donations) authorized by a tax services office or tax centre - # - K4 Factor calculated using the Canada employment amount credit (the lowest federal tax rate is used to calculate this credit) - # - K Federal constant. The constant is the tax overcharged when applying the 20.5%, 26%, 29%, and 33% rates to the annual taxable income A - # - K1 Federal non-refundable personal tax credit (the lowest federal tax rate is used to calculate this credit) - - # If the result is negative, T3 = $0. - - # Calculate the annual federal tax payable. This is factor T1. - - - # Calculate the basic provincial or territorial tax on the estimated annual taxable income, after allowable provincial or territorial personal tax credits. The annual basic provincial or territorial tax is factor T4. - # Calculate the annual provincial or territorial tax deduction. This is factor T2. - # To get the estimated federal and provincial or territorial tax deductions for a pay period, add the federal and provincial or territorial tax, and divide the result by the number of pay periods. This is factor T. - - # test_federal_with \ No newline at end of file diff --git a/l10n_ca_hr_payroll/tests/test_ca_province_payslip.py b/l10n_ca_hr_payroll/tests/test_ca_province_payslip.py new file mode 100644 index 00000000..cd95ff13 --- /dev/null +++ b/l10n_ca_hr_payroll/tests/test_ca_province_payslip.py @@ -0,0 +1,53 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from .common import TestCAPayslip +import logging + +_logger = logging.getLogger("__name__") + + +class TestPayslip(TestCAPayslip): + + def test_tax_alberta(self): + salary = 7000.0 + date_from = '2021-01-01' + date_to = '2021-01-31' + + employee = self._createEmployee() + contract = self._createContract(employee, + wage=salary, + state_id=self.get_ca_state('AB')) + + payslip = self._createPayslip(employee, date_from, date_to) + # self.assertEqual(payslip.struct_type_id, ) + self.assertEqual(payslip.contract_id, contract) + self.assertEqual(payslip.struct_id, self.structure) + self.assertEqual(payslip.employee_id.name, 'Jared') + + cats = self._getCategories(payslip) + self.assertEqual(cats['GROSS'], 7000.0) + self.assertEqual(cats['EE_CA_FIT'], -1022.02) # amount from apps.cra-arc.gc.ca + self.assertEqual(cats['EE_CA_PIT'], -538.59) # amount from apps.cra-arc.gc.ca + + # def test_tax_quebec(self): + # salary = 7000.0 + # date_from = '2021-01-01' + # date_to = '2021-01-31' + # + # employee = self._createEmployee() + # contract = self._createContract(employee, + # wage=salary, + # state_id=self.get_ca_state('QC')) + # + # + # self._log('2021 tax first payslip:') + # payslip = self._createPayslip(employee, date_from, date_to) + # # self.assertEqual(payslip.struct_type_id, ) + # self.assertEqual(payslip.contract_id, contract) + # self.assertEqual(payslip.struct_id, self.structure) + # self.assertEqual(payslip.employee_id.name, 'Jared') + # + # cats = self._getCategories(payslip) + # self.assertEqual(cats['GROSS'], 7000.0) + # self.assertEqual(cats['EE_CA_FIT'], -849.08) # amount from apps.cra-arc.gc.ca + # self.assertEqual(cats['EE_CA_PIT'], -538.59)