diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 55401bf0eb2211cae86503a2bb26185e499ce74d_LmdpdGxhYi1jaS55bWw=..9a2413fce7bc5e6d77de207260619f423e1f7e58_LmdpdGxhYi1jaS55bWw= 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,7 @@ mypy: variables: - INSTALL_SECTION: "[test,schema-validation]" + INSTALL_SECTION: "[test,schema-validation,qrcode]" test: variables: @@ -13,7 +13,7 @@ test: variables: - INSTALL_SECTION: "[test,schema-validation]" + INSTALL_SECTION: "[test,schema-validation,qrcode]" test-no-schema-validation: extends: test diff --git a/NEWS.rst b/NEWS.rst index 55401bf0eb2211cae86503a2bb26185e499ce74d_TkVXUy5yc3Q=..9a2413fce7bc5e6d77de207260619f423e1f7e58_TkVXUy5yc3Q= 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,14 @@ Changelog ========= +18.0.6.1.0 +---------- + +- Image converter is copypasted from the redner Odoo Module. long term, it must + be deleted from the redner Module + +- Add QR code converter & test it + 18.0.6.0.0 ---------- diff --git a/__init__.py b/__init__.py index 55401bf0eb2211cae86503a2bb26185e499ce74d_X19pbml0X18ucHk=..9a2413fce7bc5e6d77de207260619f423e1f7e58_X19pbml0X18ucHk= 100644 --- a/__init__.py +++ b/__init__.py @@ -53,3 +53,5 @@ Validator, ) from .xref import Xref, JsonLD_ID +from .image import ImageFile, ImageDataURL +from .qr_code import QRCodeFile, QRCodeDataURL, stringToQRCode, QRToImage diff --git a/__manifest__.py b/__manifest__.py index 55401bf0eb2211cae86503a2bb26185e499ce74d_X19tYW5pZmVzdF9fLnB5..9a2413fce7bc5e6d77de207260619f423e1f7e58_X19tYW5pZmVzdF9fLnB5 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -21,7 +21,7 @@ "name": "Converter", "license": "AGPL-3", "summary": "Convert odoo records to/from plain data structures.", - "version": "18.0.6.0.0", + "version": "18.0.6.1.0", "category": "Hidden", "author": "XCG Consulting", "website": "https://orbeet.io/", diff --git a/image.py b/image.py new file mode 100644 index 0000000000000000000000000000000000000000..9a2413fce7bc5e6d77de207260619f423e1f7e58_aW1hZ2UucHk= --- /dev/null +++ b/image.py @@ -0,0 +1,66 @@ +############################################################################## +# +# Converter Odoo module +# Copyright © 2020, 2025 XCG Consulting <https://xcg-consulting.fr> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +import base64 +from collections.abc import Mapping +from typing import Any + +from odoo import models # type: ignore[import-untyped] +from odoo.tools.mimetypes import guess_mimetype # type: ignore[import-untyped] + +from .base import Converter + + +def image(value: bytes): + # get MIME type associated with the decoded_data. + image_base64 = base64.b64decode(value) + mimetype = guess_mimetype(image_base64) + return {"body": value.decode("ascii"), "mime-type": mimetype} + + +class ImageFile(Converter): + def __init__(self, fieldname): + self.fieldname = fieldname + + def odoo_to_message( + self, instance: models.Model, ctx: Mapping | None = None + ) -> Any: + value = getattr(instance, self.fieldname) + + if not value: + return {} + + return image(value) + + +class ImageDataURL(Converter): + def __init__(self, fieldname): + self.fieldname = fieldname + + def odoo_to_message( + self, instance: models.Model, ctx: Mapping | None = None + ) -> Any: + value = getattr(instance, self.fieldname) + + if not value: + return "" + tmp = image(value) + + return "data:{};base64,{}".format(tmp["mime-type"], tmp["body"]) diff --git a/pyproject.toml b/pyproject.toml index 55401bf0eb2211cae86503a2bb26185e499ce74d_cHlwcm9qZWN0LnRvbWw=..9a2413fce7bc5e6d77de207260619f423e1f7e58_cHlwcm9qZWN0LnRvbWw= 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ [project.optional-dependencies] schema-validation = ["fastjsonschema"] +qrcode = ["qrcode"] doc = ["sphinx"] test = [] diff --git a/qr_code.py b/qr_code.py new file mode 100644 index 0000000000000000000000000000000000000000..9a2413fce7bc5e6d77de207260619f423e1f7e58_cXJfY29kZS5weQ== --- /dev/null +++ b/qr_code.py @@ -0,0 +1,97 @@ +############################################################################## +# +# Converter Odoo module +# Copyright © 2020, 2025 XCG Consulting <https://xcg-consulting.fr> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +import base64 +import io +from collections.abc import Mapping +from typing import Any + +from odoo import models # type: ignore[import-untyped] + +from .base import Converter + +_qrcode: None | Exception = None +try: + import qrcode # type: ignore[import-untyped] +except ImportError as e: + _qrcode = e + + +def QRToImage(qr): + """Generate a image dict from a QR Code""" + img = qr.make_image(fill="black", back_color="white") + buffered = io.BytesIO() + img.save(buffered, format="PNG") + + body = base64.b64encode(buffered.getvalue()).decode("ascii") + return {"body": body, "mime-type": "image/png"} + + +def stringToQRCode(data: str): + """Generate a QR code from a string and return it as a base64-encoded image.""" + if not data: + return False + + if _qrcode is not None: + raise _qrcode + + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(data) + qr.make(fit=True) + return qr + + +class QRCodeFile(Converter): + def __init__(self, fieldname): + self.fieldname = fieldname + + def odoo_to_message( + self, instance: models.Model, ctx: Mapping | None = None + ) -> Any: + value = getattr(instance, self.fieldname) + + if not value: + return {} + + return QRToImage(stringToQRCode(value)) + + +class QRCodeDataURL(Converter): + def __init__(self, fieldname): + self.fieldname = fieldname + + def odoo_to_message( + self, instance: models.Model, ctx: Mapping | None = None + ) -> Any: + value = getattr(instance, self.fieldname) + + if not value: + return "" + + image = QRToImage(stringToQRCode(value)) + return "data:{};base64,{}".format( + image["mime-type"], + image["body"], + ) diff --git a/tests/__init__.py b/tests/__init__.py index 55401bf0eb2211cae86503a2bb26185e499ce74d_dGVzdHMvX19pbml0X18ucHk=..9a2413fce7bc5e6d77de207260619f423e1f7e58_dGVzdHMvX19pbml0X18ucHk= 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,6 +4,7 @@ test_field, test_ir_model, test_mail_template, + test_qr_code, test_relation, test_switch, test_validate, diff --git a/tests/test_qr_code.py b/tests/test_qr_code.py new file mode 100644 index 0000000000000000000000000000000000000000..9a2413fce7bc5e6d77de207260619f423e1f7e58_dGVzdHMvdGVzdF9xcl9jb2RlLnB5 --- /dev/null +++ b/tests/test_qr_code.py @@ -0,0 +1,62 @@ +############################################################################## +# +# Converter Odoo module +# Copyright © 2025 XCG Consulting <https://xcg-consulting.fr> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +from unittest import skipUnless + +from odoo import tests # type: ignore[import-untyped] + +from .. import QRCodeDataURL, QRCodeFile, stringToQRCode + + +@skipUnless(tests.can_import("qrcode"), "qrcode module not available") +class TestQRCode(tests.TransactionCase): + def setUp(self): + super().setUp() + self.ready_mat = self.env["res.partner"].search( + [("name", "=", "Ready Mat")], limit=1 + ) + + def test_print_qr_code_1(self): + qr = stringToQRCode("https://google.com") + qr.print_ascii() + + def test_string_to_qr_code_2(self): + qr = stringToQRCode(self.ready_mat.website) + qr.print_ascii() + + def test_qr_code_file(self): + converter = QRCodeFile("website") + img = converter.odoo_to_message(self.ready_mat) + + self.assertEqual("image/png", img["mime-type"]) + self.assertRegex( + img["body"], + r"[A-Za-z0-9+/=]+$", + "The generated base64 body is incorrect", + ) + + def test_qr_code_data_url(self): + converter = QRCodeDataURL("website") + url = converter.odoo_to_message(self.ready_mat) + self.assertRegex( + url, + r"^data:image/png;base64,[A-Za-z0-9+/=]+$", + "The generated Data URL is not correctly formatted", + )