odoo18/addons_extensions/hr_payroll_extended/models/hr_payslip.py

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",
}