This commit is contained in:
gsinghpal
2026-04-07 20:49:21 -04:00
parent 3cc93b8783
commit 4fde4c7bd1
25 changed files with 1253 additions and 900 deletions

View File

@@ -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

View File

@@ -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',
}

View File

@@ -2,133 +2,55 @@
<odoo>
<data noupdate="1">
<!-- ============================================================ -->
<!-- CANADA SALARY STRUCTURE -->
<!-- ============================================================ -->
<!-- Salary Structure Type (if needed) -->
<!-- Structure Type -->
<record id="structure_type_canada" model="hr.payroll.structure.type">
<field name="name">Canada</field>
<field name="name">Canadian Employee</field>
<field name="country_id" ref="base.ca"/>
</record>
<!-- Canada Salary Structure -->
<!-- Salary Structure -->
<record id="hr_payroll_structure_canada" model="hr.payroll.structure">
<field name="name">Canada salary structure</field>
<field name="name">Canadian Employee Salary</field>
<field name="code">Canada</field>
<field name="type_id" ref="structure_type_canada"/>
<field name="country_id" ref="base.ca"/>
</record>
<!-- ============================================================ -->
<!-- SALARY RULE CATEGORY - CANADA (Deduction) -->
<!-- This category links to CPP, EI, Federal and Provincial Tax -->
<!-- ============================================================ -->
<record id="hr_payroll_category_canada" model="hr.salary.rule.category">
<field name="name">Deduction</field>
<field name="code">CANADA</field>
<field name="parent_id" ref="hr_payroll.DED"/>
<!-- These links are set via the UI after tax rates are created -->
<!-- cpp_deduction_id, ei_deduction_id, fed_tax_id, provincial_tax_id -->
<!-- Salary Rule Categories -->
<record id="hr_salary_rule_category_ca_cpp" model="hr.salary.rule.category">
<field name="name">CPP</field>
<field name="code">CPP</field>
</record>
<!-- ============================================================ -->
<!-- LINK SALARY RULES TO CANADA STRUCTURE -->
<!-- ============================================================ -->
<!-- Basic Salary -->
<record id="hr_rule_basic" model="hr.salary.rule">
<field name="name">Basic Salary</field>
<field name="code">BASIC</field>
<field name="sequence">1</field>
<field name="category_id" ref="hr_payroll.BASIC"/>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = payslip.paid_amount</field>
<record id="hr_salary_rule_category_ca_ei" model="hr.salary.rule.category">
<field name="name">EI</field>
<field name="code">EI</field>
</record>
<!-- House Rent Allowance -->
<record id="hr_rule_hra" model="hr.salary.rule">
<field name="name">House Rent Allowance</field>
<field name="code">HRA</field>
<field name="sequence">5</field>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="condition_select">none</field>
<field name="amount_select">fix</field>
<field name="amount_fix">0.0</field>
<record id="hr_salary_rule_category_ca_fed_tax" model="hr.salary.rule.category">
<field name="name">Federal Tax</field>
<field name="code">FED_TAX</field>
</record>
<!-- Dearness Allowance -->
<record id="hr_rule_da" model="hr.salary.rule">
<field name="name">Dearness Allowance</field>
<field name="code">DA</field>
<field name="sequence">6</field>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="condition_select">none</field>
<field name="amount_select">fix</field>
<field name="amount_fix">0.0</field>
<record id="hr_salary_rule_category_ca_prov_tax" model="hr.salary.rule.category">
<field name="name">Provincial Tax</field>
<field name="code">PROV_TAX</field>
</record>
<!-- Travel Allowance -->
<record id="hr_rule_travel" model="hr.salary.rule">
<field name="name">Travel Allowance</field>
<field name="code">Travel</field>
<field name="sequence">7</field>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="condition_select">none</field>
<field name="amount_select">fix</field>
<field name="amount_fix">0.0</field>
<record id="hr_salary_rule_category_ca_ohp" model="hr.salary.rule.category">
<field name="name">Ontario Health Premium</field>
<field name="code">OHP</field>
</record>
<!-- Meal Allowance -->
<record id="hr_rule_meal" model="hr.salary.rule">
<field name="name">Meal Allowance</field>
<field name="code">Meal</field>
<field name="sequence">8</field>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="condition_select">none</field>
<field name="amount_select">fix</field>
<field name="amount_fix">0.0</field>
<record id="hr_salary_rule_category_ca_employer" model="hr.salary.rule.category">
<field name="name">Employer Contributions</field>
<field name="code">EMPLOYER</field>
</record>
<!-- Medical Allowance -->
<record id="hr_rule_medical" model="hr.salary.rule">
<field name="name">Medical Allowance</field>
<field name="code">Medical</field>
<field name="sequence">9</field>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="condition_select">none</field>
<field name="amount_select">fix</field>
<field name="amount_fix">0.0</field>
</record>
<!-- Gross Salary -->
<record id="hr_rule_gross" model="hr.salary.rule">
<field name="name">Gross</field>
<field name="code">GROSS</field>
<field name="sequence">100</field>
<field name="category_id" ref="hr_payroll.GROSS"/>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = categories.BASIC + categories.ALW</field>
</record>
<!-- Net Salary -->
<record id="hr_rule_net" model="hr.salary.rule">
<field name="name">Net Salary</field>
<field name="code">NET</field>
<field name="sequence">200</field>
<field name="category_id" ref="hr_payroll.NET"/>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="amount_python_compute">result = categories.GROSS + categories.DED</field>
<record id="hr_salary_rule_category_ca_earnings" model="hr.salary.rule.category">
<field name="name">Earnings</field>
<field name="code">EARN</field>
<field name="country_id" ref="base.ca"/>
</record>
</data>

View File

