From b878d35c0935e788befa212b59dc6e6897294c77 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 12 Nov 2025 12:11:44 +0100 Subject: [PATCH 1/8] [REF] hr_payroll_document: Unique validation method --- hr_payroll_document/README.rst | 6 +--- hr_payroll_document/models/hr_employee.py | 28 +++++++++++++------ .../static/description/index.html | 24 ++++++---------- .../wizard/payroll_management_wizard.py | 6 ++-- 4 files changed, 32 insertions(+), 32 deletions(-) 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/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/wizard/payroll_management_wizard.py b/hr_payroll_document/wizard/payroll_management_wizard.py index 37f51b387..862ff86d0 100644 --- a/hr_payroll_document/wizard/payroll_management_wizard.py +++ b/hr_payroll_document/wizard/payroll_management_wizard.py @@ -151,7 +151,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) From ad256ee38c316b130feae41e43c1b63c12a18046 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 12 Nov 2025 12:33:01 +0100 Subject: [PATCH 2/8] [REF] hr_payroll_document: Allow overriding payroll generation --- .../wizard/payroll_management_wizard.py | 80 ++++++++++++------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/hr_payroll_document/wizard/payroll_management_wizard.py b/hr_payroll_document/wizard/payroll_management_wizard.py index 862ff86d0..a985378eb 100644 --- a/hr_payroll_document/wizard/payroll_management_wizard.py +++ b/hr_payroll_document/wizard/payroll_management_wizard.py @@ -19,55 +19,79 @@ 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 _extract_employees(self, pdf_reader): + employee_to_pages = dict() + not_found_ids = set() # Find all IDs of the employees - for page in reader.pages: + for page in pdf_reader.pages: for value in page.extract_text().split(): 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") + + employee_to_pages, not_found = self._extract_employees(reader) + + 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", From aa4d7ccd625f776b2ca72f1e7eadf9760a139a33 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Mon, 24 Nov 2025 12:39:35 +0100 Subject: [PATCH 3/8] [REF] hr_payroll_document: In tests, do not repeat wizard creation --- hr_payroll_document/tests/common.py | 53 ++++++++++++++++ .../tests/test_hr_payroll_document.py | 63 ++----------------- 2 files changed, 57 insertions(+), 59 deletions(-) create mode 100644 hr_payroll_document/tests/common.py diff --git a/hr_payroll_document/tests/common.py b/hr_payroll_document/tests/common.py new file mode 100644 index 000000000..868f40f12 --- /dev/null +++ b/hr_payroll_document/tests/common.py @@ -0,0 +1,53 @@ +import base64 + +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} + ) diff --git a/hr_payroll_document/tests/test_hr_payroll_document.py b/hr_payroll_document/tests/test_hr_payroll_document.py index edf313375..49129384a 100644 --- a/hr_payroll_document/tests/test_hr_payroll_document.py +++ b/hr_payroll_document/tests/test_hr_payroll_document.py @@ -5,74 +5,19 @@ 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} - ) - +class TestHRPayrollDocument(TestHrPayrollDocument): def fill_company_id(self): self.env.company.country_id = self.env["res.country"].search( [("name", "=", "Spain")] ) 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() From 4b2723fc4395d0b16d818609c586ec73f1e4ebd4 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 3 Dec 2025 11:20:39 +0100 Subject: [PATCH 4/8] [REF] hr_payroll_document: Allow to use fallback reader Also refactor tests to have all the needed setup in the same place --- hr_payroll_document/__manifest__.py | 6 ++- hr_payroll_document/tests/common.py | 22 +++++++++++ .../tests/test_hr_payroll_document.py | 5 --- .../wizard/payroll_management_wizard.py | 39 +++++++++++++++++-- 4 files changed, 62 insertions(+), 10 deletions(-) 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/tests/common.py b/hr_payroll_document/tests/common.py index 868f40f12..a670190ca 100644 --- a/hr_payroll_document/tests/common.py +++ b/hr_payroll_document/tests/common.py @@ -1,4 +1,6 @@ import base64 +import contextlib +from unittest import mock from odoo.tests import common from odoo.tools.misc import file_path as open_file_path @@ -51,3 +53,23 @@ def _create_wizard(cls, subject, file_path): 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 49129384a..28c4d1b86 100644 --- a/hr_payroll_document/tests/test_hr_payroll_document.py +++ b/hr_payroll_document/tests/test_hr_payroll_document.py @@ -10,11 +10,6 @@ class TestHRPayrollDocument(TestHrPayrollDocument): - def fill_company_id(self): - self.env.company.country_id = self.env["res.country"].search( - [("name", "=", "Spain")] - ) - def test_extension_error(self): self.wizard = self._create_wizard( "January", "hr_payroll_document/tests/test.docx" diff --git a/hr_payroll_document/wizard/payroll_management_wizard.py b/hr_payroll_document/wizard/payroll_management_wizard.py index a985378eb..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,13 +19,34 @@ class PayrollManagamentWizard(models.TransientModel): "ir.attachment", "payrol_rel", "doc_id", "attach_id3", copy=False, required=True ) - def _extract_employees(self, pdf_reader): + 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 pdf_reader.pages: - for value in page.extract_text().split(): + 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)] @@ -73,7 +94,17 @@ def send_payrolls(self): reader = PdfReader("/tmp/merged-pdf.pdf") - employee_to_pages, not_found = self._extract_employees(reader) + 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 = ( From c0e66c3ef95e71881316bea79c8467c30e30b3b2 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 31 Dec 2025 11:08:04 +0100 Subject: [PATCH 5/8] [ADD] hr_payroll_document_pymupdf --- hr_payroll_document_pymupdf/README.rst | 83 ++++ hr_payroll_document_pymupdf/__init__.py | 3 + hr_payroll_document_pymupdf/__manifest__.py | 20 + .../i18n/hr_payroll_document_pymupdf.pot | 26 ++ .../readme/DESCRIPTION.rst | 1 + .../static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/index.html | 424 ++++++++++++++++++ hr_payroll_document_pymupdf/tests/__init__.py | 3 + .../tests/test_broken_image.pdf | Bin 0 -> 18752 bytes .../tests/test_payroll_management.py | 18 + .../wizards/__init__.py | 3 + .../wizards/payroll_management_wizard.py | 35 ++ 12 files changed, 616 insertions(+) create mode 100644 hr_payroll_document_pymupdf/README.rst create mode 100644 hr_payroll_document_pymupdf/__init__.py create mode 100644 hr_payroll_document_pymupdf/__manifest__.py create mode 100644 hr_payroll_document_pymupdf/i18n/hr_payroll_document_pymupdf.pot create mode 100644 hr_payroll_document_pymupdf/readme/DESCRIPTION.rst create mode 100644 hr_payroll_document_pymupdf/static/description/icon.png create mode 100644 hr_payroll_document_pymupdf/static/description/index.html create mode 100644 hr_payroll_document_pymupdf/tests/__init__.py create mode 100644 hr_payroll_document_pymupdf/tests/test_broken_image.pdf create mode 100644 hr_payroll_document_pymupdf/tests/test_payroll_management.py create mode 100644 hr_payroll_document_pymupdf/wizards/__init__.py create mode 100644 hr_payroll_document_pymupdf/wizards/payroll_management_wizard.py diff --git a/hr_payroll_document_pymupdf/README.rst b/hr_payroll_document_pymupdf/README.rst new file mode 100644 index 000000000..b656cddb4 --- /dev/null +++ b/hr_payroll_document_pymupdf/README.rst @@ -0,0 +1,83 @@ +.. 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 - 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/license-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_pymupdf + :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_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=16.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..2091b1d33 --- /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": "16.0.1.0.1", + "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/readme/DESCRIPTION.rst b/hr_payroll_document_pymupdf/readme/DESCRIPTION.rst new file mode 100644 index 000000000..5c58f5be0 --- /dev/null +++ b/hr_payroll_document_pymupdf/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Use the library PyMuPDF (https://github.com/pymupdf/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 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

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 0000000000000000000000000000000000000000..2d08e2145fc9f3def2948baa1a42f3f0dd389356 GIT binary patch literal 18752 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 literal 0 HcmV?d00001 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..dacfb4d68 --- /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 From 46cff7c79290ae2a9acb9004b8eaab04ce8276d9 Mon Sep 17 00:00:00 2001 From: mymage Date: Thu, 15 Jan 2026 12:25:11 +0000 Subject: [PATCH 6/8] Added translation using Weblate (Italian) --- hr_payroll_document_pymupdf/i18n/it.po | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 hr_payroll_document_pymupdf/i18n/it.po 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" From e72e10e212f22f590e2951c9ece5e806160984f4 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 11 Feb 2026 12:22:47 +0100 Subject: [PATCH 7/8] [IMP] hr_payroll_document_pymupdf: pre-commit execution --- hr_payroll_document_pymupdf/README.rst | 23 +++++++-------- hr_payroll_document_pymupdf/pyproject.toml | 3 ++ .../readme/DESCRIPTION.md | 2 ++ .../readme/DESCRIPTION.rst | 1 - .../static/description/index.html | 29 ++++++++----------- requirements.txt | 1 + 6 files changed, 28 insertions(+), 31 deletions(-) create mode 100644 hr_payroll_document_pymupdf/pyproject.toml create mode 100644 hr_payroll_document_pymupdf/readme/DESCRIPTION.md delete mode 100644 hr_payroll_document_pymupdf/readme/DESCRIPTION.rst diff --git a/hr_payroll_document_pymupdf/README.rst b/hr_payroll_document_pymupdf/README.rst index b656cddb4..034997678 100644 --- a/hr_payroll_document_pymupdf/README.rst +++ b/hr_payroll_document_pymupdf/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 - PyMuPDF =============================== @@ -17,22 +13,23 @@ HR - Payroll Document - PyMuPDF .. |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 - :target: https://github.com/OCA/payroll/tree/16.0/hr_payroll_document_pymupdf + :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-16-0/payroll-16-0-hr_payroll_document_pymupdf + :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=16.0 + :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. +Use the library PyMuPDF (https://github.com/pymupdf/pymupdf) to attempt +reading the PDF payslip. **Table of contents** @@ -45,7 +42,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -53,12 +50,12 @@ Credits ======= Authors -~~~~~~~ +------- * PyTech Maintainers -~~~~~~~~~~~ +----------- This module is maintained by the OCA. @@ -78,6 +75,6 @@ Current `maintainer `__: |maintainer-SirPyTech| -This module is part of the `OCA/payroll `_ project on GitHub. +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/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/readme/DESCRIPTION.rst b/hr_payroll_document_pymupdf/readme/DESCRIPTION.rst deleted file mode 100644 index 5c58f5be0..000000000 --- a/hr_payroll_document_pymupdf/readme/DESCRIPTION.rst +++ /dev/null @@ -1 +0,0 @@ -Use the library PyMuPDF (https://github.com/pymupdf/pymupdf) to attempt reading the PDF payslip. diff --git a/hr_payroll_document_pymupdf/static/description/index.html b/hr_payroll_document_pymupdf/static/description/index.html index cfbfda1fc..c9bc7d854 100644 --- a/hr_payroll_document_pymupdf/static/description/index.html +++ b/hr_payroll_document_pymupdf/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +HR - Payroll Document - PyMuPDF -
+
+

