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,865 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from dateutil.relativedelta import relativedelta
class RunPayrollWizard(models.TransientModel):
_name = 'run.payroll.wizard'
_description = 'Run Payroll'
_rec_name = 'display_name'
display_name = fields.Char(compute='_compute_display_name')
@api.depends('pay_schedule', 'date_start', 'date_end')
def _compute_display_name(self):
schedule_labels = {
'weekly': 'Weekly',
'biweekly': 'Bi-Weekly',
'semi_monthly': 'Semi-Monthly',
'monthly': 'Monthly',
}
for wizard in self:
schedule = schedule_labels.get(wizard.pay_schedule, 'Bi-Weekly')
wizard.display_name = f"Run Payroll: {schedule}"
# === Wizard State ===
state = fields.Selection([
('entry', 'Enter Payroll'),
('preview', 'Preview Payroll'),
], string='State', default='entry')
# === Company & Schedule ===
company_id = fields.Many2one(
'res.company',
string='Company',
required=True,
default=lambda self: self.env.company,
)
# === Pay Period Selection (QuickBooks-like dropdown) ===
pay_period_id = fields.Many2one(
'payroll.pay.period',
string='Pay Period',
domain="[('company_id', '=', company_id)]",
)
pay_schedule = fields.Selection([
('weekly', 'Weekly'),
('biweekly', 'Bi-Weekly'),
('semi_monthly', 'Semi-Monthly'),
('monthly', 'Monthly'),
], string='Pay Schedule', required=True, default='biweekly')
# === Chart of Accounts (for journal entries) ===
bank_account_id = fields.Many2one(
'account.account',
string='Funding Account',
domain="[('account_type', 'in', ['asset_cash', 'liability_credit_card'])]",
help='Bank or credit card account to fund payroll',
)
# === Preview Summary Fields ===
total_payroll_cost = fields.Monetary(
string='Total Payroll Cost',
currency_field='currency_id',
compute='_compute_preview_totals',
)
change_from_last = fields.Float(
string='Change from Last (%)',
compute='_compute_preview_totals',
)
date_start = fields.Date(
string='Period Start',
required=True,
default=lambda self: self._default_date_start(),
)
date_end = fields.Date(
string='Period End',
required=True,
default=lambda self: self._default_date_end(),
)
pay_date = fields.Date(
string='Pay Date',
required=True,
default=lambda self: fields.Date.context_today(self) + relativedelta(days=7),
)
# Period display name for dropdown
period_display = fields.Char(
string='Period',
compute='_compute_period_display',
)
# === Employee Payroll Entries (QuickBooks-like grid) ===
entry_ids = fields.One2many(
'payroll.entry',
'wizard_id',
string='Payroll Entries',
)
# Summary totals
total_regular_hours = fields.Float(
string='Total Regular Hours',
compute='_compute_totals',
)
total_hours = fields.Float(
string='Total Hours',
compute='_compute_totals',
)
total_gross_pay = fields.Monetary(
string='Total Gross Pay',
currency_field='currency_id',
compute='_compute_totals',
)
total_net_pay = fields.Monetary(
string='Total Net Pay',
currency_field='currency_id',
compute='_compute_totals',
)
total_employee_taxes = fields.Monetary(
string='Total Employee Taxes',
currency_field='currency_id',
compute='_compute_totals',
)
total_employer_taxes = fields.Monetary(
string='Total Employer Taxes',
currency_field='currency_id',
compute='_compute_totals',
)
employee_count = fields.Integer(
string='Employees',
compute='_compute_totals',
)
selected_count = fields.Integer(
string='Selected',
compute='_compute_totals',
)
currency_id = fields.Many2one(
'res.currency',
string='Currency',
default=lambda self: self.env.company.currency_id,
)
# === Generated Payslips ===
payslip_run_id = fields.Many2one(
'hr.payslip.run',
string='Payslip Batch',
readonly=True,
)
payslip_count = fields.Integer(
string='Payslips Generated',
compute='_compute_payslip_count',
)
# Available periods for dropdown
available_period_ids = fields.Many2many(
'payroll.pay.period',
string='Available Periods',
compute='_compute_available_periods',
)
def _default_date_start(self):
"""Default to start of current pay period"""
today = fields.Date.context_today(self)
# Default to start of current bi-weekly period (Monday)
return today - relativedelta(days=today.weekday())
def _default_date_end(self):
"""Default to end of current pay period"""
today = fields.Date.context_today(self)
start = today - relativedelta(days=today.weekday())
return start + relativedelta(days=13)
@api.depends('date_start', 'date_end')
def _compute_period_display(self):
for wizard in self:
if wizard.date_start and wizard.date_end:
wizard.period_display = f"{wizard.date_start.strftime('%m.%d.%Y')} to {wizard.date_end.strftime('%m.%d.%Y')}"
else:
wizard.period_display = ''
@api.depends('company_id', 'pay_schedule')
def _compute_available_periods(self):
today = fields.Date.context_today(self)
six_months_ago = today - relativedelta(months=6)
for wizard in self:
# Get periods from last 6 months plus future periods
periods = self.env['payroll.pay.period'].search([
('company_id', '=', wizard.company_id.id),
('schedule_type', '=', wizard.pay_schedule),
'|',
('date_start', '>=', six_months_ago), # Past 6 months
('date_end', '>=', today), # Current and future
], order='date_start asc') # Sort ascending so current is near top
wizard.available_period_ids = periods
@api.depends('entry_ids', 'entry_ids.regular_hours', 'entry_ids.total_hours',
'entry_ids.gross_pay', 'entry_ids.net_pay',
'entry_ids.total_employee_tax', 'entry_ids.total_employer_tax')
def _compute_totals(self):
for wizard in self:
wizard.total_regular_hours = sum(wizard.entry_ids.mapped('regular_hours'))
wizard.total_hours = sum(wizard.entry_ids.mapped('total_hours'))
wizard.total_gross_pay = sum(wizard.entry_ids.mapped('gross_pay'))
wizard.total_net_pay = sum(wizard.entry_ids.mapped('net_pay'))
wizard.total_employee_taxes = sum(wizard.entry_ids.mapped('total_employee_tax'))
wizard.total_employer_taxes = sum(wizard.entry_ids.mapped('total_employer_tax'))
wizard.employee_count = len(wizard.entry_ids)
wizard.selected_count = len(wizard.entry_ids.filtered(lambda e: e.regular_hours > 0))
@api.depends('entry_ids', 'entry_ids.gross_pay', 'entry_ids.total_employer_tax')
def _compute_preview_totals(self):
for wizard in self:
gross = sum(wizard.entry_ids.mapped('gross_pay'))
employer_tax = sum(wizard.entry_ids.mapped('total_employer_tax'))
wizard.total_payroll_cost = gross + employer_tax
# Calculate change from last payroll
last_payroll = self.env['hr.payslip.run'].search([
('company_id', '=', wizard.company_id.id),
('state', 'in', ['close', 'paid']),
], order='date_end desc', limit=1)
if last_payroll and last_payroll.slip_ids:
last_total = sum(last_payroll.slip_ids.mapped('net_wage'))
if last_total > 0:
wizard.change_from_last = ((wizard.total_net_pay - last_total) / last_total) * 100
else:
wizard.change_from_last = 0
else:
wizard.change_from_last = 0
@api.depends('payslip_run_id')
def _compute_payslip_count(self):
for wizard in self:
if wizard.payslip_run_id:
wizard.payslip_count = len(wizard.payslip_run_id.slip_ids)
else:
wizard.payslip_count = 0
@api.onchange('pay_period_id')
def _onchange_pay_period_id(self):
"""Update dates when period is selected."""
if self.pay_period_id:
self.date_start = self.pay_period_id.date_start
self.date_end = self.pay_period_id.date_end
if self.pay_period_id.pay_date:
self.pay_date = self.pay_period_id.pay_date
@api.onchange('pay_schedule', 'date_start')
def _onchange_pay_schedule(self):
"""Adjust date_end based on pay schedule"""
if self.date_start and self.pay_schedule:
if self.pay_schedule == 'weekly':
self.date_end = self.date_start + relativedelta(days=6)
elif self.pay_schedule == 'biweekly':
self.date_end = self.date_start + relativedelta(days=13)
elif self.pay_schedule == 'semi_monthly':
if self.date_start.day <= 15:
self.date_end = self.date_start.replace(day=15)
else:
self.date_end = self.date_start + relativedelta(months=1, day=1) - relativedelta(days=1)
elif self.pay_schedule == 'monthly':
self.date_end = self.date_start + relativedelta(months=1, day=1) - relativedelta(days=1)
@api.onchange('company_id', 'pay_schedule')
def _onchange_load_employees(self):
"""Load employees when company or schedule changes."""
self._load_employees()
def _load_employees(self):
"""Load all active employees into entries."""
if not self.company_id:
return
# Clear existing entries
self.entry_ids = [(5, 0, 0)]
# Find active employees in the company
employees = self.env['hr.employee'].search([
('company_id', '=', self.company_id.id),
('active', '=', True),
])
entries = []
for employee in employees:
# Get payment method from Fusion Payroll settings
payment_method = getattr(employee, 'payment_method', 'cheque') or 'cheque'
# Get vacation pay percent from Fusion Payroll settings (vacation_rate field)
vacation_percent = getattr(employee, 'vacation_rate', 4.0) or 4.0
entry_vals = {
'employee_id': employee.id,
'payment_method': payment_method,
'vacation_pay_percent': vacation_percent,
'regular_hours': 0,
'stat_holiday_hours': 0,
}
entries.append((0, 0, entry_vals))
self.entry_ids = entries
def action_add_employee(self):
"""Open dialog to add an employee."""
return {
'type': 'ir.actions.act_window',
'name': _('Add Employee'),
'res_model': 'hr.employee',
'view_mode': 'kanban,list,form',
'target': 'new',
'context': {
'default_company_id': self.company_id.id,
'payroll_wizard_id': self.id,
},
}
def action_load_attendance(self):
"""Load attendance hours for all employees in the period."""
for entry in self.entry_ids:
entry.action_load_attendance_hours()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Attendance Loaded'),
'message': _('Loaded attendance hours for %d employees.') % len(self.entry_ids),
'type': 'success',
},
}
def action_preview_payroll(self):
"""Show the preview page with totals."""
self.ensure_one()
# Validate that at least one employee has hours
entries_with_pay = self.entry_ids.filtered(
lambda e: e.regular_hours > 0 or e.stat_holiday_hours > 0 or e.stat_pay_avg_daily_wage > 0
)
if not entries_with_pay:
raise UserError(_("No employees have hours entered. Please enter hours for at least one employee."))
# Set state to preview (this was incorrectly indented before)
self.state = 'preview'
return {
'type': 'ir.actions.act_window',
'name': _('Run Payroll'),
'res_model': 'run.payroll.wizard',
'res_id': self.id,
'view_mode': 'form',
'target': 'current',
'context': {'form_view_ref': 'fusion_payroll.run_payroll_wizard_view_form'},
}
def action_back_to_entry(self):
"""Go back to the entry page."""
self.ensure_one()
self.state = 'entry'
return {
'type': 'ir.actions.act_window',
'name': _('Run Payroll'),
'res_model': 'run.payroll.wizard',
'res_id': self.id,
'view_mode': 'form',
'target': 'current',
'context': {'form_view_ref': 'fusion_payroll.run_payroll_wizard_view_form'},
}
def action_save_for_later(self):
"""Save the payroll entries for later without processing."""
self.ensure_one()
# Update pay period status to in_progress if using periods
if self.pay_period_id:
self.pay_period_id.write({'state': 'in_progress'})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Payroll Saved'),
'message': _('Payroll entries have been saved. You can continue later.'),
'type': 'success',
'next': {'type': 'ir.actions.act_window_close'},
},
}
def action_submit_payroll(self):
"""Submit the payroll - create payslips and journal entries."""
self.ensure_one()
return self.action_generate_payslips()
def action_generate_payslips(self):
"""Generate payslips for all entries with hours."""
self.ensure_one()
# Get entries with hours
entries_with_pay = self.entry_ids.filtered(
lambda e: e.regular_hours > 0 or e.stat_holiday_hours > 0 or e.stat_pay_avg_daily_wage > 0
)
if not entries_with_pay:
raise UserError(_("No employees have hours entered. Please enter hours for at least one employee."))
# Create payslip batch
batch_name = f"Payroll {self.date_start.strftime('%Y-%m-%d')} to {self.date_end.strftime('%Y-%m-%d')}"
payslip_run = self.env['hr.payslip.run'].create({
'name': batch_name,
'date_start': self.date_start,
'date_end': self.date_end,
'company_id': self.company_id.id,
})
# Generate payslips for each entry
for entry in entries_with_pay:
# Generate payslip name
payslip_name = f"Payslip - {entry.employee_id.name} - {self.date_start.strftime('%Y/%m')}"
# Create payslip
payslip_vals = {
'name': payslip_name,
'employee_id': entry.employee_id.id,
'date_from': self.date_start,
'date_to': self.date_end,
'payslip_run_id': payslip_run.id,
'company_id': self.company_id.id,
}
# Add contract_id if employee has one
if hasattr(entry.employee_id, 'contract_id') and entry.employee_id.contract_id:
payslip_vals['contract_id'] = entry.employee_id.contract_id.id
# Create the payslip (this was incorrectly indented before)
payslip = self.env['hr.payslip'].create(payslip_vals)
# Add inputs from Fusion Payroll entry
self._add_fusion_payroll_inputs(payslip, entry)
# Compute sheet
payslip.compute_sheet()
# Override computed values with Fusion Payroll calculations
self._apply_fusion_payroll_values(payslip, entry)
self.payslip_run_id = payslip_run
# Update pay period status if using periods
if self.pay_period_id:
self.pay_period_id.write({
'state': 'in_progress',
'payslip_run_id': payslip_run.id,
})
# Create cheques for employees with payment method = 'cheque'
cheques_created = self._create_cheques_for_batch(payslip_run)
# If cheques were created, show print option
if cheques_created:
return self._action_payroll_complete_with_cheques(payslip_run, cheques_created)
return self.action_view_payslips()
def _add_fusion_payroll_inputs(self, payslip, entry):
"""Add salary inputs from Fusion Payroll entry to payslip."""
# Find or create input types
InputType = self.env['hr.payslip.input.type']
Input = self.env['hr.payslip.input']
# Regular Hours input
if entry.regular_hours > 0:
hours_type = InputType.search([('code', '=', 'HOURS')], limit=1)
if not hours_type:
hours_type = InputType.search([('code', '=', 'WORKED_HOURS')], limit=1)
if not hours_type:
hours_type = InputType.create({
'name': 'Worked Hours',
'code': 'HOURS',
})
Input.create({
'payslip_id': payslip.id,
'input_type_id': hours_type.id,
'amount': entry.regular_hours,
})
# Regular Pay (calculated amount)
if entry.regular_pay > 0:
reg_pay_type = InputType.search([('code', '=', 'REGPAY')], limit=1)
if not reg_pay_type:
reg_pay_type = InputType.create({
'name': 'Regular Pay',
'code': 'REGPAY',
})
Input.create({
'payslip_id': payslip.id,
'input_type_id': reg_pay_type.id,
'amount': entry.regular_pay,
})
# Vacation Pay
if entry.vacation_pay > 0:
vac_type = InputType.search([('code', '=', 'VAC')], limit=1)
if not vac_type:
vac_type = InputType.create({
'name': 'Vacation Pay',
'code': 'VAC',
})
Input.create({
'payslip_id': payslip.id,
'input_type_id': vac_type.id,
'amount': entry.vacation_pay,
})
# Stat Holiday Pay
if entry.stat_holiday_pay > 0:
stat_type = InputType.search([('code', '=', 'STAT')], limit=1)
if not stat_type:
stat_type = InputType.create({
'name': 'Stat Holiday Pay',
'code': 'STAT',
})
Input.create({
'payslip_id': payslip.id,
'input_type_id': stat_type.id,
'amount': entry.stat_holiday_pay,
})
# Stat Pay (Average Daily Wage)
if entry.stat_pay_avg_daily_wage > 0:
stat_avg_type = InputType.search([('code', '=', 'STATAVG')], limit=1)
if not stat_avg_type:
stat_avg_type = InputType.create({
'name': 'Stat Pay - Avg Daily Wage',
'code': 'STATAVG',
})
Input.create({
'payslip_id': payslip.id,
'input_type_id': stat_avg_type.id,
'amount': entry.stat_pay_avg_daily_wage,
})
def _apply_fusion_payroll_values(self, payslip, entry):
"""Apply Fusion Payroll calculated values to payslip lines."""
# Get or create salary rule categories
Category = self.env['hr.salary.rule.category']
# Find categories
gross_cat = Category.search([('code', '=', 'GROSS')], limit=1)
ded_cat = Category.search([('code', '=', 'DED')], limit=1)
net_cat = Category.search([('code', '=', 'NET')], limit=1)
# Update or create payslip lines with Fusion Payroll values
# This directly sets the computed amounts
# Update basic wage on payslip
payslip.write({
'basic_wage': entry.gross_pay,
'net_wage': entry.net_pay,
})
# Update payslip lines
for line in payslip.line_ids:
if line.code == 'BASIC' or line.code == 'GROSS':
line.write({'amount': entry.gross_pay, 'total': entry.gross_pay})
elif line.code == 'NET':
line.write({'amount': entry.net_pay, 'total': entry.net_pay})
# Create additional lines if they don't exist
self._ensure_payslip_lines(payslip, entry)
def _ensure_payslip_lines(self, payslip, entry):
"""Ensure all Fusion Payroll calculated values have corresponding payslip lines."""
Line = self.env['hr.payslip.line']
Rule = self.env['hr.salary.rule']
Category = self.env['hr.salary.rule.category']
# Get categories
gross_cat = Category.search([('code', '=', 'GROSS')], limit=1)
ded_cat = Category.search([('code', '=', 'DED')], limit=1)
alw_cat = Category.search([('code', '=', 'ALW')], limit=1)
# Helper to find or create rule
def get_or_create_rule(code, name, category, sequence=100):
rule = Rule.search([('code', '=', code)], limit=1)
if not rule and category:
struct = payslip.struct_id or self.env['hr.payroll.structure'].search([], limit=1)
rule = Rule.create({
'name': name,
'code': code,
'category_id': category.id,
'sequence': sequence,
'struct_id': struct.id if struct else False,
'amount_select': 'fix',
'amount_fix': 0,
})
return rule
# Check and add lines
existing_codes = payslip.line_ids.mapped('code')
# Regular Pay line
if 'REGPAY' not in existing_codes and entry.regular_pay > 0:
rule = get_or_create_rule('REGPAY', 'Regular Pay', gross_cat, 10)
if rule:
Line.create({
'slip_id': payslip.id,
'salary_rule_id': rule.id,
'name': 'Regular Pay',
'code': 'REGPAY',
'category_id': gross_cat.id if gross_cat else False,
'sequence': 10,
'quantity': entry.regular_hours,
'rate': entry.hourly_rate,
'amount': entry.regular_pay,
'total': entry.regular_pay,
})
# Vacation Pay line
if 'VAC' not in existing_codes and entry.vacation_pay > 0:
rule = get_or_create_rule('VAC', 'Vacation Pay', alw_cat or gross_cat, 20)
if rule:
Line.create({
'slip_id': payslip.id,
'salary_rule_id': rule.id,
'name': 'Vacation Pay',
'code': 'VAC',
'category_id': (alw_cat or gross_cat).id if (alw_cat or gross_cat) else False,
'sequence': 20,
'quantity': 1,
'rate': entry.vacation_pay_percent,
'amount': entry.vacation_pay,
'total': entry.vacation_pay,
})
# Stat Holiday Pay line
if 'STAT' not in existing_codes and entry.stat_holiday_pay > 0:
rule = get_or_create_rule('STAT', 'Stat Holiday Pay', alw_cat or gross_cat, 25)
if rule:
Line.create({
'slip_id': payslip.id,
'salary_rule_id': rule.id,
'name': 'Stat Holiday Pay',
'code': 'STAT',
'category_id': (alw_cat or gross_cat).id if (alw_cat or gross_cat) else False,
'sequence': 25,
'quantity': entry.stat_holiday_hours,
'rate': entry.hourly_rate,
'amount': entry.stat_holiday_pay,
'total': entry.stat_holiday_pay,
})
# Employee Tax lines (negative deductions)
if 'FIT' not in existing_codes and 'INCOMETAX' not in existing_codes and entry.income_tax > 0:
rule = get_or_create_rule('FIT', 'Federal Income Tax', ded_cat, 100)
if rule:
Line.create({
'slip_id': payslip.id,
'salary_rule_id': rule.id,
'name': 'Income Tax',
'code': 'FIT',
'category_id': ded_cat.id if ded_cat else False,
'sequence': 100,
'quantity': 1,
'rate': 100,
'amount': -entry.income_tax,
'total': -entry.income_tax,
})
if 'EI' not in existing_codes and 'EI_EMP' not in existing_codes and entry.employment_insurance > 0:
rule = get_or_create_rule('EI_EMP', 'Employment Insurance', ded_cat, 110)
if rule:
Line.create({
'slip_id': payslip.id,
'salary_rule_id': rule.id,
'name': 'Employment Insurance',
'code': 'EI_EMP',
'category_id': ded_cat.id if ded_cat else False,
'sequence': 110,
'quantity': 1,
'rate': 100,
'amount': -entry.employment_insurance,
'total': -entry.employment_insurance,
})
if 'CPP' not in existing_codes and 'CPP_EMP' not in existing_codes and entry.cpp > 0:
rule = get_or_create_rule('CPP_EMP', 'Canada Pension Plan', ded_cat, 120)
if rule:
Line.create({
'slip_id': payslip.id,
'salary_rule_id': rule.id,
'name': 'Canada Pension Plan',
'code': 'CPP_EMP',
'category_id': ded_cat.id if ded_cat else False,
'sequence': 120,
'quantity': 1,
'rate': 100,
'amount': -entry.cpp,
'total': -entry.cpp,
})
if 'CPP2' not in existing_codes and 'CPP2_EMP' not in existing_codes and entry.cpp2 > 0:
rule = get_or_create_rule('CPP2_EMP', 'Second CPP', ded_cat, 125)
if rule:
Line.create({
'slip_id': payslip.id,
'salary_rule_id': rule.id,
'name': 'Second Canada Pension Plan',
'code': 'CPP2_EMP',
'category_id': ded_cat.id if ded_cat else False,
'sequence': 125,
'quantity': 1,
'rate': 100,
'amount': -entry.cpp2,
'total': -entry.cpp2,
})
def _create_cheques_for_batch(self, payslip_run):
"""Create cheque records for employees paid by cheque."""
cheques = self.env['payroll.cheque']
for payslip in payslip_run.slip_ids:
# Check if employee's payment method is cheque
if hasattr(payslip.employee_id, 'payment_method') and \
payslip.employee_id.payment_method == 'cheque':
cheque = self.env['payroll.cheque'].create_from_payslip(payslip)
if cheque:
cheques |= cheque
return cheques
def _action_payroll_complete_with_cheques(self, payslip_run, cheques):
"""Show completion wizard with option to print cheques."""
return {
'type': 'ir.actions.act_window',
'name': _('Payroll Complete'),
'res_model': 'payroll.cheque.print.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_payslip_run_id': payslip_run.id,
'default_cheque_ids': [(6, 0, cheques.ids)],
'default_cheque_count': len(cheques),
},
}
def action_print_cheques(self):
"""Print all cheques for the current batch."""
self.ensure_one()
if not self.payslip_run_id:
raise UserError(_("No payslips have been generated yet."))
cheques = self.env['payroll.cheque'].search([
('payslip_run_id', '=', self.payslip_run_id.id),
])
if not cheques:
raise UserError(_("No cheques found for this payroll batch."))
# Assign numbers and mark as printed
for cheque in cheques:
cheque.action_assign_number()
return self.env.ref('fusion_payroll.action_report_payroll_cheque').report_action(cheques)
def action_view_payslips(self):
"""View generated payslips."""
self.ensure_one()
if not self.payslip_run_id:
raise UserError(_("No payslips have been generated yet."))
return {
'type': 'ir.actions.act_window',
'name': _('Payslip Batch'),
'res_model': 'hr.payslip.run',
'res_id': self.payslip_run_id.id,
'view_mode': 'form',
'target': 'current',
}
def action_confirm_payslips(self):
"""Confirm all generated payslips."""
self.ensure_one()
if not self.payslip_run_id:
raise UserError(_("No payslips have been generated yet."))
# Confirm all payslips in the batch
for payslip in self.payslip_run_id.slip_ids:
if payslip.state == 'draft':
payslip.action_payslip_done()
# Update pay period status
if self.pay_period_id:
self.pay_period_id.write({'state': 'paid'})
return self.action_view_payslips()
@api.model
def action_open_run_payroll(self):
"""Open the Run Payroll wizard with pre-loaded data."""
# Check/create pay period settings
settings = self.env['payroll.pay.period.settings'].get_or_create_settings()
# Auto-generate periods for past 6 months and future 6 months
self.env['payroll.pay.period'].auto_generate_periods_if_needed(
company_id=self.env.company.id,
schedule_type=settings.schedule_type,
)
# Find the current pay period (today falls within it)
today = fields.Date.context_today(self)
current_period = self.env['payroll.pay.period'].search([
('company_id', '=', self.env.company.id),
('schedule_type', '=', settings.schedule_type),
('date_start', '<=', today),
('date_end', '>=', today),
], limit=1)
# If no current period, find the next upcoming one
if not current_period:
current_period = self.env['payroll.pay.period'].search([
('company_id', '=', self.env.company.id),
('schedule_type', '=', settings.schedule_type),
('date_start', '>', today),
], order='date_start asc', limit=1)
# Create wizard with default period
wizard_vals = {
'company_id': self.env.company.id,
'pay_schedule': settings.schedule_type,
}
if current_period:
wizard_vals['pay_period_id'] = current_period.id
wizard_vals['date_start'] = current_period.date_start
wizard_vals['date_end'] = current_period.date_end
wizard_vals['pay_date'] = current_period.pay_date
wizard = self.create(wizard_vals)
wizard._load_employees()
schedule_labels = {
'weekly': 'Weekly',
'biweekly': 'Bi-Weekly',
'semi_monthly': 'Semi-Monthly',
'monthly': 'Monthly',
}
schedule = schedule_labels.get(settings.schedule_type, 'Bi-Weekly')
return {
'type': 'ir.actions.act_window',
'name': _('Run Payroll: %s') % schedule,
'res_model': 'run.payroll.wizard',
'res_id': wizard.id,
'view_mode': 'form',
'target': 'current',
'context': {'form_view_ref': 'fusion_payroll.run_payroll_wizard_view_form'},
}