odoo-report

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Odoo Email Templates & QWeb Reports Skill (v2.0)

Odoo邮件模板与QWeb报告技能包(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.
适用于Odoo 14-19版本的邮件模板与QWeb报告创建、管理、调试及迁移的综合技能包。具备wkhtmltopdf配置、阿拉伯语/RTL支持、双语报告模式以及智能版本感知语法。

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

  • 支持版本:Odoo 14、15、16、17、18、19
  • 主适配版本:Odoo 17
  • 模板数据库:已分析400+模板
  • 模式库:50+邮件模板模式、30+QWeb报告模式
  • 核心模型
    mail.template
  • 渲染引擎: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                                 │
│                                                                               │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│                     ODOO EMAIL TEMPLATE ARCHITECTURE                          │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                               │
│  mail.template                                                               │
│  ├── 继承自: mail.render.mixin (渲染引擎)                          │
│  ├── 继承自: template.reset.mixin (重置功能 - Odoo 16+)                       │
│  │                                                                           │
│  ├── 头字段 (inline_template引擎):                                 │
│  │   • subject        • email_from       • email_to                          │
│  │   • email_cc       • reply_to         • partner_to                        │
│  │                                                                           │
│  ├── 内容字段 (QWeb引擎):                                           │
│  │   • body_html                                                             │
│  │                                                                           │
│  ├── 附件字段:                                                      │
│  │   • attachment_ids (静态附件)                                               │
│  │   • report_template (Odoo 14-16) / report_template_ids (Odoo 17+)        │
│  │   • report_name (动态文件名)                                        │
│  │                                                                           │
│  └── 配置项:                                                          │
│      • 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

mail.template.send_mail_batch(res_ids)
    ├─► _generate_template(res_ids, render_fields)
    │       │
    │       ├─► _classify_per_lang()        # 按语言分组
    │       │
    │       ├─► _render_field() 处理以下字段:
    │       │       • subject (inline_template)
    │       │       • body_html (qweb)
    │       │       • email_from, email_to 等
    │       │
    │       ├─► _generate_template_recipients()
    │       │
    │       ├─► _generate_template_attachments()
    │       │       • 静态附件
    │       │       • 报告PDF生成
    │       │
    │       └─► 返回渲染后的值字典
    ├─► 创建 mail.mail 记录
    ├─► 应用 email_layout_xmlid (若已设置)
    │       └─► ir.qweb._render(layout_xmlid, context)
    └─► 通过 mail_server_id 或默认服务器发送

Two Rendering Engines

两种渲染引擎

1. Inline Template Engine (Jinja2-like)

1. 内联模板引擎(类Jinja2)

Used for:
subject
,
email_from
,
email_to
,
email_cc
,
reply_to
,
partner_to
,
lang
,
scheduled_date
python
undefined
适用字段
subject
email_from
email_to
email_cc
reply_to
partner_to
lang
scheduled_date
python
undefined

Simple field access

简单字段访问

{{ object.name }} {{ object.partner_id.name }} {{ object.company_id.name }}
{{ object.name }} {{ object.partner_id.name }} {{ object.company_id.name }}

Method calls

方法调用

{{ object.get_portal_url() }} {{ object.email_formatted }}
{{ object.get_portal_url() }} {{ object.email_formatted }}

Conditional expressions (ternary-like)

条件表达式(类似三元运算符)

{{ object.state == 'draft' and 'Quotation' or 'Order' }}
{{ 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) }}
{{ (object.user_id.email_formatted or object.company_id.email_formatted or user.email_formatted) }}

Context access

上下文访问

{{ ctx.get('proforma') and 'Proforma' or '' }}
{{ ctx.get('proforma') and 'Proforma' or '' }}

String operations

字符串操作

{{ (object.name or '').replace('/', '-') }}
undefined
{{ (object.name or '').replace('/', '-') }}
undefined

2. QWeb Engine

2. QWeb引擎

Used for:
body_html
Output Tags:
xml
<!-- 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:
xml
<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:
xml
<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:
xml
<!-- 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>

适用字段
body_html
输出标签:
xml
<!-- 转义输出(安全) -->
<t t-out="object.name"/>
<t t-out="object.partner_id.name or 'Unknown'"/>

<!-- 使用辅助函数格式化 -->
<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')"/>
条件标签:
xml
<t t-if="object.state == 'draft'">
    这是草稿状态。
</t>
<t t-elif="object.state == 'sent'">
    已发送,待确认。
</t>
<t t-else="">
    已确认。
</t>
循环标签:
xml
<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>
属性标签:
xml
<!-- 动态属性 -->
<a t-att-href="object.get_portal_url()">查看</a>

<!-- 格式化属性(带插值) -->
<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

版本特性决策矩阵

FeatureOdoo 14Odoo 15Odoo 16Odoo 17Odoo 18Odoo 19
t-out
syntax
NYYYYY
t-esc
(legacy)
YYYYYY
render_engine='qweb'
NYYYYY
template_category
NNYYYY
report_template_ids
M2M
NNNYYY
Company branding colorsNNNNNY
email_primary_color
NNNNNY
email_secondary_color
NNNNNY
mail_notification_layout_with_responsible_signature
NNYYYY
Enhanced security in sandboxNNYYYY

特性Odoo 14Odoo 15Odoo 16Odoo 17Odoo 18Odoo 19
t-out
语法
t-esc
(旧版语法)
render_engine='qweb'
template_category
report_template_ids
多对多字段
公司品牌颜色
email_primary_color
email_secondary_color
mail_notification_layout_with_responsible_signature
沙箱增强安全机制

Email Layout Templates

邮件布局模板

Available Layouts

可用布局