@@ -2,37 +2,28 @@
<odoo>
<data noupdate="1">
<!-- ============================================================ -->
<!-- CANADIAN PAYSLIP INPUT TYPES -->
<!-- These are used to pass additional pay inputs to salary rules -->
<!-- ============================================================ -->
<!-- Overtime (dollar amount) -->
<record id="input_type_ot" model="hr.payslip.input.type">
<field name="name">Overtime</field>
<field name="code">OT</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
<!-- Overtime Hours -->
<record id="input_type_ot_hours" model="hr.payslip.input.type">
<field name="name">Overtime Hours</field>
<field name="code">OT_HOURS</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
<!-- Stat Holiday Hours -->
<record id="input_type_stat_hours" model="hr.payslip.input.type">
<field name="name">Stat Holiday Hours</field>
<field name="code">STAT_HOURS</field>
<field name="country_id" ref="base.ca"/>
</record>
<!-- Bonus Amount -->
<!-- Bonus -->
<record id="input_type_bonus" model="hr.payslip.input.type">
<field name="name">Bonus</field>
<field name="code">BONUS</field>
<field name="country_id" ref="base.ca"/>
</record>
<!-- Vacation Payout -->
<record id="input_type_vacation_payout" model="hr.payslip.input.type">
<field name="name">Vacation Payout</field>
<field name="code">VACATION_PAYOUT</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
<!-- Commission -->
@@ -40,6 +31,39 @@
<field name="name">Commission</field>
<field name="code">COMMISSION</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
<!-- RRSP Deduction -->
<record id="input_type_rrsp" model="hr.payslip.input.type">
<field name="name">RRSP Deduction</field>
<field name="code">RRSP</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
<!-- Union Dues -->
<record id="input_type_union" model="hr.payslip.input.type">
<field name="name">Union Dues</field>
<field name="code">UNION</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
<!-- Vacation Pay -->
<record id="input_type_vac_pay" model="hr.payslip.input.type">
<field name="name">Vacation Pay</field>
<field name="code">VAC_PAY</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
<!-- Stat Holiday Hours -->
<record id="input_type_stat_hours" model="hr.payslip.input.type">
<field name="name">Stat Holiday Hours</field>
<field name="code">STAT_HOURS</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
<!-- Retroactive Pay -->
@@ -47,6 +71,7 @@
<field name="name">Retroactive Pay</field>
<field name="code">RETRO_PAY</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
<!-- Shift Premium -->
@@ -54,6 +79,7 @@
<field name="name">Shift Premium</field>
<field name="code">SHIFT_PREMIUM</field>
<field name="country_id" ref="base.ca"/>
<field name="struct_ids" eval="[(4, ref('hr_payroll_structure_canada'))]"/>
</record>
</data>

View File

