# -*- 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