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 models
from . import wizards from . import wizards
from . import controllers 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). Built for Odoo Enterprise Payroll (hr_payroll).
""", """,
'author': 'Your Company', 'author': 'Nexa Systems Inc.',
'website': '', 'website': '',
'license': 'LGPL-3', 'license': 'OPL-1',
'depends': [ 'depends': [
'hr_payroll', # Core payroll functionality 'hr_payroll', # Core payroll functionality
'hr_work_entry_enterprise', # For payroll menu structure (Odoo 19) '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 'hr_attendance', # For punch-in/out time tracking
'mail', # For ROE chatter/tracking 'mail', # For ROE chatter/tracking
], ],
'external_dependencies': {
'python': ['lxml'],
},
'data': [ 'data': [
# Views # Views
'views/pay_period_views.xml', 'views/pay_period_views.xml',
'views/tax_yearly_rates_views.xml',
'views/hr_employee_views.xml', 'views/hr_employee_views.xml',
'views/hr_roe_views.xml', 'views/hr_roe_views.xml',
'views/hr_payslip_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_tax_payment_schedule_views.xml',
'views/payroll_config_settings_views.xml', 'views/payroll_config_settings_views.xml',
'views/payroll_work_location_views.xml', 'views/payroll_work_location_views.xml',
'views/payroll_tax_payment_schedule_views.xml',
'views/payroll_dashboard_views.xml', 'views/payroll_dashboard_views.xml',
'views/payroll_cheque_views.xml', 'views/payroll_cheque_views.xml',
'views/payroll_cheque_print_wizard_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', 'data/hr_rule_parameter_data.xml',
# 2. Input types for additional pay (OT, Stat, Bonus) # 2. Input types for additional pay (OT, Stat, Bonus)
'data/hr_payslip_input_type_data.xml', 'data/hr_payslip_input_type_data.xml',
# 3. Legacy tax rates data # 3. Payroll structure (creates structure and category)
'data/tax_yearly_rates_data.xml',
# 4. Payroll structure (creates structure and category)
'data/hr_payroll_structure.xml', 'data/hr_payroll_structure.xml',
# 5. Canadian salary rules (references structure and parameters) # 5. Canadian salary rules (references structure and parameters)
'data/hr_salary_rules.xml', 'data/hr_salary_rules.xml',
# 6. Demo/Sample data (loads on install) # 6. Sequences
'demo/demo_data.xml', 'data/ir_sequence_data.xml',
# Security (load last to ensure all models are registered) # Security (load last to ensure all models are registered)
'security/ir.model.access.csv', 'security/ir.model.access.csv',
], ],
'demo': [], 'demo': [
'demo/demo_data.xml',
],
'images': ['static/description/icon.png'], 'images': ['static/description/icon.png'],
'assets': { 'assets': {
'web.assets_backend': [ 'web.assets_backend': [
@@ -123,4 +124,5 @@ Built for Odoo Enterprise Payroll (hr_payroll).
'installable': True, 'installable': True,
'application': True, 'application': True,
'auto_install': False, 'auto_install': False,
'post_init_hook': '_fusion_payroll_post_init',
} }

View File

@@ -2,133 +2,55 @@
<odoo> <odoo>
<data noupdate="1"> <data noupdate="1">
<!-- ============================================================ --> <!-- Structure Type -->
<!-- CANADA SALARY STRUCTURE -->
<!-- ============================================================ -->
<!-- Salary Structure Type (if needed) -->
<record id="structure_type_canada" model="hr.payroll.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"/> <field name="country_id" ref="base.ca"/>
</record> </record>
<!-- Canada Salary Structure --> <!-- Salary Structure -->
<record id="hr_payroll_structure_canada" model="hr.payroll.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="code">Canada</field>
<field name="type_id" ref="structure_type_canada"/> <field name="type_id" ref="structure_type_canada"/>
<field name="country_id" ref="base.ca"/>
</record> </record>
<!-- ============================================================ --> <!-- Salary Rule Categories -->
<!-- SALARY RULE CATEGORY - CANADA (Deduction) --> <record id="hr_salary_rule_category_ca_cpp" model="hr.salary.rule.category">
<!-- This category links to CPP, EI, Federal and Provincial Tax --> <field name="name">CPP</field>
<!-- ============================================================ --> <field name="code">CPP</field>
<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 -->
</record> </record>
<!-- ============================================================ --> <record id="hr_salary_rule_category_ca_ei" model="hr.salary.rule.category">
<!-- LINK SALARY RULES TO CANADA STRUCTURE --> <field name="name">EI</field>
<!-- ============================================================ --> <field name="code">EI</field>
<!-- 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> </record>
<!-- House Rent Allowance --> <record id="hr_salary_rule_category_ca_fed_tax" model="hr.salary.rule.category">
<record id="hr_rule_hra" model="hr.salary.rule"> <field name="name">Federal Tax</field>
<field name="name">House Rent Allowance</field> <field name="code">FED_TAX</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> </record>
<!-- Dearness Allowance --> <record id="hr_salary_rule_category_ca_prov_tax" model="hr.salary.rule.category">
<record id="hr_rule_da" model="hr.salary.rule"> <field name="name">Provincial Tax</field>
<field name="name">Dearness Allowance</field> <field name="code">PROV_TAX</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> </record>
<!-- Travel Allowance --> <record id="hr_salary_rule_category_ca_ohp" model="hr.salary.rule.category">
<record id="hr_rule_travel" model="hr.salary.rule"> <field name="name">Ontario Health Premium</field>
<field name="name">Travel Allowance</field> <field name="code">OHP</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> </record>
<!-- Meal Allowance --> <record id="hr_salary_rule_category_ca_employer" model="hr.salary.rule.category">
<record id="hr_rule_meal" model="hr.salary.rule"> <field name="name">Employer Contributions</field>
<field name="name">Meal Allowance</field> <field name="code">EMPLOYER</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> </record>
<!-- Medical Allowance --> <record id="hr_salary_rule_category_ca_earnings" model="hr.salary.rule.category">
<record id="hr_rule_medical" model="hr.salary.rule"> <field name="name">Earnings</field>
<field name="name">Medical Allowance</field> <field name="code">EARN</field>
<field name="code">Medical</field> <field name="country_id" ref="base.ca"/>
<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> </record>
</data> </data>

View File

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

View File

@@ -3,7 +3,7 @@
<data noupdate="1"> <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"> <record id="rule_parameter_ca_cpp_rate" model="hr.rule.parameter">
@@ -12,9 +12,9 @@
<field name="country_id" ref="base.ca"/> <field name="country_id" ref="base.ca"/>
<field name="description">CPP employee/employer contribution rate</field> <field name="description">CPP employee/employer contribution rate</field>
</record> </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="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> <field name="parameter_value">0.0595</field>
</record> </record>
@@ -24,10 +24,10 @@
<field name="country_id" ref="base.ca"/> <field name="country_id" ref="base.ca"/>
<field name="description">CPP basic exemption amount per year</field> <field name="description">CPP basic exemption amount per year</field>
</record> </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="rule_parameter_id" ref="rule_parameter_ca_cpp_exemption"/>
<field name="date_from">2025-01-01</field> <field name="date_from">2026-01-01</field>
<field name="parameter_value">3500.00</field> <field name="parameter_value">3500</field>
</record> </record>
<record id="rule_parameter_ca_cpp_max" model="hr.rule.parameter"> <record id="rule_parameter_ca_cpp_max" model="hr.rule.parameter">
@@ -36,14 +36,38 @@
<field name="country_id" ref="base.ca"/> <field name="country_id" ref="base.ca"/>
<field name="description">Maximum CPP employee contribution per year</field> <field name="description">Maximum CPP employee contribution per year</field>
</record> </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="rule_parameter_id" ref="rule_parameter_ca_cpp_max"/>
<field name="date_from">2025-01-01</field> <field name="date_from">2026-01-01</field>
<field name="parameter_value">4034.10</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> </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"> <record id="rule_parameter_ca_cpp2_rate" model="hr.rule.parameter">
@@ -52,9 +76,9 @@
<field name="country_id" ref="base.ca"/> <field name="country_id" ref="base.ca"/>
<field name="description">CPP2 contribution rate (on earnings above YMPE)</field> <field name="description">CPP2 contribution rate (on earnings above YMPE)</field>
</record> </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="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> <field name="parameter_value">0.04</field>
</record> </record>
@@ -64,38 +88,14 @@
<field name="country_id" ref="base.ca"/> <field name="country_id" ref="base.ca"/>
<field name="description">Maximum CPP2 employee contribution per year</field> <field name="description">Maximum CPP2 employee contribution per year</field>
</record> </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="rule_parameter_id" ref="rule_parameter_ca_cpp2_max"/>
<field name="date_from">2025-01-01</field> <field name="date_from">2026-01-01</field>
<field name="parameter_value">396.00</field> <field name="parameter_value">416.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>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- EMPLOYMENT INSURANCE (EI) PARAMETERS - 2025 --> <!-- EMPLOYMENT INSURANCE (EI) PARAMETERS - 2026 -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="rule_parameter_ca_ei_rate" model="hr.rule.parameter"> <record id="rule_parameter_ca_ei_rate" model="hr.rule.parameter">
@@ -104,10 +104,22 @@
<field name="country_id" ref="base.ca"/> <field name="country_id" ref="base.ca"/>
<field name="description">EI employee contribution rate</field> <field name="description">EI employee contribution rate</field>
</record> </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="rule_parameter_id" ref="rule_parameter_ca_ei_rate"/>
<field name="date_from">2025-01-01</field> <field name="date_from">2026-01-01</field>
<field name="parameter_value">0.0164</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>
<record id="rule_parameter_ca_ei_max" model="hr.rule.parameter"> <record id="rule_parameter_ca_ei_max" model="hr.rule.parameter">
@@ -116,10 +128,10 @@
<field name="country_id" ref="base.ca"/> <field name="country_id" ref="base.ca"/>
<field name="description">Maximum EI employee premium per year</field> <field name="description">Maximum EI employee premium per year</field>
</record> </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="rule_parameter_id" ref="rule_parameter_ca_ei_max"/>
<field name="date_from">2025-01-01</field> <field name="date_from">2026-01-01</field>
<field name="parameter_value">1077.48</field> <field name="parameter_value">1123.07</field>
</record> </record>
<record id="rule_parameter_ca_ei_employer_mult" model="hr.rule.parameter"> <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="country_id" ref="base.ca"/>
<field name="description">EI employer contribution multiplier (1.4x employee)</field> <field name="description">EI employer contribution multiplier (1.4x employee)</field>
</record> </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="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> <field name="parameter_value">1.4</field>
</record> </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"> <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="code">ca_fed_bpa</field>
<field name="country_id" ref="base.ca"/> <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>
<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="rule_parameter_id" ref="rule_parameter_ca_fed_bpa"/>
<field name="date_from">2025-01-01</field> <field name="date_from">2026-01-01</field>
<field name="parameter_value">16129</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>
<record id="rule_parameter_ca_fed_cea" model="hr.rule.parameter"> <record id="rule_parameter_ca_fed_cea" model="hr.rule.parameter">
@@ -156,54 +292,14 @@
<field name="country_id" ref="base.ca"/> <field name="country_id" ref="base.ca"/>
<field name="description">Canada Employment Amount for federal tax credit</field> <field name="description">Canada Employment Amount for federal tax credit</field>
</record> </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="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> <field name="parameter_value">1433</field>
</record> </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 --> <!-- VACATION PAY - 2026 -->
<!-- ============================================================ -->
<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 -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="rule_parameter_ca_vacation_rate" model="hr.rule.parameter"> <record id="rule_parameter_ca_vacation_rate" model="hr.rule.parameter">
@@ -212,11 +308,39 @@
<field name="country_id" ref="base.ca"/> <field name="country_id" ref="base.ca"/>
<field name="description">Vacation pay percentage (Ontario minimum 4%)</field> <field name="description">Vacation pay percentage (Ontario minimum 4%)</field>
</record> </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="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> <field name="parameter_value">0.04</field>
</record> </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> </data>
</odoo> </odoo>

View File

@@ -1,58 +1,101 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <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"> <record id="hr_rule_basic" model="hr.salary.rule">
<field name="name">Overtime Pay</field> <field name="name">Basic Pay</field>
<field name="code">OT_PAY</field> <field name="code">BASIC</field>
<field name="sequence">101</field> <field name="sequence">1</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/> <field name="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/> <field name="category_id" ref="hr_payroll.BASIC"/>
<field name="condition_select">python</field> <field name="condition_select">none</field>
<field name="condition_python">result = 'OT_HOURS' in inputs</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# Overtime Pay - 1.5x regular hourly rate result = payslip.paid_amount
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
</field> </field>
</record> </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"> <record id="hr_stat_holiday_pay" model="hr.salary.rule">
<field name="name">Stat Holiday Pay</field> <field name="name">Stat Holiday Pay</field>
<field name="code">STAT_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="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/> <field name="category_id" ref="hr_payroll.ALW"/>
<field name="condition_select">python</field> <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="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# Stat Holiday Pay stat_hours = inputs['STAT_HOURS'].amount if inputs.get('STAT_HOURS') else 0
stat_hours = inputs['STAT_HOURS'].amount if 'STAT_HOURS' in inputs else 0 basic = result_rules.get('BASIC', {}).get('total', 0)
hourly_rate = payslip.paid_amount / 86.67 if payslip.paid_amount else 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 result = stat_hours * hourly_rate
</field> </field>
</record> </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"> <record id="hr_bonus_pay" model="hr.salary.rule">
<field name="name">Bonus</field> <field name="name">Bonus</field>
<field name="code">BONUS_PAY</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="struct_id" ref="hr_payroll_structure_canada"/>
<field name="category_id" ref="hr_payroll.ALW"/> <field name="category_id" ref="hr_payroll.ALW"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
@@ -60,297 +103,468 @@ result = stat_hours * hourly_rate
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# Bonus Pay - direct amount from input result = inputs['BONUS'].amount if inputs.get('BONUS') else 0
result = inputs['BONUS'].amount if 'BONUS' in inputs else 0
</field> </field>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- CPP EMPLOYEE - Canada Pension Plan (Employee Portion) --> <!-- COMMISSION (seq 7) -->
<!-- Uses rule parameters for rates and limits --> <!-- ============================================================ -->
<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"> <record id="hr_cpp_employee" model="hr.salary.rule">
<field name="name">CPP Employee</field> <field name="name">CPP Employee</field>
<field name="code">CPP_EE</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="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="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# CPP Employee Deduction - Using Rule Parameters gross_amount = result_rules.get('GROSS', {}).get('total', 0)
CPP_RATE = payslip._rule_parameter('ca_cpp_rate') if employee.exempt_cpp:
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:
result = 0 result = 0
elif cpp > remaining:
result = -remaining
else: 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> </field>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- CPP EMPLOYER - 1:1 Match --> <!-- CPP EMPLOYER (seq 21) -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="hr_cpp_employer" model="hr.salary.rule"> <record id="hr_cpp_employer" model="hr.salary.rule">
<field name="name">CPP Employer</field> <field name="name">CPP Employer</field>
<field name="code">CPP_ER</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="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="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# CPP Employer - 1:1 match with employee result = -result_rules.get('CPP_EE', {}).get('total', 0)
result = abs(CPP_EE) if CPP_EE else 0
</field> </field>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- CPP2 EMPLOYEE - Second Canada Pension Plan --> <!-- CPP2 EMPLOYEE (seq 22) -->
<!-- Uses rule parameters for rates and limits -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="hr_cpp2_employee" model="hr.salary.rule"> <record id="hr_cpp2_employee" model="hr.salary.rule">
<field name="name">CPP2 Employee</field> <field name="name">CPP2 Employee</field>
<field name="code">CPP2_EE</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="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="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# CPP2 (Second CPP) - Using Rule Parameters gross_amount = result_rules.get('GROSS', {}).get('total', 0)
CPP2_RATE = payslip._rule_parameter('ca_cpp2_rate') if employee.exempt_cpp:
YMPE = payslip._rule_parameter('ca_ympe') result = 0
YAMPE = payslip._rule_parameter('ca_yampe') else:
CPP2_MAX = payslip._rule_parameter('ca_cpp2_max') ympe = payslip._rule_parameter('ca_ympe')
PAY_PERIODS = 24 cpp2_rate = payslip._rule_parameter('ca_cpp2_rate')
yampe = payslip._rule_parameter('ca_yampe')
gross = categories['GROSS'] cpp2_max = payslip._rule_parameter('ca_cpp2_max')
annual_equiv = gross * PAY_PERIODS 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 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)
</field> </field>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- CPP2 EMPLOYER - 1:1 Match --> <!-- CPP2 EMPLOYER (seq 23) -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="hr_cpp2_employer" model="hr.salary.rule"> <record id="hr_cpp2_employer" model="hr.salary.rule">
<field name="name">CPP2 Employer</field> <field name="name">CPP2 Employer</field>
<field name="code">CPP2_ER</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="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="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# CPP2 Employer - 1:1 match result = -result_rules.get('CPP2_EE', {}).get('total', 0)
result = abs(CPP2_EE) if CPP2_EE else 0
</field> </field>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- EI EMPLOYEE - Employment Insurance --> <!-- EI EMPLOYEE (seq 25) -->
<!-- Uses rule parameters for rates and limits -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="hr_ei_employee" model="hr.salary.rule"> <record id="hr_ei_employee" model="hr.salary.rule">
<field name="name">EI Employee</field> <field name="name">EI Employee</field>
<field name="code">EI_EE</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="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="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# EI Employee - Using Rule Parameters gross_amount = result_rules.get('GROSS', {}).get('total', 0)
EI_RATE = payslip._rule_parameter('ca_ei_rate') if employee.exempt_ei:
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:
result = 0 result = 0
elif ei > remaining:
result = -remaining
else: 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> </field>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- EI EMPLOYER - 1.4x Employee Premium --> <!-- EI EMPLOYER (seq 26) -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="hr_ei_employer" model="hr.salary.rule"> <record id="hr_ei_employer" model="hr.salary.rule">
<field name="name">EI Employer</field> <field name="name">EI Employer</field>
<field name="code">EI_ER</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="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="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# EI Employer - Using Rule Parameter for multiplier ei_employer_mult = payslip._rule_parameter('ca_ei_employer_mult')
EI_ER_MULT = payslip._rule_parameter('ca_ei_employer_mult') result = -result_rules.get('EI_EE', {}).get('total', 0) * ei_employer_mult
result = abs(EI_EE) * EI_ER_MULT if EI_EE else 0
</field> </field>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- FEDERAL INCOME TAX --> <!-- FEDERAL INCOME TAX (seq 30) -->
<!-- Uses rule parameters for brackets and credits -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="hr_fed_tax" model="hr.salary.rule"> <record id="hr_fed_tax" model="hr.salary.rule">
<field name="name">Federal Income Tax</field> <field name="name">Federal Income Tax</field>
<field name="code">FED_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="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="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# Federal Income Tax - Using Rule Parameters if hasattr(employee, 'exempt_federal_tax') and employee.exempt_federal_tax:
PAY_PERIODS = 24 result = 0
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')
gross = categories['GROSS']
annual = gross * PAY_PERIODS
# Calculate tax using brackets
tax = 0
prev_threshold = 0
for threshold, rate in brackets:
if annual &lt;= threshold:
tax += (annual - prev_threshold) * rate
break
else: else:
tax += (threshold - prev_threshold) * rate gross_amount = result_rules.get('GROSS', {}).get('total', 0)
prev_threshold = threshold 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
# Basic personal amount credit fed_brackets = [
tax_credit = BPA * brackets[0][1] # Lowest rate (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')),
]
# CPP/EI credits bpa_max = payslip._rule_parameter('ca_fed_bpa')
cpp_credit = min(abs(CPP_EE) * PAY_PERIODS if CPP_EE else 0, CPP_MAX) * brackets[0][1] bpa_min = payslip._rule_parameter('ca_fed_bpa_min')
ei_credit = min(abs(EI_EE) * PAY_PERIODS if EI_EE else 0, EI_MAX) * brackets[0][1] 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')
# Canada Employment Amount credit td1_override = employee.federal_td1_amount if hasattr(employee, 'federal_td1_amount') and employee.federal_td1_amount &gt; 0 else 0
cea_credit = CEA * brackets[0][1] 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:
fed_bpa = bpa_max - (bpa_max - bpa_min) * (annual_income - phase_out_start) / (phase_out_end - phase_out_start)
annual_tax = max(0, tax - tax_credit - cpp_credit - ei_credit - cea_credit) tax = 0
result = -(annual_tax / PAY_PERIODS) 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
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> </field>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- PROVINCIAL INCOME TAX (ONTARIO) --> <!-- PROVINCIAL INCOME TAX (seq 35) -->
<!-- Uses rule parameters for brackets and credits --> <!-- All 12 provinces/territories with surtax support -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="hr_prov_tax" model="hr.salary.rule"> <record id="hr_prov_tax" model="hr.salary.rule">
<field name="name">Provincial Income Tax</field> <field name="name">Provincial Income Tax</field>
<field name="code">PROV_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="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="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# Ontario Provincial Tax - Using Rule Parameters gross_amount = result_rules.get('GROSS', {}).get('total', 0)
PAY_PERIODS = 24 rrsp = abs(result_rules.get('RRSP', {}).get('total', 0))
brackets = payslip._rule_parameter('ca_on_brackets') union = abs(result_rules.get('UNION_DUES', {}).get('total', 0))
BPA_ON = payslip._rule_parameter('ca_on_bpa') taxable_per_period = gross_amount - rrsp - union
CPP_MAX = payslip._rule_parameter('ca_cpp_max') annual_income = taxable_per_period * 26
EI_MAX = payslip._rule_parameter('ca_ei_max')
gross = categories['GROSS'] province = employee.home_province or 'ON'
annual = gross * PAY_PERIODS
# Calculate tax using brackets if province == 'QC':
tax = 0 result = 0
prev_threshold = 0
for threshold, rate in brackets:
if annual &lt;= threshold:
tax += (annual - prev_threshold) * rate
break
else: else:
tax += (threshold - prev_threshold) * rate PROV = {
prev_threshold = threshold 'ON': {'b': [[53891, 0.0505], [107785, 0.0915], [150000, 0.1116], [220000, 0.1216], [0, 0.1316]], 'bpa': 12989, 'st': [[5818, 0.20], [7446, 0.36]]},
'AB': {'b': [[61200, 0.08], [154259, 0.10], [185111, 0.12], [246813, 0.13], [370220, 0.14], [0, 0.15]], 'bpa': 21885, 'st': []},
'BC': {'b': [[50363, 0.0506], [100728, 0.077], [115648, 0.105], [140430, 0.1229], [190405, 0.147], [265545, 0.168], [0, 0.205]], 'bpa': 12273, 'st': []},
'SK': {'b': [[54532, 0.105], [155805, 0.125], [0, 0.145]], 'bpa': 18635, 'st': []},
'MB': {'b': [[47000, 0.108], [100000, 0.1275], [0, 0.174]], 'bpa': 15780, 'st': []},
'NB': {'b': [[52333, 0.094], [104666, 0.14], [193861, 0.16], [0, 0.195]], 'bpa': 12458, 'st': []},
'NS': {'b': [[30995, 0.0879], [61991, 0.1495], [97417, 0.1667], [157124, 0.175], [0, 0.21]], 'bpa': 11481, 'st': []},
'PE': {'b': [[33928, 0.095], [65820, 0.1347], [106890, 0.166], [142250, 0.1762], [0, 0.19]], 'bpa': 12750, 'st': []},
'NL': {'b': [[44678, 0.087], [89354, 0.145], [159528, 0.158], [223340, 0.178], [285319, 0.198], [570638, 0.208], [1141275, 0.213], [0, 0.218]], 'bpa': 10382, 'st': []},
'NT': {'b': [[53003, 0.059], [106009, 0.086], [172346, 0.122], [0, 0.1405]], 'bpa': 16442, 'st': []},
'YT': {'b': [[58523, 0.064], [117045, 0.09], [181440, 0.109], [500000, 0.128], [0, 0.15]], 'bpa': 16729, 'st': []},
'NU': {'b': [[55801, 0.04], [111602, 0.07], [181439, 0.09], [0, 0.115]], 'bpa': 17091, 'st': []},
}
# Ontario Basic Personal Amount credit cfg = PROV.get(province, PROV['ON'])
tax_credit = BPA_ON * brackets[0][1] # Lowest rate prov_brackets = []
for br in cfg['b']:
t = br[0] if br[0] != 0 else float('inf')
prov_brackets.append((t, br[1]))
# CPP/EI credits at lowest rate tax = 0
cpp_credit = min(abs(CPP_EE) * PAY_PERIODS if CPP_EE else 0, CPP_MAX) * brackets[0][1] prev_bracket = 0
ei_credit = min(abs(EI_EE) * PAY_PERIODS if EI_EE else 0, EI_MAX) * brackets[0][1] 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
annual_tax = max(0, tax - tax_credit - cpp_credit - ei_credit) prov_bpa = cfg['bpa']
result = -(annual_tax / PAY_PERIODS) 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)
surtax = 0
for s in cfg['st']:
if basic_provincial_tax &gt; s[0]:
surtax += (basic_provincial_tax - s[0]) * s[1]
total_provincial_tax = basic_provincial_tax + surtax
result = -(total_provincial_tax / 26)
</field> </field>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- VACATION PAY - 4% of Earnings --> <!-- ONTARIO HEALTH PREMIUM (seq 36) -->
<!-- ============================================================ --> <!-- ============================================================ -->
<record id="hr_vacation_pay" model="hr.salary.rule"> <record id="hr_ohp" model="hr.salary.rule">
<field name="name">Vacation Pay</field> <field name="name">Ontario Health Premium</field>
<field name="code">VAC_PAY</field> <field name="code">OHP</field>
<field name="sequence">170</field> <field name="sequence">36</field>
<field name="struct_id" ref="hr_payroll_structure_canada"/> <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="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="appears_on_payslip">True</field> <field name="appears_on_payslip">True</field>
<field name="amount_python_compute"> <field name="amount_python_compute">
# Vacation Pay - Using Rule Parameter result = (
VAC_RATE = payslip._rule_parameter('ca_vacation_rate') result_rules.get('GROSS', {}).get('total', 0)
result = categories['BASIC'] * VAC_RATE + 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> </field>
</record> </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 -*- # -*- coding: utf-8 -*-
from . import tax_yearly_rates
from . import tax_yearly_rate_line
from . import hr_employee from . import hr_employee
from . import hr_contract
from . import hr_payslip from . import hr_payslip
from . import hr_roe from . import hr_roe
from . import hr_tax_remittance from . import hr_tax_remittance