@@ -3,7 +3,7 @@
<data noupdate="1">
<!-- ============================================================ -->
<!-- CANADA PENSION PLAN (CPP) PARAMETERS - 2025 -->
<!-- CANADA PENSION PLAN (CPP) PARAMETERS - 2026 -->
<!-- ============================================================ -->
<record id="rule_parameter_ca_cpp_rate" model="hr.rule.parameter">
@@ -12,9 +12,9 @@
<field name="country_id" ref="base.ca"/>
<field name="description">CPP employee/employer contribution rate</field>
</record>
<record id="rule_parameter_ca_cpp_rate_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_cpp_rate_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_cpp_rate"/>
<field name="date_from">2025-01-01</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">0.0595</field>
</record>
@@ -24,10 +24,10 @@
<field name="country_id" ref="base.ca"/>
<field name="description">CPP basic exemption amount per year</field>
</record>
<record id="rule_parameter_ca_cpp_exemption_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_cpp_exemption_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_cpp_exemption"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">3500.00</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">3500</field>
</record>
<record id="rule_parameter_ca_cpp_max" model="hr.rule.parameter">
@@ -36,14 +36,38 @@
<field name="country_id" ref="base.ca"/>
<field name="description">Maximum CPP employee contribution per year</field>
</record>
<record id="rule_parameter_ca_cpp_max_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_cpp_max_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_cpp_max"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">4034.10</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">4230.45</field>
</record>
<record id="rule_parameter_ca_ympe" model="hr.rule.parameter">
<field name="name">Canada - YMPE (CPP Ceiling 1)</field>
<field name="code">ca_ympe</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Year's Maximum Pensionable Earnings - CPP first ceiling</field>
</record>
<record id="rule_parameter_ca_ympe_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_ympe"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">74600</field>
</record>
<record id="rule_parameter_ca_yampe" model="hr.rule.parameter">
<field name="name">Canada - YAMPE (CPP Ceiling 2)</field>
<field name="code">ca_yampe</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Year's Additional Maximum Pensionable Earnings - CPP second ceiling</field>
</record>
<record id="rule_parameter_ca_yampe_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_yampe"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">85000</field>
</record>
<!-- ============================================================ -->
<!-- SECOND CANADA PENSION PLAN (CPP2) PARAMETERS - 2025 -->
<!-- SECOND CANADA PENSION PLAN (CPP2) PARAMETERS - 2026 -->
<!-- ============================================================ -->
<record id="rule_parameter_ca_cpp2_rate" model="hr.rule.parameter">
@@ -52,9 +76,9 @@
<field name="country_id" ref="base.ca"/>
<field name="description">CPP2 contribution rate (on earnings above YMPE)</field>
</record>
<record id="rule_parameter_ca_cpp2_rate_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_cpp2_rate_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_cpp2_rate"/>
<field name="date_from">2025-01-01</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">0.04</field>
</record>
@@ -64,38 +88,14 @@
<field name="country_id" ref="base.ca"/>
<field name="description">Maximum CPP2 employee contribution per year</field>
</record>
<record id="rule_parameter_ca_cpp2_max_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_cpp2_max_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_cpp2_max"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">396.00</field>
</record>
<record id="rule_parameter_ca_ympe" model="hr.rule.parameter">
<field name="name">Canada - YMPE (CPP Ceiling 1)</field>
<field name="code">ca_ympe</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Year's Maximum Pensionable Earnings - CPP first ceiling</field>
</record>
<record id="rule_parameter_ca_ympe_2025" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_ympe"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">71300.00</field>
</record>
<record id="rule_parameter_ca_yampe" model="hr.rule.parameter">
<field name="name">Canada - YAMPE (CPP Ceiling 2)</field>
<field name="code">ca_yampe</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Year's Additional Maximum Pensionable Earnings - CPP second ceiling</field>
</record>
<record id="rule_parameter_ca_yampe_2025" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_yampe"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">81200.00</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">416.00</field>
</record>
<!-- ============================================================ -->
<!-- EMPLOYMENT INSURANCE (EI) PARAMETERS - 2025 -->
<!-- EMPLOYMENT INSURANCE (EI) PARAMETERS - 2026 -->
<!-- ============================================================ -->
<record id="rule_parameter_ca_ei_rate" model="hr.rule.parameter">
@@ -104,10 +104,22 @@
<field name="country_id" ref="base.ca"/>
<field name="description">EI employee contribution rate</field>
</record>
<record id="rule_parameter_ca_ei_rate_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_ei_rate_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_ei_rate"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">0.0164</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">0.0163</field>
</record>
<record id="rule_parameter_ca_ei_max_insurable" model="hr.rule.parameter">
<field name="name">Canada - EI Maximum Insurable Earnings</field>
<field name="code">ca_ei_max_insurable</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Maximum annual insurable earnings for EI</field>
</record>
<record id="rule_parameter_ca_ei_max_insurable_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_ei_max_insurable"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">68900</field>
</record>
<record id="rule_parameter_ca_ei_max" model="hr.rule.parameter">
@@ -116,10 +128,10 @@
<field name="country_id" ref="base.ca"/>
<field name="description">Maximum EI employee premium per year</field>
</record>
<record id="rule_parameter_ca_ei_max_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_ei_max_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_ei_max"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">1077.48</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">1123.07</field>
</record>
<record id="rule_parameter_ca_ei_employer_mult" model="hr.rule.parameter">
@@ -128,26 +140,150 @@
<field name="country_id" ref="base.ca"/>
<field name="description">EI employer contribution multiplier (1.4x employee)</field>
</record>
<record id="rule_parameter_ca_ei_employer_mult_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_ei_employer_mult_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_ei_employer_mult"/>
<field name="date_from">2025-01-01</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">1.4</field>
</record>
<!-- ============================================================ -->
<!-- FEDERAL TAX PARAMETERS - 2025 -->
<!-- FEDERAL TAX BRACKETS - 2026 -->
<!-- ============================================================ -->
<record id="rule_parameter_ca_fed_bracket_1" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Bracket 1 Threshold</field>
<field name="code">ca_fed_bracket_1</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Upper limit of the first federal tax bracket</field>
</record>
<record id="rule_parameter_ca_fed_bracket_1_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_bracket_1"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">58523</field>
</record>
<record id="rule_parameter_ca_fed_rate_1" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Rate 1</field>
<field name="code">ca_fed_rate_1</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Federal tax rate for the first bracket</field>
</record>
<record id="rule_parameter_ca_fed_rate_1_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_rate_1"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">0.14</field>
</record>
<record id="rule_parameter_ca_fed_bracket_2" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Bracket 2 Threshold</field>
<field name="code">ca_fed_bracket_2</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Upper limit of the second federal tax bracket</field>
</record>
<record id="rule_parameter_ca_fed_bracket_2_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_bracket_2"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">117045</field>
</record>
<record id="rule_parameter_ca_fed_rate_2" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Rate 2</field>
<field name="code">ca_fed_rate_2</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Federal tax rate for the second bracket</field>
</record>
<record id="rule_parameter_ca_fed_rate_2_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_rate_2"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">0.205</field>
</record>
<record id="rule_parameter_ca_fed_bracket_3" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Bracket 3 Threshold</field>
<field name="code">ca_fed_bracket_3</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Upper limit of the third federal tax bracket</field>
</record>
<record id="rule_parameter_ca_fed_bracket_3_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_bracket_3"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">181440</field>
</record>
<record id="rule_parameter_ca_fed_rate_3" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Rate 3</field>
<field name="code">ca_fed_rate_3</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Federal tax rate for the third bracket</field>
</record>
<record id="rule_parameter_ca_fed_rate_3_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_rate_3"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">0.26</field>
</record>
<record id="rule_parameter_ca_fed_bracket_4" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Bracket 4 Threshold</field>
<field name="code">ca_fed_bracket_4</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Upper limit of the fourth federal tax bracket</field>
</record>
<record id="rule_parameter_ca_fed_bracket_4_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_bracket_4"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">258482</field>
</record>
<record id="rule_parameter_ca_fed_rate_4" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Rate 4</field>
<field name="code">ca_fed_rate_4</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Federal tax rate for the fourth bracket</field>
</record>
<record id="rule_parameter_ca_fed_rate_4_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_rate_4"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">0.29</field>
</record>
<record id="rule_parameter_ca_fed_rate_5" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Rate 5</field>
<field name="code">ca_fed_rate_5</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Federal tax rate for the fifth bracket (above bracket 4)</field>
</record>
<record id="rule_parameter_ca_fed_rate_5_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_rate_5"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">0.33</field>
</record>
<!-- ============================================================ -->
<!-- FEDERAL TAX CREDITS - 2026 -->
<!-- ============================================================ -->
<record id="rule_parameter_ca_fed_bpa" model="hr.rule.parameter">
<field name="name">Canada - Federal Basic Personal Amount</field>
<field name="name">Canada - Federal Basic Personal Amount (Max)</field>
<field name="code">ca_fed_bpa</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Federal basic personal amount (TD1 default)</field>
<field name="description">Federal basic personal amount - maximum (TD1 default)</field>
</record>
<record id="rule_parameter_ca_fed_bpa_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_fed_bpa_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_bpa"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">16129</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">16452</field>
</record>
<record id="rule_parameter_ca_fed_bpa_min" model="hr.rule.parameter">
<field name="name">Canada - Federal Basic Personal Amount (Min)</field>
<field name="code">ca_fed_bpa_min</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Federal basic personal amount - minimum for high-income phase-out</field>
</record>
<record id="rule_parameter_ca_fed_bpa_min_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_bpa_min"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">14829</field>
</record>
<record id="rule_parameter_ca_fed_cea" model="hr.rule.parameter">
@@ -156,54 +292,14 @@
<field name="country_id" ref="base.ca"/>
<field name="description">Canada Employment Amount for federal tax credit</field>
</record>
<record id="rule_parameter_ca_fed_cea_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_fed_cea_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_cea"/>
<field name="date_from">2025-01-01</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">1433</field>
</record>
<record id="rule_parameter_ca_fed_brackets" model="hr.rule.parameter">
<field name="name">Canada - Federal Tax Brackets</field>
<field name="code">ca_fed_brackets</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Federal income tax brackets: [(threshold, rate), ...]</field>
</record>
<record id="rule_parameter_ca_fed_brackets_2025" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_fed_brackets"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">[(57375, 0.15), (114750, 0.205), (177882, 0.26), (253414, 0.29), (float('inf'), 0.33)]</field>
</record>
<!-- ============================================================ -->
<!-- ONTARIO PROVINCIAL TAX PARAMETERS - 2025 -->
<!-- ============================================================ -->
<record id="rule_parameter_ca_on_bpa" model="hr.rule.parameter">
<field name="name">Canada Ontario - Basic Personal Amount</field>
<field name="code">ca_on_bpa</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Ontario basic personal amount</field>
</record>
<record id="rule_parameter_ca_on_bpa_2025" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_on_bpa"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">12399</field>
</record>
<record id="rule_parameter_ca_on_brackets" model="hr.rule.parameter">
<field name="name">Canada Ontario - Tax Brackets</field>
<field name="code">ca_on_brackets</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Ontario income tax brackets: [(threshold, rate), ...]</field>
</record>
<record id="rule_parameter_ca_on_brackets_2025" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_on_brackets"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">[(52886, 0.0505), (105775, 0.0915), (150000, 0.1116), (220000, 0.1216), (float('inf'), 0.1316)]</field>
</record>
<!-- ============================================================ -->
<!-- VACATION PAY RATE -->
<!-- VACATION PAY - 2026 -->
<!-- ============================================================ -->
<record id="rule_parameter_ca_vacation_rate" model="hr.rule.parameter">
@@ -212,11 +308,39 @@
<field name="country_id" ref="base.ca"/>
<field name="description">Vacation pay percentage (Ontario minimum 4%)</field>
</record>
<record id="rule_parameter_ca_vacation_rate_2025" model="hr.rule.parameter.value">
<record id="rule_parameter_ca_vacation_rate_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_vacation_rate"/>
<field name="date_from">2025-01-01</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">0.04</field>
</record>
<!-- ============================================================ -->
<!-- OVERTIME PARAMETERS - 2026 -->
<!-- ============================================================ -->
<record id="rule_parameter_ca_overtime_multiplier" model="hr.rule.parameter">
<field name="name">Canada - Overtime Pay Multiplier</field>
<field name="code">ca_overtime_multiplier</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Overtime pay multiplier (1.5x regular rate)</field>
</record>
<record id="rule_parameter_ca_overtime_multiplier_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_overtime_multiplier"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">1.5</field>
</record>
<record id="rule_parameter_ca_standard_hours_per_period" model="hr.rule.parameter">
<field name="name">Canada - Standard Hours Per Pay Period</field>
<field name="code">ca_standard_hours_per_period</field>
<field name="country_id" ref="base.ca"/>
<field name="description">Standard hours per bi-weekly pay period</field>
</record>
<record id="rule_parameter_ca_standard_hours_per_period_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="rule_parameter_ca_standard_hours_per_period"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">80</field>
</record>
</data>
</odoo>

