From e376202f4e8b10a5b09e8d1b32216ac4b4faa353 Mon Sep 17 00:00:00 2001
From: "r.perez"
Date: Tue, 17 Feb 2026 19:50:36 -0500
Subject: [PATCH 1/5] [FIX] report_py3o: make div container role alert
conditionally invisible
---
report_py3o/views/ir_actions_report.xml | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/report_py3o/views/ir_actions_report.xml b/report_py3o/views/ir_actions_report.xml
index 8669dce1fd..20bbf065fa 100644
--- a/report_py3o/views/ir_actions_report.xml
+++ b/report_py3o/views/ir_actions_report.xml
@@ -8,11 +8,13 @@
-
-
+
+
From 25d602b3689c738101143e90260f6dd2f357473e Mon Sep 17 00:00:00 2001
From: Aungkokolin1997
Date: Mon, 9 Mar 2026 02:15:58 +0000
Subject: [PATCH 2/5] [ADD] report_positioned_image
---
report_positioned_image/README.rst | 133 +++++
report_positioned_image/__init__.py | 1 +
report_positioned_image/__manifest__.py | 20 +
report_positioned_image/models/__init__.py | 3 +
.../models/ir_actions_report.py | 115 +++++
.../models/report_positioned_image.py | 116 +++++
report_positioned_image/models/res_company.py | 16 +
report_positioned_image/pyproject.toml | 3 +
report_positioned_image/readme/CONFIGURE.md | 33 ++
.../readme/CONTRIBUTORS.md | 3 +
report_positioned_image/readme/DESCRIPTION.md | 14 +
.../security/ir.model.access.csv | 3 +
.../report_positioned_image_security.xml | 12 +
.../static/description/index.html | 481 ++++++++++++++++++
report_positioned_image/tests/__init__.py | 4 +
.../tests/test_report_positioned_image.py | 274 ++++++++++
.../views/ir_actions_report_views.xml | 37 ++
.../views/report_positioned_image_views.xml | 42 ++
.../views/res_company_views.xml | 36 ++
19 files changed, 1346 insertions(+)
create mode 100644 report_positioned_image/README.rst
create mode 100644 report_positioned_image/__init__.py
create mode 100644 report_positioned_image/__manifest__.py
create mode 100644 report_positioned_image/models/__init__.py
create mode 100644 report_positioned_image/models/ir_actions_report.py
create mode 100644 report_positioned_image/models/report_positioned_image.py
create mode 100644 report_positioned_image/models/res_company.py
create mode 100644 report_positioned_image/pyproject.toml
create mode 100644 report_positioned_image/readme/CONFIGURE.md
create mode 100644 report_positioned_image/readme/CONTRIBUTORS.md
create mode 100644 report_positioned_image/readme/DESCRIPTION.md
create mode 100644 report_positioned_image/security/ir.model.access.csv
create mode 100644 report_positioned_image/security/report_positioned_image_security.xml
create mode 100644 report_positioned_image/static/description/index.html
create mode 100644 report_positioned_image/tests/__init__.py
create mode 100644 report_positioned_image/tests/test_report_positioned_image.py
create mode 100644 report_positioned_image/views/ir_actions_report_views.xml
create mode 100644 report_positioned_image/views/report_positioned_image_views.xml
create mode 100644 report_positioned_image/views/res_company_views.xml
diff --git a/report_positioned_image/README.rst b/report_positioned_image/README.rst
new file mode 100644
index 0000000000..35c7aeb93a
--- /dev/null
+++ b/report_positioned_image/README.rst
@@ -0,0 +1,133 @@
+=======================
+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
+
+Images can be assigned to a specific company or left as shared records
+(without company assignment) for use across multiple companies
+
+**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
+ - *Company*: Automatically set to the current company when creating
+ from the company form. To create shared images, leave empty.
+
+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..9fdca590fc
--- /dev/null
+++ b/report_positioned_image/models/ir_actions_report.py
@@ -0,0 +1,115 @@
+# 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",
+ )
+
+ @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 _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
+
+ 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 _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
+ ]
+
+ 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
+ header = self._inject_images_into_header(header, image_configs)
+ return bodies, res_ids, header, footer, specific_paperformat_args
+
+ 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 _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
+ )
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..edfb5f9dc8
--- /dev/null
+++ b/report_positioned_image/models/report_positioned_image.py
@@ -0,0 +1,116 @@
+# 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")
+
+ @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."))
+
+ 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.onchange("company_id")
+ def _onchange_company_id(self):
+ """Prevent assigning to a different company when created from company form."""
+ default_company_id = self.env.context.get("default_company_id")
+ if not default_company_id:
+ return
+ if self.company_id and self.company_id.id != default_company_id:
+ self.company_id = default_company_id
+ return {
+ "warning": {
+ "title": _("Company Assignment"),
+ "message": _(
+ "You cannot assign this image to a different company. "
+ "Please use the dedicated wizard to assign images to other "
+ "companies."
+ ),
+ }
+ }
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..0342fbd98b
--- /dev/null
+++ b/report_positioned_image/readme/CONFIGURE.md
@@ -0,0 +1,33 @@
+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
+ - *Company*: Automatically set to the current company when creating from
+ the company form. To create shared images, leave empty.
+
+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..0220a9c075
--- /dev/null
+++ b/report_positioned_image/readme/DESCRIPTION.md
@@ -0,0 +1,14 @@
+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
+
+Images can be assigned to a specific company or left as shared records
+(without company assignment) for use across multiple companies
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..a88a559316
--- /dev/null
+++ b/report_positioned_image/static/description/index.html
@@ -0,0 +1,481 @@
+
+
+
+
+
+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
+
+
Images can be assigned to a specific company or left as shared records
+(without company assignment) for use across multiple companies
+
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
+- Company: Automatically set to the current company when creating
+from the company form. To create shared images, leave empty.
+
+
+
+
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..e52e5302f8
--- /dev/null
+++ b/report_positioned_image/tests/test_report_positioned_image.py
@@ -0,0 +1,274 @@
+# 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)
+
+ def test_company_id_onchange_with_context(self):
+ image = (
+ self.env["report.positioned.image"]
+ .with_context(default_company_id=self.company_a.id)
+ .new(
+ {
+ "name": "Test Image",
+ "image": self.test_image,
+ "width": 10.0,
+ "height": 10.0,
+ "company_id": self.company_a.id,
+ }
+ )
+ )
+ image.company_id = self.company_b
+ result = image._onchange_company_id()
+ self.assertIsNotNone(result)
+ self.assertIn("warning", result)
+ self.assertEqual(image.company_id, self.company_a)
+ image.company_id = self.company_a
+ result = image._onchange_company_id()
+ self.assertIsNone(result)
+ self.assertEqual(image.company_id, self.company_a)
+ image.company_id = False
+ result = image._onchange_company_id()
+ self.assertIsNone(result)
+ self.assertFalse(image.company_id)
+ image_no_context = self.env["report.positioned.image"].new(
+ {
+ "name": "Free Image",
+ "image": self.test_image,
+ "width": 10.0,
+ "height": 10.0,
+ "company_id": self.company_b.id,
+ }
+ )
+ result = image_no_context._onchange_company_id()
+ self.assertIsNone(result)
+ self.assertEqual(image_no_context.company_id, self.company_b)
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..d69a373e48
--- /dev/null
+++ b/report_positioned_image/views/res_company_views.xml
@@ -0,0 +1,36 @@
+
+
+
+ res.company.form.positioned.image
+ res.company
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From f279b416a6d9d1a3c8963e47ef7f19f332df02d9 Mon Sep 17 00:00:00 2001
From: oca-ci
Date: Tue, 21 Apr 2026 18:15:54 +0000
Subject: [PATCH 3/5] [UPD] Update report_positioned_image.pot
---
.../i18n/report_positioned_image.pot | 191 ++++++++++++++++++
1 file changed, 191 insertions(+)
create mode 100644 report_positioned_image/i18n/report_positioned_image.pot
diff --git a/report_positioned_image/i18n/report_positioned_image.pot b/report_positioned_image/i18n/report_positioned_image.pot
new file mode 100644
index 0000000000..1a6ba219d2
--- /dev/null
+++ b/report_positioned_image/i18n/report_positioned_image.pot
@@ -0,0 +1,191 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * report_positioned_image
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.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: report_positioned_image
+#: model:ir.model,name:report_positioned_image.model_res_company
+msgid "Companies"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__company_id
+msgid "Company"
+msgstr ""
+
+#. module: report_positioned_image
+#. odoo-python
+#: code:addons/report_positioned_image/models/report_positioned_image.py:0
+msgid "Company Assignment"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_res_company__report_positioned_image_ids
+msgid "Company Images"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__first_page_only
+msgid "First Page Only"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__height
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_act_report_form_positioned_image
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_company_form_positioned_image
+msgid "Height (mm)"
+msgstr ""
+
+#. module: report_positioned_image
+#. odoo-python
+#: code:addons/report_positioned_image/models/report_positioned_image.py:0
+msgid "Height must be greater than zero."
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__id
+msgid "ID"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,help:report_positioned_image.field_ir_actions_report__include_company_images
+#: model:ir.model.fields,help:report_positioned_image.field_report_pdf_form__include_company_images
+msgid ""
+"If checked, company-level images will be shown in addition to report-"
+"specific images."
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__image
+msgid "Image"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_ir_actions_report__include_company_images
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_pdf_form__include_company_images
+msgid "Include Company Images"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,help:report_positioned_image.field_report_positioned_image__company_id
+msgid ""
+"Leave empty to apply to all companies. Set a specific company to restrict "
+"this image to that company only."
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__pos_left
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_act_report_form_positioned_image
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_company_form_positioned_image
+msgid "Left (mm)"
+msgstr ""
+
+#. module: report_positioned_image
+#. odoo-python
+#: code:addons/report_positioned_image/models/report_positioned_image.py:0
+msgid "Left position must be a positive value."
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__name
+msgid "Name"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model,name:report_positioned_image.model_ir_actions_report
+msgid "Report Action"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_ir_actions_report__report_positioned_image_ids
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_pdf_form__report_positioned_image_ids
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_act_report_form_positioned_image
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_company_form_positioned_image
+msgid "Report Images"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model,name:report_positioned_image.model_report_positioned_image
+msgid "Report Positioned Image"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__respect_image_ratio
+msgid "Respect Image Ratio"
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__pos_top
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_act_report_form_positioned_image
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_company_form_positioned_image
+msgid "Top (mm)"
+msgstr ""
+
+#. module: report_positioned_image
+#. odoo-python
+#: code:addons/report_positioned_image/models/report_positioned_image.py:0
+msgid "Top position must be a positive value."
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,help:report_positioned_image.field_report_positioned_image__respect_image_ratio
+msgid ""
+"When enabled, changing width or height will automatically adjust the other "
+"dimension to maintain the original image aspect ratio."
+msgstr ""
+
+#. module: report_positioned_image
+#: model:ir.model.fields,field_description:report_positioned_image.field_report_positioned_image__width
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_act_report_form_positioned_image
+#: model_terms:ir.ui.view,arch_db:report_positioned_image.view_company_form_positioned_image
+msgid "Width (mm)"
+msgstr ""
+
+#. module: report_positioned_image
+#. odoo-python
+#: code:addons/report_positioned_image/models/report_positioned_image.py:0
+msgid "Width must be greater than zero."
+msgstr ""
+
+#. module: report_positioned_image
+#. odoo-python
+#: code:addons/report_positioned_image/models/report_positioned_image.py:0
+msgid ""
+"You cannot assign this image to a different company. Please use the "
+"dedicated wizard to assign images to other companies."
+msgstr ""
From 8287bad99a85329ff5a77841fcb542e79c47b78b Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Tue, 21 Apr 2026 18:20:32 +0000
Subject: [PATCH 4/5] [BOT] post-merge updates
---
README.md | 1 +
report_positioned_image/README.rst | 51 +++++++++---------
.../static/description/icon.png | Bin 0 -> 10254 bytes
.../static/description/index.html | 37 +++++++------
setup/_metapackage/pyproject.toml | 3 +-
5 files changed, 51 insertions(+), 41 deletions(-)
create mode 100644 report_positioned_image/static/description/icon.png
diff --git a/README.md b/README.md
index 18f85670fc..5dd21b55a6 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,7 @@ addon | version | maintainers | summary
[report_partner_address](report_partner_address/) | 18.0.1.0.0 |
| Translatable partner address details for reports and portal
[report_pdf_form](report_pdf_form/) | 18.0.1.0.0 |
| Fill custom PDF form reports
[report_pdf_zip_download](report_pdf_zip_download/) | 18.0.1.0.0 | | Report PDF ZIP Download
+[report_positioned_image](report_positioned_image/) | 18.0.1.0.0 | | Add positioned images to PDF reports.
[report_py3o](report_py3o/) | 18.0.1.0.2 | | Reporting engine based on Libreoffice (ODT -> ODT, ODT -> PDF, ODT -> DOC, ODT -> DOCX, ODS -> ODS, etc.)
[report_py3o_fusion_server](report_py3o_fusion_server/) | 18.0.1.0.0 | | Let the fusion server handle format conversion.
[report_qr](report_qr/) | 18.0.1.0.0 | | Web QR Manager
diff --git a/report_positioned_image/README.rst b/report_positioned_image/README.rst
index 35c7aeb93a..d55eb0d9cf 100644
--- a/report_positioned_image/README.rst
+++ b/report_positioned_image/README.rst
@@ -1,3 +1,7 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
=======================
Report Positioned Image
=======================
@@ -7,13 +11,13 @@ Report Positioned Image
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:8bc2f08c57ac7bd7e62467501b1ac95394b9e6047b1a4fa48e08a4a99a760e2e
+ !! source digest: sha256:7492b81c95084617eacd2468003a6783881838c17e7ef230b84bf4d6733dc0e3
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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
+.. |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%2Freporting--engine-lightgray.png?logo=github
@@ -35,11 +39,10 @@ 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
+- *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
Images can be assigned to a specific company or left as shared records
(without company assignment) for use across multiple companies
@@ -59,20 +62,20 @@ To configure company-level images:
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
- - *Company*: Automatically set to the current company when creating
- from the company form. To create shared images, leave empty.
+ - 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
+ - *Company*: Automatically set to the current company when creating
+ from the company form. To create shared images, leave empty.
To configure report-specific images:
@@ -110,10 +113,10 @@ Authors
Contributors
------------
-- Quartile
+- Quartile
- - Tatsuki Kanda
- - Aung Ko Ko Lin
+ - Tatsuki Kanda
+ - Aung Ko Ko Lin
Maintainers
-----------
diff --git a/report_positioned_image/static/description/icon.png b/report_positioned_image/static/description/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11
GIT binary patch
literal 10254
zcmbt)WmufcvhH9Zc!C8B?l8#UE&&o;gF7=g3=D(IAOS+K1lK^25Zv7%L4sRw_uvvF
z*qyAk?>c**=lnR&y+1yw{;I3Hy6Ua2{<d0kcR+VvBo;
zA_X`>;1;xAPL9rQqFxd#f5{a^zW*uaW+r3+U{|fRunu`GZhy$X
z8_|Zi{zd#vIokczl8Xh*4Wi@i0+C?Rg1AB5VOEg8B>buLFCi~r5DPd2ED7QP2>^LO
zKpr7+?*I1bPaFSLLEa0l2$tj*;u8Qtc=&(RUc*VK@
zjIN{I--GfO@vl+&r^eqy_BZ3dndN_PDzMc*W^!?dIsWAWU@LBjBg6^f4F6*!-hUYh
zY$Xb}gF8b0%S1Ac@c%Rs()UCiEu3v6SiFE>h_!{gBb-H2{e=wB5o!YkT0>#LKZFw$
z?CuD0Gvfsb(|XbVxx0AL0%`gG2X+6|f;jiTHU9shtjoW-{2!|
zMN*WuOj6elhD4zqgjNpX>F#JP{)hAbenX<+FPr>7jXM&q{|x+pbj8cU<=>Ej
zWE1_%qoFVzDAZB%g@v<+1ud%<#2E~ML11jOV5pUZoXktGmzB38%te^i-3o9i$lge>z>tBcK|P2K0H9w{l#|i%$~egM)Ys{q>p<9yaE*%v2cy1wXE{AXqG1_b
znfyg@Fq*e@yC)^(@$R*j^E;skyEM6pmL$1ctg*mWiWM&q1{nj>E^)Odw$RPr
zhjesSk}k}@-e_%uZTy0t_*TJD&6%*HV0KH>xE@oBex6CL@`Ty3nH_2OF#M?6j(j|9
znRKGSfp3Q2i+|>}w?>8g$>r`|OcvG5r;p)z8DO8+O>EvYQ=_~`p}9!ReUEjUnNL@6
z+C*aoo67(sd|7QgW54@V9Y8PnBW$Q+7ZsRFA}Vj*viA!yWUfb!s*yJi6JKsXZCH4j
z*B%nJpad-DDvJ8d>xrxkkh6A}i7V3nULqHCiG~|)YY6{NE3M}c^s#PQhzhsJUf^QW
zR+F;up-dN*!)M1ZYl@d0HoqfVD2PNiQcPdzq4NDKO!8mUl{!t*ntBg_+-+lRlI0~Lr>5v!PiQj|hD7B-YFIs~6hIY*R6USZA
zlb}=UxqxpSzIsL3pPmiuixCN|3LFBd?0Ih8Y6GWQ;U>dkdXtQaQ&8H|TGAQbuHY=F
z_R83&B{1_hP7L#$^eAe?GPB_83y#HZKTwD>e-@E2P>Gk$BBb9|Ivfmdp
za~s>3=aj(;xmz8n)sI}uFO$|C>0CZbcTY$Bq6~L-Bc9=vl@X#0S~Q@j8iKzuPeQE_
zQSI)wNz~CvJ>!%QszoCfUm9}h^DL!WYAN|FtMO#kpDXq74sYC87(uvv*jiCjV?Ta&
zgO1D0OP3TEN3YnBpD6GnmsEolzEbGM{&VlTz_)J(o{nl0+TmNt{xL%L6G&UR$^aYC
zQOA#W7R%9JsC5oTZJE>_?!Ci}mNH{0ObyUd%Q!k%5J8Z`8sR!m`~|Taje`(bLD7=a
z-{-=d7w;k@DIrgU{I@K}eN`>S**Lg<@ChAf$M(&kV9TLUixqFQ>YoYHrI!K#R6`S>
z%?d5hQ@&;Gje<|uRQZb%Hhibocl9(buI?=0aZW{JYXx?ZS@Lr%G8L<d+riEi2~+{HfHK{K^VrGYNi{2-WJOiC>Pz?f*)cxKCl>1H1=$jb!^
zpmYw>eoiM0Hy7$xbbX_e5o*+{7T2&-t%-h4i7MMo;k|tSqQAeNkwHS9hWY#EV7r3|
zTmOmN{;b9OUZpp`LP(I9Wo%R#$b6YdH7GD4*p6>a2N2A04pQ*n;INQMh%+mj;x7>S
z_(H?uJ^n!r1)kJH1*s+%$al#?C^Cw{H@RA^QGB=Dubyc)XUaY>f`(VKTlIO-YNCp{1n
zOl*>jT?Dtf5fD$DY-j&B*Xmn|2-u2OB
zBL@-lFs5lhcQKXBR*cIXmi%~EJcc^5#Xpg!E^A6sXf1#$qJGRpmU~A
zcdj-cvBfx(fIRAMU(1obztJR%I7v3R-%$#~r!0sS^I(iC*5i6296*88A7I=_JhU3p
zya!aCti0R5*RFT%LW0R|;u&oJ6=P-c$le4J0bi}u!!@;xzao|l6fJ{;Mld9hGhrJg
zr_B)=4yktp)yPB@tCC_L9h1>GzXD6DA!W7xt{1)8!07~gONkEWC8@y%lciB{9ojy)
zWm$drJ_9uVJ>Q$-`@q%OM7_S>(K=__CGYB~@@mE^Z=eT|x0Rv?Z-N)LLWR
zod*Zy3v)iMX@usPX-OKBDgC8yq?fMhqf8H)A&C)Hi29YFn!NVf5!J0-F{wC&L5-3`#id=4?=2>Zp6Pdu4N6#bG&atu7
z8IET&ciXy_Tp4YjMx3yIAbw#_e2#jgGJ~ogkv-|M7|%Gio%2@mnS89NKUOM#Bzg4_
z9e9oN;^m>G*#?)AawODi6YckRPmkSKD_4b4WFpj|@|eS!B0WN@?QscYzTH`~6e%iz
z!z1>ps)CG37%(E=kZ_>re)@ODv^0^=rWU^*m;6M&gD10EYImO98JVabRe5{#wrogYUKPB@_(#e7Ej9_x;n1oHDj5GawU)A&1hWj|HzJB(q{vMTX>jOW;Jz
zBsW&SqTaR7!NXXg_A}$XnFpg_n)Zi;{e9eb*k|b(y$a}12boJ7rqQXQpVhU8HxHTl
zt8Ln!KLFyfq!%}hdMXle^qajw2g6S{z&7tQ6J(w9
z3+!HTO{_TqM{9o$RR~lKFf4b4(xLUP?QG;McNFQc_Yd_mig9Ejy9%q~Ye>rIn3};U
z)w&1@QCK;cC(;x0G&YuSad+>{c@ZsFJcUdcs@PP-x{mrO)|6_#CjMlXsMJx;Cr?FF
zVFrlt@$Z-Ll^*7d0#`5Uez@bb{Xn(BQLhScBhF!6+aIso0=l{PP7P(6-ru>nVy%AP
z+|eZpY(ooMU7rtG$l#14v=Z?@ebOjm(A2)5k_${|wAA$oq+;42wiS78ezjgWWnTrF
z`1!i2h{fM91aD8uxz?tZpE(PsL37e3$*I6%un5Bzzpn10p`j72R;3=Oaug_|Z(y)@
z9$SJN@-5d1tNIy0=7|d&_HAnDx!yDd-u#qmfuDh)0a_CVje{hvQz9rDFHJTpQ0Dg@
zGQ3t*gZlcFSXfx%OG@Cds&NDROxd^osY_)abmo^dKMUY!R~kGH%*;rutPF@Mx$zrv
z6Q1soKnYYRW#;Bi-!H)>Br0<`y+Wy~p7_<>{ljuG`Dpje=v1x}-ND<)bWBr|<}v6B
zkDTUZ^@VsH>CyR}ml4j2rB{}0q8eGwX>ExkI9yZN0)(P}$N(yi$AxmBY#Xj`(7zs{
zJbn2&jE`-*0lww_r;|fNaWm_xp;c9JHIv|RExZGKP%18qjgYa);`N-^VqXNVz{~)~
z?^&D;ouy!pKPy?%@xH`A
zSR
z7x%N3@o&{YEjfa|1;*eW_4TU{
zt;qCcY3Hj(<0DJuny*QL!y!StcG{>bhpUP%eVMq=1xcR>yZT8X9)1;rXOmQjPcANs
zr>&Qb{rr66;s|4v3iGmQlMjr9j;G6pqNs%;TsyVNd3{i~hpDX8ugdcnd&UQJzj)rH
zh>S6#n`cCJ9CwHv<2Ht$o`R5(h#r||VB?%J?s5W48;^o)b`Pi1^~}5{Y19lg{&W@LfHt*gc1`w$RfLrK{~H?A1$5
z;5v?AIhpN%gQsR6+Act9-3y
z8>jCTMnWQq-^s3#Lb|WalgB$k3F>}lyCxs<2&A;LS0}s#<|hPx9kM#B+Lu2DiD_3P
zelg;N!80(j@HNc2pXs}re%sHi+{aqBt~qUOy86?zN>7)yiCEJqy@2Gh#gzJE6j6Rx
zBQK{77zW?gLWtQ20Dzntu16k9^N>DQ@Nmbx*mOg=F=k)8VJfM%y(Xu41;8YCz+@K|
z9u7vhlT`BOnk_oMTeC;u@OhhoTeA`^34^iMihCLM_uVD>rI-9@4l7ocZl@DJ8FWZU
zB0lRBIqkHj4#pE&mD(X!e!~;G$`7f47k*
zOznM2@`&KM(|f5}sz)z%2}yJ5YmMj5Zwzr-W?v3R&@KuJ+l0zo==N@)nsbMHqHV}w
z7#_ntMGCNM21RuH^SYG+RH0sHUsF2z7ams57@2xbPj0y5)8h+caqv@P^q!do+}>+X
zzUBx|mikTawzXWYzJ4(AqAJpBF4ObmD_@gyg->oFGB6`k(8+?rFRV5P1yDkFM=8(c
z%RI)iG(rKtq-^V%B_(R9;tk6WIzA?x@cESTXg
zWYDBxkoNB5v6J8BP&n@HVtBNb@r+XYpjgub
zR4oE*$ffXJuh2g8TCaLnpNoSxJ~Jx@ayx9z5Osa)=AI#bg^5eQb<6gpR%c+Qs#N*e
z@XE4pAmjdI#0%pV7sIN>mNa^jTkd=<==2_#t-}9Ju&Z^|Lp$%B92@eN%=MRc)LK$%
z@!XAg;dQ8bt=@ZNey7+a(dy^o;QKGP@Rb5NJYQRrGEC{J=FB(Irw-MAfoP(9RK;)&jlxSCT=W;ODCf($WqRFhqN#LR^qVhK
zWhEp4`{Nnk;n0FHj}eNCZpRM`Y-@MIM&pvr7zQOZ3Ik5;CmZbR99b&22(!-07YNF)
z$o0MKej-jnvQV39{TH4r2R5univa1{ASc|VOTi4c@`t2FId|xkh5typ-rdU;1j){adk@*+(
zkHj{5B~eSy&HrPOOvl_FJ98)0V;^d`0-u0FTslgiLBQVGSTiSyu
zgMGAu&R}SbNa-DgKJb?;fe3Qys$?=;5?V`eRiq*Kj$I`}Z*x4rC~eNM=DsOq(=nUW>(+7o@O8K-_U(X?
zTyg032nXKax5W~SF5|eBj%r8Fa>i!ejC72*sd}zJ)t7Xy!gFvM`c4@*Iw>z$u)j_l
zR-Uqxymg}>Ti>i%9j*4kwfC33i~kyIQ``n)r(L
z!|H2*)Mwj4dk%e*L0tgFdW185>j4<7YwLXwcOsed`%6mS{+=&d@d!B}GkbDV*0
zNIWzW^|trz!&;qeI&mPiVDOUL70xpqVv0fpN9tjpu)@1LD9D<9}9{57j9!W$`zC6&i
zl9lKkmPh`x)5+h>>JtiRNNBW5$_)%-)#+SVSGsjX2T=+SRX05>yJZd`1hyk<@{%1+
zDu^k>J$d*Qz6BZMwHx!@O**^Tx&fsHDw%$@J0nfj^je^Ihy*aIx{B(hkBvSvh46Z9
zRO)BjjXL_IHXKo~$4es=8Wxk;Y+&nVBCXA;=MVuLgVn8Mk(*y^+kP3f?Pr~4^A}hXj9UHS}qeI%XKD3KhHnkrNH0(Y20BWl&!Kfm`EVh2;i5C
zpirU^K0nc2-I{cqvjZKVx
z=&hH#-d=gDWjVE}cMNAPJf;#NYdQ=h`twjX6yquXuCNgGx1~uk{YHAmFpQF`ZLGC=~ukEyj?cFDI
zH=@XvV#AY1EY4qb`y*;Ki>KuFB|2|toL7__Cr0S1Dl{s#y0=~7HSq~&7lpBc*VLua
zvv3r&-LM*{hq%IYP7<@)dG-G$kMrZaqs(MYoZ
zugEeJ@u(ip9rMoVtoFe;dF`^Br5x7v!rr5`hb5mJ#ocGqXHnm9m`yILjd0>UQSMv)
z^v}l5^bM6RZ6M%{mkI)
zHOoSp&dX)*xUt+kXscna#a`XxI;Ul2Sxa^i5sZc=(Q)oA^2-_3)NgTtwYui8
zR+%py;N9bY?#e^u*}Zs*yR)oKc=0pc>;!pfYHAul+oA@Ilelm;rw@FYR+SIaWS?;_
zUdw<|qqaYq(nqu>rG48E9dYAoT6GH;QRuBYK1}W#C_Z_?7~k*pJ3?MzVt&rhZTsBy
zw?nN$_Z>kimtwWcy`0?G#!)&7GjOcxCQps@p&ml8>~z(t=sjhR$6aFh!Vw5GA(lTh
z5GM)jCwloa6a}7mdfqNYE7oi`Jv$m5>5qR%9eZ=)=a
z+K4j5NpcDHHdepCS+P*{@o=yNp&TE(Sd4b0Notqso-Kt_mhDk1<-fa>T4KdY2N`U)
zxu41vD%T&k$Gl?CW81%7r#-o1TZ0&PCcy}L4TPiV;sz`|S!&w8-s$rLdM
zF&)>@`7=)65PWn#oi|8tXNb|((2ojf9d0fNZ^l7xY~dX~%*Xf-v2W-2n$i~s!4?H;
z2qbQscFN21tqB{|x1+(^G~xQSrvX&Y;V-%?b1}zjBQX{GOFcVYTcwm>>}>6^HA=$x
zn+z^Biv_5}0!#@7z1~YXJFCT2?D^jm+kH7jAqBo?M@ZdMl|2|66oLnSJXUOJtVLxe
z0vH)N^t*qrjq=eFRMV>BFEfS)-2RzKlt973;d3D}4edwIE>kGc5-o=JV56ird)RlS
z{Jg@0t-b#Ife80%!E~(7`qkZ8O~Q-8_{j7G&tqwX&&>^tm-#*{v7j-f1n0}mCR#7P
z-4FkajD2$9?4Fc7-C_|0Z_G^bxIs%tWk|aFgSQ(qkM+5PRh=g&ZeAZg35$-kn~}_;~&fP-dCNCzg>{gyW!~LZpn?aZ~Va3~H0Ta)z
z<4XPVk@;#%1S@fq<(2#8T04#8$mz>vM;(jek0>Qh!K%t5*4tU(fVYwD3Ri~=D!AmI
zV$Dt#TEDX7{lpW%tF&DOlTO)vZodn_%wYu~)ZQ}Qo^cBbDHd{YajkzNxttQW>ST<^
z2~^xhB_y1sjIF5;xchvCn{QVugIE2eYZDZ!-Y-4lJdb34*k({@M
zJ5!9Di^||~(IZ4iOoAbtggao+CaYvJynmB^;4r-tY2gS_*P!?U?hlEX;l+^*{%B2n
z)|1j9wOHQQ^5Xha>{Cu8_w^8=#6;Dz7kU~RgTqn;ynDm6{xdlkf2vk0UK^oS3yVy4
zE+v&qnlYtPHBk#X&2}r7`@K`J@^e~Qm?iRJ*tbAaZDZTmB&mWMkZp7Kj7^kth#_uX
z5z>gC(8Xz|Ie(+#&wiF3;Aey|Db(R*-U)!6;l_5@u?-$>j0SgEl5+c}Lfe-$p-dFH
zB_$bC<)x6#A_2Uuo8=^l1@}vK!gvbF#b&MoH8ac3xMxUz$LFb8KU(x$YhtHanM_sw
zYOFMBX2iNNSe&a}!;G9nv(tsW4@%3iQcqczOCF*JOBQ@4Orw=o?_vc(9$hfO`>U6&
zyY_CUa9pASiJpmv`@oR!k;&$`h8!)$uS=}d-fPddfIdMDUW@%3y1LI(1Q=e$)sz(QC*E;Nfl99YTgk+|@jl`+iF?<_D?4YqV0Zl)lO8YWC@1ZWW^mi{5ePQN<~FQ2NMG$|K{py5akJa
zkezmqhN)>MGMp$7=sOo2(7ppv``dCIwf&MaQQis7S596kkiw8Do(jO?EY4iJ4Hec6
z4Hymzu`w)cI9Pbq6GPtTP)x&Lmk;FT=ZCB4>(5}c0?;2l`p&?>&<;2(P8a3lOTNP#
zdEzF5qDpkRR&PZC&cS{7xD@qV;(g5X%xI?m$9Q
-Report Positioned Image
+README.rst
-
-
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
+- 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
@@ -398,7 +402,7 @@
Report Positioned Image
-
+
To configure company-level images:
- Go to Settings / Companies
@@ -414,8 +418,8 @@
- 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.
+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
- Company: Automatically set to the current company when creating
from the company form. To create shared images, leave empty.
@@ -438,7 +442,7 @@
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
@@ -446,15 +450,15 @@
Do not contact contributors directly about support or help with technical issues.
+