LayoutWidthUse Case
mail.mail_notification_layout
900pxFull notifications with header/footer
mail.mail_notification_light
590pxSimple notifications
mail.mail_notification_layout_with_responsible_signature
900pxUses record's
user_id
signature
布局宽度适用场景
mail.mail_notification_layout
900px带页眉/页脚的完整通知模板
mail.mail_notification_light
590px简洁通知模板
mail.mail_notification_layout_with_responsible_signature
900px使用记录的
user_id
签名

Layout Context Variables

布局上下文变量

python
{
    # 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
}

python
{
    # 消息信息
    'message': mail.message,           # 包含正文、记录名称的消息对象
    'subtype': mail.message.subtype,   # 消息子类型

    # 显示控制
    'has_button_access': Boolean,      # 是否显示操作按钮
    'button_access': {                 # CTA按钮配置
        'url': String,
        'title': String
    },
    'subtitles': List[String],         # 页眉副标题

    # 记录信息
    'record': record,                  # 关联文档
    'record_name': String,             # 显示名称
    'model_description': String,       # 模型的可读名称

    # 追踪信息
    'tracking_values': [               # 字段变更记录
        (field_name, old_value, new_value),
    ],

    # 签名
    'email_add_signature': Boolean,    # 是否包含签名
    'signature': HTML,                 # 用户签名

    # 公司信息
    'company': res.company,            # 公司对象
    'website_url': String,             # 基础URL

    # 品牌配置 (Odoo 19+)
    'company.email_primary_color': String,    # 按钮文字颜色
    'company.email_secondary_color': String,  # 按钮背景色

    # 工具函数
    'is_html_empty': Function,         # 检查HTML是否为空
}

Commands Reference

命令参考

Template Creation Commands

模板创建命令

CommandDescription
/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
命令描述
/create-email-template
为任意模型创建新邮件模板
/create-qweb-report
创建新的QWeb PDF报告
/create-notification
创建带布局的通知模板
/create-digest-email
创建摘要/汇总邮件模板

Template Management Commands

模板管理命令

CommandDescription
/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
命令描述
/list-templates
列出指定模型或模块的所有模板
/analyze-template
分析现有模板的问题
/debug-template
调试模板渲染问题
/preview-template
生成模板输出预览

Migration Commands

迁移命令

CommandDescription
/migrate-template
Migrate template between Odoo versions
/fix-template
Fix common template issues
/validate-template
Validate template syntax and context
命令描述
/migrate-template
在不同Odoo版本间迁移模板
/fix-template
修复常见模板问题
/validate-template
验证模板语法和上下文

QWeb Report Commands

QWeb报告命令

CommandDescription
/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

命令描述
/create-report-action
创建带菜单/按钮的报告动作
/style-report
为QWeb报告添加CSS样式
/add-header-footer
为报告添加页眉/页脚

Template Patterns Library

模板模式库

Pattern 1: Basic Notification Email

模式1:基础通知邮件

xml
<?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>
xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data noupdate="1">
        <record id="email_template_basic_notification" model="mail.template">
            <field name="name">基础通知</field>
            <field name="model_id" ref="model_your_model"/>
            <field name="subject">{{ object.name }} - 通知</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>尊敬的<t t-out="object.partner_id.name"/></p>
    <p>这是关于<strong t-out="object.name"/>的通知。</p>
    <t t-if="object.description">
        <p t-out="object.description"/>
    </t>
    <p>此致<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

模式2:带报告附件的文档邮件

