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(""), 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
+
+
+

+
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
+
+
+
+
To configure company-level images:
+
+- Go to Settings / Companies
+- Open your company record
+- Navigate to the Report Images tab
+- 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:
+
+- Go to Settings / Technical / Actions / Reports
+- Open the report you want to customize
+- Navigate to the Report Images tab
+- Check Include Company Images if you want to show company-level
+images in addition to report-specific images
+- 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.
+
+
+
+
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.
+
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+