From 33a9b41b9d805a7b0e11deef63bbe55934eaa2cf Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 23 Dec 2021 07:45:42 -0800 Subject: [PATCH] [MOV] l10n_ca_hr_payroll: copy from WIP 14 --- l10n_ca_hr_payroll/__init__.py | 12 ++ l10n_ca_hr_payroll/__manifest__.py | 30 +++ l10n_ca_hr_payroll/data/base.xml | 168 +++++++++++++++ l10n_ca_hr_payroll/data/ca_al_alberta.xml | 49 +++++ l10n_ca_hr_payroll/data/ca_cpp.xml | 49 +++++ l10n_ca_hr_payroll/data/federal.xml | 91 ++++++++ l10n_ca_hr_payroll/models/__init__.py | 6 + .../models/ca_payroll_config.py | 81 ++++++++ l10n_ca_hr_payroll/models/common.py | 26 +++ l10n_ca_hr_payroll/models/federal/__init__.py | 1 + l10n_ca_hr_payroll/models/federal/ca_cpp.py | 118 +++++++++++ l10n_ca_hr_payroll/models/federal/ca_ei.py | 11 + l10n_ca_hr_payroll/models/federal/ca_fit.py | 70 +++++++ l10n_ca_hr_payroll/models/hr_contract.py | 12 ++ l10n_ca_hr_payroll/models/hr_payslip.py | 36 ++++ l10n_ca_hr_payroll/models/quebec/__init__.py | 0 .../models/territory/__init__.py | 0 .../models/territory/ca_al_alberta.py | 0 .../territory/ca_bc_british_columbia.py | 0 .../models/territory/ca_mb_manitoba.py | 0 .../ca_nl_newfoundand_and_labrador.py | 0 .../models/territory/ca_ns_nova_scotia.py | 0 .../territory/ca_nt_northwest_territories.py | 0 .../models/territory/ca_nu_nunavut.py | 0 .../models/territory/ca_on_ontario.py | 0 .../territory/ca_pe_prince_edward_island.py | 0 .../models/territory/ca_sk_saskatchewan.py | 0 .../models/territory/ca_yt_yukon.py | 0 .../security/ir.model.access.csv | 2 + l10n_ca_hr_payroll/tests/__init__.py | 9 + l10n_ca_hr_payroll/tests/common.py | 195 ++++++++++++++++++ 31 files changed, 966 insertions(+) create mode 100644 l10n_ca_hr_payroll/__init__.py create mode 100644 l10n_ca_hr_payroll/__manifest__.py create mode 100644 l10n_ca_hr_payroll/data/base.xml create mode 100644 l10n_ca_hr_payroll/data/ca_al_alberta.xml create mode 100644 l10n_ca_hr_payroll/data/ca_cpp.xml create mode 100644 l10n_ca_hr_payroll/data/federal.xml create mode 100644 l10n_ca_hr_payroll/models/__init__.py create mode 100644 l10n_ca_hr_payroll/models/ca_payroll_config.py create mode 100644 l10n_ca_hr_payroll/models/common.py create mode 100644 l10n_ca_hr_payroll/models/federal/__init__.py create mode 100644 l10n_ca_hr_payroll/models/federal/ca_cpp.py create mode 100644 l10n_ca_hr_payroll/models/federal/ca_ei.py create mode 100644 l10n_ca_hr_payroll/models/federal/ca_fit.py create mode 100644 l10n_ca_hr_payroll/models/hr_contract.py create mode 100644 l10n_ca_hr_payroll/models/hr_payslip.py create mode 100644 l10n_ca_hr_payroll/models/quebec/__init__.py create mode 100644 l10n_ca_hr_payroll/models/territory/__init__.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_al_alberta.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_bc_british_columbia.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_mb_manitoba.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_nl_newfoundand_and_labrador.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_ns_nova_scotia.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_nt_northwest_territories.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_nu_nunavut.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_on_ontario.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_pe_prince_edward_island.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_sk_saskatchewan.py create mode 100644 l10n_ca_hr_payroll/models/territory/ca_yt_yukon.py create mode 100644 l10n_ca_hr_payroll/security/ir.model.access.csv create mode 100644 l10n_ca_hr_payroll/tests/__init__.py create mode 100644 l10n_ca_hr_payroll/tests/common.py diff --git a/l10n_ca_hr_payroll/__init__.py b/l10n_ca_hr_payroll/__init__.py new file mode 100644 index 00000000..013f4e73 --- /dev/null +++ b/l10n_ca_hr_payroll/__init__.py @@ -0,0 +1,12 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models + +def _post_install_hook(cr, registry): + """ + This method will set the default for the Payslip Sum Behavior + """ + cr.execute("SELECT id FROM ir_config_parameter WHERE key = 'hr_payroll.payslip.sum_behavior';") + existing = cr.fetchall() + if not existing: + cr.execute("INSERT INTO ir_config_parameter (key, value) VALUES ('hr_payroll.payslip.sum_behavior', 'date');") diff --git a/l10n_ca_hr_payroll/__manifest__.py b/l10n_ca_hr_payroll/__manifest__.py new file mode 100644 index 00000000..6de3594b --- /dev/null +++ b/l10n_ca_hr_payroll/__manifest__.py @@ -0,0 +1,30 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +{ + 'name': 'Canada - Payroll', + 'author': 'Hibou Corp. ', + 'version': '15.0.2022.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', + # 'views/hr_contract_views.xml', + # 'views/ca_payroll_config_views.xml', + ], + 'demo': [ + ], + 'auto_install': False, + 'post_init_hook': '_post_install_hook', + 'license': 'OPL-1', +} diff --git a/l10n_ca_hr_payroll/data/base.xml b/l10n_ca_hr_payroll/data/base.xml new file mode 100644 index 00000000..e2af112a --- /dev/null +++ b/l10n_ca_hr_payroll/data/base.xml @@ -0,0 +1,168 @@ + + + + + Canada Employee + + + + + + Canada Employee Standard + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/l10n_ca_hr_payroll/data/ca_al_alberta.xml b/l10n_ca_hr_payroll/data/ca_al_alberta.xml new file mode 100644 index 00000000..5476f60c --- /dev/null +++ b/l10n_ca_hr_payroll/data/ca_al_alberta.xml @@ -0,0 +1,49 @@ + + + + + CA Federal Tax Rate + ca_fed_tax_rate + + + + + + { + '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 - Federal Income Tax + + + + + + + + EE: CA Federal Income Tax + EE_CA_FIT + python + result, _ = ca_fit_federal_income_tax_withholding(payslip) + code + result, result_rate = ca_fit_federal_income_tax_withholding(payslip) + + + + + + + + \ No newline at end of file diff --git a/l10n_ca_hr_payroll/data/ca_cpp.xml b/l10n_ca_hr_payroll/data/ca_cpp.xml new file mode 100644 index 00000000..b29cb513 --- /dev/null +++ b/l10n_ca_hr_payroll/data/ca_cpp.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/l10n_ca_hr_payroll/data/federal.xml b/l10n_ca_hr_payroll/data/federal.xml new file mode 100644 index 00000000..08cee3b0 --- /dev/null +++ b/l10n_ca_hr_payroll/data/federal.xml @@ -0,0 +1,91 @@ + + + + + CA Federal Tax Rate + ca_fed_tax_rate + + + + + + [ + ( 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), + ] + + + + + + + CA Federal - Canada Revenue Agency - Federal Income Tax + + + + + EE: Canada Pension Plan + EE_CA_CPP + + + + + EE: CA Employment Insurance + EE_CA_EI + + + + + EE: Federal Income Tax Withholding + EE_CA_FIT + + + + + + + + + EE: Canada Pension Plan + EE_CA_CPP + python + result, _ = ca_cpp(payslip, categories, worked_days, inputs) + code + result, result_rate = ca_cpp(payslip, categories, worked_days, inputs) + + + + + + + + + EE: CA Employment Insurance + EE_CA_EI + python + result, _ = ca_ei(payslip, categories, worked_days, inputs) + code + result, result_rate = ca_ei(payslip, categories, worked_days, inputs) + + + + + + + + + EE: CA Federal Income Tax + EE_CA_FIT + python + result, _ = ca_fit_federal_income_tax_withholding(payslip, categories, worked_days, inputs) + code + result, result_rate = ca_fit_federal_income_tax_withholding(payslip, categories, worked_days, inputs) + + + + + \ No newline at end of file diff --git a/l10n_ca_hr_payroll/models/__init__.py b/l10n_ca_hr_payroll/models/__init__.py new file mode 100644 index 00000000..61ea9a9f --- /dev/null +++ b/l10n_ca_hr_payroll/models/__init__.py @@ -0,0 +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_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 new file mode 100644 index 00000000..e9b43db5 --- /dev/null +++ b/l10n_ca_hr_payroll/models/ca_payroll_config.py @@ -0,0 +1,81 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + + +class HRContractCanadaPayrollConfig(models.Model): + _name = 'hr.contract.ca_payroll_config' + _description = 'Contract Canada Payroll Forms' + + # Quebec https://www.revenuquebec.ca/en/online-services/tools/webras-and-winras-calculation-of-source-deductions-and-employer-contributions/ + # https://www.canada.ca/en/revenue-agency/services/forms-publications/payroll/t4127-payroll-deductions-formulas/t4127-jan.html + + name = fields.Char(string="Description") + 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?', + # ) + + # fed_fit_exempt = fields.Boolean() + # td1_fit_additional = fields.Float() + # td1_fit_tc = fields.Float() + + # 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 new file mode 100644 index 00000000..29982d35 --- /dev/null +++ b/l10n_ca_hr_payroll/models/common.py @@ -0,0 +1,26 @@ +# # 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 new file mode 100644 index 00000000..0358305d --- /dev/null +++ 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 new file mode 100644 index 00000000..53d88fb6 --- /dev/null +++ b/l10n_ca_hr_payroll/models/federal/ca_cpp.py @@ -0,0 +1,118 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + + +def ca_cpp(payslip, categories, worked_days, inputs): + if payslip.contract_id.ca_payroll_config_value('is_cpp_exempt'): + return 0.0, 0.0 + return 0.0, 0.0 + +# 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 new file mode 100644 index 00000000..4eee4e3e --- /dev/null +++ b/l10n_ca_hr_payroll/models/federal/ca_ei.py @@ -0,0 +1,11 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + + +def ca_ei(payslip, categories, worked_days, inputs): + if payslip.contract_id.ca_payroll_config_value('is_ei_exempt'): + return 0.0, 0.0 + IE = categories.GROSS # TODO how to adjust + year = payslip.dict.get_year() + D1 = -payslip.sum_category('EE_CA_EI', str(year) + '-01-01', str(year + 1) + '-01-01') + EI = round(min(889.54 - D1, 0.0158 * IE), 2) + return -EI, 100.0 diff --git a/l10n_ca_hr_payroll/models/federal/ca_fit.py b/l10n_ca_hr_payroll/models/federal/ca_fit.py new file mode 100644 index 00000000..7034c65b --- /dev/null +++ b/l10n_ca_hr_payroll/models/federal/ca_fit.py @@ -0,0 +1,70 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + + +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, 100.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: + 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 + + 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 + if T1 < 0.0: + T1 = 0.0 + + T = (T1 / P) + L + if T > 0.0: + T = round(T, 2) + return A, -(T / A * 100.0) + return 0.0, 0.0 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/models/hr_payslip.py b/l10n_ca_hr_payroll/models/hr_payslip.py new file mode 100644 index 00000000..f7b1eae0 --- /dev/null +++ b/l10n_ca_hr_payroll/models/hr_payslip.py @@ -0,0 +1,36 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo import api, fields, models + +from .federal.ca_fit import ca_fit_federal_income_tax_withholding +from .federal.ca_cpp import ca_cpp +from .federal.ca_ei import ca_ei + + +class HRPayslip(models.Model): + _inherit = 'hr.payslip' + + # From IRS Publication 15-T or logically (annually, bi-monthly) + PAY_PERIODS_IN_YEAR = { + 'annually': 1, + 'semi-annually': 2, + 'quarterly': 4, + 'bi-monthly': 6, + 'monthly': 12, + 'semi-monthly': 24, + 'bi-weekly': 26, + 'weekly': 52, + 'daily': 260, + } + + def _get_base_local_dict(self): + res = super()._get_base_local_dict() + res.update({ + 'ca_fit_federal_income_tax_withholding': ca_fit_federal_income_tax_withholding, + 'ca_cpp': ca_cpp, + 'ca_ei': ca_ei, + }) + return res + + def get_pay_periods_in_year(self): + return self.PAY_PERIODS_IN_YEAR.get(self.contract_id.schedule_pay, 0) diff --git a/l10n_ca_hr_payroll/models/quebec/__init__.py b/l10n_ca_hr_payroll/models/quebec/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/__init__.py b/l10n_ca_hr_payroll/models/territory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_al_alberta.py b/l10n_ca_hr_payroll/models/territory/ca_al_alberta.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_bc_british_columbia.py b/l10n_ca_hr_payroll/models/territory/ca_bc_british_columbia.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_mb_manitoba.py b/l10n_ca_hr_payroll/models/territory/ca_mb_manitoba.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_nl_newfoundand_and_labrador.py b/l10n_ca_hr_payroll/models/territory/ca_nl_newfoundand_and_labrador.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_ns_nova_scotia.py b/l10n_ca_hr_payroll/models/territory/ca_ns_nova_scotia.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_nt_northwest_territories.py b/l10n_ca_hr_payroll/models/territory/ca_nt_northwest_territories.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_nu_nunavut.py b/l10n_ca_hr_payroll/models/territory/ca_nu_nunavut.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_on_ontario.py b/l10n_ca_hr_payroll/models/territory/ca_on_ontario.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_pe_prince_edward_island.py b/l10n_ca_hr_payroll/models/territory/ca_pe_prince_edward_island.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_sk_saskatchewan.py b/l10n_ca_hr_payroll/models/territory/ca_sk_saskatchewan.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/models/territory/ca_yt_yukon.py b/l10n_ca_hr_payroll/models/territory/ca_yt_yukon.py new file mode 100644 index 00000000..e69de29b diff --git a/l10n_ca_hr_payroll/security/ir.model.access.csv b/l10n_ca_hr_payroll/security/ir.model.access.csv new file mode 100644 index 00000000..80e30de5 --- /dev/null +++ b/l10n_ca_hr_payroll/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_contract_ca_payroll_config,access_hr_contract_ca_payroll_config,model_hr_contract_ca_payroll_config,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/l10n_ca_hr_payroll/tests/__init__.py b/l10n_ca_hr_payroll/tests/__init__.py new file mode 100644 index 00000000..a1d77f73 --- /dev/null +++ b/l10n_ca_hr_payroll/tests/__init__.py @@ -0,0 +1,9 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import common + +# Tests moved to `l10n_ca_hr_payroll_params` +# common remains for site specific tests + +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 new file mode 100644 index 00000000..90756dc0 --- /dev/null +++ b/l10n_ca_hr_payroll/tests/common.py @@ -0,0 +1,195 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from odoo.addons.hr_payroll_hibou.tests import common + + +import logging +_logger = logging.getLogger(__name__) + +STANDARD_TOTAL_CLAIM_AMOUNT = 13808.0 + + +class TestCAPayslip(common.TestPayslip): + + def setUp(self): + super(TestCAPayslip, self).setUp() + 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 + + def _createEmployee(self): + return self.env['hr.employee'].create({ + 'birthday': '1985-03-14', + 'country_id': self.ref('base.ca'), + 'department_id': self.ref('hr.dep_rd'), + 'gender': 'male', + 'name': 'Jared' + }) + + 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') + + 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_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 + + + + # 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 + + + + + + + # _logger.warning(str(payslip.read())) + + # start asserting + + + + + # # to work in shell + # employee = env['hr.employee'].create({ + # 'birthday': '1985-03-14', + # 'country_id': env['res.country'].search([('code', '=', 'CA')]).id, + # 'department_id': env.ref('hr.dep_rd').id, + # 'gender': 'male', + # 'name': 'Jared' + # }) + # schedule_pay = 'monthly' + # salary = 80000.0 + # contract_values = { + # 'wage': salary, + # 'name': 'Test Contract', + # 'employee_id': employee.id, + # 'structure_type_id': env.ref("l10n_ca_hr_payroll.ca_structure_type_employee"), + # } + # contract = env['hr.contract'].create(contract_values) + # contract.structure_type_id.default_struct_id.schedule_pay = schedule_pay + # date_from = '2021-01-01' + # date_to = '2021-01-31' + # slip = env['hr.payslip'].create({ + # 'name': 'Test %s From: %s To: %s' % (employee.name, date_from, date_to), + # 'employee_id': employee.id, + # 'date_from': date_from, + # 'date_to': date_to + # }) + + + # 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.us_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, + # } + # + # # Backwards compatability with 'futa_type' + # if 'futa_type' in kwargs: + # kwargs['fed_940_type'] = kwargs['futa_type'] + # + # 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['us_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 + + + + + + + +