Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,380 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
# === Additional Fields for QuickBooks-style Paycheque Entry ===
cheque_id = fields.Many2one(
'payroll.cheque',
string='Cheque',
copy=False,
help='Linked cheque record for paper cheque payments',
)
cheque_number = fields.Char(
string='Cheque Number',
related='cheque_id.cheque_number',
store=True,
copy=False,
help='Cheque number for paper cheque payments',
)
cheque_state = fields.Selection(
related='cheque_id.state',
string='Cheque Status',
store=True,
)
memo = fields.Text(
string='Memo',
help='Internal notes for this paycheque',
)
paid_by = fields.Selection([
('cheque', 'Paper Cheque'),
('direct_deposit', 'Direct Deposit'),
], string='Payment Method', compute='_compute_paid_by', store=True, readonly=False)
@api.depends('employee_id', 'employee_id.payment_method')
def _compute_paid_by(self):
"""Set payment method from employee's default payment method."""
for payslip in self:
if payslip.employee_id and hasattr(payslip.employee_id, 'payment_method'):
payslip.paid_by = payslip.employee_id.payment_method or 'direct_deposit'
else:
payslip.paid_by = 'direct_deposit'
def action_print_cheque(self):
"""Print cheque for this payslip - always opens wizard to set/change cheque number."""
self.ensure_one()
if self.paid_by != 'cheque':
raise UserError(_("This payslip is not set to be paid by cheque."))
# Create cheque if not exists
if not self.cheque_id:
cheque = self.env['payroll.cheque'].create_from_payslip(self)
if cheque:
self.cheque_id = cheque.id
else:
raise UserError(_("Failed to create cheque. Check employee payment method."))
# Always open the cheque number wizard to allow changing the number
return {
'type': 'ir.actions.act_window',
'name': _('Set Cheque Number'),
'res_model': 'payroll.cheque.number.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_cheque_id': self.cheque_id.id,
},
}
def action_create_cheque(self):
"""Create a cheque for this payslip without printing."""
self.ensure_one()
if self.paid_by != 'cheque':
raise UserError(_("This payslip is not set to be paid by cheque."))
if self.cheque_id:
raise UserError(_("A cheque already exists for this payslip."))
cheque = self.env['payroll.cheque'].create_from_payslip(self)
if cheque:
self.cheque_id = cheque.id
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Cheque Created'),
'message': _('Cheque created for %s.') % self.employee_id.name,
'type': 'success',
'sticky': False,
}
}
else:
raise UserError(_("Failed to create cheque."))
# === YTD Computed Fields ===
ytd_gross = fields.Monetary(
string='YTD Gross',
compute='_compute_ytd_amounts',
currency_field='currency_id',
help='Year-to-date gross earnings',
)
ytd_cpp = fields.Monetary(
string='YTD CPP',
compute='_compute_ytd_amounts',
currency_field='currency_id',
help='Year-to-date CPP contributions (employee)',
)
ytd_cpp2 = fields.Monetary(
string='YTD CPP2',
compute='_compute_ytd_amounts',
currency_field='currency_id',
help='Year-to-date CPP2 contributions (employee)',
)
ytd_ei = fields.Monetary(
string='YTD EI',
compute='_compute_ytd_amounts',
currency_field='currency_id',
help='Year-to-date EI contributions (employee)',
)
ytd_income_tax = fields.Monetary(
string='YTD Income Tax',
compute='_compute_ytd_amounts',
currency_field='currency_id',
help='Year-to-date income tax withheld',
)
ytd_net = fields.Monetary(
string='YTD Net',
compute='_compute_ytd_amounts',
currency_field='currency_id',
help='Year-to-date net pay',
)
# === Employer Tax Totals (for display) ===
employer_cpp = fields.Monetary(
string='Employer CPP',
compute='_compute_employer_contributions',
currency_field='currency_id',
)
employer_cpp2 = fields.Monetary(
string='Employer CPP2',
compute='_compute_employer_contributions',
currency_field='currency_id',
)
employer_ei = fields.Monetary(
string='Employer EI',
compute='_compute_employer_contributions',
currency_field='currency_id',
)
total_employer_cost = fields.Monetary(
string='Total Employer Cost',
compute='_compute_employer_contributions',
currency_field='currency_id',
help='Total employer contributions (CPP + CPP2 + EI)',
)
# === Employee Tax Totals (for summary) ===
employee_cpp = fields.Monetary(
string='Employee CPP',
compute='_compute_employee_deductions',
currency_field='currency_id',
)
employee_cpp2 = fields.Monetary(
string='Employee CPP2',
compute='_compute_employee_deductions',
currency_field='currency_id',
)
employee_ei = fields.Monetary(
string='Employee EI',
compute='_compute_employee_deductions',
currency_field='currency_id',
)
employee_income_tax = fields.Monetary(
string='Income Tax',
compute='_compute_employee_deductions',
currency_field='currency_id',
)
total_employee_deductions = fields.Monetary(
string='Total Employee Deductions',
compute='_compute_employee_deductions',
currency_field='currency_id',
)
@api.depends('employee_id', 'date_from', 'line_ids', 'line_ids.total')
def _compute_ytd_amounts(self):
"""Calculate year-to-date amounts for each payslip"""
for payslip in self:
if not payslip.employee_id or not payslip.date_from:
payslip.ytd_gross = 0
payslip.ytd_cpp = 0
payslip.ytd_cpp2 = 0
payslip.ytd_ei = 0
payslip.ytd_income_tax = 0
payslip.ytd_net = 0
continue
# Get the start of the year
year_start = payslip.date_from.replace(month=1, day=1)
# Find all payslips for this employee in the same year, up to and including this one
domain = [
('employee_id', '=', payslip.employee_id.id),
('date_from', '>=', year_start),
('date_to', '<=', payslip.date_to),
('state', 'in', ['done', 'paid']),
]
# Include current payslip if it's in draft/verify state
if payslip.state in ['draft', 'verify']:
domain = ['|', ('id', '=', payslip.id)] + domain
ytd_payslips = self.search(domain)
# Calculate YTD totals
ytd_gross = 0
ytd_cpp = 0
ytd_cpp2 = 0
ytd_ei = 0
ytd_income_tax = 0
ytd_net = 0
for slip in ytd_payslips:
ytd_gross += slip.gross_wage or 0
ytd_net += slip.net_wage or 0
# Sum up specific rule amounts
for line in slip.line_ids:
code = line.code or ''
if code == 'CPP':
ytd_cpp += abs(line.total or 0)
elif code == 'CPP2':
ytd_cpp2 += abs(line.total or 0)
elif code == 'EI':
ytd_ei += abs(line.total or 0)
elif code in ['FED_TAX', 'PROV_TAX', 'INCOME_TAX']:
ytd_income_tax += abs(line.total or 0)
payslip.ytd_gross = ytd_gross
payslip.ytd_cpp = ytd_cpp
payslip.ytd_cpp2 = ytd_cpp2
payslip.ytd_ei = ytd_ei
payslip.ytd_income_tax = ytd_income_tax
payslip.ytd_net = ytd_net
@api.depends('line_ids', 'line_ids.total', 'line_ids.code')
def _compute_employer_contributions(self):
"""Calculate employer contribution totals from payslip lines"""
for payslip in self:
employer_cpp = 0
employer_cpp2 = 0
employer_ei = 0
for line in payslip.line_ids:
code = line.code or ''
if code == 'CPP_ER':
employer_cpp = abs(line.total or 0)
elif code == 'CPP2_ER':
employer_cpp2 = abs(line.total or 0)
elif code == 'EI_ER':
employer_ei = abs(line.total or 0)
payslip.employer_cpp = employer_cpp
payslip.employer_cpp2 = employer_cpp2
payslip.employer_ei = employer_ei
payslip.total_employer_cost = employer_cpp + employer_cpp2 + employer_ei
@api.depends('line_ids', 'line_ids.total', 'line_ids.code')
def _compute_employee_deductions(self):
"""Calculate employee deduction totals from payslip lines"""
for payslip in self:
employee_cpp = 0
employee_cpp2 = 0
employee_ei = 0
employee_income_tax = 0
for line in payslip.line_ids:
code = line.code or ''
if code == 'CPP':
employee_cpp = abs(line.total or 0)
elif code == 'CPP2':
employee_cpp2 = abs(line.total or 0)
elif code == 'EI':
employee_ei = abs(line.total or 0)
elif code in ['FED_TAX', 'PROV_TAX', 'INCOME_TAX']:
employee_income_tax += abs(line.total or 0)
payslip.employee_cpp = employee_cpp
payslip.employee_cpp2 = employee_cpp2
payslip.employee_ei = employee_ei
payslip.employee_income_tax = employee_income_tax
payslip.total_employee_deductions = (
employee_cpp + employee_cpp2 + employee_ei + employee_income_tax
)
# =========================================================================
# PAY TYPE IDENTIFICATION HELPERS (for T4 and ROE reporting)
# =========================================================================
@api.model
def _get_pay_type_from_code(self, code, category_code=None):
"""
Map salary rule code to pay type for ROE/T4 reporting.
Returns one of: 'salary', 'hourly', 'overtime', 'bonus', 'stat_holiday',
'commission', 'allowance', 'reimbursement', 'union_dues', 'other'
:param code: Salary rule code (e.g., 'OT_PAY', 'BONUS_PAY')
:param category_code: Category code (e.g., 'BASIC', 'ALW', 'DED')
:return: Pay type string
"""
if not code:
return 'other'
code_upper = code.upper()
# Direct code matches
if code_upper == 'OT_PAY':
return 'overtime'
elif code_upper == 'BONUS_PAY' or code_upper == 'BONUS':
return 'bonus'
elif code_upper == 'STAT_PAY' or code_upper == 'STAT_HOLIDAY':
return 'stat_holiday'
# Pattern matching for commissions
if 'COMMISSION' in code_upper or 'COMM' in code_upper:
return 'commission'
# Pattern matching for union dues
if 'UNION' in code_upper or 'DUES' in code_upper:
return 'union_dues'
# Pattern matching for reimbursements
if 'REIMBURSEMENT' in code_upper or 'REIMB' in code_upper:
return 'reimbursement'
# Pattern matching for allowances
if 'ALLOWANCE' in code_upper or 'ALW' in code_upper:
# Check if it's a reimbursement first
if 'REIMBURSEMENT' in code_upper or 'REIMB' in code_upper:
return 'reimbursement'
return 'allowance'
# Category-based identification
if category_code:
category_upper = category_code.upper()
if category_upper == 'BASIC':
return 'salary' # Could be salary or hourly, default to salary
elif category_upper == 'ALW':
# Already checked for allowance patterns above
return 'allowance'
elif category_upper == 'DED':
# Deductions - check if union dues
if 'UNION' in code_upper or 'DUES' in code_upper:
return 'union_dues'
return 'other'
return 'other'
@api.model
def _is_reimbursement(self, code, category_code=None):
"""
Check if salary rule code represents a reimbursement (non-taxable).
:param code: Salary rule code
:param category_code: Category code
:return: True if reimbursement, False otherwise
"""
if not code:
return False
code_upper = code.upper()
# Direct pattern matching
if 'REIMBURSEMENT' in code_upper or 'REIMB' in code_upper:
return True
return False