diff --git a/report_positioned_image/README.rst b/report_positioned_image/README.rst new file mode 100644 index 0000000000..1be5aebea1 --- /dev/null +++ b/report_positioned_image/README.rst @@ -0,0 +1,128 @@ +======================= +Report Positioned Image +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:8bc2f08c57ac7bd7e62467501b1ac95394b9e6047b1a4fa48e08a4a99a760e2e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Freporting--engine-lightgray.png?logo=github + :target: https://github.com/OCA/reporting-engine/tree/18.0/report_positioned_image + :alt: OCA/reporting-engine +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/reporting-engine-18-0/reporting-engine-18-0-report_positioned_image + :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/reporting-engine&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows you to add positioned images (such as watermarks, +logos, or stamps) to PDF reports. Images can be precisely positioned +using millimeter coordinates (top, left) and you can control whether +they appear on all pages or only the first page. + +The module supports two types of images: + +- *Company-level Images*: Define images at the company level that can + be included in reports by enabling the *Include Company Images* + option +- *Report-specific Images*: Configure specific images for individual + reports, filtered by company context and always shown when configured + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure company-level images: + +1. Go to *Settings / Companies* +2. Open your company record +3. Navigate to the *Report Images* tab +4. Add images with position settings: + + - Upload an image - width defaults to 50mm and height is + automatically calculated to maintain the original aspect ratio + - *Top (mm)*: Distance from the top of the page + - *Left (mm)*: Distance from the left edge of the page + - *Width (mm)*: Width of the image (changing this auto-adjusts + height) + - *Height (mm)*: Height of the image (changing this auto-adjusts + width) + - *Respect Image Ratio*: When enabled (default), changing width or + height automatically adjusts the other dimension to maintain + aspect ratio. Uncheck for manual control of both dimensions. + - *First Page Only*: Check to show only on the first page + +To configure report-specific images: + +1. Go to *Settings / Technical / Actions / Reports* +2. Open the report you want to customize +3. Navigate to the *Report Images* tab +4. Check *Include Company Images* if you want to show company-level + images in addition to report-specific images +5. Add report-specific images in the list with the same position + settings as above + +**Note**: By default, images maintain their aspect ratio. When you +upload an image, it's automatically sized to 50mm width with +proportional height. You can then adjust either dimension and the other +will update automatically to prevent distortion. + +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 +------- + +* Quartile + +Contributors +------------ + +- Quartile + + - Tatsuki Kanda + - Aung Ko Ko Lin + +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. + +This module is part of the `OCA/reporting-engine `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/report_positioned_image/__init__.py b/report_positioned_image/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/report_positioned_image/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/report_positioned_image/__manifest__.py b/report_positioned_image/__manifest__.py new file mode 100644 index 0000000000..683b0de2a9 --- /dev/null +++ b/report_positioned_image/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Report Positioned Image", + "summary": "Add positioned images to PDF reports.", + "version": "18.0.1.0.0", + "category": "Reporting", + "author": "Quartile, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/reporting-engine", + "license": "AGPL-3", + "depends": ["web", "report_qweb_element_page_visibility"], + "data": [ + "security/ir.model.access.csv", + "security/report_positioned_image_security.xml", + "views/report_positioned_image_views.xml", + "views/res_company_views.xml", + "views/ir_actions_report_views.xml", + ], + "installable": True, +} diff --git a/report_positioned_image/models/__init__.py b/report_positioned_image/models/__init__.py new file mode 100644 index 0000000000..197c7163dd --- /dev/null +++ b/report_positioned_image/models/__init__.py @@ -0,0 +1,3 @@ +from . import ir_actions_report +from . import report_positioned_image +from . import res_company diff --git a/report_positioned_image/models/ir_actions_report.py b/report_positioned_image/models/ir_actions_report.py new file mode 100644 index 0000000000..979a727e58 --- /dev/null +++ b/report_positioned_image/models/ir_actions_report.py @@ -0,0 +1,116 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from markupsafe import Markup + +from odoo import fields, models +from odoo.tools.image import image_data_uri + + +class IrActionsReport(models.Model): + _inherit = "ir.actions.report" + + include_company_images = fields.Boolean( + help="If checked, company-level images will be shown in addition to " + "report-specific images.", + ) + report_positioned_image_ids = fields.Many2many( + comodel_name="report.positioned.image", + relation="ir_actions_report_positioned_image_rel", + column1="report_id", + column2="image_id", + string="Report Images", + ) + + def _render_qweb_pdf(self, report_ref, res_ids=None, data=None): + """Set company context so _get_positioned_image_configs uses the + correct company. + """ + company = self._get_report_company(res_ids) + return super(IrActionsReport, self.with_company(company))._render_qweb_pdf( + report_ref, res_ids, data + ) + + def _prepare_html(self, html, report_model=False): + image_configs = self._get_positioned_image_configs() + if not image_configs: + return super()._prepare_html(html, report_model=report_model) + result = super()._prepare_html(html, report_model=report_model) + if not isinstance(result, tuple): + return result + bodies, res_ids, header, footer, specific_paperformat_args = result + if image_configs: + header = self._inject_images_into_header(header, image_configs) + return bodies, res_ids, header, footer, specific_paperformat_args + + def _inject_images_into_header(self, header, image_configs): + image_html = self._build_image_html(image_configs) + return self._insert_html_into_header(header, image_html) + + def _insert_html_into_header(self, header, html_to_inject): + if Markup("") in header: + return header.replace( + Markup(""), html_to_inject + Markup(""), 1 + ) + if Markup("") in header: + return header.replace( + Markup(""), Markup("") + html_to_inject, 1 + ) + return header + html_to_inject + + @staticmethod + def _build_image_html(images): + parts = [] + for image in images: + image_content = image.get("image") + if not image_content: + continue + style_parts = [ + "position: fixed", + f"top: {image.get('pos_top', 5)}mm", + f"left: {image.get('pos_left', 5)}mm", + f"width: {image.get('width', 20)}mm", + f"height: {image.get('height', 20)}mm", + ] + style = "; ".join(style_parts) + ";" + data_uri = image_data_uri(image_content) + # Use 'first-page' class from report_qweb_element_page_visibility + # for images that should only appear on the first page + css_class = "first-page" if image.get("first_page_only") else "" + class_attr = f' class="{css_class}"' if css_class else "" + parts.append( + f'' + f'' + "" + ) + return Markup("".join(parts)) + + def _get_report_company(self, res_ids): + if not res_ids or not self.model: + return self.env.company + model = self.env[self.model] + if "company_id" not in model._fields: + return self.env.company + records = model.browse(res_ids).exists() + companies = records.mapped("company_id") + return companies[0] if len(companies) == 1 else self.env.company + + def _get_positioned_image_configs(self): + company = self.env.company + images = self.report_positioned_image_ids.filtered( + lambda img: img.company_id == company or not img.company_id + ) + if self.include_company_images: + images |= company.report_positioned_image_ids + return [ + { + "image": img.image, + "pos_top": img.pos_top, + "pos_left": img.pos_left, + "width": img.width, + "height": img.height, + "first_page_only": img.first_page_only, + } + for img in images + if img.image + ] diff --git a/report_positioned_image/models/report_positioned_image.py b/report_positioned_image/models/report_positioned_image.py new file mode 100644 index 0000000000..2a283f0ef8 --- /dev/null +++ b/report_positioned_image/models/report_positioned_image.py @@ -0,0 +1,97 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +from io import BytesIO + +from PIL import Image + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ReportPositionedImage(models.Model): + _name = "report.positioned.image" + _description = "Report Positioned Image" + + name = fields.Char(required=True) + image = fields.Binary(attachment=True, required=True) + pos_top = fields.Float(string="Top (mm)", default=5.0) + pos_left = fields.Float(string="Left (mm)", default=5.0) + width = fields.Float(string="Width (mm)") + height = fields.Float(string="Height (mm)") + respect_image_ratio = fields.Boolean( + default=True, + help="When enabled, changing width or height will automatically adjust " + "the other dimension to maintain the original image aspect ratio.", + ) + first_page_only = fields.Boolean() + company_id = fields.Many2one( + comodel_name="res.company", + default=lambda self: self._default_company_id(), + help="Leave empty to apply to all companies. Set a specific company to " + "restrict this image to that company only.", + ) + + def _default_company_id(self): + return self.env.context.get("default_company_id") + + def _get_aspect_ratio(self): + """Get image aspect ratio (width/height).""" + if not self.image: + return None + try: + img = Image.open(BytesIO(base64.b64decode(self.image))) + return img.width / img.height + except Exception: + return None + + @api.onchange("image") + def _onchange_image(self): + if not self.image: + return + ratio = self._get_aspect_ratio() + if not ratio: + return + # Set default width to 50mm and calculate height maintaining aspect ratio + self.width = 50.0 + self.height = round(50.0 / ratio, 2) + + @api.onchange("width", "respect_image_ratio") + def _onchange_width(self): + if self._context.get("from_height_onchange"): + return + if not (self.respect_image_ratio and self.width): + return + ratio = self._get_aspect_ratio() + if ratio and self.width > 0: + # Set context flag to prevent circular onchange + self.with_context(from_width_onchange=True).height = round( + self.width / ratio, 2 + ) + + @api.onchange("height") + def _onchange_height(self): + if self._context.get("from_width_onchange"): + return + if not (self.respect_image_ratio and self.height): + return + ratio = self._get_aspect_ratio() + if ratio and self.height > 0: + # Set context flag to prevent circular onchange + self.with_context(from_height_onchange=True).width = round( + self.height * ratio, 2 + ) + + @api.constrains("pos_top", "pos_left", "width", "height") + def _check_positive_values(self): + """Ensure position and dimension fields have positive values.""" + for record in self: + if record.pos_top < 0: + raise ValidationError(_("Top position must be a positive value.")) + if record.pos_left < 0: + raise ValidationError(_("Left position must be a positive value.")) + if record.width <= 0: + raise ValidationError(_("Width must be greater than zero.")) + if record.height <= 0: + raise ValidationError(_("Height must be greater than zero.")) diff --git a/report_positioned_image/models/res_company.py b/report_positioned_image/models/res_company.py new file mode 100644 index 0000000000..21df82a227 --- /dev/null +++ b/report_positioned_image/models/res_company.py @@ -0,0 +1,16 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + report_positioned_image_ids = fields.Many2many( + comodel_name="report.positioned.image", + relation="res_company_positioned_image_rel", + column1="company_id", + column2="image_id", + string="Company Images", + ) diff --git a/report_positioned_image/pyproject.toml b/report_positioned_image/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/report_positioned_image/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/report_positioned_image/readme/CONFIGURE.md b/report_positioned_image/readme/CONFIGURE.md new file mode 100644 index 0000000000..9ffc4d6db9 --- /dev/null +++ b/report_positioned_image/readme/CONFIGURE.md @@ -0,0 +1,31 @@ +To configure company-level images: + +1. Go to *Settings / Companies* +2. Open your company record +3. Navigate to the *Report Images* tab +4. Add images with position settings: + - Upload an image - width defaults to 50mm and height is automatically + calculated to maintain the original aspect ratio + - *Top (mm)*: Distance from the top of the page + - *Left (mm)*: Distance from the left edge of the page + - *Width (mm)*: Width of the image (changing this auto-adjusts height) + - *Height (mm)*: Height of the image (changing this auto-adjusts width) + - *Respect Image Ratio*: When enabled (default), changing width or height + automatically adjusts the other dimension to maintain aspect ratio. + Uncheck for manual control of both dimensions. + - *First Page Only*: Check to show only on the first page + +To configure report-specific images: + +1. Go to *Settings / Technical / Actions / Reports* +2. Open the report you want to customize +3. Navigate to the *Report Images* tab +4. Check *Include Company Images* if you want to show company-level images + in addition to report-specific images +5. Add report-specific images in the list with the same position settings + as above + +**Note**: By default, images maintain their aspect ratio. When you upload an +image, it's automatically sized to 50mm width with proportional height. You can +then adjust either dimension and the other will update automatically to prevent +distortion. diff --git a/report_positioned_image/readme/CONTRIBUTORS.md b/report_positioned_image/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..c1911de180 --- /dev/null +++ b/report_positioned_image/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Quartile \<\> + - Tatsuki Kanda + - Aung Ko Ko Lin diff --git a/report_positioned_image/readme/DESCRIPTION.md b/report_positioned_image/readme/DESCRIPTION.md new file mode 100644 index 0000000000..ddc38649f9 --- /dev/null +++ b/report_positioned_image/readme/DESCRIPTION.md @@ -0,0 +1,11 @@ +This module allows you to add positioned images (such as watermarks, logos, +or stamps) to PDF reports. Images can be precisely positioned using millimeter +coordinates (top, left) and you can control whether they appear on all pages +or only the first page. + +The module supports two types of images: + +- *Company-level Images*: Define images at the company level that can be + included in reports by enabling the *Include Company Images* option +- *Report-specific Images*: Configure specific images for individual reports, + filtered by company context and always shown when configured diff --git a/report_positioned_image/security/ir.model.access.csv b/report_positioned_image/security/ir.model.access.csv new file mode 100644 index 0000000000..419de3303f --- /dev/null +++ b/report_positioned_image/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_report_positioned_image_user,report.positioned.image user,model_report_positioned_image,base.group_user,1,0,0,0 +access_report_positioned_image_manager,report.positioned.image manager,model_report_positioned_image,base.group_system,1,1,1,1 diff --git a/report_positioned_image/security/report_positioned_image_security.xml b/report_positioned_image/security/report_positioned_image_security.xml new file mode 100644 index 0000000000..aa22618b04 --- /dev/null +++ b/report_positioned_image/security/report_positioned_image_security.xml @@ -0,0 +1,12 @@ + + + + Report Positioned Image: multi-company + + [ + '|', + ('company_id', '=', False), + ('company_id', 'in', company_ids) + ] + + diff --git a/report_positioned_image/static/description/index.html b/report_positioned_image/static/description/index.html new file mode 100644 index 0000000000..1553981016 --- /dev/null +++ b/report_positioned_image/static/description/index.html @@ -0,0 +1,477 @@ + + + + + +Report Positioned Image + + + +
+

