diff --git a/hr_payroll_document/wizard/payroll_management_wizard.py b/hr_payroll_document/wizard/payroll_management_wizard.py index bb3c96fc9..dc0036685 100644 --- a/hr_payroll_document/wizard/payroll_management_wizard.py +++ b/hr_payroll_document/wizard/payroll_management_wizard.py @@ -1,4 +1,5 @@ import base64 +import pathlib from base64 import b64decode from pypdf import PdfReader, PdfWriter @@ -10,6 +11,7 @@ class PayrollManagamentWizard(models.TransientModel): _name = "payroll.management.wizard" _description = "Payroll Management" + _rec_name = "subject" subject = fields.Char( help="Enter the title of the payroll whether it is the month, week, day, etc.", @@ -19,10 +21,16 @@ class PayrollManagamentWizard(models.TransientModel): "ir.attachment", "payrol_rel", "doc_id", "attach_id3", copy=False, required=True ) + def _get_temp_path(self): + self.ensure_one() + path = f"/tmp/{self._table}_{self.id}/" + pathlib.Path(path).mkdir(exist_ok=True) + return path + def send_payrolls(self): not_found = set() self.merge_pdfs() - reader = PdfReader("/tmp/merged-pdf.pdf") + reader = PdfReader(f"{self._get_temp_path()}merged-pdf.pdf") employees = set() # Validate if company have country @@ -48,7 +56,9 @@ def send_payrolls(self): # Save pdf with payrolls of employee pdfWriter.add_page(page) - path = "/tmp/" + self.env._("Payroll ") + employee.name + ".pdf" + path = ( + self._get_temp_path() + self.env._("Payroll ") + employee.name + ".pdf" + ) if not employee.no_payroll_encryption: # Encrypt the payroll file @@ -96,13 +106,14 @@ def send_payrolls(self): def merge_pdfs(self): # Merge the pdfs together + temp_path = self._get_temp_path() pdfs = [] for file in self.payrolls: b64 = file.datas btes = b64decode(b64, validate=True) if btes[0:4] != b"%PDF": raise ValidationError(self.env._("Missing pdf file signature")) - f = open("/tmp/" + file.name, "wb") + f = open(self._get_temp_path() + file.name, "wb") f.write(btes) f.close() pdfs.append(f.name) @@ -112,7 +123,7 @@ def merge_pdfs(self): for pdf in pdfs: merger.append(pdf) - merger.write("/tmp/merged-pdf.pdf") + merger.write(f"{temp_path}merged-pdf.pdf") merger.close() def send_mail(self, employee, path): diff --git a/hr_payroll_document_queue/README.rst b/hr_payroll_document_queue/README.rst new file mode 100644 index 000000000..380203c7c --- /dev/null +++ b/hr_payroll_document_queue/README.rst @@ -0,0 +1,92 @@ +============================= +HR - Payroll Document - Queue +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9b97fc250e9f7c34ba848cdc7ac246d98c29cdd4d8f16c1d6ed18fcf14fbc4f8 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpayroll-lightgray.png?logo=github + :target: https://github.com/OCA/payroll/tree/18.0/hr_payroll_document_queue + :alt: OCA/payroll +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/payroll-18-0/payroll-18-0-hr_payroll_document_queue + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/payroll&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allow to process the payslips asynchronously. + +If a payroll is being processed asynchronously, attempting to process it +again will show a warning and prevent it from being processed. + +When the payroll has been processed, the user is notified according to +their Notification preference. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* PyTech + +Contributors +------------ + +- `PyTech `__: + + - Simone Rubino + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-SirPyTech| image:: https://github.com/SirPyTech.png?size=40px + :target: https://github.com/SirPyTech + :alt: SirPyTech + +Current `maintainer `__: + +|maintainer-SirPyTech| + +This module is part of the `OCA/payroll `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_payroll_document_queue/__init__.py b/hr_payroll_document_queue/__init__.py new file mode 100644 index 000000000..74d71de4b --- /dev/null +++ b/hr_payroll_document_queue/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import wizards diff --git a/hr_payroll_document_queue/__manifest__.py b/hr_payroll_document_queue/__manifest__.py new file mode 100644 index 000000000..6dd2dcc21 --- /dev/null +++ b/hr_payroll_document_queue/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2026 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "HR - Payroll Document - Queue", + "summary": "Process a PDF payslip aynchronously.", + "author": "PyTech, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/payroll", + "license": "AGPL-3", + "category": "Payrolls", + "version": "18.0.1.0.0", + "maintainers": [ + "SirPyTech", + ], + "depends": [ + "hr_payroll_document", + "queue_job", + ], + "data": [ + "wizards/payroll_management_wizard_views.xml", + ], +} diff --git a/hr_payroll_document_queue/pyproject.toml b/hr_payroll_document_queue/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/hr_payroll_document_queue/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/hr_payroll_document_queue/readme/CONTRIBUTORS.md b/hr_payroll_document_queue/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..f5175108f --- /dev/null +++ b/hr_payroll_document_queue/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [PyTech](https://www.pytech.it): + - Simone Rubino \ diff --git a/hr_payroll_document_queue/readme/DESCRIPTION.md b/hr_payroll_document_queue/readme/DESCRIPTION.md new file mode 100644 index 000000000..7c0e6cad2 --- /dev/null +++ b/hr_payroll_document_queue/readme/DESCRIPTION.md @@ -0,0 +1,7 @@ +Allow to process the payslips asynchronously. + +If a payroll is being processed asynchronously, attempting to process it +again will show a warning and prevent it from being processed. + +When the payroll has been processed, the user is notified according to +their Notification preference. diff --git a/hr_payroll_document_queue/static/description/index.html b/hr_payroll_document_queue/static/description/index.html new file mode 100644 index 000000000..289663914 --- /dev/null +++ b/hr_payroll_document_queue/static/description/index.html @@ -0,0 +1,432 @@ + + + + + +HR - Payroll Document - Queue + + + +
+

