odoo-report
npx skills add https://github.com/ahmed-lakosha/odoo-upgrade-skill --skill odoo-report
Agent 安装分布
Skill 文档
Odoo Email Templates & QWeb Reports Skill (v2.0)
A comprehensive skill for creating, managing, debugging, and migrating Odoo email templates and QWeb reports across versions 14-19. Features wkhtmltopdf configuration, Arabic/RTL support, bilingual report patterns, and intelligent version-aware syntax.
Configuration
- Supported Versions: Odoo 14, 15, 16, 17, 18, 19
- Primary Version: Odoo 17
- Templates Database: 400+ templates analyzed
- Pattern Library: 50+ email patterns, 30+ QWeb patterns
- Core Model:
mail.template - Rendering Engines: inline_template (Jinja2), QWeb
Quick Reference
Template Architecture
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ODOO EMAIL TEMPLATE ARCHITECTURE â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â â
â mail.template â
â âââ Inherits: mail.render.mixin (rendering engine) â
â âââ Inherits: template.reset.mixin (reset - Odoo 16+) â
â â â
â âââ Header Fields (inline_template engine): â
â â ⢠subject ⢠email_from ⢠email_to â
â â ⢠email_cc ⢠reply_to ⢠partner_to â
â â â
â âââ Content Fields (QWeb engine): â
â â ⢠body_html â
â â â
â âââ Attachment Fields: â
â â ⢠attachment_ids (static) â
â â ⢠report_template (Odoo 14-16) / report_template_ids (Odoo 17+) â
â â ⢠report_name (dynamic filename) â
â â â
â âââ Configuration: â
â ⢠email_layout_xmlid ⢠auto_delete ⢠mail_server_id â
â ⢠use_default_to ⢠scheduled_date â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Rendering Flow
mail.template.send_mail_batch(res_ids)
â
ââ⺠_generate_template(res_ids, render_fields)
â â
â ââ⺠_classify_per_lang() # Group by language
â â
â ââ⺠_render_field() for each:
â â ⢠subject (inline_template)
â â ⢠body_html (qweb)
â â ⢠email_from, email_to, etc.
â â
â ââ⺠_generate_template_recipients()
â â
â ââ⺠_generate_template_attachments()
â â ⢠Static attachments
â â ⢠Report PDF generation
â â
â ââ⺠Return rendered values dict
â
ââ⺠Create mail.mail records
â
ââ⺠Apply email_layout_xmlid (if set)
â ââ⺠ir.qweb._render(layout_xmlid, context)
â
ââ⺠Send via mail_server_id or default
Two Rendering Engines
1. Inline Template Engine (Jinja2-like)
Used for: subject, email_from, email_to, email_cc, reply_to, partner_to, lang, scheduled_date
# Simple field access
{{ object.name }}
{{ object.partner_id.name }}
{{ object.company_id.name }}
# Method calls
{{ object.get_portal_url() }}
{{ object.email_formatted }}
# Conditional expressions (ternary-like)
{{ object.state == 'draft' and 'Quotation' or 'Order' }}
# Or chains (fallback)
{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}
# Context access
{{ ctx.get('proforma') and 'Proforma' or '' }}
# String operations
{{ (object.name or '').replace('/', '-') }}
2. QWeb Engine
Used for: body_html
Output Tags:
<!-- Escaped output (safe) -->
<t t-out="object.name"/>
<t t-out="object.partner_id.name or 'Unknown'"/>
<!-- Format with helper -->
<t t-out="format_amount(object.amount_total, object.currency_id)"/>
<t t-out="format_date(object.date_order)"/>
<t t-out="format_datetime(object.create_date, tz='UTC', dt_format='long')"/>
Conditional Tags:
<t t-if="object.state == 'draft'">
This is a draft.
</t>
<t t-elif="object.state == 'sent'">
This has been sent.
</t>
<t t-else="">
This is confirmed.
</t>
Loop Tags:
<t t-foreach="object.order_line" t-as="line">
<tr>
<td t-out="line.name"/>
<td t-out="line.product_uom_qty"/>
<td t-out="format_amount(line.price_subtotal, object.currency_id)"/>
</tr>
</t>
Attribute Tags:
<!-- Dynamic attribute -->
<a t-att-href="object.get_portal_url()">View</a>
<!-- Formatted attribute (with interpolation) -->
<a t-attf-href="/web/image/product.product/{{ line.product_id.id }}/image_128">
<img t-attf-src="/web/image/product.product/{{ line.product_id.id }}/image_128"/>
</a>
Version Decision Matrix
| Feature | Odoo 14 | Odoo 15 | Odoo 16 | Odoo 17 | Odoo 18 | Odoo 19 |
|---|---|---|---|---|---|---|
t-out syntax |
N | Y | Y | Y | Y | Y |
t-esc (legacy) |
Y | Y | Y | Y | Y | Y |
render_engine='qweb' |
N | Y | Y | Y | Y | Y |
template_category |
N | N | Y | Y | Y | Y |
report_template_ids M2M |
N | N | N | Y | Y | Y |
| Company branding colors | N | N | N | N | N | Y |
email_primary_color |
N | N | N | N | N | Y |
email_secondary_color |
N | N | N | N | N | Y |
mail_notification_layout_with_responsible_signature |
N | N | Y | Y | Y | Y |
| Enhanced security in sandbox | N | N | Y | Y | Y | Y |
Email Layout Templates
Available Layouts
| Layout | Width | Use Case |
|---|---|---|
mail.mail_notification_layout |
900px | Full notifications with header/footer |
mail.mail_notification_light |
590px | Simple notifications |
mail.mail_notification_layout_with_responsible_signature |
900px | Uses record’s user_id signature |
Layout Context Variables
{
# Message Information
'message': mail.message, # Message object with body, record_name
'subtype': mail.message.subtype, # Message subtype
# Display Control
'has_button_access': Boolean, # Show action button
'button_access': { # CTA button config
'url': String,
'title': String
},
'subtitles': List[String], # Header subtitles
# Record Information
'record': record, # The document
'record_name': String, # Display name
'model_description': String, # Human-readable model
# Tracking
'tracking_values': [ # Field changes
(field_name, old_value, new_value),
],
# Signature
'email_add_signature': Boolean, # Include signature
'signature': HTML, # User signature
# Company
'company': res.company, # Company object
'website_url': String, # Base URL
# Branding (Odoo 19+)
'company.email_primary_color': String, # Button text color
'company.email_secondary_color': String, # Button background
# Utilities
'is_html_empty': Function, # Check empty HTML
}
Commands Reference
Template Creation Commands
| Command | Description |
|---|---|
/create-email-template |
Create a new email template for any model |
/create-qweb-report |
Create a new QWeb PDF report |
/create-notification |
Create a notification template with layout |
/create-digest-email |
Create a digest/summary email template |
Template Management Commands
| Command | Description |
|---|---|
/list-templates |
List all templates for a model or module |
/analyze-template |
Analyze an existing template for issues |
/debug-template |
Debug template rendering issues |
/preview-template |
Generate preview of template output |
Migration Commands
| Command | Description |
|---|---|
/migrate-template |
Migrate template between Odoo versions |
/fix-template |
Fix common template issues |
/validate-template |
Validate template syntax and context |
QWeb Report Commands
| Command | Description |
|---|---|
/create-report-action |
Create report action with menu/button |
/style-report |
Add CSS styling to QWeb report |
/add-header-footer |
Add header/footer to report |
Template Patterns Library
Pattern 1: Basic Notification Email
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<record id="email_template_basic_notification" model="mail.template">
<field name="name">Basic Notification</field>
<field name="model_id" ref="model_your_model"/>
<field name="subject">{{ object.name }} - Notification</field>
<field name="email_from">{{ (object.company_id.email or user.email_formatted) }}</field>
<field name="email_to">{{ object.partner_id.email }}</field>
<field name="email_layout_xmlid">mail.mail_notification_layout</field>
<field name="body_html" type="html">
<div>
<p>Dear <t t-out="object.partner_id.name"/>,</p>
<p>This is a notification regarding <strong t-out="object.name"/>.</p>
<t t-if="object.description">
<p t-out="object.description"/>
</t>
<p>Best regards,<br/><t t-out="object.company_id.name"/></p>
</div>
</field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>
Pattern 2: Document Email with Report Attachment
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<record id="email_template_document" model="mail.template">
<field name="name">Send Document</field>
<field name="model_id" ref="model_your_model"/>
<field name="subject">
{{ object.state in ('draft', 'sent') and 'Quotation' or 'Order' }} {{ object.name }}
</field>
<field name="email_from">{{ (object.user_id.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<!-- Odoo 17+ use report_template_ids -->
<field name="report_template_ids" eval="[(4, ref('module.report_action_id'))]"/>
<field name="report_name">{{ (object.name or 'Document').replace('/', '-') }}</field>
<field name="email_layout_xmlid">mail.mail_notification_layout</field>
<field name="body_html" type="html">
<div>
<t t-set="doc_name" t-value="'quotation' if object.state in ('draft', 'sent') else 'order'"/>
<p>Dear <t t-out="object.partner_id.name"/>,</p>
<p>Please find attached your <t t-out="doc_name"/>
<strong t-out="object.name"/> amounting to
<strong t-out="format_amount(object.amount_total, object.currency_id)"/>.</p>
<p>Do not hesitate to contact us if you have any questions.</p>
</div>
</field>
</record>
</data>
</odoo>
Pattern 3: Order Lines Table
<table border="0" cellpadding="0" cellspacing="0" width="100%"
style="border-collapse: collapse;">
<thead>
<tr style="background-color: #875A7B; color: white;">
<th style="padding: 8px;">Product</th>
<th style="padding: 8px;">Quantity</th>
<th style="padding: 8px; text-align: right;">Price</th>
</tr>
</thead>
<tbody>
<t t-foreach="object.order_line" t-as="line">
<t t-if="line.display_type == 'line_section'">
<tr>
<td colspan="3" style="font-weight: bold; padding: 8px; background-color: #f5f5f5;">
<t t-out="line.name"/>
</td>
</tr>
</t>
<t t-elif="line.display_type == 'line_note'">
<tr>
<td colspan="3" style="font-style: italic; padding: 8px;">
<t t-out="line.name"/>
</td>
</tr>
</t>
<t t-else="">
<tr t-att-style="'background-color: #f9f9f9' if line_index % 2 == 0 else ''">
<td style="padding: 8px;">
<t t-out="line.product_id.name"/>
</td>
<td style="padding: 8px; text-align: center;">
<t t-out="line.product_uom_qty"/>
<t t-out="line.product_uom.name"/>
</td>
<td style="padding: 8px; text-align: right;">
<t t-out="format_amount(line.price_subtotal, object.currency_id)"/>
</td>
</tr>
</t>
</t>
</tbody>
<tfoot>
<tr style="font-weight: bold; background-color: #f5f5f5;">
<td colspan="2" style="padding: 8px; text-align: right;">Total:</td>
<td style="padding: 8px; text-align: right;">
<t t-out="format_amount(object.amount_total, object.currency_id)"/>
</td>
</tr>
</tfoot>
</table>
Pattern 4: CTA Button
<t t-set="button_color" t-value="company.email_secondary_color or '#875A7B'"/>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td align="center" style="padding: 16px;">
<a t-att-href="object.get_portal_url()"
t-att-style="'display: inline-block; padding: 10px 20px; color: #ffffff; text-decoration: none; border-radius: 3px; background-color: %s' % button_color">
View Online
</a>
</td>
</tr>
</table>
Pattern 5: Conditional Content Based on State
<t t-if="object.state == 'draft'">
<p style="color: #856404; background-color: #fff3cd; padding: 10px; border-radius: 4px;">
<strong>Draft:</strong> This document is not yet confirmed.
</p>
</t>
<t t-elif="object.state == 'sent'">
<p style="color: #0c5460; background-color: #d1ecf1; padding: 10px; border-radius: 4px;">
<strong>Awaiting Confirmation:</strong> Please review and confirm.
</p>
</t>
<t t-elif="object.state == 'done'">
<p style="color: #155724; background-color: #d4edda; padding: 10px; border-radius: 4px;">
<strong>Completed:</strong> This document has been processed.
</p>
</t>
Pattern 6: Payment Information Block
<t t-if="object.payment_state not in ('paid', 'in_payment')">
<div style="background-color: #f8f9fa; padding: 15px; margin: 15px 0; border-radius: 4px;">
<h4 style="margin: 0 0 10px 0;">Payment Information</h4>
<t t-if="object.payment_reference">
<p><strong>Reference:</strong> <t t-out="object.payment_reference"/></p>
</t>
<t t-if="object.partner_bank_id">
<p><strong>Bank Account:</strong> <t t-out="object.partner_bank_id.acc_number"/></p>
<t t-if="object.partner_bank_id.bank_id">
<p><strong>Bank:</strong> <t t-out="object.partner_bank_id.bank_id.name"/></p>
</t>
</t>
<t t-if="object.amount_residual">
<p><strong>Amount Due:</strong>
<t t-out="format_amount(object.amount_residual, object.currency_id)"/>
</p>
</t>
</div>
</t>
QWeb Report Patterns
Pattern 1: Basic Report Structure
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Report Action -->
<record id="action_report_document" model="ir.actions.report">
<field name="name">Document Report</field>
<field name="model">your.model</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">module_name.report_document_template</field>
<field name="report_file">module_name.report_document_template</field>
<field name="print_report_name">'Document - %s' % object.name</field>
<field name="binding_model_id" ref="model_your_model"/>
<field name="binding_type">report</field>
</record>
<!-- Report Template -->
<template id="report_document_template">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="web.external_layout">
<div class="page">
<h2><t t-out="doc.name"/></h2>
<div class="row">
<div class="col-6">
<strong>Date:</strong>
<span t-field="doc.date"/>
</div>
<div class="col-6 text-end">
<strong>Reference:</strong>
<span t-out="doc.reference"/>
</div>
</div>
<!-- Content goes here -->
</div>
</t>
</t>
</t>
</template>
</odoo>
Pattern 2: Report with Table
<table class="table table-sm o_main_table">
<thead>
<tr>
<th class="text-start">Description</th>
<th class="text-center">Quantity</th>
<th class="text-end">Unit Price</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<t t-foreach="doc.line_ids" t-as="line">
<tr>
<td><span t-field="line.name"/></td>
<td class="text-center"><span t-field="line.quantity"/></td>
<td class="text-end"><span t-field="line.price_unit"/></td>
<td class="text-end"><span t-field="line.price_subtotal"/></td>
</tr>
</t>
</tbody>
</table>
<!-- Totals -->
<div class="row justify-content-end">
<div class="col-4">
<table class="table table-sm">
<tr>
<td><strong>Subtotal</strong></td>
<td class="text-end"><span t-field="doc.amount_untaxed"/></td>
</tr>
<tr>
<td>Taxes</td>
<td class="text-end"><span t-field="doc.amount_tax"/></td>
</tr>
<tr class="border-top">
<td><strong>Total</strong></td>
<td class="text-end"><span t-field="doc.amount_total"/></td>
</tr>
</table>
</div>
</div>
Pattern 3: Page Break Control
<!-- Force page break before element -->
<div style="page-break-before: always;">
<h3>New Page Content</h3>
</div>
<!-- Prevent page break inside element -->
<div style="page-break-inside: avoid;">
<table><!-- Table that should stay together --></table>
</div>
<!-- Force page break after element -->
<div style="page-break-after: always;">
<p>End of section</p>
</div>
Validation Rules
MANDATORY Pre-Flight Checks
Before creating any template, Claude MUST validate:
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â TEMPLATE VALIDATION CHECKLIST â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â â
â 1. MODEL VALIDATION â
â â¡ Model exists in target Odoo version â
â â¡ Model has required fields (partner_id, etc.) â
â â¡ Model inherits mail.thread (if notification) â
â â
â 2. FIELD VALIDATION â
â â¡ All {{ object.field }} references exist â
â â¡ All t-out="object.field" references exist â
â â¡ Related fields are valid (object.partner_id.name) â
â â
â 3. SYNTAX VALIDATION â
â â¡ QWeb tags properly closed â
â â¡ Jinja2 expressions balanced {{ }} â
â â¡ No mixing of t-esc (Odoo 14) and t-out (Odoo 15+) â
â â
â 4. VERSION COMPATIBILITY â
â â¡ report_template vs report_template_ids (Odoo 17+) â
â â¡ template_category (Odoo 16+) â
â â¡ Company branding colors (Odoo 19+) â
â â
â 5. SECURITY VALIDATION â
â â¡ No unsafe eval() or exec() â
â â¡ No arbitrary file access â
â â¡ Sandbox-safe expressions â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Error Recovery
Common Errors and Solutions
| Error | Cause | Solution |
|---|---|---|
AttributeError: 'NoneType' has no attribute 'name' |
Null field access | Use object.field_id.name or '' |
QWebException: t-esc is deprecated |
Old syntax in Odoo 15+ | Replace t-esc with t-out |
KeyError: 'format_amount' |
Missing context helper | Ensure mail.render.mixin is inherited |
ValidationError: Invalid XML |
Malformed QWeb | Check XML structure, close all tags |
NameError: name 'object' is not defined |
Wrong rendering context | Use template’s model context |
Debug Template Rendering
# In Odoo shell
template = env['mail.template'].browse(TEMPLATE_ID)
record = env['your.model'].browse(RECORD_ID)
# Render and inspect
rendered = template._render_field(
'body_html',
[record.id],
compute_lang=True
)
print(rendered[record.id])
Module-Specific Templates
Sales Module
| Template ID | Purpose | Model |
|---|---|---|
email_template_edi_sale |
Send quotation/order | sale.order |
mail_template_sale_confirmation |
Order confirmation | sale.order |
mail_template_sale_payment_executed |
Payment received | sale.order |
Purchase Module
| Template ID | Purpose | Model |
|---|---|---|
email_template_edi_purchase |
Send RFQ | purchase.order |
email_template_edi_purchase_done |
Send PO | purchase.order |
email_template_edi_purchase_reminder |
Delivery reminder | purchase.order |
Accounting Module
| Template ID | Purpose | Model |
|---|---|---|
email_template_edi_invoice |
Send invoice | account.move |
email_template_edi_credit_note |
Send credit note | account.move |
mail_template_data_payment_receipt |
Payment receipt | account.payment |
HR Recruitment
| Template ID | Purpose | Model |
|---|---|---|
email_template_data_applicant_employee |
Applicant to employee | hr.employee |
email_template_data_applicant_congratulations |
Congratulations | hr.applicant |
email_template_data_applicant_refuse |
Refusal notice | hr.applicant |
Best Practices
1. Always Use Fallbacks
<!-- Good -->
<t t-out="object.partner_id.name or 'Valued Customer'"/>
<!-- Bad - will fail if partner_id is None -->
<t t-out="object.partner_id.name"/>
2. Use Format Helpers
<!-- Good - consistent formatting -->
<t t-out="format_amount(object.amount_total, object.currency_id)"/>
<t t-out="format_date(object.date_order)"/>
<!-- Bad - manual formatting -->
<t t-out="'$%.2f' % object.amount_total"/>
3. Respect Translations
<!-- Good - translatable -->
<p>Dear <t t-out="object.partner_id.name"/>,</p>
<!-- Bad - hardcoded in template, use data records instead -->
4. Use Layouts for Consistency
<!-- Good - uses company branding -->
<field name="email_layout_xmlid">mail.mail_notification_layout</field>
<!-- Avoid - custom inline styling for every email -->
5. Handle Empty HTML
<t t-if="not is_html_empty(object.description)">
<div t-out="object.description"/>
</t>
6. Version-Specific Syntax
<!-- Odoo 15+ -->
<t t-out="value"/>
<!-- Odoo 14 only -->
<t t-esc="value"/>
7. Report Attachment Handling
<!-- Odoo 14-16: Single report -->
<field name="report_template" ref="module.report_action"/>
<!-- Odoo 17+: Multiple reports -->
<field name="report_template_ids" eval="[(4, ref('module.report_action'))]"/>
8. Dynamic Filenames
# Safe filename generation
<field name="report_name">{{ (object.name or 'Document').replace('/', '-') }}</field>
wkhtmltopdf Setup & Configuration
â ï¸ CRITICAL: wkhtmltopdf is REQUIRED for PDF Reports
Odoo uses wkhtmltopdf to convert QWeb HTML to PDF. Without proper configuration, PDF generation will fail.
Installation
# Windows
winget install wkhtmltopdf.wkhtmltox
# OR download from: https://wkhtmltopdf.org/downloads.html
# Ubuntu/Debian
sudo apt-get install wkhtmltopdf
# macOS
brew install wkhtmltopdf
Odoo Configuration (MANDATORY)
Add to your odoo.conf:
[options]
# Windows
bin_path = C:\Program Files\wkhtmltopdf\bin
# Linux
bin_path = /usr/local/bin
# macOS (Homebrew)
bin_path = /opt/homebrew/bin
Verification
After server restart, check logs for:
Will use the Wkhtmltopdf binary at C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe
Common Errors
| Error | Cause | Solution |
|---|---|---|
Unable to find Wkhtmltopdf |
bin_path not configured | Add bin_path to odoo.conf |
PDF generation timeout |
Network resources requested | Remove external URLs (fonts, images) |
Blank PDF generated |
CSS/SCSS errors | Check browser console, validate SCSS |
Exit with code 1 |
HTML syntax error | Validate QWeb template XML |
Key Limitation: OFFLINE Mode
wkhtmltopdf runs WITHOUT network access. This means:
- â Google Fonts CDN will NOT load
- â External images will NOT render
- â External CSS will NOT apply
- â
Use
web.external_layoutfor fonts - â Embed images as base64 or use Odoo attachments
Arabic/RTL & Multilingual Reports
â ï¸ CRITICAL: UTF-8 Encoding Requirement
Arabic text displaying as ÃçêÃËñé instead of ÙØ§ØªÙرة indicates UTF-8 â Latin-1 encoding corruption.
MANDATORY Template Wrapper
ALWAYS use this structure for non-Latin text support:
<template id="report_document">
<t t-call="web.html_container"> <!-- â
Provides UTF-8 meta tag -->
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout"> <!-- â
Loads proper fonts -->
<div class="page">
<!-- Your content here -->
</div>
</t>
</t>
</t>
</template>
â WRONG Patterns (Will Cause Encoding Issues)
<!-- WRONG: Custom HTML without proper encoding -->
<template id="report_document">
<html>
<head><title>Report</title></head>
<body>
ÙØ§ØªÙرة <!-- Will display as ÃçêÃËñé -->
</body>
</html>
</template>
<!-- WRONG: Missing web.html_container -->
<template id="report_document">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<!-- Missing outer container! -->
</t>
</t>
</template>
Bilingual Label Pattern
<!-- Side-by-side: English | Arabic -->
<th style="background: #1a5276; color: white; padding: 12px;">
Date | Ø§ÙØªØ§Ø±ÙØ®
</th>
<!-- Stacked: Arabic on top, English below -->
<th style="background: #1a5276; color: white; padding: 12px;">
<div>Ø§ÙØªØ§Ø±ÙØ®</div>
<div style="font-size: 10px; font-weight: normal;">Date</div>
</th>
RTL Text Alignment
<!-- Force RTL for Arabic paragraphs -->
<div style="direction: rtl; text-align: right;">
ÙØ±Ø¬Ù إرسا٠ØÙØ§ÙØ§ØªÙÙ
عÙÙ Ø§ÙØØ³Ø§Ø¨ اÙÙ
ذÙÙØ± Ø£Ø¹ÙØ§Ù
</div>
<!-- Mixed content: Use CSS classes -->
<style>
.rtl { direction: rtl; text-align: right; }
.ltr { direction: ltr; text-align: left; }
</style>
Currency Symbol Corruption
If $ displays as $Ã or similar:
- Cause: Same UTF-8 encoding issue
- Solution: Use
web.html_containerwrapper
Paper Format Configuration
Custom Paper Format Template
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="paperformat_custom" model="report.paperformat">
<field name="name">Custom Invoice Format</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<field name="margin_top">20</field>
<field name="margin_bottom">20</field>
<field name="margin_left">15</field>
<field name="margin_right">15</field>
<field name="header_line" eval="False"/>
<field name="header_spacing">0</field>
<field name="dpi">90</field>
</record>
</odoo>
Linking Paper Format to Report
<record id="action_report_invoice" model="ir.actions.report">
<field name="name">Custom Invoice</field>
<field name="model">account.move</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">module.report_invoice_template</field>
<field name="paperformat_id" ref="module.paperformat_custom"/>
<!-- ... other fields ... -->
</record>
Standard Paper Formats
| Format | Dimensions | Use Case |
|---|---|---|
A4 |
210 Ã 297 mm | Standard international |
Letter |
216 Ã 279 mm | US standard |
Legal |
216 Ã 356 mm | Legal documents |
A5 |
148 Ã 210 mm | Small documents |
Custom |
Set page_width/page_height | Special sizes |
Orientation Options
Portrait– Vertical (default)Landscape– Horizontal (wide tables, charts)
Report SCSS Styling
Correct Asset Bundle
# __manifest__.py
{
'assets': {
'web.report_assets_common': [
'module/static/src/scss/report_styles.scss',
],
},
}
â ï¸ CRITICAL: Google Fonts Pitfall
// â BROKEN - Semicolons in URL break SCSS parsing
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
// â
FIXED - Use weight range syntax
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300..700&display=swap');
// â
BEST - Don't use Google Fonts (wkhtmltopdf is offline!)
// Rely on system fonts via web.external_layout
Why External Fonts Fail
- wkhtmltopdf runs offline (no network access)
- Google Fonts CDN requests timeout
- Result: Fallback to system fonts
Recommended Font Stack
// Safe fonts that work in PDF generation
$report-font-family: 'DejaVu Sans', 'Arial', 'Helvetica', sans-serif;
.page {
font-family: $report-font-family;
}
Color Scheme Pattern
// Define colors once
$primary-color: #1a5276; // Dark blue
$secondary-color: #d5dbdb; // Light gray
$accent-color: #f39c12; // Orange/Gold
$border-color: #bdc3c7;
$text-dark: #2c3e50;
$text-muted: #7f8c8d;
// Table header
.table-header, thead tr {
background-color: $primary-color;
color: white;
}
// Label cells
.label-cell {
background-color: $secondary-color;
color: $primary-color;
font-weight: bold;
}
// Alternating rows
tbody tr:nth-child(even) {
background-color: rgba($secondary-color, 0.3);
}
Print-Specific Styles
@media print {
// Ensure colors print
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
// Page breaks
.page-break-before { page-break-before: always; }
.page-break-after { page-break-after: always; }
.no-break { page-break-inside: avoid; }
}
Debug Report Workflow
Systematic Diagnosis Steps
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â REPORT DEBUG WORKFLOW â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â â
â STEP 1: Check Infrastructure â
â âââââââââââââââââââââââââââââ â
â â¡ wkhtmltopdf installed? â wkhtmltopdf --version â
â â¡ bin_path in odoo.conf? â
â â¡ Server restarted after config change? â
â â¡ Server log shows wkhtmltopdf path? â
â â
â STEP 2: Validate Template Structure â
â ââââââââââââââââââââââââââââââââââââ â
â â¡ Uses web.html_container wrapper? â
â â¡ Uses web.external_layout? â
â â¡ Has t-foreach docs loop? â
â â¡ Content inside div.page? â
â â
â STEP 3: Check Report Action â
â âââââââââââââââââââââââââââ â
â â¡ report_name matches template id? â
â â¡ report_file matches template id? â
â â¡ binding_model_id correct? â
â â¡ report_type = 'qweb-pdf'? â
â â
â STEP 4: Test Render in Shell â
â ââââââââââââââââââââââââââââ â
â python odoo-bin shell -d DATABASE â
â >>> report = env.ref('module.report_action') â
â >>> pdf, _ = report._render_qweb_pdf([record_id]) â
â >>> # Check console for errors â
â â
â STEP 5: Browser Cache â
â âââââââââââââââââââââ â
â â¡ Clear browser cache (Ctrl+Shift+R) â
â â¡ Clear Odoo asset cache if needed â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Issue-Specific Debugging
Encoding Issues (Arabic, Chinese, etc.):
Symptom: ÃçêÃËñé instead of ÙØ§ØªÙرة
Cause: Missing web.html_container
Fix: Add <t t-call="web.html_container"> as outermost wrapper
Blank PDF:
Symptom: PDF generates but is empty
Causes:
1. CSS syntax error â Check SCSS for semicolons in URLs
2. Missing template â Verify report_name matches template id
3. No records â Check docs variable has records
Fonts Not Rendering:
Symptom: Wrong font in PDF
Cause: External font URLs (wkhtmltopdf offline)
Fix: Use web.external_layout or system fonts only
Timeout/Hang:
Symptom: PDF generation hangs or times out
Cause: External resources being fetched
Fix: Remove all external URLs (CDNs, external images)
Clear Asset Cache
# Odoo shell
env['ir.attachment'].search([
('url', 'like', '/web/assets/')
]).unlink()
env.cr.commit()
Bilingual Invoice Template (Complete Example)
Based on proven sadad_invoice implementation:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Paper Format -->
<record id="paperformat_bilingual_invoice" model="report.paperformat">
<field name="name">Bilingual Invoice A4</field>
<field name="format">A4</field>
<field name="orientation">Portrait</field>
<field name="margin_top">25</field>
<field name="margin_bottom">25</field>
<field name="margin_left">15</field>
<field name="margin_right">15</field>
<field name="dpi">90</field>
</record>
<!-- Report Action -->
<record id="action_report_bilingual_invoice" model="ir.actions.report">
<field name="name">Bilingual Invoice</field>
<field name="model">account.move</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">module.report_bilingual_invoice_document</field>
<field name="report_file">module.report_bilingual_invoice_document</field>
<field name="paperformat_id" ref="paperformat_bilingual_invoice"/>
<field name="binding_model_id" ref="account.model_account_move"/>
<field name="binding_type">report</field>
<field name="print_report_name">'Invoice - %s' % object.name</field>
</record>
<!-- CRITICAL: Proper wrapper structure for UTF-8 -->
<template id="report_bilingual_invoice_document">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<t t-call="module.report_bilingual_invoice_content"/>
</t>
</t>
</t>
</template>
<!-- Content Template -->
<template id="report_bilingual_invoice_content">
<div class="page" style="font-family: Arial, sans-serif; font-size: 12px;">
<!-- Bilingual Header -->
<div style="text-align: center; margin-bottom: 30px; border-bottom: 3px solid #1a5276; padding-bottom: 15px;">
<h1 style="color: #1a5276; margin: 0;">
Sales Invoice | ÙØ§ØªÙرة Ù
Ø¨ÙØ¹Ø§Øª
</h1>
<h2 style="margin: 10px 0 0 0;" t-field="o.name"/>
</div>
<!-- Invoice Info Grid -->
<table style="width: 100%; border-collapse: collapse; margin-bottom: 25px;">
<tr>
<td style="width: 25%; border: 1px solid #bdc3c7; padding: 10px; background: #d5dbdb; color: #1a5276; font-weight: bold;">
Invoice Date | ØªØ§Ø±ÙØ® اÙÙØ§ØªÙرة
</td>
<td style="width: 25%; border: 1px solid #bdc3c7; padding: 10px;">
<span t-field="o.invoice_date" t-options='{"widget": "date"}'/>
</td>
<td style="width: 25%; border: 1px solid #bdc3c7; padding: 10px; background: #d5dbdb; color: #1a5276; font-weight: bold;">
Due Date | ØªØ§Ø±ÙØ® Ø§ÙØ§Ø³ØªØÙاÙ
</td>
<td style="width: 25%; border: 1px solid #bdc3c7; padding: 10px;">
<span t-field="o.invoice_date_due" t-options='{"widget": "date"}'/>
</td>
</tr>
<tr>
<td style="border: 1px solid #bdc3c7; padding: 10px; background: #d5dbdb; color: #1a5276; font-weight: bold;">
Customer | Ø§ÙØ¹Ù
ÙÙ
</td>
<td colspan="3" style="border: 1px solid #bdc3c7; padding: 10px;">
<span t-field="o.partner_id.name"/>
</td>
</tr>
</table>
<!-- Invoice Lines Table -->
<table style="width: 100%; border-collapse: collapse; margin-bottom: 25px;">
<thead>
<tr style="background: #1a5276; color: white;">
<th style="padding: 12px; border: 1px solid #1a5276; text-align: center; width: 5%;">
#
</th>
<th style="padding: 12px; border: 1px solid #1a5276; text-align: left;">
<div>اÙÙØµÙ</div>
<div style="font-size: 10px; font-weight: normal;">Description</div>
</th>
<th style="padding: 12px; border: 1px solid #1a5276; text-align: center; width: 10%;">
<div>اÙÙÙ
ÙØ©</div>
<div style="font-size: 10px; font-weight: normal;">Qty</div>
</th>
<th style="padding: 12px; border: 1px solid #1a5276; text-align: right; width: 15%;">
<div>Ø§ÙØ³Ø¹Ø±</div>
<div style="font-size: 10px; font-weight: normal;">Unit Price</div>
</th>
<th style="padding: 12px; border: 1px solid #1a5276; text-align: right; width: 15%;">
<div>Ø§ÙØ¥Ø¬Ù
اÙÙ</div>
<div style="font-size: 10px; font-weight: normal;">Subtotal</div>
</th>
</tr>
</thead>
<tbody>
<t t-set="line_num" t-value="0"/>
<t t-foreach="o.invoice_line_ids.filtered(lambda l: not l.display_type)" t-as="line">
<t t-set="line_num" t-value="line_num + 1"/>
<tr t-att-style="'background-color: #f9f9f9;' if line_index % 2 == 0 else ''">
<td style="border: 1px solid #bdc3c7; padding: 10px; text-align: center;">
<t t-out="line_num"/>
</td>
<td style="border: 1px solid #bdc3c7; padding: 10px;">
<t t-out="line.name"/>
</td>
<td style="border: 1px solid #bdc3c7; padding: 10px; text-align: center;">
<span t-field="line.quantity"/>
<t t-if="line.product_uom_id">
<span t-field="line.product_uom_id.name"/>
</t>
</td>
<td style="border: 1px solid #bdc3c7; padding: 10px; text-align: right;">
<span t-field="line.price_unit" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
<td style="border: 1px solid #bdc3c7; padding: 10px; text-align: right;">
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</tr>
</t>
</tbody>
</table>
<!-- Totals Section -->
<div style="margin-top: 20px;">
<table style="width: 40%; margin-left: auto; border-collapse: collapse;">
<tr>
<td style="border: 1px solid #bdc3c7; padding: 10px; background: #d5dbdb; color: #1a5276; font-weight: bold;">
Subtotal | اÙÙ
جÙ
ÙØ¹ اÙÙØ±Ø¹Ù
</td>
<td style="border: 1px solid #bdc3c7; padding: 10px; text-align: right;">
<span t-field="o.amount_untaxed" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</tr>
<t t-if="o.amount_tax">
<tr>
<td style="border: 1px solid #bdc3c7; padding: 10px; background: #d5dbdb; color: #1a5276; font-weight: bold;">
Tax | Ø§ÙØ¶Ø±Ùبة
</td>
<td style="border: 1px solid #bdc3c7; padding: 10px; text-align: right;">
<span t-field="o.amount_tax" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</tr>
</t>
<tr style="font-size: 14px;">
<td style="border: 2px solid #1a5276; padding: 12px; background: #1a5276; color: white; font-weight: bold;">
Total | Ø§ÙØ¥Ø¬Ù
اÙÙ
</td>
<td style="border: 2px solid #1a5276; padding: 12px; text-align: right; font-weight: bold;">
<span t-field="o.amount_total" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</tr>
</table>
</div>
<!-- Payment Terms (RTL for Arabic) -->
<t t-if="o.invoice_payment_term_id">
<div style="margin-top: 30px; padding: 15px; background: #f8f9fa; border-radius: 4px;">
<strong>Payment Terms | Ø´Ø±ÙØ· Ø§ÙØ¯Ùع:</strong>
<span t-field="o.invoice_payment_term_id.name"/>
</div>
</t>
</div>
</template>
</odoo>
Report Validation Checklist
Pre-Flight Checks (MANDATORY)
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â QWEB REPORT VALIDATION CHECKLIST â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â â
â 1. INFRASTRUCTURE â
â â¡ wkhtmltopdf installed and in PATH â
â â¡ bin_path configured in odoo.conf â
â â¡ Server restarted after config change â
â â¡ Server log confirms wkhtmltopdf path â
â â
â 2. TEMPLATE STRUCTURE (for non-Latin text) â
â â¡ web.html_container as OUTERMOST wrapper â
â â¡ t-foreach docs loop â
â â¡ web.external_layout for company header/fonts â
â â¡ Content inside div.page â
â â
â 3. ENCODING VERIFICATION â
â â¡ NOT using custom <html> tags â
â â¡ NOT importing external fonts via CDN â
â â¡ Arabic/Chinese text renders correctly â
â â¡ Currency symbols display correctly (no $Ã) â
â â
â 4. SCSS/CSS VALIDATION â
â â¡ Using web.report_assets_common bundle â
â â¡ No semicolons in @import URLs â
â â¡ No external CDN resources â
â â¡ System fonts only (DejaVu, Arial, etc.) â
â â
â 5. REPORT ACTION FIELDS â
â â¡ report_name = 'module.template_id' â
â â¡ report_file = 'module.template_id' â
â â¡ binding_model_id = ref('module.model_xxx') â
â â¡ report_type = 'qweb-pdf' â
â â¡ paperformat_id linked (if custom) â
â â
â 6. MANIFEST FILE â
â â¡ depends includes target module (account, sale, etc.) â
â â¡ data files in correct order (paperformat before action) â
â â¡ assets bundle = web.report_assets_common â
â â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
Post-Generation Testing
# 1. Update module
python -m odoo -c conf/project.conf -d database -u module --stop-after-init
# 2. Verify wkhtmltopdf in server log
# Look for: "Will use the Wkhtmltopdf binary at..."
# 3. Generate test PDF
# Navigate to record > Print menu > Select report
# 4. Verify PDF content
# - All text displays correctly (especially non-Latin)
# - Currency symbols correct
# - Layout matches design
# - Page breaks work correctly
File Locations Reference
Core Template Locations
odoo/addons/mail/
âââ models/
â âââ mail_template.py # Main model
â âââ mail_render_mixin.py # Rendering engine
âââ data/
â âââ mail_template_data.xml # Base templates
âââ views/
âââ mail_template_views.xml # UI views
odoo/addons/sale/
âââ data/
âââ mail_template_data.xml # Sales templates
odoo/addons/purchase/
âââ data/
âââ mail_template_data.xml # Purchase templates
odoo/addons/account/
âââ data/
âââ mail_template_data.xml # Accounting templates
Related Documentation
| Document | Path | Purpose |
|---|---|---|
| Email Research | C:\odoo\researches\ODOO_EMAIL_TEMPLATES_COMPLETE_RESEARCH.md |
Full research documentation |
| Odoo 17 CLAUDE.md | odoo17\CLAUDE.md |
Development commands |
| Design System | odoo17\DESIGN_SYSTEM_RULES.md |
Theme styling rules |
Changelog
v2.0.0 – Major Enhancement Release (January 2026)
Based on lessons learned from sadad_invoice_report development.
NEW SECTIONS:
-
wkhtmltopdf Setup & Configuration
- Installation commands (Windows, Linux, macOS)
- Odoo
bin_pathconfiguration (MANDATORY) - Common errors and solutions
- Offline mode limitations explained
-
Arabic/RTL & Multilingual Reports
- CRITICAL: UTF-8 encoding requirements
web.html_container+web.external_layoutpattern- Wrong patterns that cause encoding corruption
- Bilingual label patterns (side-by-side, stacked)
- RTL text alignment
-
Paper Format Configuration
- Custom paper format template
- Linking to report actions
- Standard formats reference (A4, Letter, Legal)
-
Report SCSS Styling
- Correct asset bundle (
web.report_assets_common) - Google Fonts pitfall (semicolons break SCSS)
- Why external fonts fail in wkhtmltopdf
- Recommended font stack
- Color scheme patterns
- Print-specific styles
- Correct asset bundle (
-
Debug Report Workflow
- Systematic 5-step diagnosis
- Issue-specific debugging guides
- Asset cache clearing
-
Bilingual Invoice Template
- Complete working example based on sadad_invoice
- Paper format, report action, templates included
- Proven UTF-8/Arabic support
-
Report Validation Checklist
- 6-category pre-flight checks
- Post-generation testing steps
ISSUES PREVENTED:
| Issue | Time Saved |
|---|---|
| Arabic text encoding corruption | 2+ hours |
| wkhtmltopdf configuration | 30 min |
| Google Fonts/external resources | 1 hour |
| SCSS semicolon parsing | 1 hour |
v1.0.0 – Initial Release
- Email template patterns (50+)
- QWeb report patterns (30+)
- Version decision matrix (Odoo 14-19)
- Commands reference
- Validation rules
- Module-specific templates
QR Code & Barcode in Reports
ZATCA/Saudi e-Invoice QR Code (Legally Required)
Saudi Arabia’s ZATCA requires a QR code on all B2C invoices. Use Odoo’s built-in barcode API:
<!-- Built-in Odoo barcode route â no extra library needed -->
<img t-att-src="'/report/barcode/QR/%s' % (o.l10n_sa_qr_code_str or '')"
style="max-width:100px; max-height:100px;"
t-if="o.l10n_sa_qr_code_str"/>
<!-- Generic QR code from any string -->
<img t-att-src="'/report/barcode/?type=QR&value=%s&width=150&height=150'
% (o.name or '')"
style="width:150px; height:150px;"/>
<!-- Product barcode (Code128) -->
<img t-att-src="'/report/barcode/Code128/%s' % (line.product_id.barcode or '')"
style="height:40px;"
t-if="line.product_id.barcode"/>
Available Barcode Types
| Type | Use Case |
|---|---|
QR |
QR codes for invoices, payments, URLs |
Code128 |
Product barcodes (GS1-128) |
EAN13 |
Retail product codes |
EAN8 |
Short retail product codes |
Code39 |
Alphanumeric codes |
ITF |
Shipping/logistics |
Python: Custom QR Code with qrcode Library
# In report model or controller
import qrcode
import base64
from io import BytesIO
def _get_qr_code_base64(self, data: str) -> str:
"""Generate QR code and return as base64 string for embedding in report."""
qr = qrcode.QRCode(version=1, box_size=10, border=4)
qr.add_data(data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffered = BytesIO()
img.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode()
<!-- Use in report template (pass via report values) -->
<img t-att-src="'data:image/png;base64,%s' % o._get_qr_code_base64(o.name)"
style="width:100px; height:100px;"/>
wkhtmltopdf Barcode Note
External image URLs (e.g.,
https://...) will NOT render in wkhtmltopdf when using--disable-external-links. Always use Odoo’s internal/report/barcode/route which renders server-side.
Report Wizard (User-Configurable Reports)
When a report needs user input (date range, grouping, language selection), use a TransientModel wizard:
1. Wizard Model
# wizard/report_wizard.py
from odoo import api, fields, models
class MyReportWizard(models.TransientModel):
_name = 'my.report.wizard'
_description = 'My Report Wizard'
date_from = fields.Date(
string='From Date',
required=True,
default=fields.Date.context_today,
)
date_to = fields.Date(
string='To Date',
required=True,
default=fields.Date.context_today,
)
partner_ids = fields.Many2many(
'res.partner',
string='Partners',
help='Leave empty to include all partners',
)
report_type = fields.Selection([
('summary', 'Summary'),
('detailed', 'Detailed'),
], string='Report Type', default='summary', required=True)
def action_print_report(self):
"""Generate and return the report action."""
self.ensure_one()
data = {
'form': {
'date_from': str(self.date_from),
'date_to': str(self.date_to),
'partner_ids': self.partner_ids.ids,
'report_type': self.report_type,
}
}
return self.env.ref('my_module.action_my_report').report_action(
self, data=data
)
2. Wizard View
<!-- views/report_wizard_views.xml -->
<record id="view_my_report_wizard_form" model="ir.ui.view">
<field name="name">my.report.wizard.form</field>
<field name="model">my.report.wizard</field>
<field name="arch" type="xml">
<form string="Generate Report">
<group>
<group string="Date Range">
<field name="date_from"/>
<field name="date_to"/>
</group>
<group string="Filters">
<field name="partner_ids" widget="many2many_tags"/>
<field name="report_type"/>
</group>
</group>
<footer>
<button name="action_print_report" type="object"
string="Print Report" class="btn-primary"/>
<button string="Cancel" class="btn-secondary"
special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Menu action to open wizard -->
<record id="action_open_my_report_wizard" model="ir.actions.act_window">
<field name="name">My Report</field>
<field name="res_model">my.report.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
3. Report Template Receiving Wizard Data
<!-- reports/my_report.xml -->
<template id="report_my_report">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<!-- Access wizard data via 'data' context variable -->
<t t-set="date_from" t-value="data['form']['date_from']"/>
<t t-set="date_to" t-value="data['form']['date_to']"/>
<t t-set="report_type" t-value="data['form']['report_type']"/>
<t t-call="web.external_layout">
<div class="page">
<h2>
Report: <t t-esc="date_from"/> to <t t-esc="date_to"/>
</h2>
<!-- Conditional content based on wizard selection -->
<t t-if="report_type == 'detailed'">
<!-- Detailed view -->
</t>
<t t-else="">
<!-- Summary view -->
</t>
</div>
</t>
</t>
</t>
</template>
4. Report Action (for wizard)
<record id="action_my_report" model="ir.actions.report">
<field name="name">My Report</field>
<field name="model">my.report.wizard</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">my_module.report_my_report</field>
<field name="report_file">my_module.report_my_report</field>
<!-- Note: binding_model_id is NOT set for wizard-triggered reports -->
</record>
Manifest Entry for Wizard
'data': [
'security/ir.model.access.csv', # Add: access_my_report_wizard,...
'wizard/report_wizard_views.xml',
'reports/my_report.xml',
'views/menus.xml',
],
Odoo Report Plugin v2.0 TaqaTechno – Professional Email Templates & QWeb Reports Supports Odoo 14-19 | Arabic/RTL Ready