Report Positioned Image

+ + +

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

+

This module allows you to add positioned images (such as watermarks, +logos, or stamps) to PDF reports. Images can be precisely positioned +using millimeter coordinates (top, left) and you can control whether +they appear on all pages or only the first page.

+

The module supports two types of images:

+
    +
  • Company-level Images: Define images at the company level that can +be included in reports by enabling the Include Company Images +option
  • +
  • Report-specific Images: Configure specific images for individual +reports, filtered by company context and always shown when configured
  • +
+

Table of contents

+ +
+

Configuration

+

To configure company-level images:

+
    +
  1. Go to Settings / Companies
  2. +
  3. Open your company record
  4. +
  5. Navigate to the Report Images tab
  6. +
  7. Add images with position settings:
      +
    • Upload an image - width defaults to 50mm and height is +automatically calculated to maintain the original aspect ratio
    • +
    • Top (mm): Distance from the top of the page
    • +
    • Left (mm): Distance from the left edge of the page
    • +
    • Width (mm): Width of the image (changing this auto-adjusts +height)
    • +
    • Height (mm): Height of the image (changing this auto-adjusts +width)
    • +
    • Respect Image Ratio: When enabled (default), changing width or +height automatically adjusts the other dimension to maintain +aspect ratio. Uncheck for manual control of both dimensions.
    • +
    • First Page Only: Check to show only on the first page
    • +
    +
  8. +