View File

@@ -1,58 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<data noupdate="0">
<!-- ============================================================ -->
<!-- OVERTIME PAY - 1.5x Regular Rate -->
<!-- BASIC PAY (seq 1) -->
<!-- ============================================================ -->
<record id="hr_overtime_pay" model="hr.salary.rule">
<field name="name">Overtime Pay</field>
<field name="code">OT_PAY</field>
<field name="sequence">101</field>
<record id="hr_rule_basic" model="hr.salary.rule">
<field name="name">Basic Pay</field>
<field name="code">BASIC</field>
<field name="sequence">1</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'OT_HOURS' in inputs</field>
<field name="category_id" ref="hr_payroll.BASIC"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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
</field>
</record>
<!-- ============================================================ -->
<!-- STAT HOLIDAY PAY -->
<!-- OVERTIME PAY (seq 3) -->
<!-- ============================================================ -->
<record id="hr_overtime_pay" model="hr.salary.rule">
<field name="name">Overtime Pay</field>
<field name="code">OT_PAY</field>
<field name="sequence">3</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="condition_select">python</field>
<field name="condition_python">result = (inputs.get('OT_HOURS') and inputs['OT_HOURS'].amount &gt; 0) or (inputs.get('OT') and inputs['OT'].amount &gt; 0)</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
if inputs.get('OT_HOURS') and inputs['OT_HOURS'].amount &gt; 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 &gt; 0:
result = inputs['OT'].amount
else:
result = 0
</field>
</record>
<!-- ============================================================ -->
<!-- STAT HOLIDAY PAY (seq 4) -->
<!-- ============================================================ -->
<record id="hr_stat_holiday_pay" model="hr.salary.rule">
<field name="name">Stat Holiday Pay</field>
<field name="code">STAT_PAY</field>
<field name="sequence">102</field>
<field name="sequence">4</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'STAT_HOURS' in inputs</field>
<field name="condition_python">result = inputs.get('STAT_HOURS') and inputs['STAT_HOURS'].amount &gt; 0</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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
</field>
</record>
<!-- ============================================================ -->
<!-- BONUS PAY -->
<!-- VACATION PAY (seq 5) -->
<!-- ============================================================ -->
<record id="hr_vacation_pay" model="hr.salary.rule">
<field name="name">Vacation Pay</field>
<field name="code">VAC_PAY</field>
<field name="sequence">5</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_salary_rule_category_ca_earnings"/>
<field name="condition_select">python</field>
<field name="condition_python">result = inputs.get('VAC_PAY') and inputs['VAC_PAY'].amount &gt; 0</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
if inputs.get('VAC_PAY') and inputs['VAC_PAY'].amount &gt; 0:
result = round(float(inputs['VAC_PAY'].amount), 2)
else:
result = 0
</field>
</record>
<!-- ============================================================ -->
<!-- BONUS PAY (seq 6) -->
<!-- ============================================================ -->
<record id="hr_bonus_pay" model="hr.salary.rule">
<field name="name">Bonus</field>
<field name="code">BONUS_PAY</field>
<field name="sequence">103</field>
<field name="sequence">6</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="condition_select">python</field>
@@ -60,297 +103,468 @@ result = stat_hours * hourly_rate
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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
</field>
</record>
<!-- ============================================================ -->
<!-- CPP EMPLOYEE - Canada Pension Plan (Employee Portion) -->
<!-- Uses rule parameters for rates and limits -->
<!-- COMMISSION (seq 7) -->
<!-- ============================================================ -->
<record id="hr_commission_pay" model="hr.salary.rule">
<field name="name">Commission</field>
<field name="code">COMMISSION</field>
<field name="sequence">7</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="condition_select">python</field>
<field name="condition_python">result = inputs.get('COMMISSION') and inputs['COMMISSION'].amount &gt; 0</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
result = inputs['COMMISSION'].amount if inputs.get('COMMISSION') else 0
</field>
</record>
<!-- ============================================================ -->
<!-- RETROACTIVE PAY (seq 8) -->
<!-- ============================================================ -->
<record id="hr_retro_pay" model="hr.salary.rule">
<field name="name">Retroactive Pay</field>
<field name="code">RETRO_PAY</field>
<field name="sequence">8</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="condition_select">python</field>
<field name="condition_python">result = inputs.get('RETRO_PAY') and inputs['RETRO_PAY'].amount &gt; 0</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
result = inputs['RETRO_PAY'].amount if inputs.get('RETRO_PAY') else 0
</field>
</record>
<!-- ============================================================ -->
<!-- SHIFT PREMIUM (seq 9) -->
<!-- ============================================================ -->
<record id="hr_shift_premium" model="hr.salary.rule">
<field name="name">Shift Premium</field>
<field name="code">SHIFT_PREMIUM</field>
<field name="sequence">9</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="condition_select">python</field>
<field name="condition_python">result = inputs.get('SHIFT_PREMIUM') and inputs['SHIFT_PREMIUM'].amount &gt; 0</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
result = inputs['SHIFT_PREMIUM'].amount if inputs.get('SHIFT_PREMIUM') else 0
</field>
</record>
<!-- ============================================================ -->
<!-- GROSS (seq 10) -->
<!-- ============================================================ -->
<record id="hr_rule_gross" model="hr.salary.rule">
<field name="name">Gross</field>
<field name="code">GROSS</field>
<field name="sequence">10</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.GROSS"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
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)
)
</field>
</record>
<!-- ============================================================ -->
<!-- RRSP DEDUCTION (seq 15) -->
<!-- ============================================================ -->
<record id="hr_rrsp_deduction" model="hr.salary.rule">
<field name="name">RRSP Deduction</field>
<field name="code">RRSP</field>
<field name="sequence">15</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.DED"/>
<field name="condition_select">python</field>
<field name="condition_python">result = inputs.get('RRSP') and inputs['RRSP'].amount &gt; 0</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
result = -(inputs['RRSP'].amount if inputs.get('RRSP') else 0)
</field>
</record>
<!-- ============================================================ -->
<!-- UNION DUES (seq 16) -->
<!-- ============================================================ -->
<record id="hr_union_dues" model="hr.salary.rule">
<field name="name">Union Dues</field>
<field name="code">UNION_DUES</field>
<field name="sequence">16</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.DED"/>
<field name="condition_select">python</field>
<field name="condition_python">result = inputs.get('UNION') and inputs['UNION'].amount &gt; 0</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
result = -(inputs['UNION'].amount if inputs.get('UNION') else 0)
</field>
</record>
<!-- ============================================================ -->
<!-- CPP EMPLOYEE (seq 20) -->
<!-- ============================================================ -->
<record id="hr_cpp_employee" model="hr.salary.rule">
<field name="name">CPP Employee</field>
<field name="code">CPP_EE</field>
<field name="sequence">150</field>
<field name="sequence">20</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.DED"/>
<field name="category_id" ref="hr_salary_rule_category_ca_cpp"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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 &lt;= 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)
</field>
</record>
<!-- ============================================================ -->
<!-- CPP EMPLOYER - 1:1 Match -->
<!-- CPP EMPLOYER (seq 21) -->
<!-- ============================================================ -->
<record id="hr_cpp_employer" model="hr.salary.rule">
<field name="name">CPP Employer</field>
<field name="code">CPP_ER</field>
<field name="sequence">151</field>
<field name="sequence">21</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.COMP"/>
<field name="category_id" ref="hr_salary_rule_category_ca_employer"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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)
</field>
</record>
<!-- ============================================================ -->
<!-- CPP2 EMPLOYEE - Second Canada Pension Plan -->
<!-- Uses rule parameters for rates and limits -->
<!-- CPP2 EMPLOYEE (seq 22) -->
<!-- ============================================================ -->
<record id="hr_cpp2_employee" model="hr.salary.rule">
<field name="name">CPP2 Employee</field>
<field name="code">CPP2_EE</field>
<field name="sequence">152</field>
<field name="sequence">22</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.DED"/>
<field name="category_id" ref="hr_salary_rule_category_ca_cpp"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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 &gt; period_ympe:
cpp2_pensionable = min(gross_amount, period_ceiling) - period_ympe
result = -min(cpp2_pensionable * cpp2_rate, period_max)
else:
result = 0
</field>
</record>
<!-- ============================================================ -->
<!-- CPP2 EMPLOYER - 1:1 Match -->
<!-- CPP2 EMPLOYER (seq 23) -->
<!-- ============================================================ -->
<record id="hr_cpp2_employer" model="hr.salary.rule">
<field name="name">CPP2 Employer</field>
<field name="code">CPP2_ER</field>
<field name="sequence">153</field>
<field name="sequence">23</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.COMP"/>
<field name="category_id" ref="hr_salary_rule_category_ca_employer"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# CPP2 Employer - 1:1 match
result = abs(CPP2_EE) if CPP2_EE else 0
result = -result_rules.get('CPP2_EE', {}).get('total', 0)
</field>
</record>
<!-- ============================================================ -->
<!-- EI EMPLOYEE - Employment Insurance -->
<!-- Uses rule parameters for rates and limits -->
<!-- EI EMPLOYEE (seq 25) -->
<!-- ============================================================ -->
<record id="hr_ei_employee" model="hr.salary.rule">
<field name="name">EI Employee</field>
<field name="code">EI_EE</field>
<field name="sequence">154</field>
<field name="sequence">25</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.DED"/>
<field name="category_id" ref="hr_salary_rule_category_ca_ei"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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 &lt;= 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)
</field>
</record>
<!-- ============================================================ -->
<!-- EI EMPLOYER - 1.4x Employee Premium -->
<!-- EI EMPLOYER (seq 26) -->
<!-- ============================================================ -->
<record id="hr_ei_employer" model="hr.salary.rule">
<field name="name">EI Employer</field>
<field name="code">EI_ER</field>
<field name="sequence">155</field>
<field name="sequence">26</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.COMP"/>
<field name="category_id" ref="hr_salary_rule_category_ca_employer"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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
</field>
</record>
<!-- ============================================================ -->
<!-- FEDERAL INCOME TAX -->
<!-- Uses rule parameters for brackets and credits -->
<!-- FEDERAL INCOME TAX (seq 30) -->
<!-- ============================================================ -->
<record id="hr_fed_tax" model="hr.salary.rule">
<field name="name">Federal Income Tax</field>
<field name="code">FED_TAX</field>
<field name="sequence">160</field>
<field name="sequence">30</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.DED"/>
<field name="category_id" ref="hr_salary_rule_category_ca_fed_tax"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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 &lt;= 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 &gt; 0 else 0
if td1_override:
fed_bpa = td1_override
elif annual_income &lt;= phase_out_start:
fed_bpa = bpa_max
elif annual_income &gt;= 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 &gt; 0:
tax += taxable_in_bracket * rate
prev_bracket = bracket
if annual_income &lt;= 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)
</field>
</record>
<!-- ============================================================ -->
<!-- PROVINCIAL INCOME TAX (ONTARIO) -->
<!-- Uses rule parameters for brackets and credits -->
<!-- PROVINCIAL INCOME TAX (seq 35) -->
<!-- All 12 provinces/territories with surtax support -->
<!-- ============================================================ -->
<record id="hr_prov_tax" model="hr.salary.rule">
<field name="name">Provincial Income Tax</field>
<field name="code">PROV_TAX</field>
<field name="sequence">161</field>
<field name="sequence">35</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.DED"/>
<field name="category_id" ref="hr_salary_rule_category_ca_prov_tax"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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 &lt;= threshold:
tax += (annual - prev_threshold) * rate
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': []},
}
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]))
tax = 0
prev_bracket = 0
for bracket, rate in prov_brackets:
taxable_in_bracket = min(annual_income, bracket) - prev_bracket
if taxable_in_bracket &gt; 0:
tax += taxable_in_bracket * rate
prev_bracket = bracket
if annual_income &lt;= bracket:
break
else:
tax += (threshold - prev_threshold) * rate
prev_threshold = threshold
# Ontario Basic Personal Amount credit
tax_credit = BPA_ON * brackets[0][1] # Lowest rate
prov_bpa = cfg['bpa']
if employee.provincial_claim_amount and employee.provincial_claim_amount &gt; 0:
prov_bpa = employee.provincial_claim_amount
prov_credit = prov_bpa * prov_brackets[0][1]
basic_provincial_tax = max(tax - prov_credit, 0)
# 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]
surtax = 0
for s in cfg['st']:
if basic_provincial_tax &gt; s[0]:
surtax += (basic_provincial_tax - s[0]) * s[1]
annual_tax = max(0, tax - tax_credit - cpp_credit - ei_credit)
result = -(annual_tax / PAY_PERIODS)
total_provincial_tax = basic_provincial_tax + surtax
result = -(total_provincial_tax / 26)
</field>
</record>
<!-- ============================================================ -->
<!-- VACATION PAY - 4% of Earnings -->
<!-- ONTARIO HEALTH PREMIUM (seq 36) -->
<!-- ============================================================ -->
<record id="hr_vacation_pay" model="hr.salary.rule">
<field name="name">Vacation Pay</field>
<field name="code">VAC_PAY</field>
<field name="sequence">170</field>
<record id="hr_ohp" model="hr.salary.rule">
<field name="name">Ontario Health Premium</field>
<field name="code">OHP</field>
<field name="sequence">36</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/>
<field name="category_id" ref="hr_salary_rule_category_ca_ohp"/>
<field name="condition_select">python</field>
<field name="condition_python">
province = employee.home_province or 'ON'
result = (province == 'ON')
</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
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 &lt;= 20000:
ohp = 0
elif annual_income &lt;= 36000:
ohp = min((annual_income - 20000) * 0.06, 300)
elif annual_income &lt;= 48000:
ohp = 300 + min((annual_income - 36000) * 0.06, 150)
elif annual_income &lt;= 72000:
ohp = 450 + min((annual_income - 48000) * 0.0025, 150)
elif annual_income &lt;= 200000:
ohp = 600 + min((annual_income - 72000) * 0.0025, 300)
else:
ohp = 900
result = -(ohp / 26)
</field>
</record>
<!-- ============================================================ -->
<!-- NET PAY (seq 100) -->
<!-- ============================================================ -->
<record id="hr_rule_net" model="hr.salary.rule">
<field name="name">Net Pay</field>
<field name="code">NET</field>
<field name="sequence">100</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.NET"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="appears_on_payslip">True</field>
<field name="amount_python_compute">
# 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)
)
</field>
</record>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="seq_payroll_cheque" model="ir.sequence">
<field name="name">Payroll Cheque</field>
<field name="code">payroll.cheque</field>
<field name="prefix">CHQ</field>
<field name="padding">6</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_hr_roe" model="ir.sequence">
<field name="name">Record of Employment</field>
<field name="code">hr.roe</field>
<field name="prefix">ROE</field>
<field name="padding">6</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_hr_tax_remittance" model="ir.sequence">
<field name="name">Tax Remittance</field>
<field name="code">hr.tax.remittance</field>
<field name="prefix">REM</field>
<field name="padding">6</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

