odoo-report
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOdoo 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 defaultmail.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: , , , , , , ,
subjectemail_fromemail_toemail_ccreply_topartner_tolangscheduled_datepython
undefined适用字段:、、、、、、、
subjectemail_fromemail_toemail_ccreply_topartner_tolangscheduled_datepython
undefinedSimple 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('/', '-') }}
undefined2. QWeb Engine
2. QWeb引擎
Used for:
body_htmlOutput 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
版本特性决策矩阵
| Feature | Odoo 14 | Odoo 15 | Odoo 16 | Odoo 17 | Odoo 18 | Odoo 19 |
|---|---|---|---|---|---|---|
| N | Y | Y | Y | Y | Y |
| Y | Y | Y | Y | Y | Y |
| N | Y | Y | Y | Y | Y |
| N | N | Y | Y | Y | Y |
| 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 |
| 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 |
|---|---|---|---|---|---|---|
| 否 | 是 | 是 | 是 | 是 | 是 |
| 是 | 是 | 是 | 是 | 是 | 是 |
| 否 | 是 | 是 | 是 | 是 | 是 |
| 否 | 否 | 是 | 是 | 是 | 是 |
| 否 | 否 | 否 | 是 | 是 | 是 |
| 公司品牌颜色 | 否 | 否 | 否 | 否 | 否 | 是 |
| 否 | 否 | 否 | 否 | 否 | 是 |
| 否 | 否 | 否 | 否 | 否 | 是 |
| 否 | 否 | 是 | 是 | 是 | 是 |
| 沙箱增强安全机制 | 否 | 否 | 是 | 是 | 是 | 是 |
Email Layout Templates
邮件布局模板
Available Layouts
可用布局
| Layout | Width | Use Case |
|---|---|---|
| 900px | Full notifications with header/footer |
| 590px | Simple notifications |
| 900px | Uses record's |
| 布局 | 宽度 | 适用场景 |
|---|---|---|
| 900px | 带页眉/页脚的完整通知模板 |
| 590px | 简洁通知模板 |
| 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是否为空
}Commands Reference
命令参考
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 |
| 命令 | 描述 |
|---|---|
| 列出指定模型或模块的所有模板 |
| 分析现有模板的问题 |
| 调试模板渲染问题 |
| 生成模板输出预览 |
Migration Commands
迁移命令
| 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>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
常见错误及解决方案
| Error | Cause | Solution |
|---|---|---|
| Null field access | Use |
| Old syntax in Odoo 15+ | Replace |
| Missing context helper | Ensure |
| Malformed QWeb | Check XML structure, close all tags |
| Wrong rendering context | Use template's model context |
| 错误 | 原因 | 解决方案 |
|---|---|---|
| 访问了空字段 | 使用 |
| 在Odoo15+中使用了旧语法 | 将 |
| 缺少上下文辅助函数 | 确保继承了 |
| QWeb格式错误 | 检查XML结构,闭合所有标签 |
| 渲染上下文错误 | 使用模板对应的模型上下文 |
Debug Template Rendering
调试模板渲染
python
undefinedpython
undefinedIn 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 ID | Purpose | Model |
|---|---|---|
| Send quotation/order | sale.order |
| Order confirmation | sale.order |
| Payment received | sale.order |
| 模板ID | 用途 | 模型 |
|---|---|---|
| 发送报价单/订单 | sale.order |
| 订单确认 | sale.order |
| 收款通知 | sale.order |
Purchase Module
采购模块
| Template ID | Purpose | Model |
|---|---|---|
| Send RFQ | purchase.order |
| Send PO | purchase.order |
| Delivery reminder | purchase.order |
| 模板ID | 用途 | 模型 |
|---|---|---|
| 发送询价单 | purchase.order |
| 发送采购订单 | purchase.order |
| 交货提醒 | purchase.order |
Accounting Module
会计模块
| Template ID | Purpose | Model |
|---|---|---|
| Send invoice | account.move |
| Send credit note | account.move |
| Payment receipt | account.payment |
| 模板ID | 用途 | 模型 |
|---|---|---|
| 发送发票 | account.move |
| 发送贷项通知单 | account.move |
| 付款收据 | account.payment |
HR Recruitment
HR招聘模块
| Template ID | Purpose | Model |
|---|---|---|
| Applicant to employee | hr.employee |
| Congratulations | hr.applicant |
| Refusal notice | hr.applicant |
| 模板ID | 用途 | 模型 |
|---|---|---|
| 应聘者转员工通知 | hr.employee |
| 录用通知 | hr.applicant |
| 拒信通知 | 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
undefinedpython
undefinedSafe 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
undefinedbash
undefinedWindows
Windows
winget install wkhtmltopdf.wkhtmltox
winget install wkhtmltopdf.wkhtmltox
OR download from: https://wkhtmltopdf.org/downloads.html
Ubuntu/Debian
Ubuntu/Debian
sudo apt-get install wkhtmltopdf
sudo apt-get install wkhtmltopdf
macOS
macOS
brew install wkhtmltopdf
undefinedbrew install wkhtmltopdf
undefinedOdoo Configuration (MANDATORY)
Odoo配置(必须)
Add to your :
odoo.confini
[options]在中添加:
odoo.confini
[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
undefinedbin_path = /opt/homebrew/bin
undefinedVerification
验证配置
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.exeCommon Errors
常见错误
| Error | Cause | Solution |
|---|---|---|
| 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 |
| 错误 | 原因 | 解决方案 |
|---|---|---|
| 未配置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
web.external_layout - ✅ 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 wrapper
web.html_container
若显示为或类似:
$$Â- 原因:同样是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
标准纸张格式
| 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 | 特殊尺寸 |
Orientation Options
方向选项
- - Vertical (default)
Portrait - - Horizontal (wide tables, charts)
Landscape
- - 纵向(默认)
Portrait - - 横向(宽表格、图表)
Landscape
Report SCSS Styling
报告SCSS样式
Correct Asset Bundle
正确的资源包配置
python
undefinedpython
undefinedmanifest.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_layoutscss
// ❌ 错误写法 - 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 wrapperBlank 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 recordsFonts Not Rendering:
Symptom: Wrong font in PDF
Cause: External font URLs (wkhtmltopdf offline)
Fix: Use web.external_layout or system fonts onlyTimeout/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
undefinedpython
undefinedOdoo 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
undefinedbash
undefined1. 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 templatesodoo/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 | | Full research documentation |
| Odoo 17 CLAUDE.md | | Development commands |
| Design System | | Theme styling rules |
| 文档 | 路径 | 用途 |
|---|---|---|
| 邮件模板研究 | | 完整研究文档 |
| Odoo17开发指南 | | 开发命令参考 |
| 设计系统规则 | | 主题样式规则 |
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 configuration (MANDATORY)
bin_path - Common errors and solutions
- Offline mode limitations explained
-
Arabic/RTL & Multilingual Reports
- CRITICAL: UTF-8 encoding requirements
- +
web.html_containerpatternweb.external_layout - Wrong patterns that cause encoding corruption
- Bilingual label patterns (side-by-side, stacked)
- RTL text alignment
-
Paper Format Configuration
- Custom paper format template
- Linking to report actions
- Standard formats reference (A4, Letter, Legal)
-
Report SCSS Styling
- Correct asset bundle ()
web.report_assets_common - Google Fonts pitfall (semicolons break SCSS)
- Why external fonts fail in wkhtmltopdf
- Recommended font stack
- Color scheme patterns
- Print-specific styles
- Correct asset bundle (
-
Debug Report Workflow
- Systematic 5-step diagnosis
- Issue-specific debugging guides
- Asset cache clearing
-
Bilingual Invoice Template
- Complete working example based on sadad_invoice
- Paper format, report action, templates included
- Proven UTF-8/Arabic support
-
Report Validation Checklist
- 6-category pre-flight checks
- Post-generation testing steps
ISSUES PREVENTED:
| Issue | Time Saved |
|---|---|
| Arabic text encoding corruption | 2+ hours |
| wkhtmltopdf configuration | 30 min |
| Google Fonts/external resources | 1 hour |
| SCSS semicolon parsing | 1 hour |
基于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&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库生成自定义二维码
python
undefinedpython
undefinedIn 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 usinghttps://.... Always use Odoo's internal--disable-external-linksroute which renders server-side./report/barcode/
当使用时,外部图片URL(如--disable-external-links)无法在wkhtmltopdf中渲染。 始终使用Odoo内部的https://...路由,它在服务器端渲染。/report/barcode/
Report Wizard (User-Configurable Reports)
报告向导(用户可配置报告)
When a report needs user input (date range, grouping, language selection), use a wizard:
TransientModel当报告需要用户输入(日期范围、分组、语言选择)时,使用向导:
TransientModel1. Wizard Model
1. 向导模型
python
undefinedpython
undefinedwizard/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
)undefinedfrom 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
)undefined2. 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就绪