250 lines
11 KiB
Python
250 lines
11 KiB
Python
import base64
|
|
import io
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from odoo.tools.misc import format_amount, format_date, xlsxwriter
|
|
|
|
|
|
class HrPayslip(models.Model):
|
|
_inherit = "hr.payslip"
|
|
|
|
@api.model
|
|
def default_get(self, default_fields):
|
|
"""If we're creating a new account through a many2one, there are chances that we typed the account code
|
|
instead of its name. In that case, switch both fields values.
|
|
"""
|
|
context = {}
|
|
defaults = super(HrPayslip, self).default_get(default_fields)
|
|
|
|
if 'finance_manager_user_id' in default_fields:
|
|
finance_manager = self.env['ir.config_parameter'].sudo().get_param('hr_employee_extended.emp_finance_manager')
|
|
if finance_manager:
|
|
defaults['finance_manager_user_id'] = int(finance_manager)
|
|
return defaults
|
|
|
|
finance_approval_state = fields.Selection([
|
|
("not_sent", "Not Sent"),
|
|
("pending", "Pending Finance"),
|
|
("approved", "Approved"),
|
|
("rejected", "Rejected"),
|
|
], string="Finance Approval", default="not_sent", copy=False, tracking=True)
|
|
finance_manager_user_id = fields.Many2one("res.users", string="Finance Manager", copy=False, tracking=True)
|
|
finance_submitted_date = fields.Datetime(string="Sent to Finance On", copy=False, readonly=True)
|
|
finance_approved_date = fields.Datetime(string="Finance Approved On", copy=False, readonly=True)
|
|
finance_rejected_date = fields.Datetime(string="Finance Rejected On", copy=False, readonly=True)
|
|
finance_rejection_reason = fields.Text(string="Finance Rejection Reason", copy=False, readonly=True)
|
|
can_finance_approve = fields.Boolean(compute="_compute_can_finance_approve")
|
|
|
|
def action_payslip_cancel(self):
|
|
if not self.env.user.has_group("hr_payroll_extended.group_payroll_finance_approver") and not self.env.user._is_system() and self.filtered(lambda slip: slip.state == 'done'):
|
|
raise UserError(_("Cannot cancel a payslip that is done."))
|
|
self.write({'state': 'cancel'})
|
|
self.mapped('payslip_run_id').action_close()
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
payslips = super().create(vals_list)
|
|
payslips._check_duplicate_payslip_periods()
|
|
return payslips
|
|
|
|
def write(self, vals):
|
|
res = super().write(vals)
|
|
if {"employee_id", "date_from", "date_to", "state"} & vals.keys():
|
|
self._check_duplicate_payslip_periods()
|
|
return res
|
|
|
|
@api.depends("finance_manager_user_id", "finance_approval_state", "state")
|
|
def _compute_can_finance_approve(self):
|
|
for payslip in self:
|
|
payslip.can_finance_approve = (
|
|
payslip.state == "done"
|
|
and payslip.finance_approval_state == "pending"
|
|
and payslip.finance_manager_user_id == self.env.user
|
|
)
|
|
|
|
def _get_config_user(self, param_name, error_message):
|
|
user_id = self.env["ir.config_parameter"].sudo().get_param(param_name)
|
|
if not user_id:
|
|
raise UserError(error_message)
|
|
user = self.env["res.users"].sudo().browse(int(user_id)).exists()
|
|
if not user:
|
|
raise UserError(error_message)
|
|
return user
|
|
|
|
def _get_finance_manager(self):
|
|
user = self._get_config_user(
|
|
"hr_employee_extended.emp_finance_manager",
|
|
_("Set the finance manager in employee settings."),
|
|
)
|
|
finance_group = self.env.ref("hr_payroll_extended.group_payroll_finance_approver", raise_if_not_found=False)
|
|
if finance_group and finance_group not in user.groups_id:
|
|
user.sudo().write({"groups_id": [(4, finance_group.id)]})
|
|
return user
|
|
|
|
def _get_employee_hr(self):
|
|
return self._get_config_user(
|
|
"hr_employee_extended.emp_hr_id",
|
|
_("Set the HR user in employee settings."),
|
|
)
|
|
|
|
def _send_payroll_mail(self, user, subject, body):
|
|
if not user.email:
|
|
raise UserError(_("Please configure an email address for %s.", user.name))
|
|
self.env["mail.mail"].sudo().create({
|
|
"subject": subject,
|
|
"body_html": body,
|
|
"email_to": user.email,
|
|
"auto_delete": True,
|
|
}).send()
|
|
|
|
def _send_finance_submission_mail(self, finance_manager):
|
|
for payslip in self:
|
|
subject = _("Payslip pending finance approval: %s", payslip.display_name)
|
|
body = _(
|
|
"<p>Dear %(finance)s,</p>"
|
|
"<p>The payslip for %(employee)s (%(period)s) has been validated by HR and is pending your confirmation.</p>"
|
|
"<p>Amount: %(amount)s</p>",
|
|
finance=finance_manager.name,
|
|
employee=payslip.employee_id.name,
|
|
period="%s - %s" % (format_date(self.env, payslip.date_from), format_date(self.env, payslip.date_to)),
|
|
amount=format_amount(self.env, payslip.net_wage, payslip.currency_id) if payslip.currency_id else payslip.net_wage,
|
|
)
|
|
payslip._send_payroll_mail(finance_manager, subject, body)
|
|
|
|
def _send_hr_finance_result_mail(self, approved=True):
|
|
hr_user = self._get_employee_hr()
|
|
for payslip in self:
|
|
if approved:
|
|
subject = _("Payslip paid by finance: %s", payslip.display_name)
|
|
body = _(
|
|
"<p>Dear %(hr)s,</p>"
|
|
"<p>The finance manager confirmed payment for %(employee)s (%(period)s).</p>",
|
|
hr=hr_user.name,
|
|
employee=payslip.employee_id.name,
|
|
period="%s - %s" % (format_date(self.env, payslip.date_from), format_date(self.env, payslip.date_to)),
|
|
)
|
|
else:
|
|
subject = _("Payslip rejected by finance: %s", payslip.display_name)
|
|
body = _(
|
|
"<p>Dear %(hr)s,</p>"
|
|
"<p>The finance manager rejected the payslip for %(employee)s (%(period)s).</p>"
|
|
"<p><strong>Reason:</strong> %(reason)s</p>",
|
|
hr=hr_user.name,
|
|
employee=payslip.employee_id.name,
|
|
period="%s - %s" % (format_date(self.env, payslip.date_from), format_date(self.env, payslip.date_to)),
|
|
reason=payslip.finance_rejection_reason or "",
|
|
)
|
|
payslip._send_payroll_mail(hr_user, subject, body)
|
|
|
|
def _mark_pending_finance_approval(self):
|
|
finance_manager = self._get_finance_manager()
|
|
now = fields.Datetime.now()
|
|
slips = self.filtered(lambda slip: slip.state == "done" and not slip.payslip_run_id)
|
|
if not slips:
|
|
return
|
|
slips.write({
|
|
"finance_approval_state": "pending",
|
|
"finance_manager_user_id": finance_manager.id,
|
|
"finance_submitted_date": now,
|
|
"finance_approved_date": False,
|
|
"finance_rejected_date": False,
|
|
"finance_rejection_reason": False,
|
|
})
|
|
slips.mapped("payslip_run_id").write({
|
|
"finance_manager_user_id": finance_manager.id,
|
|
"finance_approval_state": "pending",
|
|
"finance_submitted_date": now,
|
|
"finance_approved_date": False,
|
|
"finance_rejected_date": False,
|
|
"finance_rejection_reason": False,
|
|
})
|
|
slips._send_finance_submission_mail(finance_manager)
|
|
|
|
def action_payslip_done(self):
|
|
result = super().action_payslip_done()
|
|
self._mark_pending_finance_approval()
|
|
return result
|
|
|
|
def _check_current_user_is_finance_manager(self):
|
|
finance_manager = self._get_finance_manager()
|
|
if self.env.user != finance_manager and not self.env.user._is_system():
|
|
raise UserError(_("Only the configured finance manager can confirm or reject these payslips."))
|
|
|
|
def action_payslip_paid(self):
|
|
self._check_current_user_is_finance_manager()
|
|
pending_slips = self.filtered(lambda slip: slip.state != "paid")
|
|
if any(slip.finance_approval_state != "pending" for slip in pending_slips):
|
|
raise UserError(_("Only payslips pending finance approval can be marked as paid."))
|
|
result = super().action_payslip_paid()
|
|
approved_slips = self.filtered(lambda slip: slip.state == "paid")
|
|
approved_slips.write({
|
|
"finance_approval_state": "approved",
|
|
"finance_approved_date": fields.Datetime.now(),
|
|
})
|
|
approved_slips._send_hr_finance_result_mail(approved=True)
|
|
return result
|
|
|
|
def action_finance_reject_wizard(self):
|
|
self.ensure_one()
|
|
self._check_current_user_is_finance_manager()
|
|
if self.state != "done" or self.finance_approval_state != "pending":
|
|
raise UserError(_("Only payslips pending finance approval can be rejected."))
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"name": _("Reject Payslip"),
|
|
"res_model": "hr.payslip.finance.reject.wizard",
|
|
"view_mode": "form",
|
|
"target": "new",
|
|
"context": {"default_payslip_ids": [(6, 0, self.ids)]},
|
|
}
|
|
|
|
def action_finance_download_xlsx(self):
|
|
return self._action_finance_download_xlsx()
|
|
|
|
def action_finance_download_pdf(self):
|
|
return self.env.ref("hr_payroll_extended.action_report_finance_payment_list").report_action(self)
|
|
|
|
def _get_finance_payment_rows(self):
|
|
rows = []
|
|
for slip in self:
|
|
rows.append({
|
|
"employee": slip.employee_id.name or "",
|
|
"bank_account": slip.employee_id.bank_account_id.acc_number or "",
|
|
"amount": slip.net_wage,
|
|
"currency": slip.currency_id,
|
|
})
|
|
return rows
|
|
|
|
def _action_finance_download_xlsx(self, filename=None):
|
|
if not self:
|
|
raise UserError(_("There are no payslips to export."))
|
|
output = io.BytesIO()
|
|
workbook = xlsxwriter.Workbook(output, {"in_memory": True})
|
|
worksheet = workbook.add_worksheet("Payment List")
|
|
header_format = workbook.add_format({"bold": True, "bg_color": "#E0E0E0", "align": "center"})
|
|
money_format = workbook.add_format({"num_format": "#,##0.00"})
|
|
headers = [_("Employee Name"), _("Bank Account"), _("Amount")]
|
|
worksheet.write_row(0, 0, headers, header_format)
|
|
for row_idx, row in enumerate(self._get_finance_payment_rows(), start=1):
|
|
worksheet.write(row_idx, 0, row["employee"])
|
|
worksheet.write(row_idx, 1, row["bank_account"])
|
|
worksheet.write_number(row_idx, 2, row["amount"] or 0.0, money_format)
|
|
worksheet.set_column(0, 0, 28)
|
|
worksheet.set_column(1, 1, 26)
|
|
worksheet.set_column(2, 2, 16)
|
|
workbook.close()
|
|
attachment = self.env["ir.attachment"].sudo().create({
|
|
"name": filename or _("Finance Payment List.xlsx"),
|
|
"type": "binary",
|
|
"datas": base64.b64encode(output.getvalue()),
|
|
"mimetype": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
"res_model": self._name,
|
|
"res_id": self[:1].id,
|
|
})
|
|
return {
|
|
"type": "ir.actions.act_url",
|
|
"url": "/web/content/%s?download=true" % attachment.id,
|
|
"target": "self",
|
|
}
|