# HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1741790782 -3600 # Wed Mar 12 15:46:22 2025 +0100 # Branch 18.0 # Node ID db5d81f4e22751a3875d2ca7b192ee42e4094cad # Parent 55401bf0eb2211cae86503a2bb26185e499ce74d copypaste image converter from redner module diff --git a/__init__.py b/__init__.py --- a/__init__.py +++ b/__init__.py @@ -53,3 +53,4 @@ Validator, ) from .xref import Xref, JsonLD_ID +from .image import ImageFile, ImageDataURL diff --git a/image.py b/image.py new file mode 100644 --- /dev/null +++ b/image.py @@ -0,0 +1,68 @@ +############################################################################## +# +# 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 "" + + content = base64.b64decode(value) + mimetype = guess_mimetype(content) + + return "data:{};base64,{}".format(mimetype, value.decode("ascii")) # HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1741861521 -3600 # Thu Mar 13 11:25:21 2025 +0100 # Branch 18.0 # Node ID b68390723c6794e3ddd91776b02b935150def92b # Parent db5d81f4e22751a3875d2ca7b192ee42e4094cad refactor image.py diff --git a/image.py b/image.py --- a/image.py +++ b/image.py @@ -61,8 +61,6 @@ if not value: return "" + tmp = image(value) - content = base64.b64decode(value) - mimetype = guess_mimetype(content) - - return "data:{};base64,{}".format(mimetype, value.decode("ascii")) + return "data:{};base64,{}".format(tmp["mime-type"], tmp["body"]) # HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1742204691 -3600 # Mon Mar 17 10:44:51 2025 +0100 # Branch 18.0 # Node ID 9a2413fce7bc5e6d77de207260619f423e1f7e58 # Parent b68390723c6794e3ddd91776b02b935150def92b Add qrcode converter diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,11 +9,11 @@ mypy: variables: - INSTALL_SECTION: "[test,schema-validation]" + INSTALL_SECTION: "[test,schema-validation,qrcode]" 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 --- 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 --- a/__init__.py +++ b/__init__.py @@ -54,3 +54,4 @@ ) 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 --- 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/pyproject.toml b/pyproject.toml --- 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 --- /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 --- 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 --- /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", + )