diff --git a/hr_payroll_document/README.rst b/hr_payroll_document/README.rst index f07323c9a..794ad66fc 100644 --- a/hr_payroll_document/README.rst +++ b/hr_payroll_document/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ===================== HR - Payroll Document ===================== @@ -17,7 +13,7 @@ HR - Payroll Document .. |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/license-AGPL--3-blue.png +.. |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 diff --git a/hr_payroll_document/__manifest__.py b/hr_payroll_document/__manifest__.py index 0d0d87da7..4310ae11c 100644 --- a/hr_payroll_document/__manifest__.py +++ b/hr_payroll_document/__manifest__.py @@ -8,7 +8,11 @@ "version": "17.0.1.1.2", "depends": ["hr", "base_vat"], "maintainers": ["peluko00"], - "external_dependencies": {"python": ["pypdf"]}, + "external_dependencies": { + "python": [ + "pypdf", + ], + }, "data": [ "wizard/payroll_management_wizard.xml", "security/ir.model.access.csv", diff --git a/hr_payroll_document/models/hr_employee.py b/hr_payroll_document/models/hr_employee.py index 1609f8f4e..545d19c6d 100644 --- a/hr_payroll_document/models/hr_employee.py +++ b/hr_payroll_document/models/hr_employee.py @@ -1,4 +1,7 @@ -from odoo import _, fields, models +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -44,10 +47,19 @@ def action_get_payroll_tree_view(self): ) return action - def write(self, vals): - res = super().write(vals) - if "identification_id" in vals and not self.env["res.partner"].simple_vat_check( - self.env.company.country_id.code, vals["identification_id"] - ): - raise ValidationError(_("The field identification ID is not valid")) - return res + def _validate_payroll_identification(self, code=None): + # Override if the identification should be validated in another way + if code is None and len(self) == 1: + code = self.identification_id + if country_code := self.env.company.country_id.code: + is_valid = self.env["res.partner"].simple_vat_check(country_code, code) + else: + is_valid = True + return is_valid + + @api.constrains("identification_id") + def _constrain_payroll_identification(self): + # Only check the employees that have an `identification_id` + for employee in self.filtered("identification_id"): + if not employee._validate_payroll_identification(): + raise ValidationError(_("The field identification ID is not valid")) diff --git a/hr_payroll_document/static/description/index.html b/hr_payroll_document/static/description/index.html index baa080cea..16495fcaf 100644 --- a/hr_payroll_document/static/description/index.html +++ b/hr_payroll_document/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +HR - Payroll Document -
+
+

HR - Payroll Document

- - -Odoo Community Association - -
-

HR - Payroll Document

-

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

+

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

This module have a wizard view to manage the different payrolls of employees which is identified by the identification_id attribute.

By default, the employee’s payroll is encrypted using their @@ -393,7 +388,7 @@

HR - Payroll Document

-

Bug Tracker

+

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 @@ -401,15 +396,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • APSL
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -442,6 +437,5 @@

Maintainers

