313 lines
11 KiB
Python
313 lines
11 KiB
Python
from odoo import models, fields, api, _
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from collections import defaultdict
|
|
from functools import reduce
|
|
|
|
|
|
|
|
class HrPayslipRun(models.Model):
|
|
_inherit = 'hr.payslip.run'
|
|
|
|
@api.model
|
|
def get_consolidated_attendance_data(self, payslip_run_id):
|
|
"""
|
|
Returns consolidated attendance and leave data for all employees in the payslip run
|
|
"""
|
|
# Get all payslips in this batch
|
|
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
|
|
|
|
if not payslips:
|
|
return []
|
|
|
|
result = []
|
|
for slip in payslips:
|
|
employee = slip.employee_id
|
|
contract = slip.contract_id
|
|
|
|
# Get attendance data
|
|
attendance_days = self._get_attendance_days(slip)
|
|
worked_days = self._get_worked_days(slip)
|
|
leave_days = self._get_leave_days(slip)
|
|
lop_days = self._get_lop_days(slip)
|
|
|
|
# Get leave balances
|
|
leave_balances = self._get_leave_balances(employee,slip.date_from,slip.date_to)
|
|
leave_taken = self._get_leave_taken(slip)
|
|
|
|
result.append({
|
|
'id': slip.id,
|
|
'employee_id': (employee.id, employee.name),
|
|
'employee_code': employee.employee_id or '',
|
|
'department_id': (employee.department_id.id,
|
|
employee.department_id.name) if employee.department_id else False,
|
|
'total_days': (slip.date_to - slip.date_from).days + 1,
|
|
'worked_days': worked_days,
|
|
'attendance_days': attendance_days,
|
|
'leave_days': leave_days,
|
|
'lop_days': lop_days,
|
|
'doj':employee.doj,
|
|
'birthday':employee.birthday,
|
|
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
|
|
'sick_leave_balance': leave_balances.get('LEAVE110', 0),
|
|
'casual_leave_balance': leave_balances.get('LEAVE120', 0),
|
|
'privilege_leave_balance': leave_balances.get('LEAVE100', 0),
|
|
'sick_leave_taken': leave_taken.get('sick', 0),
|
|
'casual_leave_taken': leave_taken.get('casual', 0),
|
|
'privilege_leave_taken': leave_taken.get('privilege', 0),
|
|
'state': slip.state,
|
|
'lines':self.get_payslip_lines_data(slip),
|
|
})
|
|
|
|
return result
|
|
|
|
@api.model
|
|
def sub_columns(self,payslip_run_id):
|
|
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
|
|
|
|
names = payslips.line_ids.filtered(lambda x:x.amount != 0)
|
|
code_name_dict = {line.code+line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code : line.name for line in names}
|
|
|
|
columns = []
|
|
for code, name in code_name_dict.items():
|
|
columns.append({
|
|
'title': name,
|
|
'dataIndx': code,
|
|
'width': 150,
|
|
'editable': False,
|
|
'summary': {'type': "sum_"},
|
|
})
|
|
return columns
|
|
|
|
def save_consolidated_attendance_data(self, payslip_run_id, data):
|
|
"""
|
|
Saves the edited attendance and leave data from the grid
|
|
"""
|
|
self.ensure_one()
|
|
|
|
for item in data:
|
|
slip = self.env['hr.payslip'].browse(item['id'])
|
|
if slip.state != 'draft':
|
|
raise UserError(_("Cannot edit payslip in %s state") % slip.state)
|
|
|
|
# Update LOP days
|
|
if 'lop_days' in item:
|
|
self._update_lop_days(slip, float(item['lop_days']))
|
|
|
|
# Update leave days taken
|
|
leave_updates = {}
|
|
if 'sick_leave_taken' in item:
|
|
leave_updates['sick'] = float(item['sick_leave_taken'])
|
|
if 'casual_leave_taken' in item:
|
|
leave_updates['casual'] = float(item['casual_leave_taken'])
|
|
if 'privilege_leave_taken' in item:
|
|
leave_updates['privilege'] = float(item['privilege_leave_taken'])
|
|
|
|
if leave_updates:
|
|
self._update_leave_taken(slip, leave_updates)
|
|
|
|
return True
|
|
|
|
def recalculate_lop_days(self, payslip_run_id):
|
|
"""
|
|
Recalculates LOP days for all payslips in the batch based on attendance
|
|
"""
|
|
self.ensure_one()
|
|
payslips = self.env['hr.payslip'].search([
|
|
('payslip_run_id', '=', payslip_run_id),
|
|
('state', '=', 'draft')
|
|
])
|
|
|
|
for slip in payslips:
|
|
attendance_days = self._get_attendance_days(slip)
|
|
expected_days = (slip.date_to - slip.date_from).days + 1
|
|
lop_days = expected_days - attendance_days
|
|
self._update_lop_days(slip, lop_days)
|
|
|
|
return True
|
|
|
|
def validate_all_attendance_data(self, payslip_run_id):
|
|
"""
|
|
Marks all payslips in the batch as validated
|
|
"""
|
|
self.ensure_one()
|
|
payslips = self.env['hr.payslip'].search([
|
|
('payslip_run_id', '=', payslip_run_id),
|
|
('state', '=', 'draft')
|
|
])
|
|
|
|
if not payslips:
|
|
raise UserError(_("No draft payslips found in this batch"))
|
|
|
|
payslips.write({'state': 'verify'})
|
|
return True
|
|
|
|
# Helper methods
|
|
def _get_attendance_days(self, payslip):
|
|
"""
|
|
Returns number of days employee was present (based on attendance records)
|
|
"""
|
|
attendance_records = self.env['hr.attendance'].search([
|
|
('employee_id', '=', payslip.employee_id.id),
|
|
('check_in', '>=', payslip.date_from),
|
|
('check_in', '<=', payslip.date_to)
|
|
])
|
|
|
|
# Group by day
|
|
unique_days = set()
|
|
for att in attendance_records:
|
|
unique_days.add(att.check_in.date())
|
|
|
|
return len(unique_days)
|
|
|
|
def _get_worked_days(self, payslip):
|
|
"""
|
|
Returns number of working days (excluding weekends and holidays)
|
|
"""
|
|
return payslip._get_worked_days_line_number_of_days('WORK100') # Assuming WORK100 is your work code
|
|
|
|
def get_payslip_lines_data(self, payslip_id):
|
|
list = []
|
|
for line in payslip_id.line_ids:
|
|
list.append({
|
|
'name': line.name,
|
|
'code': line.code + line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code,
|
|
'category_id': line.category_id.name if line.category_id else False,
|
|
'amount': line.amount,
|
|
'quantity': line.quantity,
|
|
'rate': line.rate
|
|
})
|
|
return list
|
|
|
|
def _get_leave_days(self, payslip):
|
|
"""
|
|
Returns total leave days taken in this period
|
|
"""
|
|
leave_lines = payslip.worked_days_line_ids.filtered(
|
|
lambda l: l.code in ['LEAVE110', 'LEAVE90', 'LEAVE100', 'LEAVE120'] # Your leave codes
|
|
)
|
|
return sum(leave_lines.mapped('number_of_days'))
|
|
|
|
def _get_lop_days(self, payslip):
|
|
"""
|
|
Returns LOP days from payslip
|
|
"""
|
|
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LEAVE90')
|
|
return lop_line.number_of_days if lop_line else 0
|
|
|
|
|
|
def _get_leave_taken(self, payslip):
|
|
"""
|
|
Returns leave days taken in this payslip period
|
|
"""
|
|
leave_lines = payslip.worked_days_line_ids
|
|
return {
|
|
'sick': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE110').mapped('number_of_days')),
|
|
'casual': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE120').mapped('number_of_days')),
|
|
'privilege': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE100').mapped('number_of_days')),
|
|
}
|
|
|
|
def _update_lop_days(self, payslip, days):
|
|
"""
|
|
Updates LOP days in the payslip
|
|
"""
|
|
WorkedDays = self.env['hr.payslip.worked_days']
|
|
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LOP')
|
|
|
|
if lop_line:
|
|
if days > 0:
|
|
lop_line.write({'number_of_days': days})
|
|
else:
|
|
lop_line.unlink()
|
|
elif days > 0:
|
|
WorkedDays.create({
|
|
'payslip_id': payslip.id,
|
|
'name': _('Loss of Pay'),
|
|
'code': 'LOP',
|
|
'number_of_days': days,
|
|
'number_of_hours': days * 8, # Assuming 8-hour work day
|
|
'contract_id': payslip.contract_id.id,
|
|
})
|
|
|
|
def _get_leave_balances(self, employee, date_from, date_to):
|
|
Leave = self.env['hr.leave']
|
|
Allocation = self.env['hr.leave.allocation']
|
|
leave_types = self.env['hr.leave.type'].search([])
|
|
|
|
balances = {}
|
|
for leave_type in leave_types:
|
|
# Approved allocations within or before payslip period
|
|
allocations = Allocation.search([
|
|
('employee_id', '=', employee.id),
|
|
('state', '=', 'validate'),
|
|
('holiday_status_id', '=', leave_type.id),
|
|
('date_from', '<=', str(date_to)), # Allocation should be active during payslip
|
|
])
|
|
allocated = sum(alloc.number_of_days for alloc in allocations)
|
|
|
|
# Approved leaves within the payslip period
|
|
leaves = Leave.search([
|
|
('employee_id', '=', employee.id),
|
|
('state', '=', 'validate'),
|
|
('holiday_status_id', '=', leave_type.id),
|
|
('request_date_to', '<=', str(date_to))
|
|
])
|
|
used = sum(leave.number_of_days for leave in leaves)
|
|
|
|
# Key: leave code or fallback to name
|
|
code = leave_type.work_entry_type_id.code
|
|
balances[code] = allocated - used
|
|
|
|
return balances
|
|
|
|
def _update_leave_taken(self, payslip, leave_data):
|
|
"""
|
|
Updates leave days taken in the payslip
|
|
"""
|
|
WorkedDays = self.env['hr.payslip.worked_days']
|
|
|
|
for leave_type, days in leave_data.items():
|
|
code = leave_type.upper()
|
|
line = payslip.worked_days_line_ids.filtered(lambda l: l.code == code)
|
|
|
|
|
|
if line:
|
|
if days > 0:
|
|
line.write({'number_of_days': days})
|
|
else:
|
|
line.unlink()
|
|
elif days > 0:
|
|
WorkedDays.create({
|
|
'payslip_id': payslip.id,
|
|
'name': _(leave_type.capitalize() + ' Leave'),
|
|
'code': code,
|
|
'number_of_days': days,
|
|
'number_of_hours': days * 8, # Assuming 8-hour work day
|
|
'contract_id': payslip.contract_id.id,
|
|
})
|
|
|
|
|
|
|
|
class HrPayslip(models.Model):
|
|
_inherit = 'hr.payslip'
|
|
|
|
|
|
|
|
def get_payslip_lines_data(self, payslip_id):
|
|
payslip = self.browse(payslip_id)
|
|
return [{
|
|
'name': line.name,
|
|
'code': line.code,
|
|
'category_id': (line.category_id.id, line.category_id.name),
|
|
'amount': line.amount,
|
|
'quantity': line.quantity,
|
|
'rate': line.rate
|
|
} for line in payslip.line_ids]
|
|
|
|
def action_open_payslips(self):
|
|
self.ensure_one()
|
|
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.action_view_hr_payslip_month_form")
|
|
action['views'] = [[False, "form"]]
|
|
action['res_id'] = self.id
|
|
action['target'] = 'new'
|
|
return action
|