Files
Odoo-Modules/fusion_payroll/models/payroll_config_settings.py
2026-02-22 01:22:18 -05:00

547 lines
22 KiB
Python

# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from datetime import date
class PayrollConfigSettings(models.Model):
"""
Payroll Configuration Settings
One record per company storing all payroll-related settings.
"""
_name = 'payroll.config.settings'
_description = 'Payroll Configuration Settings'
_rec_name = 'company_id'
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
default=lambda self: self.env.company,
ondelete='cascade',
)
@api.model
def default_get(self, fields_list):
"""Ensure we get or create settings for the current company."""
res = super().default_get(fields_list)
company_id = self.env.context.get('default_company_id') or self.env.company.id
if 'company_id' in fields_list:
res['company_id'] = company_id
return res
@api.model_create_multi
def create(self, vals_list):
"""Ensure only one settings record per company."""
records = self.browse()
for vals in vals_list:
company_id = vals.get('company_id') or self.env.company.id
# Check if settings already exist for this company
existing = self.search([('company_id', '=', company_id)], limit=1)
if existing:
# Update existing instead of creating new
existing.write(vals)
records |= existing
else:
# Create new record
new_record = super(PayrollConfigSettings, self).create([vals])
records |= new_record
return records
currency_id = fields.Many2one(
related='company_id.currency_id',
string='Currency',
)
# =========================================================================
# GENERAL TAX INFO
# =========================================================================
company_legal_name = fields.Char(
string='Company Legal Name',
help='Legal name of the company (may differ from trade/DBA name)',
)
company_legal_street = fields.Char(
string='Street Address',
help='Legal address street',
)
company_legal_street2 = fields.Char(
string='Street Address 2',
)
company_legal_city = fields.Char(
string='City',
)
company_legal_country_id = fields.Many2one(
'res.country',
string='Country',
default=lambda self: self.env.ref('base.ca', raise_if_not_found=False),
)
company_legal_state_id = fields.Many2one(
'res.country.state',
string='Province',
domain="[('country_id', '=?', company_legal_country_id)]",
)
@api.onchange('company_legal_country_id')
def _onchange_company_legal_country_id(self):
"""Clear state when country changes."""
if self.company_legal_country_id and self.company_legal_state_id:
if self.company_legal_state_id.country_id != self.company_legal_country_id:
self.company_legal_state_id = False
company_legal_zip = fields.Char(
string='Postal Code',
)
# =========================================================================
# CONTACT INFORMATION
# =========================================================================
payroll_contact_first_name = fields.Char(
string='First Name',
help='First name of the primary payroll contact',
)
payroll_contact_last_name = fields.Char(
string='Last Name',
help='Last name of the primary payroll contact',
)
payroll_contact_phone = fields.Char(
string='Business Phone',
help='Business phone number for payroll contact',
)
payroll_contact_email = fields.Char(
string='Email Address',
help='Email address for payroll contact (required for ROE and T4 forms)',
)
# =========================================================================
# FEDERAL TAX INFO
# =========================================================================
cra_business_number = fields.Char(
string='CRA Business Number',
help='Canada Revenue Agency Business Number',
)
cra_reference_number = fields.Char(
string='Reference Number',
help='CRA Reference Number (RP prefix)',
default='0001',
)
cra_owner1_sin = fields.Char(
string='SIN: Owner 1',
help='Social Insurance Number for Owner 1',
)
cra_owner2_sin = fields.Char(
string='SIN: Owner 2',
help='Social Insurance Number for Owner 2 (optional)',
)
cra_representative_rac = fields.Char(
string='Representative Identifier (RAC)',
help='CRA Representative Authorization Code (optional)',
)
federal_tax_payment_frequency = fields.Selection([
('monthly', 'Monthly'),
('quarterly', 'Quarterly'),
('annually', 'Annually'),
], string='Payment Frequency', default='monthly')
federal_tax_effective_date = fields.Date(
string='Effective Date',
help='Date when this payment frequency became effective',
)
federal_tax_form_type = fields.Char(
string='Form Type',
default='Form PD7A',
help='CRA form type (e.g., Form PD7A)',
)
federal_tax_display = fields.Char(
string='Current Schedule',
compute='_compute_federal_tax_display',
help='Display format: "Form PD7A, paying monthly since 01/01/2019"',
)
# =========================================================================
# PROVINCIAL TAX INFO (One2many to payment schedules)
# =========================================================================
provincial_tax_schedule_ids = fields.One2many(
'payroll.tax.payment.schedule',
'config_id',
string='Provincial Tax Payment Schedules',
help='Date-effective payment schedules by province',
)
# =========================================================================
# EMAIL NOTIFICATIONS
# =========================================================================
notification_email_primary_ids = fields.Many2many(
'res.partner',
'payroll_config_notification_primary_rel',
'config_id',
'partner_id',
string='Primary Notification Recipients',
help='Contacts who will receive primary payroll notifications',
)
notification_send_to_options = [
('you', 'Send to you'),
('accountants', 'Send to accountant(s)'),
('both', 'Send to you and accountant(s)'),
]
notification_setup_send_to = fields.Selection(
notification_send_to_options,
string='Setup Notifications',
default='both',
)
notification_form_filing_send_to = fields.Selection(
notification_send_to_options,
string='Form Filing Notifications',
default='both',
)
notification_payday_send_to = fields.Selection(
notification_send_to_options,
string='Payday Notifications',
default='both',
)
notification_tax_send_to = fields.Selection(
notification_send_to_options,
string='Tax Notifications',
default='both',
)
notification_payday_reminders_send_to = fields.Selection(
notification_send_to_options,
string='Payday Reminders',
default='both',
)
notification_tax_setup_reminders_send_to = fields.Selection(
notification_send_to_options,
string='Tax Setup Reminders',
default='both',
)
# =========================================================================
# PRINTING PREFERENCES
# =========================================================================
print_preference = fields.Selection([
('pay_stubs_only', 'Pay stubs only'),
('paycheques_and_stubs', 'Paycheques and pay stubs on QuickBooks-compatible cheque paper'),
], string='Print Preference', default='paycheques_and_stubs')
paystub_layout = fields.Selection([
('one_pay_stub', 'Paycheque and 1 pay stub'),
('two_pay_stubs', 'Paycheque and 2 pay stubs'),
], string='Paystub Layout',
help='Number of pay stubs per paycheque',
default='two_pay_stubs',
)
show_accrued_vacation_hours = fields.Boolean(
string='Show Accrued Vacation Hours on Pay Stub',
default=True,
)
show_accrued_vacation_balance = fields.Boolean(
string='Show Accrued Vacation Balance on Pay Stub',
default=False,
)
# =========================================================================
# DIRECT DEPOSIT
# =========================================================================
direct_deposit_funding_time = fields.Selection([
('1-day', '1-day'),
('2-day', '2-day'),
('3-day', '3-day'),
], string='Funding Time', default='2-day',
help='Time for direct deposit funds to be available',
)
direct_deposit_funding_limit = fields.Monetary(
string='Funding Limit',
currency_field='currency_id',
default=0.0,
help='Maximum amount per payroll for direct deposit (0 = no limit)',
)
direct_deposit_funding_period_days = fields.Integer(
string='Funding Period (Days)',
default=6,
help='Period in days for funding limit calculation',
)
# =========================================================================
# BANK ACCOUNTS
# =========================================================================
payroll_bank_account_id = fields.Many2one(
'account.journal',
string='Payroll Bank Account',
domain="[('type', 'in', ('bank', 'cash')), ('company_id', '=', company_id)]",
help='Bank or cash journal used for payroll payments',
ondelete='set null',
)
@api.onchange('company_id')
def _onchange_company_id(self):
"""Update journal and account domains when company changes."""
if self.company_id:
return {
'domain': {
'payroll_bank_account_id': [('type', 'in', ('bank', 'cash')), ('company_id', '=', self.company_id.id)],
'account_bank_account_id': [('account_type', '=', 'asset_cash'), ('company_ids', 'parent_of', self.company_id.id)],
'account_wage_expense_id': [('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', self.company_id.id)],
'account_employer_tax_expense_id': [('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', self.company_id.id)],
'account_federal_tax_liability_id': [('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', self.company_id.id)],
'account_ontario_tax_liability_id': [('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', self.company_id.id)],
'account_vacation_pay_liability_id': [('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', self.company_id.id)],
}
}
else:
return {
'domain': {
'payroll_bank_account_id': [('type', 'in', ('bank', 'cash'))],
'account_bank_account_id': [('account_type', '=', 'asset_cash')],
'account_wage_expense_id': [('account_type', 'in', ('expense', 'liability_current'))],
'account_employer_tax_expense_id': [('account_type', 'in', ('expense', 'liability_current'))],
'account_federal_tax_liability_id': [('account_type', 'in', ('expense', 'liability_current'))],
'account_ontario_tax_liability_id': [('account_type', 'in', ('expense', 'liability_current'))],
'account_vacation_pay_liability_id': [('account_type', 'in', ('expense', 'liability_current'))],
}
}
payroll_principal_officer_info = fields.Text(
string='Principal Officer Information',
help='Information about the principal officer for payroll',
)
# =========================================================================
# WORKERS' COMPENSATION
# =========================================================================
workers_comp_province = fields.Selection([
('AB', 'Alberta'),
('BC', 'British Columbia'),
('MB', 'Manitoba'),
('NB', 'New Brunswick'),
('NL', 'Newfoundland and Labrador'),
('NS', 'Nova Scotia'),
('NT', 'Northwest Territories'),
('NU', 'Nunavut'),
('ON', 'Ontario'),
('PE', 'Prince Edward Island'),
('QC', 'Quebec'),
('SK', 'Saskatchewan'),
('YT', 'Yukon'),
], string='Province', help='Province for workers\' compensation')
workers_comp_class = fields.Char(
string='Workers\' Comp Class',
help='Workers\' compensation class code',
)
workers_comp_account_number = fields.Char(
string='Workers\' Comp Account Number',
help='Workers\' compensation account number',
)
# =========================================================================
# ACCOUNTING PREFERENCES (Optional - requires account module)
# =========================================================================
account_bank_account_id = fields.Many2one(
'account.account',
string='Paycheque and Payroll Tax Payments',
domain="[('account_type', '=', 'asset_cash'), ('company_ids', 'parent_of', company_id)]",
help='Bank and cash account in chart of accounts for payroll',
ondelete='set null',
)
account_wage_expense_id = fields.Many2one(
'account.account',
string='Wage Expenses',
domain="[('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', company_id)]",
help='Account for wage expenses (expense or current liability)',
ondelete='set null',
)
account_employer_tax_expense_id = fields.Many2one(
'account.account',
string='Employer Tax Expenses',
domain="[('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', company_id)]",
help='Account for employer tax expenses (expense or current liability)',
ondelete='set null',
)
account_federal_tax_liability_id = fields.Many2one(
'account.account',
string='Federal Tax Liability',
domain="[('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', company_id)]",
help='Account for federal tax liabilities (expense or current liability)',
ondelete='set null',
)
account_ontario_tax_liability_id = fields.Many2one(
'account.account',
string='Ontario Tax Liability',
domain="[('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', company_id)]",
help='Account for Ontario tax liabilities (expense or current liability)',
ondelete='set null',
)
account_vacation_pay_liability_id = fields.Many2one(
'account.account',
string='Vacation Pay Liability',
domain="[('account_type', 'in', ('expense', 'liability_current')), ('company_ids', 'parent_of', company_id)]",
help='Account for vacation pay liabilities (expense or current liability)',
ondelete='set null',
)
account_class_tracking = fields.Selection([
('none', 'I don\'t use classes for payroll transactions'),
('same', 'I use the same class for all employees'),
('different', 'I use different classes for different employees'),
], string='Class Tracking',
default='none',
help='How do you want to track classes for payroll transactions in QuickBooks?',
)
# =========================================================================
# AUTO PAYROLL
# =========================================================================
auto_payroll_enabled = fields.Boolean(
string='Auto Payroll Enabled',
default=False,
compute='_compute_auto_payroll',
help='Whether auto payroll is currently enabled (feature not implemented)',
)
auto_payroll_ineligibility_reason = fields.Char(
string='Ineligibility Reason',
compute='_compute_auto_payroll',
help='Reason why auto payroll is not available',
)
# =========================================================================
# WORK LOCATIONS (One2many)
# =========================================================================
work_location_ids = fields.One2many(
'payroll.work.location',
'company_id',
string='Work Locations',
)
# =========================================================================
# COMPUTED FIELDS
# =========================================================================
@api.depends('federal_tax_form_type', 'federal_tax_payment_frequency', 'federal_tax_effective_date')
def _compute_federal_tax_display(self):
"""Format federal tax schedule display."""
for record in self:
if record.federal_tax_effective_date and record.federal_tax_payment_frequency:
freq_map = {
'monthly': 'monthly',
'quarterly': 'quarterly',
'annually': 'annually',
}
freq = freq_map.get(record.federal_tax_payment_frequency, '')
form_type = record.federal_tax_form_type or 'Form PD7A'
date_str = record.federal_tax_effective_date.strftime('%m/%d/%Y')
record.federal_tax_display = f"{form_type}, paying {freq} since {date_str}"
else:
record.federal_tax_display = ''
@api.depends('company_id')
def _compute_auto_payroll(self):
"""Compute auto payroll eligibility (currently always False)."""
for record in self:
# Feature not implemented yet
record.auto_payroll_enabled = False
# Check for employees enrolled (placeholder logic)
employee_count = self.env['hr.employee'].search_count([
('company_id', '=', record.company_id.id),
])
if employee_count == 0:
record.auto_payroll_ineligibility_reason = 'No employees enrolled'
else:
record.auto_payroll_ineligibility_reason = 'Feature not yet implemented'
# =========================================================================
# HELPER METHODS
# =========================================================================
def get_payroll_contact_name(self):
"""Return full name of payroll contact."""
self.ensure_one()
parts = []
if self.payroll_contact_first_name:
parts.append(self.payroll_contact_first_name)
if self.payroll_contact_last_name:
parts.append(self.payroll_contact_last_name)
return ' '.join(parts) if parts else ''
def get_primary_notification_emails(self):
"""Return comma-separated list of emails from primary notification recipients."""
self.ensure_one()
emails = []
for partner in self.notification_email_primary_ids:
if partner.email:
emails.append(partner.email)
return ', '.join(emails) if emails else ''
def get_primary_notification_partners(self):
"""Return recordset of partners who should receive primary notifications."""
self.ensure_one()
return self.notification_email_primary_ids.filtered(lambda p: p.email)
def get_cra_payroll_account_number(self):
"""Return formatted CRA payroll account number."""
self.ensure_one()
if self.cra_business_number and self.cra_reference_number:
return f"{self.cra_business_number}RP{self.cra_reference_number.zfill(4)}"
return ''
def get_current_tax_schedule(self, province, check_date=None):
"""Get current tax payment schedule for a province."""
self.ensure_one()
if not check_date:
check_date = date.today()
schedule = self.provincial_tax_schedule_ids.filtered(
lambda s: s.province == province and s.is_current
)
return schedule[0] if schedule else False
_sql_constraints = [
('unique_company', 'unique(company_id)', 'Only one payroll settings record is allowed per company.'),
]
@api.model
def get_settings(self, company_id=None):
"""Get or create settings for a company."""
if not company_id:
company_id = self.env.company.id
settings = self.search([('company_id', '=', company_id)], limit=1)
if not settings:
settings = self.create({'company_id': company_id})
return settings
def action_save(self):
"""Save settings."""
self.ensure_one()
# Settings are auto-saved, this is just for the button
return True
def action_edit_federal_tax(self):
"""Toggle edit mode for federal tax fields."""
self.ensure_one()
# In a real implementation, this would toggle visibility of edit fields
# For now, fields are always editable
return True
def action_align_printer(self):
"""Action for printer alignment (placeholder)."""
self.ensure_one()
# This would typically open a wizard or generate a test print
# For now, just return a notification
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Printer Alignment'),
'message': _('Printer alignment feature will be implemented in a future update.'),
'type': 'info',
},
}
def action_open_location(self):
"""Open work location form (used from tree view)."""
# This is called from the tree view button
# The actual location opening is handled by the tree view's default behavior
return True
@api.model
def name_get(self):
"""Return display name with company."""
result = []
for record in self:
name = f"Payroll Settings - {record.company_id.name}"
result.append((record.id, name))
return result