193 lines
8.3 KiB
Python
193 lines
8.3 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import api, fields, models, _, Command
|
|
from odoo.exceptions import AccessError
|
|
|
|
import logging
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class HrPayslip(models.Model):
|
|
_inherit = 'hr.payslip'
|
|
|
|
expense_ids = fields.One2many(
|
|
comodel_name='hr.expense',
|
|
inverse_name='payslip_id',
|
|
string='Expenses',
|
|
help="Expenses to reimburse to employee.",
|
|
)
|
|
expenses_count = fields.Integer(compute='_compute_expenses_count', compute_sudo=True)
|
|
|
|
def _compute_input_line_ids(self):
|
|
super()._compute_input_line_ids()
|
|
self._update_expense_input_line_ids(search_new_valid_expenses=True)
|
|
|
|
@api.depends('expense_ids')
|
|
def _compute_expenses_count(self):
|
|
for payslip in self:
|
|
payslip.expenses_count = len(payslip.expense_ids)
|
|
|
|
@api.model
|
|
def _issues_dependencies(self):
|
|
return super()._issues_dependencies() + [
|
|
'expense_ids',
|
|
'struct_id.rule_ids',
|
|
'struct_id.rule_ids.code',
|
|
'struct_id.rule_ids.account_debit',
|
|
'struct_id.rule_ids.account_debit.account_type',
|
|
]
|
|
|
|
def _get_errors_by_slip(self):
|
|
# EXTENDS hr_payroll
|
|
errors_by_slip = super()._get_errors_by_slip()
|
|
draft_slips = self.filtered(lambda ps: ps.state == 'draft')
|
|
for (struct, company), slips in draft_slips.filtered('expense_ids').grouped(lambda slip: (slip.struct_id, slip.company_id)).items():
|
|
expense_rules = struct.rule_ids.filtered(lambda rule: rule.code == 'EXPENSES').with_company(company)
|
|
if not expense_rules:
|
|
for slip in slips:
|
|
errors_by_slip[slip].append({
|
|
'message': _('No rule to handle expenses'),
|
|
'action_text': _("Rules"),
|
|
'action': struct.rule_ids._get_records_action(),
|
|
'level': 'danger',
|
|
})
|
|
elif not expense_rules.filtered(
|
|
lambda rule: rule.account_debit and rule.account_debit.account_type == 'liability_payable'
|
|
):
|
|
for slip in slips:
|
|
errors_by_slip[slip].append({
|
|
'message': _('No debit account for EXPENSES rules'),
|
|
'action_text': _("Expense rules"),
|
|
'action': expense_rules._get_records_action(),
|
|
'level': 'danger',
|
|
})
|
|
return errors_by_slip
|
|
|
|
def action_payslip_cancel(self):
|
|
# Remove the link to the cancelled payslip so it can be linked to another payslip
|
|
# EXTENDS hr_payroll
|
|
res = super().action_payslip_cancel()
|
|
expenses_sudo = self.expense_ids.sudo()
|
|
expenses_sudo.payslip_id = False
|
|
self._update_expense_input_line_ids()
|
|
return res
|
|
|
|
def action_payslip_draft(self):
|
|
# We can add the new or previously unlinked expenses to the payslip
|
|
# EXTENDS hr_payroll
|
|
res = super().action_payslip_draft()
|
|
self._link_expenses_to_payslip(clear_existing=False) # Add the new expenses to the payslip, but keep the already linked ones
|
|
return res
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
# EXTENDS hr_payroll
|
|
payslips = super().create(vals_list)
|
|
draft_slips = payslips.filtered(lambda p: p.employee_id and p.state == 'draft')
|
|
if not draft_slips:
|
|
return payslips
|
|
draft_slips._link_expenses_to_payslip()
|
|
return payslips
|
|
|
|
def write(self, vals):
|
|
# EXTENDS hr_payroll
|
|
res = super().write(vals)
|
|
if 'expense_ids' in vals:
|
|
self._update_expense_input_line_ids()
|
|
if 'input_line_ids' in vals:
|
|
self._update_expenses()
|
|
return res
|
|
|
|
def _get_employee_expenses_to_refund_in_payslip(self):
|
|
return self.env['hr.expense'].search([
|
|
('employee_id', 'in', self.employee_id.ids),
|
|
('state', 'in', ('approved', 'posted')),
|
|
('payment_mode', '=', 'own_account'),
|
|
('refund_in_payslip', '=', True),
|
|
('payslip_id', '=', False)])
|
|
|
|
def _link_expenses_to_payslip(self, clear_existing=True):
|
|
"""
|
|
Link expenses to a payslip if the payslip is in draft state and the expense is not already linked to a payslip.
|
|
"""
|
|
if not (self.env.is_superuser() or self.env.user.has_group('hr_payroll.group_hr_payroll_user')):
|
|
raise AccessError(_(
|
|
"You don't have the access rights to link an expense to a payslip. You need to be a payroll officer to do that.")
|
|
)
|
|
|
|
expenses_sudo = self.sudo()._get_employee_expenses_to_refund_in_payslip()
|
|
# group by employee
|
|
expenses_by_employee = expenses_sudo.grouped('employee_id')
|
|
for slip_sudo in self.sudo():
|
|
payslip_expenses = expenses_by_employee.get(slip_sudo.employee_id, self.env['hr.expense'])
|
|
if not slip_sudo.struct_id.rule_ids.filtered(lambda rule: rule.code == 'EXPENSES'):
|
|
continue
|
|
if slip_sudo.expense_ids and clear_existing:
|
|
slip_sudo.expense_ids = [Command.set(payslip_expenses.ids)]
|
|
elif payslip_expenses:
|
|
slip_sudo.expense_ids = [Command.link(expense.id) for expense in payslip_expenses]
|
|
|
|
def _update_expense_input_line_ids(self, search_new_valid_expenses=False):
|
|
"""
|
|
Update the payslip input lines to reflect the total amount of the expenses.
|
|
:param bool search_new_valid_expenses:
|
|
If False, will only compute the lines using the expenses already linked to the payslip.
|
|
If True, will take all the expenses that are valid for this payslip (do not link them to the payslip)
|
|
"""
|
|
expense_type = self.env.ref('hr_payroll_expense.expense_other_input', raise_if_not_found=False)
|
|
|
|
expenses_by_employee_sudo = {}
|
|
if search_new_valid_expenses:
|
|
expenses_by_employee_sudo = self.sudo()._get_employee_expenses_to_refund_in_payslip().grouped('employee_id')
|
|
|
|
if not expense_type:
|
|
_logger.warning("The 'hr_payroll_expense.expense_other_input' payslip input type is missing.")
|
|
return # We cannot do anything without the expense type
|
|
|
|
for payslip in self:
|
|
# Sudo to bypass access rights, as we just need to read the expense's total amounts
|
|
expenses_sudo = payslip.sudo().expense_ids
|
|
if expenses_by_employee_sudo:
|
|
employee_expenses_sudo = expenses_by_employee_sudo.get(payslip.employee_id, self.env['hr.expense'])
|
|
if employee_expenses_sudo:
|
|
del expenses_by_employee_sudo[payslip.employee_id] # To avoid double assignations
|
|
expenses_sudo += employee_expenses_sudo
|
|
total = sum(expenses_sudo.mapped('total_amount'))
|
|
lines_to_remove = payslip.input_line_ids.filtered(lambda x: x.input_type_id == expense_type)
|
|
input_lines_vals = [Command.delete(line.id) for line in lines_to_remove]
|
|
if total:
|
|
input_lines_vals.append(Command.create({
|
|
'amount': total,
|
|
'input_type_id': expense_type.id
|
|
}))
|
|
payslip.input_line_ids = input_lines_vals
|
|
|
|
def _update_expenses(self):
|
|
expense_type = self.env.ref('hr_payroll_expense.expense_other_input', raise_if_not_found=False)
|
|
if not expense_type:
|
|
return # We cannot do anything without the expense type
|
|
for payslip_sudo in self.sudo():
|
|
if not payslip_sudo.input_line_ids.filtered(lambda line: line.input_type_id == expense_type):
|
|
# Sudo to bypass access rights, as we just need to unlink the two models
|
|
payslip_sudo.expense_ids.payslip_id = False
|
|
|
|
def action_open_expenses(self):
|
|
return_action = {
|
|
'type': 'ir.actions.act_window',
|
|
'name': _('Reimbursed Expenses'),
|
|
'res_model': 'hr.expense',
|
|
}
|
|
if len(self.expense_ids.ids) > 1:
|
|
return_action.update({
|
|
'view_mode': 'list,form',
|
|
'domain': [('id', 'in', self.expense_ids.ids)],
|
|
})
|
|
else:
|
|
return_action.update({
|
|
'view_mode': 'form',
|
|
'res_id': self.expense_ids.id,
|
|
})
|
|
return return_action
|