View File

@@ -1,134 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- ============================================================ -->
<!-- FISCAL YEAR 2025 -->
<!-- Note: This references account.fiscal.year - ensure it exists -->
<!-- ============================================================ -->
<!-- ============================================================ -->
<!-- FEDERAL TAX RATES 2025 -->
<!-- Source: Canada Revenue Agency (CRA) 2025 Tax Brackets -->
<!-- ============================================================ -->
<record id="federal_tax" model="tax.yearly.rates">
<field name="tax_type">federal</field>
<field name="canada_emp_amount">1433.00</field>
<!-- fiscal_year field should reference your fiscal year record -->
</record>
<!-- Federal Tax Bracket 1: 15% on first $55,867 -->
<record id="federal_tax_line_1" model="tax.yearly.rate.line">
<field name="tax_id" ref="federal_tax"/>
<field name="tax_bracket">55867.00</field>
<field name="tax_rate">15.00</field>
<field name="tax_constant">0.00</field>
</record>
<!-- Federal Tax Bracket 2: 20.5% on $55,867 to $111,733 -->
<record id="federal_tax_line_2" model="tax.yearly.rate.line">
<field name="tax_id" ref="federal_tax"/>
<field name="tax_bracket">111733.00</field>
<field name="tax_rate">20.50</field>
<field name="tax_constant">0.00</field>
</record>
<!-- Federal Tax Bracket 3: 26% on $111,733 to $173,205 -->
<record id="federal_tax_line_3" model="tax.yearly.rate.line">
<field name="tax_id" ref="federal_tax"/>
<field name="tax_bracket">173205.00</field>
<field name="tax_rate">26.00</field>
<field name="tax_constant">0.00</field>
</record>
<!-- Federal Tax Bracket 4: 29% on $173,205 to $246,752 -->
<record id="federal_tax_line_4" model="tax.yearly.rate.line">
<field name="tax_id" ref="federal_tax"/>
<field name="tax_bracket">246752.00</field>
<field name="tax_rate">29.00</field>
<field name="tax_constant">0.00</field>
</record>
<!-- Federal Tax Bracket 5: 33% on over $246,752 -->
<record id="federal_tax_line_5" model="tax.yearly.rate.line">
<field name="tax_id" ref="federal_tax"/>
<field name="tax_bracket">246752.01</field>
<field name="tax_rate">33.00</field>
<field name="tax_constant">0.00</field>
</record>
<!-- ============================================================ -->
<!-- PROVINCIAL TAX RATES 2025 - ONTARIO -->
<!-- Source: Ontario 2025 Tax Brackets -->
<!-- ============================================================ -->
<record id="provincial_tax" model="tax.yearly.rates">
<field name="tax_type">provincial</field>
<!-- fiscal_year field should reference your fiscal year record -->
</record>
<!-- Ontario Tax Bracket 1: 5.05% on first $52,886 -->
<record id="provincial_tax_line_1" model="tax.yearly.rate.line">
<field name="tax_id" ref="provincial_tax"/>
<field name="tax_bracket">52886.00</field>
<field name="tax_rate">5.05</field>
<field name="tax_constant">0.00</field>
</record>
<!-- Ontario Tax Bracket 2: 9.15% on $52,886 to $105,775 -->
<record id="provincial_tax_line_2" model="tax.yearly.rate.line">
<field name="tax_id" ref="provincial_tax"/>
<field name="tax_bracket">105775.00</field>
<field name="tax_rate">9.15</field>
<field name="tax_constant">0.00</field>
</record>
<!-- Ontario Tax Bracket 3: 11.16% on $105,775 to $150,000 -->
<record id="provincial_tax_line_3" model="tax.yearly.rate.line">
<field name="tax_id" ref="provincial_tax"/>
<field name="tax_bracket">150000.00</field>
<field name="tax_rate">11.16</field>
<field name="tax_constant">0.00</field>
</record>
<!-- Ontario Tax Bracket 4: 12.16% on $150,000 to $220,000 -->
<record id="provincial_tax_line_4" model="tax.yearly.rate.line">
<field name="tax_id" ref="provincial_tax"/>
<field name="tax_bracket">220000.00</field>
<field name="tax_rate">12.16</field>
<field name="tax_constant">0.00</field>
</record>
<!-- Ontario Tax Bracket 5: 13.16% on over $220,000 -->
<record id="provincial_tax_line_5" model="tax.yearly.rate.line">
<field name="tax_id" ref="provincial_tax"/>
<field name="tax_bracket">220000.01</field>
<field name="tax_rate">13.16</field>
<field name="tax_constant">0.00</field>
</record>
<!-- ============================================================ -->
<!-- CPP - CANADA PENSION PLAN 2025 -->
<!-- ============================================================ -->
<record id="cpp_ded" model="tax.yearly.rates">
<field name="ded_type">cpp</field>
<field name="emp_contribution_rate">5.95</field>
<field name="employer_contribution_rate">5.95</field>
<field name="exemption">134.61</field>
<field name="max_cpp">4034.10</field>
<!-- fiscal_year field should reference your fiscal year record -->
</record>
<!-- ============================================================ -->
<!-- EI - EMPLOYMENT INSURANCE 2025 -->
<!-- ============================================================ -->
<record id="ei_ded" model="tax.yearly.rates">
<field name="ded_type">ei</field>
<field name="ei_rate">1.64</field>
<field name="ei_earnings">65700.00</field>
<field name="emp_ei_amount">1077.48</field>
<field name="employer_ei_amount">1508.47</field>
<!-- fiscal_year field should reference your fiscal year record -->
</record>
</data>
</odoo>