xml
<?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>
xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data noupdate="1">
        <record id="email_template_document" model="mail.template">
            <field name="name">发送文档</field>
            <field name="model_id" ref="model_your_model"/>
            <field name="subject">
                {{ object.state in ('draft', 'sent') and '报价单' or '订单' }} {{ 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+ 使用 report_template_ids -->
            <field name="report_template_ids" eval="[(4, ref('module.report_action_id'))]"/>
            <field name="report_name">{{ (object.name or '文档').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="'报价单' if object.state in ('draft', 'sent') else '订单'"/>
    <p>尊敬的<t t-out="object.partner_id.name"/></p>
    <p>请查收附件中的<t t-out="doc_name"/>
       <strong t-out="object.name"/>,金额为
       <strong t-out="format_amount(object.amount_total, object.currency_id)"/></p>
    <p>如有疑问,欢迎随时联系我们。</p>
</div>
            </field>
        </record>
    </data>
</odoo>

Pattern 3: Order Lines Table

模式3:订单行表格

xml
<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>
xml
<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;">产品</th>
            <th style="padding: 8px;">数量</th>
            <th style="padding: 8px; text-align: right;">价格</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;">总计:</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

模式4:CTA按钮

xml
<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>
xml
<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">
                在线查看
            </a>
        </td>
    </tr>
</table>

Pattern 5: Conditional Content Based on State

模式5:基于状态的条件内容

xml
<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>
xml
<t t-if="object.state == 'draft'">
    <p style="color: #856404; background-color: #fff3cd; padding: 10px; border-radius: 4px;">
        <strong>草稿:</strong> 此文档尚未确认。
    </p>
</t>
<t t-elif="object.state == 'sent'">
    <p style="color: #0c5460; background-color: #d1ecf1; padding: 10px; border-radius: 4px;">
        <strong>待确认:</strong> 请审核并确认。
    </p>
</t>
<t t-elif="object.state == 'done'">
    <p style="color: #155724; background-color: #d4edda; padding: 10px; border-radius: 4px;">
        <strong>已完成:</strong> 此文档已处理。
    </p>
</t>

Pattern 6: Payment Information Block

模式6:付款信息块

xml
<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>

xml
<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;">付款信息</h4>
        <t t-if="object.payment_reference">
            <p><strong>参考号:</strong> <t t-out="object.payment_reference"/></p>
        </t>
        <t t-if="object.partner_bank_id">
            <p><strong>银行账户:</strong> <t t-out="object.partner_bank_id.acc_number"/></p>
            <t t-if="object.partner_bank_id.bank_id">
                <p><strong>银行:</strong> <t t-out="object.partner_bank_id.bank_id.name"/></p>
            </t>
        </t>
        <t t-if="object.amount_residual">
            <p><strong>应付金额:</strong>
               <t t-out="format_amount(object.amount_residual, object.currency_id)"/>
            </p>
        </t>
    </div>
</t>

QWeb Report Patterns

QWeb报告模式

Pattern 1: Basic Report Structure

模式1:基础报告结构

xml
<?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>
xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <!-- 报告动作 -->
    <record id="action_report_document" model="ir.actions.report">
        <field name="name">文档报告</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">'文档 - %s' % object.name</field>
        <field name="binding_model_id" ref="model_your_model"/>
        <field name="binding_type">report</field>
    </record>

    <!-- 报告模板 -->
    <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>日期:</strong>
                                <span t-field="doc.date"/>
                            </div>
                            <div class="col-6 text-end">
                                <strong>参考号:</strong>
                                <span t-out="doc.reference"/>
                            </div>
                        </div>

                        <!-- 内容区域 -->

                    </div>
                </t>
            </t>
        </t>
    </template>
</odoo>

Pattern 2: Report with Table

模式2:带表格的报告

xml
<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>
xml
<table class="table table-sm o_main_table">
    <thead>
        <tr>
            <th class="text-start">描述</th>
            <th class="text-center">数量</th>
            <th class="text-end">单价</th>
            <th class="text-end">金额</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>

<!-- 总计区域 -->
<div class="row justify-content-end">
    <div class="col-4">
        <table class="table table-sm">
            <tr>
                <td><strong>小计</strong></td>
                <td class="text-end"><span t-field="doc.amount_untaxed"/></td>
            </tr>
            <tr>
                <td>税费</td>
                <td class="text-end"><span t-field="doc.amount_tax"/></td>
            </tr>
            <tr class="border-top">
                <td><strong>总计</strong></td>
                <td class="text-end"><span t-field="doc.amount_total"/></td>
            </tr>
        </table>
    </div>
</div>

Pattern 3: Page Break Control

模式3:分页控制

xml
<!-- 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>

xml
<!-- 在元素前强制分页 -->
<div style="page-break-before: always;">
    <h3>新页面内容</h3>
</div>

<!-- 禁止在元素内分页 -->
<div style="page-break-inside: avoid;">
    <table><!-- 需保持完整的表格 --></table>
</div>

<!-- 在元素后强制分页 -->
<div style="page-break-after: always;">
    <p>章节结束</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                                   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

创建任何模板前,必须验证以下内容:
┌─────────────────────────────────────────────────────────────────┐
│                 TEMPLATE VALIDATION CHECKLIST                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  1. 模型验证                                                     │
│     □ 目标Odoo版本中存在该模型                                    │
│     □ 模型包含必填字段(如partner_id等)                           │
│     □ 模型继承了mail.thread(若用于通知)                           │
│                                                                   │
│  2. 字段验证                                                     │
│     □ 所有{{ object.field }}引用均存在                            │
│     □ 所有t-out="object.field"引用均存在                          │
│     □ 关联字段有效(如object.partner_id.name)                    │
│                                                                   │
│  3. 语法验证                                                      │
│     □ QWeb标签已正确闭合                                          │
│     □ Jinja2表达式{{ }}配对平衡                          │
│     □ 未混合使用t-esc(Odoo 14)和t-out(Odoo 15+)语法         │
│                                                                   │
│  4. 版本兼容性                                                    │
│     □ 正确使用report_template(Odoo14-16)或report_template_ids(Odoo17+)          │
│     □ 正确使用template_category(Odoo16+)                               │
│     □ 正确使用公司品牌颜色(Odoo19+)                         │
│                                                                   │
│  5. 安全验证                                                      │
│     □ 无不安全的eval()或exec()调用                                 │
│     □ 无任意文件访问权限                                   │
│     □ 表达式符合沙箱安全要求                                   │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Error Recovery

错误排查

Common Errors and Solutions

常见错误及解决方案

ErrorCauseSolution
AttributeError: 'NoneType' has no attribute 'name'
Null field accessUse
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 helperEnsure
mail.render.mixin
is inherited
ValidationError: Invalid XML
Malformed QWebCheck XML structure, close all tags
NameError: name 'object' is not defined
Wrong rendering contextUse template's model context
错误原因解决方案
AttributeError: 'NoneType' has no attribute 'name'
访问了空字段使用
object.field_id.name or ''
QWebException: t-esc is deprecated
在Odoo15+中使用了旧语法
t-esc
替换为
t-out
KeyError: 'format_amount'
缺少上下文辅助函数确保继承了
mail.render.mixin
ValidationError: Invalid XML
QWeb格式错误检查XML结构,闭合所有标签
NameError: name 'object' is not defined
渲染上下文错误使用模板对应的模型上下文

Debug Template Rendering

调试模板渲染

python
undefined
python
undefined

In Odoo shell

在Odoo Shell中执行

template = env['mail.template'].browse(TEMPLATE_ID) record = env['your.model'].browse(RECORD_ID)
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])

---
rendered = template._render_field( 'body_html', [record.id], compute_lang=True ) print(rendered[record.id])

---

Module-Specific Templates

模块专属模板

Sales Module

销售模块

Template IDPurposeModel
email_template_edi_sale
Send quotation/ordersale.order
mail_template_sale_confirmation
Order confirmationsale.order
mail_template_sale_payment_executed
Payment receivedsale.order
模板ID用途模型
email_template_edi_sale
发送报价单/订单sale.order
mail_template_sale_confirmation
订单确认sale.order
mail_template_sale_payment_executed
收款通知sale.order

Purchase Module

采购模块

Template IDPurposeModel
email_template_edi_purchase
Send RFQpurchase.order
email_template_edi_purchase_done
Send POpurchase.order
email_template_edi_purchase_reminder
Delivery reminderpurchase.order
模板ID用途模型
email_template_edi_purchase
发送询价单purchase.order
email_template_edi_purchase_done
发送采购订单purchase.order
email_template_edi_purchase_reminder
交货提醒purchase.order

Accounting Module

会计模块

Template IDPurposeModel
email_template_edi_invoice
Send invoiceaccount.move
email_template_edi_credit_note
Send credit noteaccount.move
mail_template_data_payment_receipt
Payment receiptaccount.payment
模板ID用途模型
email_template_edi_invoice
发送发票account.move
email_template_edi_credit_note
发送贷项通知单account.move
mail_template_data_payment_receipt
付款收据account.payment

HR Recruitment

HR招聘模块

Template IDPurposeModel
email_template_data_applicant_employee
Applicant to employeehr.employee
email_template_data_applicant_congratulations
Congratulationshr.applicant
email_template_data_applicant_refuse
Refusal noticehr.applicant

模板ID用途模型
email_template_data_applicant_employee
应聘者转员工通知hr.employee
email_template_data_applicant_congratulations
录用通知hr.applicant
email_template_data_applicant_refuse
拒信通知hr.applicant

Best Practices

最佳实践

1. Always Use Fallbacks

1. 始终使用回退值

xml
<!-- 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"/>
xml
<!-- 推荐写法 -->
<t t-out="object.partner_id.name or '尊贵客户'"/>

<!-- 不推荐写法 - 若partner_id为空会报错 -->
<t t-out="object.partner_id.name"/>

2. Use Format Helpers

2. 使用格式化辅助函数

xml
<!-- 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"/>
xml
<!-- 推荐写法 - 格式统一 -->
<t t-out="format_amount(object.amount_total, object.currency_id)"/>
<t t-out="format_date(object.date_order)"/>

<!-- 不推荐写法 - 手动格式化 -->
<t t-out="'$%.2f' % object.amount_total"/>

3. Respect Translations

3. 支持多语言翻译

xml
<!-- Good - translatable -->
<p>Dear <t t-out="object.partner_id.name"/>,</p>

<!-- Bad - hardcoded in template, use data records instead -->
xml
<!-- 推荐写法 - 可翻译 -->
<p>尊敬的<t t-out="object.partner_id.name"/></p>

<!-- 不推荐写法 - 模板中硬编码,应使用数据记录替代 -->

4. Use Layouts for Consistency

4. 使用布局保证一致性

xml
<!-- Good - uses company branding -->
<field name="email_layout_xmlid">mail.mail_notification_layout</field>

<!-- Avoid - custom inline styling for every email -->
xml
<!-- 推荐写法 - 使用公司品牌布局 -->
<field name="email_layout_xmlid">mail.mail_notification_layout</field>

<!-- 不推荐写法 - 每个邮件都自定义内联样式 -->

5. Handle Empty HTML

5. 处理空HTML内容

xml
<t t-if="not is_html_empty(object.description)">
    <div t-out="object.description"/>
</t>
xml
<t t-if="not is_html_empty(object.description)">
    <div t-out="object.description"/>
</t>

6. Version-Specific Syntax

6. 版本专属语法

xml
<!-- Odoo 15+ -->
<t t-out="value"/>

<!-- Odoo 14 only -->
<t t-esc="value"/>
xml
<!-- Odoo15+ -->
<t t-out="value"/>

<!-- 仅Odoo14 -->
<t t-esc="value"/>

7. Report Attachment Handling

7. 报告附件处理

xml
<!-- 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'))]"/>
xml
<!-- Odoo14-16: 单个报告 -->
<field name="report_template" ref="module.report_action"/>

<!-- Odoo17+: 多个报告 -->
<field name="report_template_ids" eval="[(4, ref('module.report_action'))]"/>

8. Dynamic Filenames

8. 动态文件名

python
undefined
python
undefined

Safe filename generation

安全的文件名生成

<field name="report_name">{{ (object.name or 'Document').replace('/', '-') }}</field>

---
<field name="report_name">{{ (object.name or '文档').replace('/', '-') }}</field>

---

wkhtmltopdf Setup & Configuration

wkhtmltopdf安装与配置

⚠️ CRITICAL: wkhtmltopdf is REQUIRED for PDF Reports

⚠️ 重要提示:PDF报告必须安装wkhtmltopdf

Odoo uses wkhtmltopdf to convert QWeb HTML to PDF. Without proper configuration, PDF generation will fail.
Odoo使用wkhtmltopdf将QWeb HTML转换为PDF。配置不当会导致PDF生成失败。

Installation

安装命令

bash
undefined
bash
undefined

Windows

Windows

winget install wkhtmltopdf.wkhtmltox
winget install wkhtmltopdf.wkhtmltox

Ubuntu/Debian

Ubuntu/Debian

sudo apt-get install wkhtmltopdf
sudo apt-get install wkhtmltopdf

macOS

macOS

brew install wkhtmltopdf
undefined
brew install wkhtmltopdf
undefined

Odoo Configuration (MANDATORY)

Odoo配置(必须)

Add to your
odoo.conf
:
ini
[options]
odoo.conf
中添加:
ini
[options]

Windows

Windows

bin_path = C:\Program Files\wkhtmltopdf\bin
bin_path = C:\Program Files\wkhtmltopdf\bin

Linux

Linux

bin_path = /usr/local/bin
bin_path = /usr/local/bin

macOS (Homebrew)

macOS (Homebrew)

bin_path = /opt/homebrew/bin
undefined
bin_path = /opt/homebrew/bin
undefined

Verification

验证配置

After server restart, check logs for:
Will use the Wkhtmltopdf binary at C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe
重启服务器后,检查日志是否包含:
Will use the Wkhtmltopdf binary at C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe

Common Errors

常见错误

ErrorCauseSolution
Unable to find Wkhtmltopdf
bin_path not configuredAdd bin_path to odoo.conf
PDF generation timeout
Network resources requestedRemove external URLs (fonts, images)
Blank PDF generated
CSS/SCSS errorsCheck browser console, validate SCSS
Exit with code 1
HTML syntax errorValidate QWeb template XML
错误原因解决方案
Unable to find Wkhtmltopdf
未配置bin_path在odoo.conf中添加bin_path
PDF generation timeout
请求了网络资源移除外部URL(字体、图片)
Blank PDF generated
CSS/SCSS错误检查浏览器控制台,验证SCSS语法
Exit with code 1
HTML语法错误验证QWeb模板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_layout
    for fonts
  • ✅ Embed images as base64 or use Odoo attachments

wkhtmltopdf运行时无网络访问权限,这意味着:
  • ❌ Google Fonts CDN无法加载
  • ❌ 外部图片无法渲染
  • ❌ 外部CSS无法应用
  • ✅ 使用
    web.external_layout
    获取字体
  • ✅ 将图片嵌入为base64或使用Odoo附件

Arabic/RTL & Multilingual Reports

阿拉伯语/RTL与多语言报告

⚠️ CRITICAL: UTF-8 Encoding Requirement

⚠️ 重要提示:必须使用UTF-8编码

Arabic text displaying as
ÙØ§ØªÙˆØ±Ø©
instead of
فاتورة
indicates UTF-8 → Latin-1 encoding corruption.
阿拉伯文本显示为
ÙØ§ØªÙˆØ±Ø©
而非
فاتورة
,表示UTF-8到Latin-1的编码损坏。

MANDATORY Template Wrapper

强制模板结构

ALWAYS use this structure for non-Latin text support:
xml
<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>
始终使用以下结构支持非拉丁语系文本
xml
<template id="report_document">
    <t t-call="web.html_container">        <!-- ✅ 提供UTF-8元标签 -->
        <t t-foreach="docs" t-as="o">
            <t t-call="web.external_layout"> <!-- ✅ 加载正确字体 -->
                <div class="page">
                    <!-- 内容区域 -->
                </div>
            </t>
        </t>
    </t>
</template>

❌ WRONG Patterns (Will Cause Encoding Issues)

❌ 错误写法(会导致编码问题)

xml
<!-- 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>
xml
<!-- 错误:自定义HTML无正确编码 -->
<template id="report_document">
    <html>
        <head><title>Report</title></head>
        <body>
            فاتورة  <!-- 会显示为ÙØ§ØªÙˆØ±Ø© -->
        </body>
    </html>
</template>

<!-- 错误:缺少web.html_container -->
<template id="report_document">
    <t t-foreach="docs" t-as="o">
        <t t-call="web.external_layout">
            <!-- 缺少外层容器! -->
        </t>
    </t>
</template>

Bilingual Label Pattern

双语标签模式

xml
<!-- 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>
xml
<!-- 并排显示:英文 | 阿拉伯语 -->
<th style="background: #1a5276; color: white; padding: 12px;">
    Date | التاريخ
</th>

<!-- 堆叠显示:阿拉伯语在上,英语在下 -->
<th style="background: #1a5276; color: white; padding: 12px;">
    <div>التاريخ</div>
    <div style="font-size: 10px; font-weight: normal;">Date</div>
</th>

RTL Text Alignment

RTL文本对齐

xml
<!-- 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>
xml
<!-- 强制阿拉伯语段落使用RTL -->
<div style="direction: rtl; text-align: right;">
    يرجى إرسال حوالاتكم على الحساب المذكور أعلاه
</div>

<!-- 混合内容:使用CSS类 -->
<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_container
    wrapper

$
显示为
或类似:
  • 原因:同样是UTF-8编码问题
  • 解决方案:使用
    web.html_container
    外层包裹

Paper Format Configuration

纸张格式配置

Custom Paper Format Template

自定义纸张格式模板

xml
<?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>
xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="paperformat_custom" model="report.paperformat">
        <field name="name">自定义发票格式</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

关联纸张格式与报告

xml
<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>
xml
<record id="action_report_invoice" model="ir.actions.report">
    <field name="name">自定义发票</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"/>
    <!-- 其他字段... -->
</record>

Standard Paper Formats

标准纸张格式

FormatDimensionsUse Case
A4
210 × 297 mmStandard international
Letter
216 × 279 mmUS standard
Legal
216 × 356 mmLegal documents
A5
148 × 210 mmSmall documents
Custom
Set page_width/page_heightSpecial sizes
格式尺寸适用场景
A4
210 × 297 mm国际标准
Letter
216 × 279 mm美国标准
Legal
216 × 356 mm法律文档
A5
148 × 210 mm小型文档
Custom
设置page_width/page_height特殊尺寸

Orientation Options

方向选项

  • Portrait
    - Vertical (default)
  • Landscape
    - Horizontal (wide tables, charts)

  • Portrait
    - 纵向(默认)
  • Landscape
    - 横向(宽表格、图表)

Report SCSS Styling

报告SCSS样式

Correct Asset Bundle

正确的资源包配置

python
undefined
python
undefined

manifest.py

manifest.py

{ 'assets': { 'web.report_assets_common': [ 'module/static/src/scss/report_styles.scss', ], }, }
undefined
{ 'assets': { 'web.report_assets_common': [ 'module/static/src/scss/report_styles.scss', ], }, }
undefined

⚠️ CRITICAL: Google Fonts Pitfall

⚠️ 重要提示:Google Fonts陷阱

scss
// ❌ 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
scss
// ❌ 错误写法 - URL中的分号会导致SCSS解析失败
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');

// ✅ 正确写法 - 使用权重范围语法
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300..700&display=swap');

// ✅ 最佳方案 - 不使用Google Fonts(wkhtmltopdf离线!)
// 依赖web.external_layout提供的系统字体

Why External Fonts Fail

外部字体失效原因

  1. wkhtmltopdf runs offline (no network access)
  2. Google Fonts CDN requests timeout
  3. Result: Fallback to system fonts
  1. wkhtmltopdf离线运行(无网络访问权限)
  2. Google Fonts CDN请求超时
  3. 结果:回退到系统字体

Recommended Font Stack

推荐字体栈

scss
// Safe fonts that work in PDF generation
$report-font-family: 'DejaVu Sans', 'Arial', 'Helvetica', sans-serif;

.page {
    font-family: $report-font-family;
}
scss
// 适用于PDF生成的安全字体
$report-font-family: 'DejaVu Sans', 'Arial', 'Helvetica', sans-serif;

.page {
    font-family: $report-font-family;
}

Color Scheme Pattern

配色方案模式

scss
// 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);
}
scss
// 统一定义颜色
$primary-color: #1a5276;      // 深蓝色
$secondary-color: #d5dbdb;    // 浅灰色
$accent-color: #f39c12;       // 橙色/金色
$border-color: #bdc3c7;
$text-dark: #2c3e50;
$text-muted: #7f8c8d;

// 表格表头
.table-header, thead tr {
    background-color: $primary-color;
    color: white;
}

// 标签单元格
.label-cell {
    background-color: $secondary-color;
    color: $primary-color;
    font-weight: bold;
}

// 交替行颜色
tbody tr:nth-child(even) {
    background-color: rgba($secondary-color, 0.3);
}

Print-Specific Styles

打印专属样式

scss
@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; }
}

