Initial commit
This commit is contained in:
546
fusion_payroll/models/payroll_config_settings.py
Normal file
546
fusion_payroll/models/payroll_config_settings.py
Normal file
@@ -0,0 +1,546 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user