395 lines
18 KiB
Python
395 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
from odoo import models, fields, api, _
|
|
from odoo.exceptions import UserError
|
|
from datetime import datetime
|
|
|
|
|
|
class ReconcileLeaves(models.Model):
|
|
_name = 'reconcile.leaves'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
_rec_name = 'employee_id'
|
|
_description = 'Reconcile Leaves'
|
|
|
|
date = fields.Date()
|
|
contract_start_date = fields.Date(related='employee_id.contract_id.date_start')
|
|
contract_end_date = fields.Date(related='employee_id.contract_id.date_end')
|
|
start_vacation_date = fields.Datetime(related='yearly_vacation.date_from')
|
|
end_vacation_date = fields.Datetime(related='yearly_vacation.date_to')
|
|
leave_duration = fields.Float()
|
|
leave_amount = fields.Float()
|
|
salary = fields.Float()
|
|
total_allowance = fields.Float()
|
|
total_deduction = fields.Float()
|
|
total_loans = fields.Float()
|
|
net = fields.Float()
|
|
state = fields.Selection(selection=[
|
|
("draft", _("Draft")),
|
|
("submit", _("Submit")),
|
|
("direct_manager", _("Direct Manager")),
|
|
("hr_manager", _("HR Manager")),
|
|
("finance_manager", _("Finance Manager")),
|
|
("gm_manager", _("GM Manager")),
|
|
("pay", _("Pay")),
|
|
("refuse", _("Refuse"))], default='draft', tracking=True)
|
|
|
|
# Relational fields
|
|
employee_id = fields.Many2one('hr.employee')
|
|
department_id = fields.Many2one('hr.department', related='employee_id.department_id')
|
|
job_id = fields.Many2one('hr.job', related='employee_id.job_id')
|
|
contract_id = fields.Many2one('hr.contract', related='employee_id.contract_id')
|
|
yearly_vacation = fields.Many2one('hr.holidays')
|
|
calculation_method = fields.Many2many('hr.salary.rule')
|
|
journal_id = fields.Many2one('account.journal')
|
|
account_id = fields.Many2one('account.account')
|
|
allowance_deduction = fields.One2many('reconcile.allowance.deduction', 'reconcile_id')
|
|
journal_entry_ids = fields.One2many('reconcile.allowance.journal', 'journal_entry_ids_lines')
|
|
account_move_id = fields.Many2one('account.move')
|
|
loans_ids = fields.Many2many(
|
|
'hr.loan.salary.advance',
|
|
domain="[('employee_id','=','self.employee_id.id'),('state','=','pay'),('months','=','1')]")
|
|
|
|
def unlink(self):
|
|
for item in self:
|
|
if item.state != 'draft':
|
|
raise UserError(_('You can not delete record in state not in draft'))
|
|
return super(ReconcileLeaves, self).unlink()
|
|
|
|
# To get salary rules from holiday setting
|
|
@api.onchange('yearly_vacation')
|
|
def _get_salary_rule(self):
|
|
for rec in self:
|
|
rec.calculation_method = False
|
|
if rec.yearly_vacation:
|
|
rec.calculation_method = rec.yearly_vacation.holiday_status_id.salary_rules_ids.ids
|
|
else:
|
|
rec.calculation_method = False
|
|
|
|
# one reconcile holiday for each holiday request
|
|
@api.constrains('yearly_vacation')
|
|
def constain_holiday_request(self):
|
|
for rec in self:
|
|
holdiay_req = self.env['reconcile.leaves'].search([('employee_id', '=', rec.employee_id.id),
|
|
('yearly_vacation', '=', rec.yearly_vacation.id)])
|
|
for itms in holdiay_req:
|
|
if len(holdiay_req) > 1:
|
|
raise UserError(
|
|
_('This Holiday Request Has Been Reconcile %s') % rec.yearly_vacation.display_name)
|
|
|
|
# default loans lines and domain
|
|
@api.onchange('employee_id')
|
|
def _get_loans_ids_and_domain(self):
|
|
record_list = []
|
|
record_ids = self.env['hr.loan.salary.advance'].search(
|
|
[('employee_id', '=', self.employee_id.id), ('state', '=', 'pay'), ('months', '=', '1')])
|
|
for item in record_ids:
|
|
if item.remaining_loan_amount > 0:
|
|
record_list.append(item.id)
|
|
for line in self:
|
|
line.loans_ids = record_list
|
|
domain = [('employee_id', '=', line.employee_id.id), ('state', '=', 'validate1')]
|
|
res = {'domain': {'loans_ids': [('id', 'in', record_ids.ids)],
|
|
'yearly_vacation': domain}}
|
|
return res
|
|
|
|
# dynamic domain for yearly_vacation when employee_id changed
|
|
@api.onchange('employee_id')
|
|
def onchange_employee_id(self):
|
|
for rec in self:
|
|
domain = [('employee_id', '=', rec.employee_id.id), ('state', '=', 'validate1')]
|
|
res = {'domain': {'yearly_vacation': domain}}
|
|
return res
|
|
|
|
# compute leave duration when change yearly_vacation
|
|
@api.onchange('yearly_vacation', 'calculation_method')
|
|
def compute_leave_duration(self):
|
|
self.leave_amount, self.leave_duration = 0.0, 0.0
|
|
|
|
if self.start_vacation_date and self.end_vacation_date:
|
|
date_start = fields.Datetime.from_string(self.start_vacation_date)
|
|
date_end = fields.Datetime.from_string(self.end_vacation_date)
|
|
days_start = date_start.day
|
|
days_end = date_end.day
|
|
duration = date_end - date_start
|
|
if days_start == 31 or days_end == 31: # payroll for 30 days
|
|
self.leave_duration = duration.days
|
|
else:
|
|
self.leave_duration = duration.days + 1
|
|
|
|
if self.salary:
|
|
if self.leave_duration > 0:
|
|
self.leave_amount = self.salary / (30 / self.leave_duration)
|
|
|
|
# Compute employee salary when calculation method changed
|
|
@api.onchange('yearly_vacation', 'calculation_method', 'employee_id', 'loans_ids')
|
|
def compute_employee_salary_fom_calculation_method(self):
|
|
# Initialize fields values
|
|
self.salary, self.total_allowance, self.total_deduction, self.total_loans, self.net = 0.0, 0.0, 0.0, 0.0, 0.0
|
|
|
|
if self.calculation_method:
|
|
# initialize variables
|
|
total = 0.0
|
|
allowance_deduction_list = []
|
|
|
|
for item in self.calculation_method:
|
|
# check if there is a contract
|
|
if self.contract_id:
|
|
# Check if item in calculation method is deduction or allowance to subtract or add
|
|
if item.category_id.rule_type == 'deduction':
|
|
total -= self.compute_rule(item, self.contract_id)
|
|
else:
|
|
total += self.compute_rule(item, self.contract_id)
|
|
|
|
if self.leave_duration > 0:
|
|
record = self.env['reconcile.allowance.deduction'].create({
|
|
'salary_rule_id': item.id,
|
|
'amount': (self.compute_rule(item, self.contract_id) / (30 / self.leave_duration))})
|
|
allowance_deduction_list.append(record.id)
|
|
else:
|
|
raise UserError(_('Employee "%s" has no contract') % self.employee_id.name)
|
|
|
|
self.allowance_deduction = self.env['reconcile.allowance.deduction'].browse(allowance_deduction_list)
|
|
self.salary = total
|
|
|
|
# Compute total allowance , total deduction and net
|
|
if self.allowance_deduction:
|
|
for item in self.allowance_deduction:
|
|
if item.category_id.rule_type == 'allowance':
|
|
self.total_allowance += item.amount
|
|
elif item.category_id.rule_type == 'deduction':
|
|
self.total_deduction += item.amount
|
|
self.net = self.total_allowance - self.total_deduction
|
|
else:
|
|
self.salary, self.total_allowance, self.total_deduction, self.net = 0.0, 0.0, 0.0, 0.0
|
|
self.allowance_deduction = self.env['reconcile.allowance.deduction'].browse([])
|
|
|
|
# Compute total loans and net
|
|
if self.loans_ids:
|
|
for line in self.loans_ids:
|
|
self.total_loans += line.remaining_loan_amount
|
|
self.net -= self.total_loans
|
|
else:
|
|
self.total_loans = 0.0
|
|
|
|
# Compute salary rules
|
|
def compute_rule(self, rule, contract):
|
|
localdict = dict(employee=contract.employee_id, contract=contract)
|
|
|
|
if rule.amount_select == 'percentage':
|
|
total_percent = 0
|
|
if rule.related_benefits_discounts:
|
|
for line in rule.related_benefits_discounts:
|
|
if line.amount_select == 'fix':
|
|
total_percent += self.compute_rule(line, contract)
|
|
elif line.amount_select == 'percentage':
|
|
total_percent += self.compute_rule(line, contract)
|
|
else:
|
|
total_percent += self.compute_rule(line, contract)
|
|
if total_percent:
|
|
if rule.salary_type == 'fixed':
|
|
try:
|
|
return float(total_percent * rule.amount_percentage / 100)
|
|
except:
|
|
raise UserError(
|
|
_('Wrong percentage base or quantity defined for salary rule %s (%s).') % (
|
|
rule.name, rule.code))
|
|
elif rule.salary_type == 'related_levels':
|
|
levels_ids = rule.salary_amount_ids.filtered(
|
|
lambda item: item.salary_scale_level.id == contract.salary_level.id)
|
|
if levels_ids:
|
|
for l in levels_ids:
|
|
try:
|
|
return float(l.salary * total_percent / 100)
|
|
except:
|
|
raise UserError(
|
|
_('Wrong quantity defined for salary rule %s (%s).') % (
|
|
rule.name, rule.code))
|
|
else:
|
|
return 0
|
|
elif rule.salary_type == 'related_groups':
|
|
groups_ids = rule.salary_amount_ids.filtered(
|
|
lambda item: item.salary_scale_group.id == contract.salary_group.id)
|
|
if groups_ids:
|
|
for g in groups_ids:
|
|
try:
|
|
return float(g.salary * total_percent / 100)
|
|
except:
|
|
raise UserError(
|
|
_('Wrong quantity defined for salary rule %s (%s).') % (
|
|
rule.name, rule.code))
|
|
else:
|
|
return 0
|
|
elif rule.salary_type == 'related_degrees':
|
|
degrees_ids = rule.salary_amount_ids.filtered(
|
|
lambda item: item.salary_scale_degree.id == contract.salary_degree.id)
|
|
if degrees_ids:
|
|
for d in degrees_ids:
|
|
try:
|
|
return float(d.salary * total_percent / 100)
|
|
except:
|
|
raise UserError(
|
|
_('Wrong quantity defined for salary rule %s (%s).') % (
|
|
rule.name, rule.code))
|
|
else:
|
|
return 0
|
|
else:
|
|
try:
|
|
return 0
|
|
except:
|
|
raise UserError(_('There is no total for rule : %s') % (rule.name))
|
|
|
|
elif rule.amount_select == 'fix':
|
|
return rule._compute_rule(localdict)[0]
|
|
|
|
else:
|
|
return rule._compute_rule(localdict)[0]
|
|
|
|
# change state to draft and cancel account move if it's state draft
|
|
def re_draft(self):
|
|
if self.account_move_id:
|
|
if self.account_move_id.state == 'draft':
|
|
self.account_move_id.unlink()
|
|
self.account_move_id = False
|
|
else:
|
|
raise UserError(_(
|
|
'You can not re-draft reconcile leaves because account move with ID "%s" in state Posted') % self.account_move_id.name)
|
|
|
|
for item in self.loans_ids:
|
|
last_date = fields.Datetime.from_string(self.write_date).date().month
|
|
for install in item.deduction_lines:
|
|
loan_date = fields.Datetime.from_string(install.write_date).date().month
|
|
if loan_date >= last_date and install.paid == True:
|
|
install.paid = False
|
|
item.state = 'pay'
|
|
self.state = 'draft'
|
|
|
|
# change state to submit
|
|
def submit(self):
|
|
self.constain_holiday_request()
|
|
self.state = 'submit'
|
|
|
|
# change state to direct_manager
|
|
def direct_manager(self):
|
|
self.state = 'direct_manager'
|
|
|
|
# change state to hr_manager
|
|
def hr_manager(self):
|
|
self.state = 'hr_manager'
|
|
|
|
# change state to finance_manager
|
|
def finance_manager(self):
|
|
self.state = 'finance_manager'
|
|
|
|
# change state to gm_manager
|
|
def gm_manager(self):
|
|
self.state = 'gm_manager'
|
|
|
|
# change state to create_journal_entry
|
|
def pay(self):
|
|
line_vals = []
|
|
if self.allowance_deduction:
|
|
for item in self.allowance_deduction:
|
|
# check if amount greater than 0.0 to fill move account lines
|
|
if item.amount > 0.0:
|
|
# check for deduction credit account
|
|
if item.salary_rule_id.category_id.rule_type == 'deduction':
|
|
if not item.salary_rule_id.rule_credit_account_id:
|
|
raise UserError(_('Undefined credit account for salary rule %s (%s).') % (
|
|
item.salary_rule_id.name, item.salary_rule_id.code))
|
|
# check for allowance debit account
|
|
elif item.salary_rule_id.category_id.rule_type == 'allowance':
|
|
if not item.salary_rule_id.rule_debit_account_id:
|
|
raise UserError(_('Undefined credit account for salary rule %s (%s).') % (
|
|
item.salary_rule_id.name, item.salary_rule_id.code))
|
|
else:
|
|
if not item.salary_rule_id.rule_credit_account_id:
|
|
raise UserError(_(
|
|
'Check account debit for salary rule "%s" ') % item.salary_rule_id.name)
|
|
|
|
# fill move lines with allowance deduction
|
|
if item.salary_rule_id.category_id.rule_type == 'allowance':
|
|
line_vals.append({
|
|
'name': 'Employee %s allowance.' % (self.employee_id.name),
|
|
'debit': abs(item.amount),
|
|
'account_id': item.salary_rule_id.rule_debit_account_id.id,
|
|
'partner_id': self.employee_id.user_id.partner_id.id})
|
|
|
|
elif item.salary_rule_id.category_id.rule_type == 'deduction':
|
|
line_vals.append({
|
|
'name': 'Employee %s deduction.' % (self.employee_id.name),
|
|
'credit': abs(item.amount),
|
|
'account_id': item.salary_rule_id.rule_credit_account_id.id,
|
|
'partner_id': self.employee_id.user_id.partner_id.id})
|
|
else:
|
|
line_vals.append({
|
|
'name': 'Employee %s rule.' % (self.employee_id.name),
|
|
'debit': abs(item.amount),
|
|
'account_id': item.salary_rule_id.rule_debit_account_id.id,
|
|
'partner_id': self.employee_id.user_id.partner_id.id})
|
|
|
|
for item in self.loans_ids:
|
|
line_vals.append({
|
|
'name': 'Employee %s loan.' % (self.employee_id.name),
|
|
'credit': abs(item.remaining_loan_amount),
|
|
'account_id': item.request_type.account_id.id,
|
|
'partner_id': self.employee_id.user_id.partner_id.id})
|
|
|
|
for install in item.deduction_lines:
|
|
if install.paid == False:
|
|
install.paid = True
|
|
item.state = 'closed'
|
|
|
|
line_vals.append({
|
|
'name': 'Employee %s Net.' % (self.employee_id.name),
|
|
'credit': abs(self.net),
|
|
'account_id': self.journal_id.default_account_id.id,
|
|
'partner_id': self.employee_id.user_id.partner_id.id})
|
|
|
|
# Constrain on net that must be greater than zero
|
|
if self.net < 0.0:
|
|
raise UserError(_('The Net must be not negative value .'))
|
|
|
|
move = self.env['account.move'].create({
|
|
'state': 'draft',
|
|
'journal_id': self.journal_id.id,
|
|
'date': self.date,
|
|
'ref': 'Reconcile leave of "%s" ' % self.employee_id.name,
|
|
'line_ids': [(0, 0, value) for value in line_vals]
|
|
})
|
|
self.write({'account_move_id': move.id})
|
|
self.state = 'pay'
|
|
|
|
# change state to refuse
|
|
def refuse(self):
|
|
self.state = 'refuse'
|
|
|
|
# Override create function
|
|
@api.model
|
|
def create(self, values):
|
|
result = super(ReconcileLeaves, self).create(values)
|
|
if result.net < 0.0:
|
|
raise UserError(_('The Net must be not negative value .'))
|
|
return result
|
|
|
|
|
|
class ReconcileAllowanceDeduction(models.Model):
|
|
_name = 'reconcile.allowance.deduction'
|
|
_description = 'Reconcile Allowance Deduction'
|
|
|
|
amount = fields.Float()
|
|
|
|
# relational fields
|
|
reconcile_id = fields.Many2one('reconcile.leaves')
|
|
salary_rule_id = fields.Many2one('hr.salary.rule')
|
|
category_id = fields.Many2one('hr.salary.rule.category', related='salary_rule_id.category_id')
|
|
|
|
|
|
class ReconcileAllowanceJournal(models.Model):
|
|
_name = 'reconcile.allowance.journal'
|
|
_description = 'Reconcile Allowance Journal'
|
|
|
|
amount = fields.Float()
|
|
|
|
# relational fields
|
|
journal_entry_ids_lines = fields.Many2one('reconcile.leaves')
|
|
journal_id = fields.Many2one('account.journal')
|