scss
@media print {
    // 确保颜色正常打印
    * {
        -webkit-print-color-adjust: exact !important;
        print-color-adjust: exact !important;
    }

    // 分页控制
    .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                             │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│                 REPORT DEBUG WORKFLOW                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  步骤1:检查基础设施                                    │
│  ─────────────────────────────                                   │
│  □ 是否已安装wkhtmltopdf? → 执行wkhtmltopdf --version               │
│  □ 是否已在odoo.conf中配置bin_path?                                        │
│  □ 配置变更后是否重启了服务器?                         │
│  □ 服务器日志是否显示wkhtmltopdf路径?                           │
│                                                                   │
│  步骤2:验证模板结构                             │
│  ────────────────────────────────────                            │
│  □ 是否使用web.html_container外层包裹?                              │
│  □ 是否使用web.external_layout?                                     │
│  □ 是否包含t-foreach docs循环?                                      │
│  □ 内容是否在div.page内部?                                    │
│                                                                   │
│  步骤3:检查报告动作                                     │
│  ───────────────────────────                                     │
│  □ report_name是否与模板ID匹配?                              │
│  □ report_file是否与模板ID匹配?                              │
│  □ binding_model_id是否正确?                                     │
│  □ report_type是否为'qweb-pdf'?                                     │
│                                                                   │
│  步骤4:在Shell中测试渲染                                    │
│  ────────────────────────────                                    │
│  python odoo-bin shell -d DATABASE                               │
│  >>> report = env.ref('module.report_action')                    │
│  >>> pdf, _ = report._render_qweb_pdf([record_id])              │
│  >>> # 检查控制台错误                                  │
│                                                                   │
│  步骤5:浏览器缓存                                           │
│  ─────────────────────                                           │
│  □ 清除浏览器缓存(Ctrl+Shift+R)                           │
│  □ 必要时清除Odoo资源缓存                             │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

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)
编码问题(阿拉伯语、中文等):
症状:显示ÙØ§ØªÙˆØ±Ø©而非فاتورة
原因:缺少web.html_container
解决方案:添加<t t-call="web.html_container">作为最外层包裹
空白PDF:
症状:PDF生成但为空
原因:
  1. CSS语法错误 → 检查SCSS中URL的分号
  2. 模板缺失 → 验证report_name与模板ID匹配
  3. 无数据 → 检查docs变量是否包含记录