HR - Payroll Document - Queue

+ + +

Beta License: AGPL-3 OCA/payroll Translate me on Weblate Try me on Runboat

+

Allow to process the payslips asynchronously.

+

If a payroll is being processed asynchronously, attempting to process it +again will show a warning and prevent it from being processed.

+

When the payroll has been processed, the user is notified according to +their Notification preference.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • PyTech
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

SirPyTech

+

This module is part of the OCA/payroll project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/hr_payroll_document_queue/tests/__init__.py b/hr_payroll_document_queue/tests/__init__.py new file mode 100644 index 000000000..cbc8cf7e3 --- /dev/null +++ b/hr_payroll_document_queue/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_payroll_management diff --git a/hr_payroll_document_queue/tests/test_payroll_management.py b/hr_payroll_document_queue/tests/test_payroll_management.py new file mode 100644 index 000000000..be6345752 --- /dev/null +++ b/hr_payroll_document_queue/tests/test_payroll_management.py @@ -0,0 +1,73 @@ +# Copyright 2026 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import exceptions + +from odoo.addons.hr_payroll_document.tests.test_hr_payroll_document import ( + TestHRPayrollDocument, +) +from odoo.addons.mail.tests.common import MailCase +from odoo.addons.queue_job.tests.common import trap_jobs + + +class TestPayrollManagement(MailCase, TestHRPayrollDocument): + def test_job_creation(self): + """The job is created.""" + # Arrange + self.fill_company_id() + employee = self.employee_emp + employee.identification_id = "51000278D" + + # Act + with trap_jobs() as trap: + self.wizard.send_payrolls_async() + + # Assert + trap.assert_jobs_count(1) + + def test_process_same_payroll(self): + """The same payroll cannot be processed if it is already being processed.""" + # Arrange + self.fill_company_id() + self.employee_emp.identification_id = "51000278D" + self.wizard.send_payrolls_async() + other_wizard = self.wizard.copy() + other_wizard.payrolls = self.wizard.payrolls + + # Act + with self.assertRaises(exceptions.UserError) as ue: + other_wizard.send_payrolls() + + # Assert + exc_message = ue.exception.args[0] + self.assertIn("cannot be processed", exc_message) + self.assertTrue(other_wizard.is_payroll_being_processed) + self.assertFalse(other_wizard.is_send_visible) + + def test_email_notification(self): + """ + If the user has "email" notification preference, + when the payrolls are processed the user is notified. + """ + # Arrange + self.fill_company_id() + employee = self.employee_emp + employee.identification_id = "51000278D" + author_partner = self.env.ref("base.partner_root") + payman_user = self.user_admin + payman_user.notification_type = "email" + wizard = self.wizard + + # Act + with trap_jobs() as trap, self.mock_mail_gateway(): + wizard.with_user(payman_user).send_payrolls_async() + trap.perform_enqueued_jobs() + + # Assert + email = self.assertMailMail( + payman_user.partner_id, + "sent", + author=author_partner, + ) + self.assertEqual(email.model, wizard._name) + self.assertEqual(email.res_id, wizard.id) diff --git a/hr_payroll_document_queue/wizards/__init__.py b/hr_payroll_document_queue/wizards/__init__.py new file mode 100644 index 000000000..723401a24 --- /dev/null +++ b/hr_payroll_document_queue/wizards/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import payroll_management_wizard diff --git a/hr_payroll_document_queue/wizards/payroll_management_wizard.py b/hr_payroll_document_queue/wizards/payroll_management_wizard.py new file mode 100644 index 000000000..ccaca879e --- /dev/null +++ b/hr_payroll_document_queue/wizards/payroll_management_wizard.py @@ -0,0 +1,92 @@ +# Copyright 2026 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from itertools import chain + +from odoo import _, api, exceptions, fields, models + +from odoo.addons.queue_job.job import CANCELLED, DONE, FAILED + + +class PayrollManagamentWizard(models.TransientModel): + _inherit = "payroll.management.wizard" + + is_payroll_being_processed = fields.Boolean( + string="Is being processed", + help="One of the selected payrolls is being processed.", + compute="_compute_is_payroll_being_processed", + compute_sudo=True, + ) + is_send_visible = fields.Boolean( + string="Send button is visible", + compute="_compute_is_send_visible", + ) + + def _notify_async_processed(self, send_result): + """Notify the result to the user that processed the payrolls.""" + self.ensure_one() + odoobot = self.env.ref("base.user_root") + user = self.env.user + return ( + self.env["mail.thread"] + .with_user(odoobot) + .message_notify( + partner_ids=user.partner_id.ids, + subject=send_result["params"]["title"], + body=send_result["params"]["message"], + model=self._name, + res_id=self.id, + ) + ) + + def send_payrolls(self): + if not self.is_send_visible: + # We are already hiding the buttons in the UI, + # but this public method can still be executed. + raise exceptions.UserError(_("The selected payrolls cannot be processed.")) + + result = super().send_payrolls() + + if self.env.context.get("job_uuid"): + self._notify_async_processed(result) + return result + + def send_payrolls_async(self): + return self.with_delay().send_payrolls() + + def _get_payrolls_being_processed(self): + jobs = self.env["queue.job"].search( + [ + ("model_name", "=", self._name), + ("method_name", "=", "send_payrolls"), + ("state", "not in", [CANCELLED, DONE, FAILED]), + ] + ) + jobs_records_ids = [ + wizard.id + for wizard in chain.from_iterable(jobs.mapped("records")) + if wizard not in self + ] + jobs_records = self.browse(jobs_records_ids).exists() + return jobs_records.payrolls + + @api.depends( + "payrolls", + ) + def _compute_is_payroll_being_processed(self): + jobs_payrolls_checksums = set( + self._get_payrolls_being_processed().mapped("checksum") + ) + + for wizard in self: + wizard.is_payroll_being_processed = any( + payroll.checksum in jobs_payrolls_checksums + for payroll in wizard.payrolls + ) + + @api.depends( + "is_payroll_being_processed", + ) + def _compute_is_send_visible(self): + for wizard in self: + wizard.is_send_visible = not wizard.is_payroll_being_processed diff --git a/hr_payroll_document_queue/wizards/payroll_management_wizard_views.xml b/hr_payroll_document_queue/wizards/payroll_management_wizard_views.xml new file mode 100644 index 000000000..be3e63bc6 --- /dev/null +++ b/hr_payroll_document_queue/wizards/payroll_management_wizard_views.xml @@ -0,0 +1,47 @@ + + + + + Add Async buttons and fields Payroll Management form view + payroll.management.wizard + + + + + + + + + + + + diff --git a/setup/hr_payroll_document_queue/odoo/addons/hr_payroll_document_queue b/setup/hr_payroll_document_queue/odoo/addons/hr_payroll_document_queue new file mode 120000 index 000000000..1fe87fe7a --- /dev/null +++ b/setup/hr_payroll_document_queue/odoo/addons/hr_payroll_document_queue @@ -0,0 +1 @@ +../../../../hr_payroll_document_queue \ No newline at end of file diff --git a/setup/hr_payroll_document_queue/setup.py b/setup/hr_payroll_document_queue/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/hr_payroll_document_queue/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)