+

To configure report-specific images:

+
    +
  1. Go to Settings / Technical / Actions / Reports
  2. +
  3. Open the report you want to customize
  4. +
  5. Navigate to the Report Images tab
  6. +
  7. Check Include Company Images if you want to show company-level +images in addition to report-specific images
  8. +
  9. Add report-specific images in the list with the same position +settings as above
  10. +
+

Note: By default, images maintain their aspect ratio. When you +upload an image, it’s automatically sized to 50mm width with +proportional height. You can then adjust either dimension and the other +will update automatically to prevent distortion.

+
+
+

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

+
    +
  • Quartile
  • +
+
+
+

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.

+

This module is part of the OCA/reporting-engine project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/report_positioned_image/tests/__init__.py b/report_positioned_image/tests/__init__.py new file mode 100644 index 0000000000..20a6a6863a --- /dev/null +++ b/report_positioned_image/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_report_positioned_image diff --git a/report_positioned_image/tests/test_report_positioned_image.py b/report_positioned_image/tests/test_report_positioned_image.py new file mode 100644 index 0000000000..a76a99fe0f --- /dev/null +++ b/report_positioned_image/tests/test_report_positioned_image.py @@ -0,0 +1,234 @@ +# Copyright 2026 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from markupsafe import Markup + +from odoo import Command +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestReportPositionedImage(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company_a = cls.env.ref("base.main_company") + cls.company_b = cls.env["res.company"].create({"name": "Company B"}) + cls.report = cls.env["ir.actions.report"].create( + { + "name": "Test Report", + "model": "res.partner", + "report_type": "qweb-pdf", + "report_name": "test_report", + } + ) + # Create a simple 1x1 transparent PNG for testing (base64-encoded) + cls.test_image = ( + b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhg" + b"GAWjR9awAAAABJRU5ErkJggg==" + ) + cls.image_a = cls.env["report.positioned.image"].create( + { + "name": "Company A Image", + "image": cls.test_image, + "pos_top": 10.0, + "pos_left": 15.0, + "width": 25.0, + "height": 30.0, + "first_page_only": False, + "company_id": cls.company_a.id, + } + ) + cls.company_a.write( + {"report_positioned_image_ids": [Command.set([cls.image_a.id])]} + ) + cls.image_b = cls.env["report.positioned.image"].create( + { + "name": "Company B Image", + "image": cls.test_image, + "pos_top": 50.0, + "pos_left": 60.0, + "width": 70.0, + "height": 80.0, + "first_page_only": True, + "company_id": cls.company_b.id, + } + ) + cls.company_b.write( + {"report_positioned_image_ids": [Command.set([cls.image_b.id])]} + ) + cls.global_image = cls.env["report.positioned.image"].create( + { + "name": "Global Image", + "image": cls.test_image, + "pos_top": 5.0, + "pos_left": 5.0, + "width": 10.0, + "height": 10.0, + "company_id": False, + } + ) + + def test_company_images_respects_company_context(self): + self.report.include_company_images = True + configs = self.report.with_company( + self.company_a + )._get_positioned_image_configs() + self.assertEqual(len(configs), 1) + self.assertEqual(configs[0]["pos_top"], 10.0) + self.assertEqual(configs[0]["pos_left"], 15.0) + self.assertFalse(configs[0]["first_page_only"]) + configs = self.report.with_company( + self.company_b + )._get_positioned_image_configs() + self.assertEqual(len(configs), 1) + self.assertEqual(configs[0]["pos_top"], 50.0) + self.assertEqual(configs[0]["pos_left"], 60.0) + self.assertTrue(configs[0]["first_page_only"]) + + def test_report_images_filter_by_company(self): + self.report.write( + { + "include_company_images": False, + "report_positioned_image_ids": [ + Command.set([self.image_a.id, self.image_b.id]) + ], + } + ) + configs = self.report.with_company( + self.company_a + )._get_positioned_image_configs() + self.assertEqual(len(configs), 1) + self.assertEqual(configs[0]["pos_top"], 10.0) + configs = self.report.with_company( + self.company_b + )._get_positioned_image_configs() + self.assertEqual(len(configs), 1) + self.assertEqual(configs[0]["pos_top"], 50.0) + + def test_combined_company_and_report_images(self): + custom_image = self.env["report.positioned.image"].create( + { + "name": "Custom Report Image", + "image": self.test_image, + "pos_top": 100.0, + "pos_left": 110.0, + "width": 120.0, + "height": 130.0, + "first_page_only": False, + "company_id": self.company_a.id, + } + ) + self.report.write( + { + "include_company_images": True, + "report_positioned_image_ids": [Command.set([custom_image.id])], + } + ) + configs = self.report.with_company( + self.company_a + )._get_positioned_image_configs() + self.assertEqual(len(configs), 2) + self.assertEqual(configs[0]["pos_top"], 100.0) + self.assertEqual(configs[1]["pos_top"], 10.0) + + def test_validation_negative_dimensions(self): + with self.assertRaises(ValidationError): + self.env["report.positioned.image"].create( + { + "name": "Invalid Image", + "image": self.test_image, + "width": -10.0, + "company_id": self.company_a.id, + } + ) + with self.assertRaises(ValidationError): + self.image_a.write({"height": -5.0}) + + def test_build_image_html_positioning(self): + images = [ + { + "image": self.test_image, + "pos_top": 5, + "pos_left": 10, + "width": 20, + "height": 15, + } + ] + html = self.report._build_image_html(images) + html_str = str(html) + self.assertIn("position: fixed", html_str) + self.assertIn("top: 5mm", html_str) + self.assertIn("left: 10mm", html_str) + self.assertIn("width: 20mm", html_str) + self.assertIn("height: 15mm", html_str) + self.assertIn('") + result = self.report._inject_images_into_header(header, images) + result_str = str(result) + # Should contain the first-page class + self.assertIn('class="first-page"', result_str) + + def test_global_images_appear_for_all_companies(self): + self.report.write( + { + "report_positioned_image_ids": [ + Command.set([self.global_image.id, self.image_a.id]) + ] + } + ) + configs_a = self.report.with_company( + self.company_a + )._get_positioned_image_configs() + self.assertEqual(len(configs_a), 2) + # Company B sees: global only (not image_a) + configs_b = self.report.with_company( + self.company_b + )._get_positioned_image_configs() + self.assertEqual(len(configs_b), 1) diff --git a/report_positioned_image/views/ir_actions_report_views.xml b/report_positioned_image/views/ir_actions_report_views.xml new file mode 100644 index 0000000000..ee27a39005 --- /dev/null +++ b/report_positioned_image/views/ir_actions_report_views.xml @@ -0,0 +1,37 @@ + + + + ir.actions.report.positioned.image + ir.actions.report + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/report_positioned_image/views/report_positioned_image_views.xml b/report_positioned_image/views/report_positioned_image_views.xml new file mode 100644 index 0000000000..25e21faf7b --- /dev/null +++ b/report_positioned_image/views/report_positioned_image_views.xml @@ -0,0 +1,42 @@ + + + + report.positioned.image.view.form + report.positioned.image + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + report.positioned.image.view.tree + report.positioned.image + + + + + + + + +
diff --git a/report_positioned_image/views/res_company_views.xml b/report_positioned_image/views/res_company_views.xml new file mode 100644 index 0000000000..a14398b9e6 --- /dev/null +++ b/report_positioned_image/views/res_company_views.xml @@ -0,0 +1,40 @@ + + + + res.company.form.positioned.image + res.company + + + + + + + + + + + + + + + + + + + + + +