字体未渲染:
症状:PDF中字体错误
原因:使用了外部字体URL(wkhtmltopdf离线)
解决方案:仅使用web.external_layout或系统字体
超时/卡顿:
症状:PDF生成卡顿或超时
原因:请求外部资源
解决方案:移除所有外部URL(CDN、外部图片)

Clear Asset Cache

清除资源缓存

python
undefined
python
undefined

Odoo shell

Odoo Shell中执行

env['ir.attachment'].search([ ('url', 'like', '/web/assets/') ]).unlink() env.cr.commit()

---
env['ir.attachment'].search([ ('url', 'like', '/web/assets/') ]).unlink() env.cr.commit()

---

Bilingual Invoice Template (Complete Example)

双语发票模板(完整示例)

Based on proven sadad_invoice implementation:
xml
<?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>

基于成熟的sadad_invoice实现:
xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <!-- 纸张格式 -->
    <record id="paperformat_bilingual_invoice" model="report.paperformat">
        <field name="name">双语发票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>

    <!-- 报告动作 -->
    <record id="action_report_bilingual_invoice" model="ir.actions.report">
        <field name="name">双语发票</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>

    <!-- 关键:正确的外层结构以支持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>

    <!-- 内容模板 -->
    <template id="report_bilingual_invoice_content">
        <div class="page" style="font-family: Arial, sans-serif; font-size: 12px;">

            <!-- 双语页眉 -->
            <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>

            <!-- 发票信息网格 -->
            <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>

            <!-- 发票行表格 -->
            <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>

            <!-- 总计区域 -->
            <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>

            <!-- 付款条款(阿拉伯语使用RTL) -->
            <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                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│              QWEB REPORT VALIDATION CHECKLIST                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  1. 基础设施                                               │
