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支持、双语报告模式以及智能版本感知语法。
- 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:
- Rendering Engines: inline_template (Jinja2), QWeb
- 支持版本:Odoo 14、15、16、17、18、19
- 主适配版本:Odoo 17
- 模板数据库:已分析400+模板
- 模式库:50+邮件模板模式、30+QWeb报告模式
- 核心模型:
- 渲染引擎:inline_template(Jinja2)、QWeb
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 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
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)
Simple field access
简单字段访问
{{ object.name }}
{{ object.partner_id.name }}
{{ object.company_id.name }}
{{ object.name }}
{{ object.partner_id.name }}
{{ object.company_id.name }}
{{ 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) }}
{{ ctx.get('proforma') and 'Proforma' or '' }}
{{ ctx.get('proforma') and 'Proforma' or '' }}
{{ (object.name or '').replace('/', '-') }}
{{ (object.name or '').replace('/', '-') }}
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>
输出标签:
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
版本特性决策矩阵
| Feature | Odoo 14 | Odoo 15 | Odoo 16 | Odoo 17 | Odoo 18 | Odoo 19 |
|---|
| syntax | N | Y | Y | Y | Y | Y |
| (legacy) | Y | Y | Y | Y | Y | Y |
| N | Y | Y | Y | Y | Y |
| N | N | Y | Y | Y | Y |
| M2M | N | N | N | Y | Y | Y |
| Company branding colors | N | N | N | N | N | Y |
| N | N | N | N | N | Y |
| N | N | N | N | N | Y |
mail_notification_layout_with_responsible_signature
| N | N | Y | Y | Y | Y |
| Enhanced security in sandbox | N | N | Y | Y | Y | Y |
| 特性 | Odoo 14 | Odoo 15 | Odoo 16 | Odoo 17 | Odoo 18 | Odoo 19 |
|---|
| 语法 | 否 | 是 | 是 | 是 | 是 | 是 |
| (旧版语法) | 是 | 是 | 是 | 是 | 是 | 是 |
| 否 | 是 | 是 | 是 | 是 | 是 |
| 否 | 否 | 是 | 是 | 是 | 是 |
| 多对多字段 | 否 | 否 | 否 | 是 | 是 | 是 |
| 公司品牌颜色 | 否 | 否 | 否 | 否 | 否 | 是 |
| 否 | 否 | 否 | 否 | 否 | 是 |
| 否 | 否 | 否 | 否 | 否 | 是 |
mail_notification_layout_with_responsible_signature
| 否 | 否 | 是 | 是 | 是 | 是 |
| 沙箱增强安全机制 | 否 | 否 | 是 | 是 | 是 | 是 |
Email Layout Templates
邮件布局模板
| Layout | Width | Use Case |
|---|
mail.mail_notification_layout
| 900px | Full notifications with header/footer |
mail.mail_notification_light
| 590px | Simple notifications |
mail.mail_notification_layout_with_responsible_signature
| 900px | Uses record's signature |
| 布局 | 宽度 | 适用场景 |
|---|
mail.mail_notification_layout
| 900px | 带页眉/页脚的完整通知模板 |
mail.mail_notification_light
| 590px | 简洁通知模板 |
mail.mail_notification_layout_with_responsible_signature
| 900px | 使用记录的签名 |
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是否为空
}
Template Creation Commands
模板创建命令
| Command | Description |
|---|
| Create a new email template for any model |
| Create a new QWeb PDF report |
| Create a notification template with layout |
| Create a digest/summary email template |
| 命令 | 描述 |
|---|
| 为任意模型创建新邮件模板 |
| 创建新的QWeb PDF报告 |
| 创建带布局的通知模板 |
| 创建摘要/汇总邮件模板 |
Template Management Commands
模板管理命令
| Command | Description |
|---|
| List all templates for a model or module |
| Analyze an existing template for issues |
| Debug template rendering issues |
| Generate preview of template output |
| 命令 | 描述 |
|---|
| 列出指定模型或模块的所有模板 |
| 分析现有模板的问题 |
| 调试模板渲染问题 |
| 生成模板输出预览 |
| Command | Description |
|---|
| Migrate template between Odoo versions |
| Fix common template issues |
| Validate template syntax and context |
| 命令 | 描述 |
|---|
| 在不同Odoo版本间迁移模板 |
| 修复常见模板问题 |
| 验证模板语法和上下文 |
QWeb Report Commands
QWeb报告命令
| Command | Description |
|---|
| Create report action with menu/button |
| Add CSS styling to QWeb report |
| Add header/footer to report |
| 命令 | 描述 |
|---|
| 创建带菜单/按钮的报告动作 |
| 为QWeb报告添加CSS样式 |
| 为报告添加页眉/页脚 |
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>
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()调用 │
│ □ 无任意文件访问权限 │
│ □ 表达式符合沙箱安全要求 │
│ │
└─────────────────────────────────────────────────────────────────┘
Common Errors and Solutions
常见错误及解决方案
| Error | Cause | Solution |
|---|
AttributeError: 'NoneType' has no attribute 'name'
| Null field access | Use object.field_id.name or ''
|
QWebException: t-esc is deprecated
| Old syntax in Odoo 15+ | Replace with |
KeyError: 'format_amount'
| Missing context helper | Ensure is inherited |
ValidationError: Invalid XML
| Malformed QWeb | Check XML structure, close all tags |
NameError: name 'object' is not defined
| Wrong rendering context | Use template's model context |
| 错误 | 原因 | 解决方案 |
|---|
AttributeError: 'NoneType' has no attribute 'name'
| 访问了空字段 | 使用object.field_id.name or ''
|
QWebException: t-esc is deprecated
| 在Odoo15+中使用了旧语法 | 将替换为 |
KeyError: 'format_amount'
| 缺少上下文辅助函数 | 确保继承了 |
ValidationError: Invalid XML
| QWeb格式错误 | 检查XML结构,闭合所有标签 |
NameError: name 'object' is not defined
| 渲染上下文错误 | 使用模板对应的模型上下文 |
Debug Template Rendering
调试模板渲染
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
模块专属模板
| Template ID | Purpose | Model |
|---|
| Send quotation/order | sale.order |
mail_template_sale_confirmation
| Order confirmation | sale.order |
mail_template_sale_payment_executed
| Payment received | sale.order |
| 模板ID | 用途 | 模型 |
|---|
| 发送报价单/订单 | sale.order |
mail_template_sale_confirmation
| 订单确认 | sale.order |
mail_template_sale_payment_executed
| 收款通知 | sale.order |
| Template ID | Purpose | Model |
|---|
email_template_edi_purchase
| Send RFQ | purchase.order |
email_template_edi_purchase_done
| Send PO | purchase.order |
email_template_edi_purchase_reminder
| Delivery reminder | purchase.order |
| 模板ID | 用途 | 模型 |
|---|
email_template_edi_purchase
| 发送询价单 | purchase.order |
email_template_edi_purchase_done
| 发送采购订单 | purchase.order |
email_template_edi_purchase_reminder
| 交货提醒 | purchase.order |
| Template ID | Purpose | Model |
|---|
email_template_edi_invoice
| Send invoice | account.move |
email_template_edi_credit_note
| Send credit note | account.move |
mail_template_data_payment_receipt
| Payment receipt | account.payment |
| 模板ID | 用途 | 模型 |
|---|
email_template_edi_invoice
| 发送发票 | account.move |
email_template_edi_credit_note
| 发送贷项通知单 | account.move |
mail_template_data_payment_receipt
| 付款收据 | account.payment |
| Template ID | Purpose | Model |
|---|
email_template_data_applicant_employee
| Applicant to employee | hr.employee |
email_template_data_applicant_congratulations
| Congratulations | hr.applicant |
email_template_data_applicant_refuse
| Refusal notice | hr.applicant |
| 模板ID | 用途 | 模型 |
|---|
email_template_data_applicant_employee
| 应聘者转员工通知 | hr.employee |
email_template_data_applicant_congratulations
| 录用通知 | hr.applicant |
email_template_data_applicant_refuse
| 拒信通知 | hr.applicant |
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. 动态文件名
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生成失败。
winget install wkhtmltopdf.wkhtmltox
winget install wkhtmltopdf.wkhtmltox
Ubuntu/Debian
Ubuntu/Debian
sudo apt-get install wkhtmltopdf
sudo apt-get install wkhtmltopdf
Odoo Configuration (MANDATORY)
Odoo配置(必须)
bin_path = C:\Program Files\wkhtmltopdf\bin
bin_path = C:\Program Files\wkhtmltopdf\bin
bin_path = /usr/local/bin
bin_path = /usr/local/bin
macOS (Homebrew)
macOS (Homebrew)
bin_path = /opt/homebrew/bin
bin_path = /opt/homebrew/bin
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
| Error | Cause | Solution |
|---|
Unable to find Wkhtmltopdf
| bin_path not configured | Add bin_path to odoo.conf |
| Network resources requested | Remove external URLs (fonts, images) |
| CSS/SCSS errors | Check browser console, validate SCSS |
| HTML syntax error | Validate QWeb template XML |
| 错误 | 原因 | 解决方案 |
|---|
Unable to find Wkhtmltopdf
| 未配置bin_path | 在odoo.conf中添加bin_path |
| 请求了网络资源 | 移除外部URL(字体、图片) |
| CSS/SCSS错误 | 检查浏览器控制台,验证SCSS语法 |
| 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 for fonts
- ✅ Embed images as base64 or use Odoo attachments
wkhtmltopdf运行时无网络访问权限,这意味着:
- ❌ Google Fonts CDN无法加载
- ❌ 外部图片无法渲染
- ❌ 外部CSS无法应用
- ✅ 使用获取字体
- ✅ 将图片嵌入为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 wrapper
- 原因:同样是UTF-8编码问题
- 解决方案:使用外层包裹
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
标准纸张格式
| Format | Dimensions | Use Case |
|---|
| 210 × 297 mm | Standard international |
| 216 × 279 mm | US standard |
| 216 × 356 mm | Legal documents |
| 148 × 210 mm | Small documents |
| Set page_width/page_height | Special sizes |
| 格式 | 尺寸 | 适用场景 |
|---|
| 210 × 297 mm | 国际标准 |
| 216 × 279 mm | 美国标准 |
| 216 × 356 mm | 法律文档 |
| 148 × 210 mm | 小型文档 |
| 设置page_width/page_height | 特殊尺寸 |
- - Vertical (default)
- - Horizontal (wide tables, charts)
Report SCSS Styling
报告SCSS样式
Correct Asset Bundle
正确的资源包配置
{
'assets': {
'web.report_assets_common': [
'module/static/src/scss/report_styles.scss',
],
},
}
{
'assets': {
'web.report_assets_common': [
'module/static/src/scss/report_styles.scss',
],
},
}
⚠️ 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
外部字体失效原因
- wkhtmltopdf runs offline (no network access)
- Google Fonts CDN requests timeout
- Result: Fallback to system fonts
- wkhtmltopdf离线运行(无网络访问权限)
- Google Fonts CDN请求超时
- 结果:回退到系统字体
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、外部图片)
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
生成后测试
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
相关文档
| Document | Path | Purpose |
|---|
| Email Research | C:\TQ-WorkSpace\odoo\researches\ODOO_EMAIL_TEMPLATES_COMPLETE_RESEARCH.md
| Full research documentation |
| Odoo 17 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\DESIGN_SYSTEM_RULES.md
| 主题样式规则 |
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 configuration (MANDATORY)
- Common errors and solutions
- Offline mode limitations explained
-
Arabic/RTL & Multilingual Reports
- CRITICAL: UTF-8 encoding requirements
- + 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 ()
- 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:
| Issue | Time Saved |
|---|
| Arabic text encoding corruption | 2+ hours |
| wkhtmltopdf configuration | 30 min |
| Google Fonts/external resources | 1 hour |
| SCSS semicolon parsing | 1 hour |
基于sadad_invoice_report开发经验优化。
新增章节:
-
wkhtmltopdf安装与配置
- 多系统安装命令(Windows、Linux、macOS)
- Odoo 配置(必须)
- 常见错误及解决方案
- 离线模式限制说明
-
阿拉伯语/RTL与多语言报告
- 重要提示:UTF-8编码要求
- + 标准模式
- 导致编码损坏的错误写法
- 双语标签模式(并排、堆叠)
- RTL文本对齐方法
-
纸张格式配置
- 自定义纸张格式模板
- 关联纸张格式与报告动作
- 标准纸张格式参考(A4、Letter等)
-
报告SCSS样式
- 正确的资源包配置()
- Google Fonts陷阱(分号解析问题)
- 外部字体失效原因
- 推荐字体栈
- 配色方案模式
- 打印专属样式
-
报告调试流程
- 5步系统化诊断步骤
- 特定问题调试指南
- 资源缓存清除方法
-
双语发票模板
- 基于sadad_invoice的完整可用示例
- 包含纸张格式、报告动作、内容模板
- 已验证UTF-8/阿拉伯语支持
-
报告验证清单
避免的问题:
| 问题 | 节省时间 |
|---|
| 阿拉伯语文本编码损坏 | 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&value=%s&width=150&height=150'
% (o.name or '')"
style="width:150px; height:150px;"/>
<!-- Product barcode (Code128) -->
<img t-att-src="'/report/barcode/Code128/%s' % (line.product_id.barcode or '')"
style="height:40px;"
t-if="line.product_id.barcode"/>
沙特阿拉伯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&value=%s&width=150&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
支持的条形码类型
| Type | Use Case |
|---|
| QR codes for invoices, payments, URLs |
| Product barcodes (GS1-128) |
| Retail product codes |
| Short retail product codes |
| Alphanumeric codes |
| Shipping/logistics |
| 类型 | 适用场景 |
|---|
| 发票、付款、URL等二维码 |
| 产品条形码(GS1-128) |
| 零售产品编码 |
| 短零售产品编码 |
| 字母数字编码 |
| 物流/运输编码 |
Python: Custom QR Code with qrcode Library
Python:使用qrcode库生成自定义二维码
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.,
) will NOT render in wkhtmltopdf when using
.
Always use Odoo's internal
route which renders server-side.
当使用
时,外部图片URL(如
)无法在wkhtmltopdf中渲染。
始终使用Odoo内部的
路由,它在服务器端渲染。
Report Wizard (User-Configurable Reports)
报告向导(用户可配置报告)
When a report needs user input (date range, grouping, language selection), use a
wizard:
当报告需要用户输入(日期范围、分组、语言选择)时,使用
向导:
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
)
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
)
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就绪