-
diff --git a/hr_payroll_document/tests/common.py b/hr_payroll_document/tests/common.py new file mode 100644 index 000000000..a670190ca --- /dev/null +++ b/hr_payroll_document/tests/common.py @@ -0,0 +1,75 @@ +import base64 +import contextlib +from unittest import mock + +from odoo.tests import common +from odoo.tools.misc import file_path as open_file_path + +from odoo.addons.mail.tests.common import mail_new_test_user + + +class TestHrPayrollDocument(common.TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env.user.tz = "Europe/Brussels" + cls.user_admin = cls.env.ref("base.user_admin") + + # Fix Company without country + cls.env.company.country_id = False + + # Test users to use through the various tests + cls.user_employee = mail_new_test_user( + cls.env, login="david", groups="base.group_user" + ) + cls.user_employee_id = cls.user_employee.id + + # Hr Data + cls.employee_emp = cls.env["hr.employee"].create( + { + "name": "David Employee", + "user_id": cls.user_employee_id, + "company_id": 1, + "identification_id": "30831011V", + } + ) + + cls.wizard = cls._create_wizard("January", "hr_payroll_document/tests/test.pdf") + + @classmethod + def _create_wizard(cls, subject, file_path): + with open(open_file_path(file_path), "rb") as pdf_file: + encoded_string = base64.b64encode(pdf_file.read()) + ir_values = { + "name": "test", + "type": "binary", + "datas": encoded_string, + "store_fname": encoded_string, + "res_model": "payroll.management.wizard", + "res_id": 1, + } + cls.attachment = cls.env["ir.attachment"].create(ir_values) + cls.subject = subject + return cls.env["payroll.management.wizard"].create( + {"payrolls": [cls.attachment.id], "subject": cls.subject} + ) + + @contextlib.contextmanager + def _mock_valid_identification(self, employee, identification_code): + def _mocked_validate_payroll_identification(self, code=None): + if code is None: + code = employee.identification_id + return code == identification_code + + with mock.patch.object( + type(employee), + "_validate_payroll_identification", + _mocked_validate_payroll_identification, + ) as patch: + patch.side_effect = _mocked_validate_payroll_identification + yield + + def fill_company_id(self): + self.env.company.country_id = self.env["res.country"].search( + [("name", "=", "Spain")] + ) diff --git a/hr_payroll_document/tests/test_hr_payroll_document.py b/hr_payroll_document/tests/test_hr_payroll_document.py index edf313375..28c4d1b86 100644 --- a/hr_payroll_document/tests/test_hr_payroll_document.py +++ b/hr_payroll_document/tests/test_hr_payroll_document.py @@ -5,74 +5,14 @@ from odoo import _ from odoo.exceptions import UserError, ValidationError -from odoo.tests import common -from odoo.tools.misc import file_path -from odoo.addons.mail.tests.common import mail_new_test_user +from odoo.addons.hr_payroll_document.tests.common import TestHrPayrollDocument -class TestHRPayrollDocument(common.TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.env.user.tz = "Europe/Brussels" - cls.user_admin = cls.env.ref("base.user_admin") - - # Fix Company without country - cls.env.company.country_id = False - - # Test users to use through the various tests - cls.user_employee = mail_new_test_user( - cls.env, login="david", groups="base.group_user" - ) - cls.user_employee_id = cls.user_employee.id - - # Hr Data - cls.employee_emp = cls.env["hr.employee"].create( - { - "name": "David Employee", - "user_id": cls.user_employee_id, - "company_id": 1, - "identification_id": "30831011V", - } - ) - - with open(file_path("hr_payroll_document/tests/test.pdf"), "rb") as pdf_file: - encoded_string = base64.b64encode(pdf_file.read()) - ir_values = { - "name": "test", - "type": "binary", - "datas": encoded_string, - "store_fname": encoded_string, - "res_model": "payroll.management.wizard", - "res_id": 1, - } - cls.attachment = cls.env["ir.attachment"].create(ir_values) - cls.subject = "January" - cls.wizard = cls.env["payroll.management.wizard"].create( - {"payrolls": [cls.attachment.id], "subject": cls.subject} - ) - - def fill_company_id(self): - self.env.company.country_id = self.env["res.country"].search( - [("name", "=", "Spain")] - ) - +class TestHRPayrollDocument(TestHrPayrollDocument): def test_extension_error(self): - with open(file_path("hr_payroll_document/tests/test.docx"), "rb") as pdf_file: - encoded_string = base64.b64encode(pdf_file.read()) - ir_values = { - "name": "test", - "type": "binary", - "datas": encoded_string, - "store_fname": encoded_string, - "res_model": "payroll.management.wizard", - "res_id": 1, - } - self.attachment = self.env["ir.attachment"].create(ir_values) - self.subject = "January" - self.wizard = self.env["payroll.management.wizard"].create( - {"payrolls": [self.attachment.id], "subject": self.subject} + self.wizard = self._create_wizard( + "January", "hr_payroll_document/tests/test.docx" ) with self.assertRaises(ValidationError): self.wizard.send_payrolls() diff --git a/hr_payroll_document/wizard/payroll_management_wizard.py b/hr_payroll_document/wizard/payroll_management_wizard.py index 37f51b387..1866a96c3 100644 --- a/hr_payroll_document/wizard/payroll_management_wizard.py +++ b/hr_payroll_document/wizard/payroll_management_wizard.py @@ -1,7 +1,7 @@ import base64 from base64 import b64decode -from pypdf import PdfReader, PdfWriter +from pypdf import PdfReader, PdfWriter, errors from odoo import _, fields, models from odoo.exceptions import UserError, ValidationError @@ -19,55 +19,110 @@ class PayrollManagamentWizard(models.TransientModel): "ir.attachment", "payrol_rel", "doc_id", "attach_id3", copy=False, required=True ) - def send_payrolls(self): - not_found = set() - self.merge_pdfs() - reader = PdfReader("/tmp/merged-pdf.pdf") - employees = set() - - # Validate if company have country - if not self.env.company.country_id: - raise UserError(_("You must to filled country field of company")) + def _get_fallback_reader(self, pdf_reader): + # Override to use another reader + pass + + def _read_page_content(self, pdf_reader, page, fallback_reader=None): + try: + page_content = page.extract_text().split() + except errors.PdfReadError: + if fallback_reader: + # The original page cannot be read: + # read the simplified page in the fallback_reader + page_number = pdf_reader.get_page_number(page) + fallback_page = fallback_reader.get_page(page_number) + page_content = fallback_page.extract_text().split() + else: + raise + return page_content + + def _extract_employees(self, pdf_reader, fallback_reader=None): + employee_to_pages = dict() + not_found_ids = set() # Find all IDs of the employees - for page in reader.pages: - for value in page.extract_text().split(): + for page in pdf_reader.pages: + page_content = self._read_page_content( + pdf_reader, page, fallback_reader=fallback_reader + ) + for value in page_content: if self.validate_id(value) and value != self.env.company.vat: employee = self.env["hr.employee"].search( [("identification_id", "=", value)] ) if employee: - employees.add(employee) + employee_to_pages.setdefault(employee, []).append(page) else: - not_found.add(value) + not_found_ids.add(value) + break - for employee in list(employees): - pdfWriter = PdfWriter() - for page in reader.pages: - if employee.identification_id in page.extract_text(): - # Save pdf with payrolls of employee - pdfWriter.add_page(page) + return employee_to_pages, not_found_ids - path = "/tmp/" + _("Payroll ") + employee.name + ".pdf" + def _build_employee_payroll(self, file_name, pdf_pages, encryption_key=None): + """Return the path to the created payroll. - if not employee.no_payroll_encryption: - # Encrypt the payroll file - # with the identification identifier of the employee - pdfWriter.encrypt(employee.identification_id, algorithm="AES-256") + Optionally encrypt the payroll file with `encryption_key`. + """ + pdfWriter = PdfWriter() + for page in pdf_pages: + pdfWriter.add_page(page) - f = open(path, "wb") - pdfWriter.write(f) - f.close() + path = "/tmp/" + file_name - # Send payroll to the employee - self.send_mail(employee, path) + if encryption_key: + pdfWriter.encrypt(encryption_key, algorithm="AES-256") + + with open(path, "wb") as f: + pdfWriter.write(f) + return path + def _show_employees_action(self): action = self.env["ir.actions.actions"]._for_xml_id( "hr_payroll_document.payrolls_view_action" ) action["views"] = [ [self.env.ref("hr_payroll_document.view_payroll_tree").id, "list"] ] + return action + + def send_payrolls(self): + self.merge_pdfs() + # Validate if company have country + if not self.env.company.country_id: + raise UserError(_("You must to filled country field of company")) + + reader = PdfReader("/tmp/merged-pdf.pdf") + + try: + employee_to_pages, not_found = self._extract_employees(reader) + except errors.PdfReadError: + # Couldn't read the file, try again with another reader + fallback_reader = self._get_fallback_reader(reader) + if fallback_reader: + employee_to_pages, not_found = self._extract_employees( + reader, fallback_reader=fallback_reader + ) + else: + raise + + for employee, pages in employee_to_pages.items(): + encryption_key = ( + None if employee.no_payroll_encryption else employee.identification_id + ) + path = self._build_employee_payroll( + _( + "Payroll %(subject)s %(employee)s.pdf", + employee=employee.name, + subject=self.subject, + ), + pages, + encryption_key=encryption_key, + ) + # Send payroll to the employee + self.send_mail(employee, path) + + action = self._show_employees_action() if not_found: return { "type": "ir.actions.client", @@ -151,7 +206,5 @@ def send_mail(self, employee, path): employee.id, force_send=True ) - def validate_id(self, number): - return self.env["res.partner"].simple_vat_check( - self.env.company.country_id.code, number - ) + def validate_id(self, code): + return self.env["hr.employee"]._validate_payroll_identification(code=code) diff --git a/hr_payroll_document_pymupdf/README.rst b/hr_payroll_document_pymupdf/README.rst new file mode 100644 index 000000000..034997678 --- /dev/null +++ b/hr_payroll_document_pymupdf/README.rst @@ -0,0 +1,80 @@ +=============================== +HR - Payroll Document - PyMuPDF +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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/17.0/hr_payroll_document_pymupdf + :alt: OCA/payroll +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/payroll-17-0/payroll-17-0-hr_payroll_document_pymupdf + :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=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Use the library PyMuPDF (https://github.com/pymupdf/pymupdf) to attempt +reading the PDF payslip. + +**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 + +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_pymupdf/__init__.py b/hr_payroll_document_pymupdf/__init__.py new file mode 100644 index 000000000..74d71de4b --- /dev/null +++ b/hr_payroll_document_pymupdf/__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_pymupdf/__manifest__.py b/hr_payroll_document_pymupdf/__manifest__.py new file mode 100644 index 000000000..0cac71dc9 --- /dev/null +++ b/hr_payroll_document_pymupdf/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "HR - Payroll Document - PyMuPDF", + "summary": "Try harder to read a PDF payslip with PyMuPDF.", + "author": "PyTech, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/payroll", + "license": "AGPL-3", + "category": "Payrolls", + "version": "17.0.1.0.0", + "depends": [ + "hr_payroll_document", + ], + "maintainers": [ + "SirPyTech", + ], + "external_dependencies": { + "python": [ + "PyMuPDF", + ], + }, +} diff --git a/hr_payroll_document_pymupdf/i18n/hr_payroll_document_pymupdf.pot b/hr_payroll_document_pymupdf/i18n/hr_payroll_document_pymupdf.pot new file mode 100644 index 000000000..75a717f16 --- /dev/null +++ b/hr_payroll_document_pymupdf/i18n/hr_payroll_document_pymupdf.pot @@ -0,0 +1,26 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_payroll_document_pymupdf +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_payroll_document_pymupdf +#: model:ir.model,name:hr_payroll_document_pymupdf.model_payroll_management_wizard +msgid "Payroll Management" +msgstr "" + +#. module: hr_payroll_document_pymupdf +#. odoo-python +#: code:addons/hr_payroll_document_pymupdf/tests/test_payroll_management.py:0 +#, python-format +msgid "Payrolls sent" +msgstr "" diff --git a/hr_payroll_document_pymupdf/i18n/it.po b/hr_payroll_document_pymupdf/i18n/it.po new file mode 100644 index 000000000..5bd7b911b --- /dev/null +++ b/hr_payroll_document_pymupdf/i18n/it.po @@ -0,0 +1,29 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_payroll_document_pymupdf +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-01-22 09:50+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" + +#. module: hr_payroll_document_pymupdf +#: model:ir.model,name:hr_payroll_document_pymupdf.model_payroll_management_wizard +msgid "Payroll Management" +msgstr "Gestione retribuzione" + +#. module: hr_payroll_document_pymupdf +#. odoo-python +#: code:addons/hr_payroll_document_pymupdf/tests/test_payroll_management.py:0 +#, python-format +msgid "Payrolls sent" +msgstr "Retribuzioni inviate" diff --git a/hr_payroll_document_pymupdf/pyproject.toml b/hr_payroll_document_pymupdf/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/hr_payroll_document_pymupdf/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/hr_payroll_document_pymupdf/readme/DESCRIPTION.md b/hr_payroll_document_pymupdf/readme/DESCRIPTION.md new file mode 100644 index 000000000..6b5cfcab6 --- /dev/null +++ b/hr_payroll_document_pymupdf/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +Use the library PyMuPDF () to +attempt reading the PDF payslip. diff --git a/hr_payroll_document_pymupdf/static/description/icon.png b/hr_payroll_document_pymupdf/static/description/icon.png new file mode 100644 index 000000000..1dcc49c24 Binary files /dev/null and b/hr_payroll_document_pymupdf/static/description/icon.png differ diff --git a/hr_payroll_document_pymupdf/static/description/index.html b/hr_payroll_document_pymupdf/static/description/index.html new file mode 100644 index 000000000..c9bc7d854 --- /dev/null +++ b/hr_payroll_document_pymupdf/static/description/index.html @@ -0,0 +1,419 @@ + + + + + +HR - Payroll Document - PyMuPDF + + + +
+

HR - Payroll Document - PyMuPDF

+ + +

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

+

Use the library PyMuPDF (https://github.com/pymupdf/pymupdf) to attempt +reading the PDF payslip.

+

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
  • +
+
+
+

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_pymupdf/tests/__init__.py b/hr_payroll_document_pymupdf/tests/__init__.py new file mode 100644 index 000000000..cbc8cf7e3 --- /dev/null +++ b/hr_payroll_document_pymupdf/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_pymupdf/tests/test_broken_image.pdf b/hr_payroll_document_pymupdf/tests/test_broken_image.pdf new file mode 100644 index 000000000..2d08e2145 Binary files /dev/null and b/hr_payroll_document_pymupdf/tests/test_broken_image.pdf differ diff --git a/hr_payroll_document_pymupdf/tests/test_payroll_management.py b/hr_payroll_document_pymupdf/tests/test_payroll_management.py new file mode 100644 index 000000000..f2070d2c6 --- /dev/null +++ b/hr_payroll_document_pymupdf/tests/test_payroll_management.py @@ -0,0 +1,18 @@ +from odoo import _ + +from odoo.addons.hr_payroll_document.tests.common import TestHrPayrollDocument + + +class TestPayrollManagement(TestHrPayrollDocument): + def test_pdf_broken_image(self): + """If the PDF cannot be processed with PyPDF, try with another reader.""" + self.fill_company_id() + identification_code = "xXXXXXXXXXXXXXXX" + with self._mock_valid_identification(self.employee_emp, identification_code): + self.employee_emp.identification_id = identification_code + self.wizard = self._create_wizard( + "Subject", "hr_payroll_document_pymupdf/tests/test_broken_image.pdf" + ) + with self._mock_valid_identification(self.employee_emp, identification_code): + result_action = self.wizard.send_payrolls() + self.assertEqual(result_action["params"]["title"], _("Payrolls sent")) diff --git a/hr_payroll_document_pymupdf/wizards/__init__.py b/hr_payroll_document_pymupdf/wizards/__init__.py new file mode 100644 index 000000000..723401a24 --- /dev/null +++ b/hr_payroll_document_pymupdf/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_pymupdf/wizards/payroll_management_wizard.py b/hr_payroll_document_pymupdf/wizards/payroll_management_wizard.py new file mode 100644 index 000000000..07572e891 --- /dev/null +++ b/hr_payroll_document_pymupdf/wizards/payroll_management_wizard.py @@ -0,0 +1,35 @@ +# Copyright 2025 Simone Rubino - PyTech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import io + +import pymupdf +import pypdf +from reportlab.pdfgen import canvas + +from odoo import models + + +class PayrollManagamentWizard(models.TransientModel): + _inherit = "payroll.management.wizard" + + def _get_fallback_reader(self, pdf_reader): + reader = super()._get_fallback_reader(pdf_reader) + if not reader: + # Read the file with another reader + doc = pymupdf.Document(stream=pdf_reader.stream) + + # Create a new PDF with only the extracted content + pdf_content = io.BytesIO() + pdf_canvas = canvas.Canvas(pdf_content) + for page_number in range(pdf_reader.get_num_pages()): + page_content = doc[page_number].get_text().split() + # Create a new page with the read content + pdf_canvas.drawString(0, 0, " ".join(page_content)) + pdf_canvas.showPage() + pdf_canvas.save() + + # Return a PyPDF reader for the new PDF, + # that now is readable + reader = pypdf.PdfReader(pdf_content) + return reader diff --git a/requirements.txt b/requirements.txt index 1ab56ace6..455268548 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ # generated from manifests external_dependencies +PyMuPDF pypdf