│     □ wkhtmltopdf已安装且在PATH中                         │
│     □ odoo.conf中已配置bin_path                          │
│     □ 配置变更后已重启服务器                       │
│     □ 服务器日志已确认wkhtmltopdf路径                      │
│                                                                   │
│  2. 模板结构(非拉丁语系文本)                     │
│     □ web.html_container作为最外层包裹                   │
│     □ 包含t-foreach docs循环                                        │
│     □ 使用web.external_layout获取公司页眉/字体              │
│     □ 内容在div.page内部                                    │
│                                                                   │
│  3. 编码验证                                        │
│     □ 未使用自定义<html>标签                               │
│     □ 未通过CDN引入外部字体                      │
│     □ 阿拉伯语/中文文本渲染正确                      │
│     □ 货币符号显示正确(无$Â)                │
│                                                                   │
│  4. SCSS/CSS验证                                          │
│     □ 使用web.report_assets_common资源包                      │
│     □ @import URL中无分号                             │
│     □ 无外部CDN资源                                  │
│     □ 仅使用系统字体(DejaVu、Arial等)                   │
│                                                                   │
│  5. 报告动作字段                                         │
│     □ report_name = 'module.template_id'                        │
│     □ report_file = 'module.template_id'                        │
│     □ binding_model_id = ref('module.model_xxx')                │
│     □ report_type = 'qweb-pdf'                                  │
│     □ 已关联paperformat_id(若自定义)                         │
│                                                                   │
│  6. 清单文件                                                │
│     □ depends包含目标模块(account、sale等)      │
│     □ 数据文件顺序正确(纸张格式在报告动作之前)   │
│     □ 资源包为web.report_assets_common                  │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Post-Generation Testing