View File

@@ -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

View File

@@ -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: <strong>%s</strong>' % 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',

View File

@@ -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
self.message_post(
body=f'T4A PDF generated: <strong>{filename}</strong>',
attachment_ids=[(0, 0, {
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: <strong>{filename}</strong>',
attachment_ids=[attachment.id],
)
return {

View File

@@ -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

View File

@@ -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',
)

View File

@@ -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({

View File

@@ -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

View File

@@ -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
@@ -301,24 +302,79 @@ class PayrollEntry(models.TransientModel):
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
annual = gross * PAY_PERIODS
emp = entry.employee_id
# Simplified tax calculations (bi-weekly)
# Income tax: ~15-20% average for Canadian employees
entry.income_tax = round(gross * 0.128, 2) # Approximate federal + provincial
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)
# EI: 1.64% of gross (2025 rate) up to maximum
entry.employment_insurance = round(min(gross * 0.0164, 1049.12 / 26), 2)
# 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)
# 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)
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)
# CPP2: 4% on earnings above first ceiling (2025)
entry.cpp2 = 0 # Only applies if earnings exceed $71,300/year
# 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

View File

@@ -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

View File

@@ -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({

View File

@@ -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,
},

View File

@@ -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')

View File

@@ -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')

View File

@@ -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
@@ -51,3 +49,4 @@ access_cheque_layout_settings_user,cheque.layout.settings user,model_cheque_layo
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
access_hr_tax_remittance_sequence,hr.tax.remittance.sequence,model_hr_tax_remittance_sequence,hr.group_hr_manager,1,1,1,1
1 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
2 access_hr_employee_terminate_wizard access_hr_employee_terminate_wizard model_hr_employee_terminate_wizard hr.group_hr_user 1 1 1 1
3 access_hr_roe_user access_hr_roe_user model_hr_roe hr.group_hr_user 1 1 1 0
4 access_hr_roe_manager access_hr_roe_manager model_hr_roe hr.group_hr_manager 1 1 1 1
49 access_cheque_layout_settings_manager cheque.layout.settings manager model_cheque_layout_settings hr.group_hr_manager 1 1 1 1
50 access_cheque_layout_preview_wizard_user cheque.layout.preview.wizard user model_cheque_layout_preview_wizard hr.group_hr_user 1 1 1 1
51 access_payroll_cheque_number_wizard_user payroll.cheque.number.wizard user model_payroll_cheque_number_wizard hr.group_hr_user 1 1 1 1
52 access_hr_tax_remittance_sequence hr.tax.remittance.sequence model_hr_tax_remittance_sequence hr.group_hr_manager 1 1 1 1

View File

@@ -37,6 +37,10 @@
string="Generate T4 Slips"
type="object"
class="btn-primary"/>
<button name="action_export_xml"
string="Export CRA XML"
type="object"
class="btn-primary"/>
<button name="action_fill_pdf"
string="Fill PDF"
type="object"
@@ -78,9 +82,12 @@
<field name="tax_year"/>
<field name="cra_business_number"/>
</group>
<group string="Contact">
<group string="Contact / Transmitter">
<field name="contact_name"/>
<field name="contact_phone"/>
<field name="contact_email"/>
<field name="transmitter_bn"/>
<field name="transmitter_name"/>
<field name="proprietor_sin"/>
<field name="filing_date"/>
</group>

View File

@@ -33,6 +33,10 @@
<field name="arch" type="xml">
<form string="T4A Summary">
<header>
<button name="action_export_xml"
string="Export CRA XML"
type="object"
class="btn-primary"/>
<button name="action_mark_filed"
string="Mark as Filed"
type="object"
@@ -60,9 +64,12 @@
<field name="tax_year"/>
<field name="cra_business_number"/>
</group>
<group string="Contact">
<group string="Contact / Transmitter">
<field name="contact_name"/>
<field name="contact_phone"/>
<field name="contact_email"/>
<field name="transmitter_bn"/>
<field name="transmitter_name"/>
<field name="filing_date"/>
</group>
</group>

View File

@@ -1,75 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tax Yearly Rates Tree View -->
<record id="tax_rates_tree_view" model="ir.ui.view">
<field name="name">tax.rates.tree.view</field>
<field name="model">tax.yearly.rates</field>
<field name="priority">16</field>
<field name="arch" type="xml">
<list string="Tax Lines">
<field name="fiscal_year"/>
<field name="tax_type"/>
<field name="ded_type"/>
</list>
</field>
</record>
<!-- Tax Yearly Rates Form View -->
<record id="tax_rates_form_view" model="ir.ui.view">
<field name="name">tax.rates.form.view</field>
<field name="model">tax.yearly.rates</field>
<field name="priority">16</field>
<field name="arch" type="xml">
<form>
<sheet>
<field name="fiscal_year" required="1"/>
<!-- Canada Pension Plan Section -->
<group name="Canada Pension Plan">
<field name="ded_type" invisible="ded_type not in ['cpp']"/>
<group name="CPP" invisible="ded_type not in ['cpp']">
<separator string="Canadian Pension Plan"/>
<field name="emp_contribution_rate"/>
<field name="employer_contribution_rate"/>
<field name="exemption"/>
<field name="max_cpp"/>
</group>
</group>
<!-- Employment Insurance Section -->
<group name="ei">
<field name="ded_type" invisible="ded_type not in ['ei']"/>
<group name="emp_ins" invisible="ded_type not in ['ei']">
<separator string="Employment Insurance"/>
<field name="ei_earnings"/>
<field name="emp_ei_amount"/>
<field name="employer_ei_amount"/>
<field name="ei_rate"/>
</group>
</group>
<!-- Tax Brackets Section -->
<group name="test">
<group name="testing" invisible="not tax_type">
<separator string="Taxes"/>
<field name="tax_type"/>
<field name="tax_yearly_rate_ids">
<list editable="bottom">
<field name="tax_bracket" required="1"/>
<field name="tax_rate" required="1"/>
<field name="tax_constant" required="1"/>
</list>
</field>
<field name="canada_emp_amount" required="1"
invisible="tax_type not in ['federal'] and ded_type not in ['cpp','ei']"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Actions and Menus moved to fusion_payroll_menus.xml -->
</odoo>