From 0361f57dd68e9b3bc559248c63960bcbb9fe2e12 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Tue, 10 Feb 2026 09:47:34 +0100 Subject: [PATCH 1/2] [IMP] hr_payroll_document: Thread safe If multiple wizards are sending payrolls at the same time, every wizard acts in their own folder. Also add display name. --- .../wizard/payroll_management_wizard.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/hr_payroll_document/wizard/payroll_management_wizard.py b/hr_payroll_document/wizard/payroll_management_wizard.py index 6ce2d4f47..90a86ab71 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, errors @@ -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." @@ -62,6 +64,12 @@ def _extract_employees(self, pdf_reader, fallback_reader=None): return employee_to_pages, not_found_ids + 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 _build_employee_payroll(self, file_name, pdf_pages, encryption_key=None): """Return the path to the created payroll. @@ -71,7 +79,7 @@ def _build_employee_payroll(self, file_name, pdf_pages, encryption_key=None): for page in pdf_pages: pdfWriter.add_page(page) - path = "/tmp/" + file_name + path = self._get_temp_path() + file_name if encryption_key: pdfWriter.encrypt(encryption_key, algorithm="AES-256") @@ -99,7 +107,7 @@ def send_payrolls(self): if not self.env.company.country_id: raise UserError(_("You must to filled country field of company")) - reader = PdfReader("/tmp/merged-pdf.pdf") + reader = PdfReader(f"{self._get_temp_path()}merged-pdf.pdf") try: employee_to_pages, not_found = self._extract_employees(reader) @@ -158,13 +166,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(_("Missing pdf file signature")) - f = open("/tmp/" + file.name, "wb") + f = open(temp_path + file.name, "wb") f.write(btes) f.close() pdfs.append(f.name) @@ -174,7 +183,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): From 7579a15acd117a87c74e6d0be9b7653e544b0a07 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Mon, 9 Feb 2026 11:55:21 +0100 Subject: [PATCH 2/2] [ADD] hr_payroll_document_queue --- hr_payroll_document_queue/README.rst | 90 ++++ hr_payroll_document_queue/__init__.py | 3 + hr_payroll_document_queue/__manifest__.py | 22 + .../readme/CONTRIBUTORS.rst | 3 + .../readme/DESCRIPTION.rst | 5 + .../static/description/index.html | 430 ++++++++++++++++++ hr_payroll_document_queue/tests/__init__.py | 3 + .../tests/test_payroll_management.py | 71 +++ hr_payroll_document_queue/wizards/__init__.py | 3 + .../wizards/payroll_management_wizard.py | 89 ++++ .../payroll_management_wizard_views.xml | 59 +++ .../odoo/addons/hr_payroll_document_queue | 1 + setup/hr_payroll_document_queue/setup.py | 6 + 13 files changed, 785 insertions(+) create mode 100644 hr_payroll_document_queue/README.rst create mode 100644 hr_payroll_document_queue/__init__.py create mode 100644 hr_payroll_document_queue/__manifest__.py create mode 100644 hr_payroll_document_queue/readme/CONTRIBUTORS.rst create mode 100644 hr_payroll_document_queue/readme/DESCRIPTION.rst create mode 100644 hr_payroll_document_queue/static/description/index.html create mode 100644 hr_payroll_document_queue/tests/__init__.py create mode 100644 hr_payroll_document_queue/tests/test_payroll_management.py create mode 100644 hr_payroll_document_queue/wizards/__init__.py create mode 100644 hr_payroll_document_queue/wizards/payroll_management_wizard.py create mode 100644 hr_payroll_document_queue/wizards/payroll_management_wizard_views.xml create mode 120000 setup/hr_payroll_document_queue/odoo/addons/hr_payroll_document_queue create mode 100644 setup/hr_payroll_document_queue/setup.py diff --git a/hr_payroll_document_queue/README.rst b/hr_payroll_document_queue/README.rst new file mode 100644 index 000000000..341f7924e --- /dev/null +++ b/hr_payroll_document_queue/README.rst @@ -0,0 +1,90 @@ +============================= +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/16.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-16-0/payroll-16-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=16.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..20c3ffa14 --- /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": "16.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/readme/CONTRIBUTORS.rst b/hr_payroll_document_queue/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..3e2b10402 --- /dev/null +++ b/hr_payroll_document_queue/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `PyTech `_: + + * Simone Rubino diff --git a/hr_payroll_document_queue/readme/DESCRIPTION.rst b/hr_payroll_document_queue/readme/DESCRIPTION.rst new file mode 100644 index 000000000..b1e36f7b2 --- /dev/null +++ b/hr_payroll_document_queue/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +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..659c27ba2 --- /dev/null +++ b/hr_payroll_document_queue/static/description/index.html @@ -0,0 +1,430 @@ + + + + + +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..0466ce01c --- /dev/null +++ b/hr_payroll_document_queue/tests/test_payroll_management.py @@ -0,0 +1,71 @@ +# 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.common 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..368fe04b2 --- /dev/null +++ b/hr_payroll_document_queue/wizards/payroll_management_wizard.py @@ -0,0 +1,89 @@ +# Copyright 2026 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import _, api, exceptions, fields, models +from odoo.tools.misc import flatten + +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.partner_root") + user = self.env.user + return self.env["mail.thread"].message_notify( + author_id=odoobot.id, + 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_id in flatten(jobs.mapped("record_ids")) + if wizard_id not in self.ids + ] + 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..d43b4770e --- /dev/null +++ b/hr_payroll_document_queue/wizards/payroll_management_wizard_views.xml @@ -0,0 +1,59 @@ + + + + + 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, +)