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 = _( "

Dear %(finance)s,

" "

The payslip for %(employee)s (%(period)s) has been validated by HR and is pending your confirmation.

" "

Amount: %(amount)s

", 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 = _( "

Dear %(hr)s,

" "

The finance manager confirmed payment for %(employee)s (%(period)s).

", 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 = _( "

Dear %(hr)s,

" "

The finance manager rejected the payslip for %(employee)s (%(period)s).

" "

Reason: %(reason)s

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