HR - Payroll Document - PyMuPDF

- - -Odoo Community Association - -
-

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.

+

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

    @@ -388,23 +384,23 @@

    HR - Payroll Document - PyMuPDF

-

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 -feedback.

+feedback.

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

-

Credits

+

Credits

-

Authors

+

Authors

  • PyTech
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -414,11 +410,10 @@

Maintainers

promote its widespread use.

Current maintainer:

SirPyTech

-

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

+

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/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 From cf208d7b2ccf498cebc5e36052a5e29534cac468 Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 11 Feb 2026 12:22:53 +0100 Subject: [PATCH 8/8] [MIG] hr_payroll_document_pymupdf: Migration to 17.0 --- hr_payroll_document_pymupdf/__manifest__.py | 2 +- hr_payroll_document_pymupdf/tests/test_payroll_management.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hr_payroll_document_pymupdf/__manifest__.py b/hr_payroll_document_pymupdf/__manifest__.py index 2091b1d33..0cac71dc9 100644 --- a/hr_payroll_document_pymupdf/__manifest__.py +++ b/hr_payroll_document_pymupdf/__manifest__.py @@ -5,7 +5,7 @@ "website": "https://github.com/OCA/payroll", "license": "AGPL-3", "category": "Payrolls", - "version": "16.0.1.0.1", + "version": "17.0.1.0.0", "depends": [ "hr_payroll_document", ], diff --git a/hr_payroll_document_pymupdf/tests/test_payroll_management.py b/hr_payroll_document_pymupdf/tests/test_payroll_management.py index dacfb4d68..f2070d2c6 100644 --- a/hr_payroll_document_pymupdf/tests/test_payroll_management.py +++ b/hr_payroll_document_pymupdf/tests/test_payroll_management.py @@ -11,7 +11,7 @@ def test_pdf_broken_image(self): 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"] + "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()