View File

@@ -4,6 +4,7 @@ import base64
import os import os
import io import io
from datetime import date from datetime import date
from lxml import etree
from odoo import models, fields, api, tools from odoo import models, fields, api, tools
from odoo.exceptions import UserError from odoo.exceptions import UserError
@@ -15,16 +16,6 @@ class HrT4Summary(models.Model):
_order = 'tax_year desc' _order = 'tax_year desc'
_inherit = ['mail.thread', 'mail.activity.mixin'] _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 = [ STATE_SELECTION = [
('draft', 'Draft'), ('draft', 'Draft'),
('generated', 'Generated'), ('generated', 'Generated'),
@@ -205,6 +196,9 @@ class HrT4Summary(models.Model):
xml_filename = fields.Char( xml_filename = fields.Char(
string='XML Filename', 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 === # === Box 74: SIN of Proprietor ===
proprietor_sin = fields.Char( proprietor_sin = fields.Char(
@@ -270,7 +264,7 @@ class HrT4Summary(models.Model):
payslips = self.env['hr.payslip'].search([ payslips = self.env['hr.payslip'].search([
('company_id', '=', self.company_id.id), ('company_id', '=', self.company_id.id),
('state', 'in', ['validated', 'paid']), ('state', 'in', ['done', 'paid']),
('date_from', '>=', year_start), ('date_from', '>=', year_start),
('date_to', '<=', year_end), ('date_to', '<=', year_end),
]) ])
@@ -305,6 +299,8 @@ class HrT4Summary(models.Model):
box_40_allowances = 0 box_40_allowances = 0
box_42_commissions = 0 box_42_commissions = 0
box_44_union_dues = 0 box_44_union_dues = 0
rrsp = 0
union_dues = 0
for ps in emp_payslips: for ps in emp_payslips:
# Process each payslip line # Process each payslip line
@@ -357,8 +353,12 @@ class HrT4Summary(models.Model):
ei_ee += amount ei_ee += amount
elif code == 'EI_ER': elif code == 'EI_ER':
ei_er += amount ei_er += amount
elif code in ('FED_TAX', 'PROV_TAX'): elif code in ('FED_TAX', 'PROV_TAX', 'OHP'):
income_tax += amount income_tax += amount
elif code == 'RRSP':
rrsp += amount
elif code == 'UNION_DUES':
union_dues += amount
# Add GROSS to employment income (Box 14) # Add GROSS to employment income (Box 14)
# GROSS already includes all taxable income (salary, overtime, bonus, allowances, commissions, etc.) # GROSS already includes all taxable income (salary, overtime, bonus, allowances, commissions, etc.)
@@ -387,7 +387,8 @@ class HrT4Summary(models.Model):
# New boxes # New boxes
'box_40_taxable_benefits': box_40_allowances, 'box_40_taxable_benefits': box_40_allowances,
'box_42_commissions': box_42_commissions, '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' self.state = 'generated'
@@ -410,6 +411,166 @@ class HrT4Summary(models.Model):
'filing_date': date.today(), '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): def _get_pdf_text_coordinates(self):
"""Get text overlay coordinates for flattened PDF """Get text overlay coordinates for flattened PDF
Returns dict mapping field names to (x, y, font_size, font_name) tuples 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', 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 Benefits Code ===
t4_dental_code = fields.Selection( t4_dental_code = fields.Selection(
related='employee_id.t4_dental_code', related='employee_id.t4_dental_code',

View File

@@ -4,6 +4,7 @@ import base64
import os import os
import io import io
from datetime import date from datetime import date
from lxml import etree
from odoo import models, fields, api from odoo import models, fields, api
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo import tools from odoo import tools
@@ -128,6 +129,15 @@ class HrT4ASummary(models.Model):
string='Telephone', 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 Information ===
filing_date = fields.Date( filing_date = fields.Date(
string='Filing Date', string='Filing Date',
@@ -158,6 +168,123 @@ class HrT4ASummary(models.Model):
'filing_date': date.today(), '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): class HrT4ASlip(models.Model):
"""T4A Slip - One per recipient per tax year""" """T4A Slip - One per recipient per tax year"""
@@ -486,16 +613,17 @@ class HrT4ASlip(models.Model):
}) })
# Post to chatter # Post to chatter
self.message_post( attachment = self.env['ir.attachment'].create({
body=f'T4A PDF generated: <strong>{filename}</strong>',
attachment_ids=[(0, 0, {
'name': filename, 'name': filename,
'type': 'binary', 'type': 'binary',
'datas': pdf_data, 'datas': pdf_data,
'res_model': self._name, 'res_model': self._name,
'res_id': self.id, 'res_id': self.id,
'mimetype': 'application/pdf', 'mimetype': 'application/pdf',
})], })
self.message_post(
body=f'T4A PDF generated: <strong>{filename}</strong>',
attachment_ids=[attachment.id],
) )
return { return {

View File

@@ -208,9 +208,16 @@ class HrPayslip(models.Model):
('date_to', '<=', payslip.date_to), ('date_to', '<=', payslip.date_to),
('state', 'in', ['done', 'paid']), ('state', 'in', ['done', 'paid']),
] ]
# Include current payslip if it's in draft/verify state
if payslip.state in ['draft', 'verify']: 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) ytd_payslips = self.search(domain)
@@ -229,13 +236,13 @@ class HrPayslip(models.Model):
# Sum up specific rule amounts # Sum up specific rule amounts
for line in slip.line_ids: for line in slip.line_ids:
code = line.code or '' code = line.code or ''
if code == 'CPP': if code == 'CPP_EE':
ytd_cpp += abs(line.total or 0) ytd_cpp += abs(line.total or 0)
elif code == 'CPP2': elif code == 'CPP2_EE':
ytd_cpp2 += abs(line.total or 0) ytd_cpp2 += abs(line.total or 0)
elif code == 'EI': elif code == 'EI_EE':
ytd_ei += abs(line.total or 0) 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) ytd_income_tax += abs(line.total or 0)
payslip.ytd_gross = ytd_gross payslip.ytd_gross = ytd_gross
@@ -278,13 +285,13 @@ class HrPayslip(models.Model):
for line in payslip.line_ids: for line in payslip.line_ids:
code = line.code or '' code = line.code or ''
if code == 'CPP': if code == 'CPP_EE':
employee_cpp = abs(line.total or 0) employee_cpp = abs(line.total or 0)
elif code == 'CPP2': elif code == 'CPP2_EE':
employee_cpp2 = abs(line.total or 0) employee_cpp2 = abs(line.total or 0)
elif code == 'EI': elif code == 'EI_EE':
employee_ei = abs(line.total or 0) 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) employee_income_tax += abs(line.total or 0)
payslip.employee_cpp = employee_cpp 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 # Find all confirmed payslips in the period
payslips = self.env['hr.payslip'].search([ payslips = self.env['hr.payslip'].search([
('company_id', '=', self.company_id.id), ('company_id', '=', self.company_id.id),
('state', 'in', ['validated', 'paid']), ('state', 'in', ['done', 'paid']),
('date_from', '>=', self.period_start), ('date_from', '>=', self.period_start),
('date_to', '<=', self.period_end), ('date_to', '<=', self.period_end),
]) ])
@@ -212,7 +212,7 @@ class HrTaxRemittance(models.Model):
ei_ee += amount ei_ee += amount
elif code == 'EI_ER': elif code == 'EI_ER':
ei_er += amount ei_er += amount
elif code in ('FED_TAX', 'PROV_TAX'): elif code in ('FED_TAX', 'PROV_TAX', 'OHP'):
income_tax += amount income_tax += amount
self.write({ self.write({

View File

@@ -358,38 +358,40 @@ class PayrollCheque(models.Model):
get_ytd_amount('GROSS') or 0) get_ytd_amount('GROSS') or 0)
# Get vacation pay # Get vacation pay
vacation_pay_current = get_line_amount('VAC') or get_line_amount('VACATION') or 0 vacation_pay_current = get_line_amount('VAC_PAY') or 0
vacation_pay_ytd = get_ytd_amount('VAC') or get_ytd_amount('VACATION') or 0 vacation_pay_ytd = get_ytd_amount('VAC_PAY') or 0
# Get stat holiday pay # Get stat holiday pay
stat_pay_current = get_line_amount('STAT') or get_line_amount('STATHOLIDAY') or 0 stat_pay_current = get_line_amount('STAT_PAY') or 0
stat_pay_ytd = get_ytd_amount('STAT') or get_ytd_amount('STATHOLIDAY') or 0 stat_pay_ytd = get_ytd_amount('STAT_PAY') or 0
# Get taxes - these are negative in payslip, so use abs() # Get taxes - these are negative in payslip, so use abs()
# First try to get from payslip lines fed_tax_current = abs(get_line_amount('FED_TAX') or 0)
income_tax_current = abs(get_line_amount('FIT') or get_line_amount('INCOMETAX') or 0) prov_tax_current = abs(get_line_amount('PROV_TAX') or 0)
ei_current = abs(get_line_amount('EI_EMP') or get_line_amount('EI') or 0) ohp_current = abs(get_line_amount('OHP') or 0)
cpp_current = abs(get_line_amount('CPP_EMP') or get_line_amount('CPP') or 0) income_tax_current = fed_tax_current + prov_tax_current + ohp_current
cpp2_current = abs(get_line_amount('CPP2_EMP') or get_line_amount('CPP2') or 0) 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 # If individual line values are 0, calculate from payslip totals
total_taxes_from_lines = income_tax_current + ei_current + cpp_current + cpp2_current 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: 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 total_taxes_calculated = payslip.basic_wage - payslip.net_wage
if total_taxes_calculated > 0: 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 gross = payslip.basic_wage
cpp_current = min(gross * 0.0595, 3867.50) # 2025 CPP max cpp_current = min(gross * 0.0595, 4230.45 / 26)
ei_current = min(gross * 0.0163, 1049.12) # 2025 EI max ei_current = min(gross * 0.0163, 1123.07 / 26)
income_tax_current = max(0, total_taxes_calculated - cpp_current - ei_current) 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) fed_tax_ytd = abs(get_ytd_amount('FED_TAX') or 0)
ei_ytd = abs(get_ytd_amount('EI_EMP') or get_ytd_amount('EI') or 0) prov_tax_ytd = abs(get_ytd_amount('PROV_TAX') or 0)
cpp_ytd = abs(get_ytd_amount('CPP_EMP') or get_ytd_amount('CPP') or 0) ohp_ytd = abs(get_ytd_amount('OHP') or 0)
cpp2_ytd = abs(get_ytd_amount('CPP2_EMP') or get_ytd_amount('CPP2') 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 # Calculate totals
total_taxes_current = income_tax_current + ei_current + cpp_current + cpp2_current 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') @api.depends('gross_pay', 'employee_id')
def _compute_taxes(self): def _compute_taxes(self):
"""Calculate employee tax deductions.""" """Calculate employee tax deductions (preview estimate for bi-weekly)."""
PAY_PERIODS = 26
for entry in self: for entry in self:
if entry.gross_pay <= 0: if entry.gross_pay <= 0:
entry.income_tax = 0 entry.income_tax = 0
@@ -301,24 +302,79 @@ class PayrollEntry(models.TransientModel):
entry.total_employee_tax = 0 entry.total_employee_tax = 0
continue continue
# Get tax rates from parameters or use defaults
# These are simplified calculations - actual payroll uses full tax rules
gross = entry.gross_pay gross = entry.gross_pay
annual = gross * PAY_PERIODS
emp = entry.employee_id
# Simplified tax calculations (bi-weekly) is_cpp_exempt = getattr(emp, 'exempt_cpp', False)
# Income tax: ~15-20% average for Canadian employees is_ei_exempt = getattr(emp, 'exempt_ei', False)
entry.income_tax = round(gross * 0.128, 2) # Approximate federal + provincial is_fed_exempt = getattr(emp, 'exempt_federal_tax', False)
# EI: 1.64% of gross (2025 rate) up to maximum # Federal tax estimate using 2026 brackets + BPA phase-out
entry.employment_insurance = round(min(gross * 0.0164, 1049.12 / 26), 2) 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) fed_tax = 0
cpp_exempt = 3500 / 26 # Annual exemption / 26 pay periods prev = 0
pensionable = max(0, gross - cpp_exempt) for threshold, rate in fed_brackets:
entry.cpp = round(min(pensionable * 0.0595, 4034.10 / 26), 2) 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) # Ontario provincial estimate (default province)
entry.cpp2 = 0 # Only applies if earnings exceed $71,300/year 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 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': if line.category_id.code == 'BASIC':
emp_data[emp_key]['regular_pay'] += line.total or 0 emp_data[emp_key]['regular_pay'] += line.total or 0
if hasattr(line, 'code') and line.code: 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 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]['vacation_pay'] += line.total or 0
emp_data[emp_key]['total'] += getattr(slip, 'gross_wage', 0) 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_data = defaultdict(lambda: {'employee': 0, 'company': 0})
deduction_codes = { deduction_codes = {
'CPP': {'name': 'Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP_ER'}, 'CPP_EE': {'name': 'Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP_ER'},
'CPP2': {'name': 'Second Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP2_ER'}, 'CPP2_EE': {'name': 'Second Canada Pension Plan', 'type': 'Tax', 'employer_code': 'CPP2_ER'},
'EI': {'name': 'Employment Insurance', 'type': 'Tax', 'employer_code': 'EI_ER'}, 'EI_EE': {'name': 'Employment Insurance', 'type': 'Tax', 'employer_code': 'EI_ER'},
} }
for slip in payslips: for slip in payslips:
@@ -309,9 +309,9 @@ class PayrollReportDeductions(models.AbstractModel):
if line.code in deduction_codes: if line.code in deduction_codes:
deduction_data[line.code]['employee'] += abs(line.total or 0) deduction_data[line.code]['employee'] += abs(line.total or 0)
elif line.code.endswith('_ER'): elif line.code.endswith('_ER'):
base_code = line.code[:-3] ee_code = line.code.replace('_ER', '_EE')
if base_code in deduction_codes: if ee_code in deduction_codes:
deduction_data[base_code]['company'] += abs(line.total or 0) deduction_data[ee_code]['company'] += abs(line.total or 0)
lines = [] lines = []
for code, info in deduction_codes.items(): for code, info in deduction_codes.items():
@@ -378,8 +378,8 @@ class PayrollReportWorkersComp(models.AbstractModel):
continue continue
province = 'ON' # Default, would get from employee address province = 'ON' # Default, would get from employee address
if hasattr(slip.employee_id, 'province_of_employment'): if hasattr(slip.employee_id, 'home_province'):
province = getattr(slip.employee_id, 'province_of_employment', None) or 'ON' province = getattr(slip.employee_id, 'home_province', None) or 'ON'
province_data[province]['wages'] += getattr(slip, 'gross_wage', 0) or 0 province_data[province]['wages'] += getattr(slip, 'gross_wage', 0) or 0

View File

@@ -208,7 +208,7 @@ class PayrollReportPayrollDetails(models.AbstractModel):
# Tax breakdown # Tax breakdown
tax_lines = payslip.line_ids.filtered( 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: for line in tax_lines:
lines.append({ lines.append({

View File

@@ -49,12 +49,12 @@ class PayrollReportTaxLiability(models.AbstractModel):
# Calculate totals by tax type # Calculate totals by tax type
tax_totals = { tax_totals = {
'income_tax': {'name': _('Income Tax'), 'amount': 0, 'codes': ['FED_TAX', 'PROV_TAX']}, 'income_tax': {'name': _('Income Tax'), 'amount': 0, 'codes': ['FED_TAX', 'PROV_TAX', 'OHP']},
'ei_employee': {'name': _('Employment Insurance'), 'amount': 0, 'codes': ['EI']}, 'ei_employee': {'name': _('Employment Insurance'), 'amount': 0, 'codes': ['EI_EE']},
'ei_employer': {'name': _('Employment Insurance Employer'), 'amount': 0, 'codes': ['EI_ER']}, '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']}, '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']}, 'cpp2_employer': {'name': _('Second Canada Pension Plan Employer'), 'amount': 0, 'codes': ['CPP2_ER']},
} }
@@ -260,15 +260,15 @@ class PayrollReportTaxWageSummary(models.AbstractModel):
tax_data = [ tax_data = [
{ {
'name': _('Income Tax'), 'name': _('Income Tax'),
'codes': ['FED_TAX', 'PROV_TAX'], 'codes': ['FED_TAX', 'PROV_TAX', 'OHP'],
'total_wages': total_wages, 'total_wages': total_wages,
'excess_wages': 0, # No excess for income tax 'excess_wages': 0,
}, },
{ {
'name': _('Employment Insurance'), 'name': _('Employment Insurance'),
'codes': ['EI'], 'codes': ['EI_EE'],
'total_wages': total_wages, 'total_wages': total_wages,
'excess_wages': 0, # Would need to calculate based on max 'excess_wages': 0,
}, },
{ {
'name': _('Employment Insurance Employer'), 'name': _('Employment Insurance Employer'),
@@ -278,7 +278,7 @@ class PayrollReportTaxWageSummary(models.AbstractModel):
}, },
{ {
'name': _('Canada Pension Plan'), 'name': _('Canada Pension Plan'),
'codes': ['CPP'], 'codes': ['CPP_EE'],
'total_wages': total_wages, 'total_wages': total_wages,
'excess_wages': 0, 'excess_wages': 0,
}, },
@@ -290,7 +290,7 @@ class PayrollReportTaxWageSummary(models.AbstractModel):
}, },
{ {
'name': _('Second Canada Pension Plan'), 'name': _('Second Canada Pension Plan'),
'codes': ['CPP2'], 'codes': ['CPP2_EE'],
'total_wages': total_wages, 'total_wages': total_wages,
'excess_wages': 0, '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 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_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_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 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_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_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_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" string="Generate T4 Slips"
type="object" type="object"
class="btn-primary"/> class="btn-primary"/>
<button name="action_export_xml"
string="Export CRA XML"
type="object"
class="btn-primary"/>
<button name="action_fill_pdf" <button name="action_fill_pdf"
string="Fill PDF" string="Fill PDF"
type="object" type="object"
@@ -78,9 +82,12 @@
<field name="tax_year"/> <field name="tax_year"/>
<field name="cra_business_number"/> <field name="cra_business_number"/>
</group> </group>
<group string="Contact"> <group string="Contact / Transmitter">
<field name="contact_name"/> <field name="contact_name"/>
<field name="contact_phone"/> <field name="contact_phone"/>
<field name="contact_email"/>
<field name="transmitter_bn"/>
<field name="transmitter_name"/>
<field name="proprietor_sin"/> <field name="proprietor_sin"/>
<field name="filing_date"/> <field name="filing_date"/>
</group> </group>

View File

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