生成后测试

bash
undefined
bash
undefined

1. Update module

1. 更新模块

python -m odoo -c conf/project.conf -d database -u module --stop-after-init
python -m odoo -c conf/project.conf -d database -u module --stop-after-init

2. Verify wkhtmltopdf in server log

2. 检查服务器日志中的wkhtmltopdf信息

Look for: "Will use the Wkhtmltopdf binary at..."

查找:"Will use the Wkhtmltopdf binary at..."

3. Generate test PDF

3. 生成测试PDF

Navigate to record > Print menu > Select report

导航到记录 > 打印菜单 > 选择报告

4. Verify PDF content

4. 验证PDF内容

- 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

odoo/addons/mail/
├── models/
│   ├── mail_template.py           # 主模型
│   └── mail_render_mixin.py       # 渲染引擎
├── data/
│   └── mail_template_data.xml     # 基础模板
└── views/
    └── mail_template_views.xml    # UI视图

odoo/addons/sale/
└── data/
    └── mail_template_data.xml     # 销售模块模板

odoo/addons/purchase/
└── data/
    └── mail_template_data.xml     # 采购模块模板

odoo/addons/account/
└── data/
    └── mail_template_data.xml     # 会计模块模板

Related Documentation

相关文档

DocumentPathPurpose
Email Research
C:\TQ-WorkSpace\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

文档路径用途
邮件模板研究
C:\TQ-WorkSpace\odoo\researches\ODOO_EMAIL_TEMPLATES_COMPLETE_RESEARCH.md
完整研究文档
Odoo17开发指南
odoo17\CLAUDE.md
开发命令参考
设计系统规则
odoo17\DESIGN_SYSTEM_RULES.md
主题样式规则

Changelog

更新日志

v2.0.0 - Major Enhancement Release (January 2026)

v2.0.0 - 重大增强版本(2026年1月)

Based on lessons learned from sadad_invoice_report development.
NEW SECTIONS:
  • wkhtmltopdf Setup & Configuration
    • Installation commands (Windows, Linux, macOS)
    • Odoo
      bin_path
      configuration (MANDATORY)
    • Common errors and solutions
    • Offline mode limitations explained
  • Arabic/RTL & Multilingual Reports
    • CRITICAL: UTF-8 encoding requirements
    • web.html_container
      +
      web.external_layout
      pattern
    • 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
  • 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:
IssueTime Saved
Arabic text encoding corruption2+ hours
wkhtmltopdf configuration30 min
Google Fonts/external resources1 hour
SCSS semicolon parsing1 hour
基于sadad_invoice_report开发经验优化。
新增章节:
  • wkhtmltopdf安装与配置
    • 多系统安装命令(Windows、Linux、macOS)
    • Odoo
      bin_path
      配置(必须)
    • 常见错误及解决方案
    • 离线模式限制说明
  • 阿拉伯语/RTL与多语言报告
    • 重要提示:UTF-8编码要求
    • web.html_container
      +
      web.external_layout
      标准模式
    • 导致编码损坏的错误写法
    • 双语标签模式(并排、堆叠)
    • RTL文本对齐方法
  • 纸张格式配置
    • 自定义纸张格式模板
    • 关联纸张格式与报告动作
    • 标准纸张格式参考(A4、Letter等)
  • 报告SCSS样式
    • 正确的资源包配置(
      web.report_assets_common
    • Google Fonts陷阱(分号解析问题)
    • 外部字体失效原因
    • 推荐字体栈
    • 配色方案模式
    • 打印专属样式
  • 报告调试流程
    • 5步系统化诊断步骤
    • 特定问题调试指南
    • 资源缓存清除方法
  • 双语发票模板
    • 基于sadad_invoice的完整可用示例
    • 包含纸张格式、报告动作、内容模板
    • 已验证UTF-8/阿拉伯语支持
  • 报告验证清单
    • 6大类预检检查
    • 生成后测试步骤
避免的问题:
问题节省时间
阿拉伯语文本编码损坏2+小时
wkhtmltopdf配置问题30分钟
Google Fonts/外部资源问题1小时
SCSS分号解析问题1小时

v1.0.0 - Initial Release

v1.0.0 - 初始版本

  • Email template patterns (50+)
  • QWeb report patterns (30+)
  • Version decision matrix (Odoo 14-19)
  • Commands reference
  • Validation rules
  • Module-specific templates


  • 50+邮件模板模式
  • 30+QWeb报告模式
  • Odoo14-19版本决策矩阵
  • 命令参考
  • 验证规则
  • 模块专属模板


QR Code & Barcode in Reports

报告中的二维码与条形码

ZATCA/Saudi e-Invoice QR Code (Legally Required)

ZATCA/沙特电子发票二维码(法律要求)

Saudi Arabia's ZATCA requires a QR code on all B2C invoices. Use Odoo's built-in barcode API:
xml
<!-- 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&amp;value=%s&amp;width=150&amp;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"/>
沙特阿拉伯ZATCA要求所有B2C发票必须包含二维码。使用Odoo内置的条形码API:
xml
<!-- Odoo内置条形码路由 — 无需额外库 -->
<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"/>

<!-- 基于任意字符串生成通用二维码 -->
<img t-att-src="'/report/barcode/?type=QR&amp;value=%s&amp;width=150&amp;height=150'
     % (o.name or '')"
     style="width:150px; height:150px;"/>

<!-- 产品条形码(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

支持的条形码类型

TypeUse 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
类型适用场景
QR
发票、付款、URL等二维码
Code128
产品条形码(GS1-128)
EAN13
零售产品编码
EAN8
短零售产品编码
Code39
字母数字编码
ITF
物流/运输编码

Python: Custom QR Code with qrcode Library

Python:使用qrcode库生成自定义二维码

python
undefined
python
undefined

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()

```xml
<!-- 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;"/>
import qrcode import base64 from io import BytesIO
def _get_qr_code_base64(self, data: str) -> str: """生成二维码并返回base64字符串,用于嵌入报告。""" 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()

```xml
<!-- 在报告模板中使用(通过报告值传递) -->
<img t-att-src="'data:image/png;base64,%s' % o._get_qr_code_base64(o.name)"
     style="width:100px; height:100px;"/>

wkhtmltopdf Barcode Note

wkhtmltopdf条形码注意事项

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.

当使用
--disable-external-links
时,外部图片URL(如
https://...
)无法在wkhtmltopdf中渲染。 始终使用Odoo内部的
/report/barcode/
路由,它在服务器端渲染。

Report Wizard (User-Configurable Reports)

报告向导(用户可配置报告)

When a report needs user input (date range, grouping, language selection), use a
TransientModel
wizard:
当报告需要用户输入(日期范围、分组、语言选择)时,使用
TransientModel
向导:

1. Wizard Model

1. 向导模型

python
undefined
python
undefined

wizard/report_wizard.py

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
    )
undefined
from odoo import api, fields, models
class MyReportWizard(models.TransientModel): _name = 'my.report.wizard' _description = '我的报告向导'
date_from = fields.Date(
    string='开始日期',
    required=True,
    default=fields.Date.context_today,
)
date_to = fields.Date(
    string='结束日期',
    required=True,
    default=fields.Date.context_today,
)
partner_ids = fields.Many2many(
    'res.partner',
    string='客户',
    help='留空则包含所有客户',
)
report_type = fields.Selection([
    ('summary', '摘要'),
    ('detailed', '明细'),
], string='报告类型', default='summary', required=True)

def action_print_report(self):
    """生成并返回报告动作。"""
    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
    )
undefined

2. Wizard View

2. 向导视图

xml
<!-- 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>
xml
<!-- 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="生成报告">
            <group>
                <group string="日期范围">
                    <field name="date_from"/>
                    <field name="date_to"/>
                </group>
                <group string="筛选条件">
                    <field name="partner_ids" widget="many2many_tags"/>
                    <field name="report_type"/>
                </group>
            </group>
            <footer>
                <button name="action_print_report" type="object"
                        string="打印报告" class="btn-primary"/>
                <button string="取消" class="btn-secondary"
                        special="cancel"/>
            </footer>
        </form>
    </field>
</record>

<!-- 打开向导的菜单动作 -->
<record id="action_open_my_report_wizard" model="ir.actions.act_window">
    <field name="name">我的报告</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

3. 接收向导数据的报告模板

xml
<!-- 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>
xml
<!-- reports/my_report.xml -->
<template id="report_my_report">
    <t t-call="web.html_container">
        <t t-foreach="docs" t-as="o">
            <!-- 通过'data'上下文变量访问向导数据 -->
            <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>
                        报告:<t t-esc="date_from"/><t t-esc="date_to"/>
                    </h2>
                    <!-- 根据向导选择显示条件内容 -->
                    <t t-if="report_type == 'detailed'">
                        <!-- 明细视图 -->
                    </t>
                    <t t-else="">
                        <!-- 摘要视图 -->
                    </t>
                </div>
            </t>
        </t>
    </t>
</template>

4. Report Action (for wizard)

4. 报告动作(适配向导)

xml
<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>
xml
<record id="action_my_report" model="ir.actions.report">
    <field name="name">我的报告</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>
    <!-- 注意:向导触发的报告不设置binding_model_id -->
</record>

Manifest Entry for Wizard

向导的清单配置

python
'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
python
'data': [
    'security/ir.model.access.csv',  # 添加:access_my_report_wizard,...
    'wizard/report_wizard_views.xml',
    'reports/my_report.xml',
    'views/menus.xml',
],

Odoo Report Plugin v2.0 TaqaTechno - 专业邮件模板与QWeb报告 支持Odoo14-19 | 阿拉伯语/RTL就绪