diff --git a/addons_extensions/hr_employee_extended/models/res_config_settings.py b/addons_extensions/hr_employee_extended/models/res_config_settings.py
index 85a4f3112..da6467054 100644
--- a/addons_extensions/hr_employee_extended/models/res_config_settings.py
+++ b/addons_extensions/hr_employee_extended/models/res_config_settings.py
@@ -11,3 +11,23 @@ class ResConfigSettings(models.TransientModel):
domain=lambda self: [
('groups_id', 'in', self.env.ref('hr.group_hr_manager').id)])
+ notice_period = fields.Boolean(string='Notice Period',
+ help='Enable to configure a notice period'
+ ' for an employee.',
+ config_parameter='hr_employee_extended.notice_period')
+ no_of_days = fields.Integer(string='Notice Period Days',
+ help='Set the number of days for the notice'
+ ' period.',
+ config_parameter='hr_employee_extended.no_of_days')
+ emp_it_manager = fields.Many2one('res.users',config_parameter='hr_employee_extended.emp_it_manager', string='Employee IT Manager',
+ domain=lambda self: [
+ ('groups_id', 'in', self.env.ref('base.group_user').id)])
+
+ emp_finance_manager = fields.Many2one('res.users',config_parameter='hr_employee_extended.emp_finance_manager', string='Employee Finance Manager',
+ domain=lambda self: [
+ ('groups_id', 'in', self.env.ref('base.group_user').id)])
+
+ emp_admin = fields.Many2one('res.users',config_parameter='hr_employee_extended.emp_admin', string='Employee Admin',
+ domain=lambda self: [
+ ('groups_id', 'in', self.env.ref('base.group_user').id)])
+
diff --git a/addons_extensions/hr_employee_extended/views/res_config_settings.xml b/addons_extensions/hr_employee_extended/views/res_config_settings.xml
index f4f6eb402..6b623fb06 100644
--- a/addons_extensions/hr_employee_extended/views/res_config_settings.xml
+++ b/addons_extensions/hr_employee_extended/views/res_config_settings.xml
@@ -14,6 +14,40 @@
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", + } diff --git a/addons_extensions/hr_payroll_extended/models/hr_payslip_run.py b/addons_extensions/hr_payroll_extended/models/hr_payslip_run.py index 939f67e10..1fcba9162 100644 --- a/addons_extensions/hr_payroll_extended/models/hr_payslip_run.py +++ b/addons_extensions/hr_payroll_extended/models/hr_payslip_run.py @@ -1,11 +1,41 @@ from dateutil.relativedelta import relativedelta -from odoo import api, models +from odoo import api, fields, models, _ +from odoo.exceptions import UserError class HrPayslipRun(models.Model): _inherit = "hr.payslip.run" + @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(HrPayslipRun, 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") + @api.onchange("date_start") def _onchange_date_start_extended(self): for payslip_run in self: @@ -16,3 +46,81 @@ class HrPayslipRun(models.Model): else: payslip_run.date_end = payslip_run.date_start + relativedelta(months=1) + @api.depends("finance_manager_user_id", "finance_approval_state", "state") + def _compute_can_finance_approve(self): + for payslip_run in self: + payslip_run.can_finance_approve = ( + payslip_run.state == "close" + and payslip_run.finance_approval_state == "pending" + and payslip_run.finance_manager_user_id == self.env.user + ) + + def _send_finance_submission_mail(self, finance_manager): + for payslip_run in self: + subject = _("Payslip batch pending finance approval: %s", payslip_run.name) + body = _( + "Dear %(finance)s,
" + "The payslip batch %(batch)s has been validated by HR and is pending your confirmation.
" + "Payslips: %(count)s
", + finance=finance_manager.name, + batch=payslip_run.name, + count=len(payslip_run.slip_ids), + ) + payslip_run.slip_ids[:1]._send_payroll_mail(finance_manager, subject, body) + + def action_validate(self): + result = super().action_validate() + for payslip_run in self.filtered(lambda run: run.state == "close"): + finance_manager = payslip_run.slip_ids[:1]._get_finance_manager() + now = fields.Datetime.now() + payslip_run.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, + }) + payslip_run.slip_ids.filtered(lambda slip: slip.state == "done").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, + }) + payslip_run._send_finance_submission_mail(finance_manager) + return result + + def action_paid(self): + for payslip_run in self: + if payslip_run.finance_approval_state != "pending": + raise UserError(_("Only batches pending finance approval can be marked as paid.")) + result = super().action_paid() + self.write({ + "finance_approval_state": "approved", + "finance_approved_date": fields.Datetime.now(), + }) + return result + + def action_finance_reject_wizard(self): + self.ensure_one() + self.slip_ids[:1]._check_current_user_is_finance_manager() + if self.state != "close" or self.finance_approval_state != "pending": + raise UserError(_("Only batches pending finance approval can be rejected.")) + return { + "type": "ir.actions.act_window", + "name": _("Reject Payslip Batch"), + "res_model": "hr.payslip.finance.reject.wizard", + "view_mode": "form", + "target": "new", + "context": {"default_payslip_run_id": self.id}, + } + + def action_finance_download_xlsx(self): + self.ensure_one() + return self.slip_ids._action_finance_download_xlsx(filename="%s.xlsx" % (self.name or _("Finance Payment List"))) + + def action_finance_download_pdf(self): + self.ensure_one() + return self.env.ref("hr_payroll_extended.action_report_finance_payment_list").report_action(self.slip_ids) diff --git a/addons_extensions/hr_payroll_extended/report/finance_payment_list_report.xml b/addons_extensions/hr_payroll_extended/report/finance_payment_list_report.xml new file mode 100644 index 000000000..867a901db --- /dev/null +++ b/addons_extensions/hr_payroll_extended/report/finance_payment_list_report.xml @@ -0,0 +1,56 @@ + +| Employee Name | +Bank Account | +Amount | +
|---|---|---|
| + + | ++ + | ++ + | +
| Total | ++ + | +|
+ Your application has been posted successfully,
+ We usually respond within 3 days...
+
Your contact information is:
+
+ I usually answer applications within 3 days.
+
+ The next step is either a call or a meeting in our offices.
+
+ Feel free to contact me if you want a faster
+ feedback or if you don't get news from me
+ quickly enough.
+