Initial commit
This commit is contained in:
380
fusion_payroll/models/hr_payslip.py
Normal file
380
fusion_payroll/models/hr_payslip.py
Normal 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
|
||||
Reference in New Issue
Block a user