├── 1.md ├── 10.md ├── 11.md ├── 12.md ├── 13.md ├── 14.md ├── 2.md ├── 3.md ├── 4.md ├── 5.md ├── 6.md ├── 7.md ├── 8.md ├── 9.md ├── README.md └── source-code ├── ch03 └── library_app │ ├── __init__.py │ ├── __manifest__.py │ ├── controllers │ ├── __init__.py │ └── main.py │ ├── demo │ └── demo.xml │ ├── models │ ├── __init__.py │ └── library_book.py │ ├── security │ ├── ir.model.access.csv │ └── library_security.xml │ ├── static │ └── description │ │ └── icon.png │ ├── tests │ ├── __init__.py │ └── test_book.py │ └── views │ ├── book_list_template.xml │ ├── book_view.xml │ └── library_menu.xml ├── ch04 ├── library_app │ ├── __init__.py │ ├── __manifest__.py │ ├── __pycache__ │ │ └── __init__.cpython-39.pyc │ ├── controllers │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-39.pyc │ │ │ ├── controllers.cpython-39.pyc │ │ │ └── main.cpython-39.pyc │ │ ├── controllers.py │ │ └── main.py │ ├── demo │ │ └── demo.xml │ ├── models │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-39.pyc │ │ │ ├── library_book.cpython-39.pyc │ │ │ └── models.cpython-39.pyc │ │ └── library_book.py │ ├── security │ │ ├── ir.model.access.csv │ │ └── library_security.xml │ ├── static │ │ └── description │ │ │ └── icon.png │ ├── tests │ │ ├── __init__.py │ │ └── test_book.py │ └── views │ │ ├── book_list_template.xml │ │ ├── book_view.xml │ │ ├── library_menu.xml │ │ ├── templates.xml │ │ └── views.xml └── library_member │ ├── __init__.py │ ├── __manifest__.py │ ├── __pycache__ │ └── __init__.cpython-39.pyc │ ├── controllers │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ └── main.cpython-39.pyc │ └── main.py │ ├── models │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── library_book.cpython-39.pyc │ │ └── library_member.cpython-39.pyc │ ├── library_book.py │ └── library_member.py │ ├── security │ ├── ir.model.access.csv │ └── library_security.xml │ └── views │ ├── book_list_template.xml │ ├── book_view.xml │ ├── library_menu.xml │ └── member_view.xml ├── ch05 ├── library_app │ ├── __init__.py │ ├── __manifest__.py │ ├── __pycache__ │ │ └── __init__.cpython-39.pyc │ ├── controllers │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-39.pyc │ │ │ ├── controllers.cpython-39.pyc │ │ │ └── main.cpython-39.pyc │ │ ├── controllers.py │ │ └── main.py │ ├── data │ │ ├── library.book.csv │ │ ├── book_demo.xml │ │ ├── library.book.csv │ │ └── res.partner.csv │ ├── demo │ │ └── demo.xml │ ├── models │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-39.pyc │ │ │ ├── library_book.cpython-39.pyc │ │ │ └── models.cpython-39.pyc │ │ └── library_book.py │ ├── security │ │ ├── ir.model.access.csv │ │ └── library_security.xml │ ├── static │ │ └── description │ │ │ └── icon.png │ ├── tests │ │ ├── __init__.py │ │ └── test_book.py │ └── views │ │ ├── book_list_template.xml │ │ ├── book_view.xml │ │ ├── library_menu.xml │ │ ├── templates.xml │ │ └── views.xml └── library_member │ ├── __init__.py │ ├── __manifest__.py │ ├── __pycache__ │ └── __init__.cpython-39.pyc │ ├── controllers │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ └── main.cpython-39.pyc │ └── main.py │ ├── models │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── library_book.cpython-39.pyc │ │ └── library_member.cpython-39.pyc │ ├── library_book.py │ └── library_member.py │ ├── security │ ├── ir.model.access.csv │ └── library_security.xml │ └── views │ ├── book_list_template.xml │ ├── book_view.xml │ ├── library_menu.xml │ └── member_view.xml ├── ch06 ├── library_app │ ├── __init__.py │ ├── __manifest__.py │ ├── __pycache__ │ │ └── __init__.cpython-39.pyc │ ├── controllers │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-39.pyc │ │ │ ├── controllers.cpython-39.pyc │ │ │ └── main.cpython-39.pyc │ │ ├── controllers.py │ │ └── main.py │ ├── data │ │ ├── library.book.csv │ │ ├── book_demo.xml │ │ ├── library.book.csv │ │ └── res.partner.csv │ ├── demo │ │ └── demo.xml │ ├── models │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-39.pyc │ │ │ ├── library_book.cpython-39.pyc │ │ │ ├── models.cpython-39.pyc │ │ │ └── res_partner.cpython-39.pyc │ │ ├── library_book.py │ │ ├── library_book_category.py │ │ └── res_partner.py │ ├── security │ │ ├── ir.model.access.csv │ │ └── library_security.xml │ ├── static │ │ └── description │ │ │ └── icon.png │ ├── tests │ │ ├── __init__.py │ │ └── test_book.py │ └── views │ │ ├── book_list_template.xml │ │ ├── book_view.xml │ │ ├── library_menu.xml │ │ ├── templates.xml │ │ └── views.xml └── library_member │ ├── __init__.py │ ├── __manifest__.py │ ├── __pycache__ │ └── __init__.cpython-39.pyc │ ├── controllers │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ └── main.cpython-39.pyc │ └── main.py │ ├── models │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── library_book.cpython-39.pyc │ │ └── library_member.cpython-39.pyc │ ├── library_book.py │ └── library_member.py │ ├── security │ ├── ir.model.access.csv │ └── library_security.xml │ └── views │ ├── book_list_template.xml │ ├── book_view.xml │ ├── library_menu.xml │ └── member_view.xml ├── ch07 └── ch07_recorsets_code.py ├── ch08 ├── library_app │ ├── __init__.py │ ├── __manifest__.py │ ├── controllers │ │ ├── __init__.py │ │ └── main.py │ ├── data │ │ ├── book_demo.xml │ │ ├── library.book.csv │ │ └── res.partner.csv │ ├── demo │ │ └── demo.xml │ ├── models │ │ ├── __init__.py │ │ ├── library_book.py │ │ ├── library_book_category.py │ │ └── res_partner.py │ ├── security │ │ ├── ir.model.access.csv │ │ └── library_security.xml │ ├── static │ │ └── description │ │ │ └── icon.png │ ├── tests │ │ ├── __init__.py │ │ └── test_book.py │ └── views │ │ ├── book_list_template.xml │ │ ├── book_view.xml │ │ └── library_menu.xml ├── library_checkout │ ├── __init__.py │ ├── __manifest__.py │ ├── data │ │ └── library_checkout_stage.xml │ ├── models │ │ ├── __init__.py │ │ ├── library_checkout.py │ │ ├── library_checkout_line.py │ │ └── library_checkout_stage.py │ ├── security │ │ └── ir.model.access.csv │ ├── tests │ │ ├── __init__.py │ │ └── test_checkout_mass_message.py │ ├── views │ │ ├── checkout_view.xml │ │ └── library_menu.xml │ └── wizard │ │ ├── __init__.py │ │ ├── checkout_mass_message.py │ │ └── checkout_mass_message_wizard_view.xml └── library_member │ ├── __init__.py │ ├── __manifest__.py │ ├── controllers │ ├── __init__.py │ └── main.py │ ├── models │ ├── __init__.py │ ├── library_book.py │ └── library_member.py │ ├── security │ ├── ir.model.access.csv │ └── library_security.xml │ └── views │ ├── book_list_template.xml │ ├── book_view.xml │ ├── library_menu.xml │ └── member_view.xml └── ch09 └── client_app ├── library.py ├── library_odoorpc.py └── library_xmlrpc.py /12.md: -------------------------------------------------------------------------------- 1 | # 第十二章 Odoo 12开发之报表和服务端 QWeb 2 | 3 | 本文为[最好用的免费ERP系统Odoo 12开发手册](README.md)系列文章第十二篇。 4 | 5 | 报表是业务应用非常有价值的功能,内置的 QWeb 引擎是报表的默认引擎。使用 QWeb 模板设计的报表可生成 HTML 文件并被转化成 PDF。也就是说我们可以很便捷地利用已学习的 QWeb 知识,应用到业务报表中。本文中我们将为图书馆应用添加一个报表,复习 QWeb生成报表的关键技巧。包括像汇总一类计算、翻译和纸张样式打印。 6 | 7 | 本文主要内容有: 8 | 9 | - 安装wkhtmltopdf 10 | - 创建业务报表 11 | - QWeb 报表模板 12 | - 在报表中展示数据 13 | - 渲染图片 14 | - 报表汇总 15 | - 定义纸质格式 16 | - 在报表中启用语言翻译 17 | - 使用自定义 SQL 建立报表 18 | 19 | 20 | 21 | ## 开发准备 22 | 23 | 我们将继续使用library_app插件模块进行学习,该模块在第三章 [Odoo 12 开发之创建第一个 Odoo 应用](3.md)中初次创建,然后在第五章 [Odoo 12开发之导入、导出以及模块数据](5.md)和第六章 [Odoo 12开发之模型 - 结构化应用数据](6.md)中进行了改进。相关代码请参见 [GitHub 仓库](source-code/chapter11)。本文完成后代码也请参见[GitHub 仓库](source-code/chapter12)。 24 | 25 | ## 安装wkhtmltopdf 26 | 27 | 要正确地生成报表,应安装wkhtmltopdf工具的推荐版本,该工具的名称表示Webkit HTML to PDF。Odoo使用它来将渲染的 HTML 页面转化为 PDF 文档。有些版本的wkhtmltopdf库已知存在问题,比如不打印页面头部和底部,所以需挑选使用的版本。从Odoo 10开始,官方支持了0.12.5版本,这也是官方推荐的版本。 28 | 29 | > **小贴士:**官方Odoo项目有一个 wiki 页面,保持了对于wkthtmltopdf使用的信息和推荐。可通过 [GitHub](https://github.com/odoo/odoo/wiki/Wkhtmltopdf) 进行查看。 30 | 31 | 不幸的是你的主机系统,不论是Debian/Ubuntu或其它系统,所提供的安装包版本都不太一致。所以我们应下载和安装对于当前操作系统和 CPU 类型的推荐版本包。下载链接请见 [GitHub](https://github.com/wkhtmltopdf/wkhtmltopdf/releases)。 32 | 33 | 首先应确保系统中所安装的不是错误的版本: 34 | 35 | ``` 36 | wkhtmltopdf --version 37 | ``` 38 | 39 | 如果上述命令打印的结果不是我们需要的版本,应对其进行卸载。在Debian/Ubuntu系统中,使用的命令如下: 40 | 41 | ``` 42 | sudo apt-get remove --purge wkhtmltopdf 43 | ``` 44 | 45 | 下一步我们需要下载适合我们系统的安装包并进行安装。通过[GitHub](https://github.com/wkhtmltopdf/wkhtmltopdf/releases)下载链接进行查看。对于0.12.5,最新 Ubuntu 安装版本是针对Ubuntu 14.04 LTS稳定版,但对其后的Ubuntu系统应该同样生效。我们在最近发布的Ubuntu 64系统中进行安装,下载命令如下: 46 | 47 | ``` 48 | wget "https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.bionic_amd64.deb" -O /tmp/wkhtml.deb 49 | ``` 50 | 51 | 下一步应进行安装。安装本地deb文件并不会自动安装依赖,因此需要执行第二步来完成安装: 52 | 53 | ``` 54 | sudo dpkg -i /tmp/wkhtml.deb 55 | ``` 56 | 57 | 这时可能会显示缺少依赖的错误,以下命令可解决这一问题: 58 | 59 | ``` 60 | sudo apt-get -f install 61 | ``` 62 | 63 | 现在,我们可以检查wkhtmltopdf库是否正确安装并确认是否为所需版本: 64 | 65 | ``` 66 | $ wkhtmltopdf --version 67 | wkhtmltopdf 0.12.5 (with patched qt) 68 | ``` 69 | 70 | 此时Odoo服务的启动日志就不会再提示You need Wkhtmltopdf to print a pdf version of the report的信息了。 71 | 72 | ## 创建业务报表 73 | 74 | 我们会继续使用前面文章所使用的library_app模块,进添加实现报表的文件。我们将创建的报表会长成这样 75 | 76 | ![Odoo 12图书项目业务报表](http://alanhou.org/homepage/wp-content/uploads/2019/01/report-final.jpg) 77 | 78 | 报表文件应放在模块子文件夹/reports中。首先我们来添加一个reports/library_book_report.xml数据文件,不要忘记在__manifest__.py文件的 data 下导入该文件。先在reports/library_book_report.xml文件中声明一个新报表: 79 | 80 | ``` 81 | 82 | 83 | 88 | 89 | ``` 90 | 91 | 标签是对向ir.actions.report.xml写入数据的简写形式,这个模型是客户操作的特殊类型。它的数据可通过Settings > Technical > Actions >Reports菜单进行查看。 92 | 93 | ![Odoo 12 Technical菜单查看报表](http://alanhou.org/homepage/wp-content/uploads/2019/01/technical-report.jpg) 94 | 95 | > **小贴士:**在设计报表时,我们可能更倾向保留为report_type="qweb-html"然后在完成时再修改为qweb-pdf文件。这样在QWeb模板中可更快速的生成报表并且更易于检查 HTML 结果。 96 | 97 | 执行完模块升级(~/odoo-dev/odoo/-bin -d dev12 -u library_app)后,图书表单视图中会在顶部显示一个 Print 按钮(列表视图中也有),它在Actions按钮的左侧,其中包含添加的运行报表的选项(Library Books)。 98 | 99 | ![Odoo 12图书项目报表生成菜单](http://alanhou.org/homepage/wp-content/uploads/2019/01/library-book-report.jpg) 100 | 101 | 现在还无法生成报表,因为我们还没有进行定义。这是一个QWeb报表,因此需要用到QWeb模板。name 属性标识所使用的模板。与其它标识符引用不同,name 属性中需要添加模块前缀,我们必须使用完整的引用名称.。 102 | 103 | ## QWeb 报表模板 104 | 105 | 在下面的代码可以看出,这个报表遵循一个基本框架。仅需在reports/library_book_report.xml文件元素后添加如下代码: 106 | 107 | ``` 108 | 121 | ``` 122 | 123 | 这里最重要的元素是使用标准报表结构的t-call指令。web.html_container模板进行支持 HTML 文档的基本设置。web.external_layout模板使用相应公司的相关设置处理报表头部和底部。可将其替换为web.internal_layout模板,它将只使用一个基本的头部。 124 | 125 | > ℹ️ **Odoo 11中的修改** 126 | > 对报表的支持布局从report 模块移到了 web 模块中。也就是说此前版本中使用**report**.external_layout或**report**.internal_layout的引用 ,在11.0中引用应修改为web.<...>。 127 | 128 | external_layout模板可由用户自定义,Odoo 11引入了这一选项,在Settings > General Settings菜单中,然后相关内容在Business Documents > Document Template版块: 129 | 130 | ![Odoo 12 external_layout模板](http://alanhou.org/homepage/wp-content/uploads/2019/01/external-layout.jpg) 131 | 132 | 这里我们可以点击Change Document Template来从几个可用的模板中选取,甚至是点击Edit Layout来自定义所选模板的 XML。这一个报表框架适用于列表式报表,即报表中每条记录显示为一行。报表头部通常显示标题,底部区域则显示汇总。 133 | 134 | 另一种格式是文档报表,每条记录是单独一页,比如邮件。这种情况报表结构如下: 135 | 136 | ``` 137 | 148 | ``` 149 | 150 | 我们会创建一个列表式报表,所以还会使用此前的框架。现在我们已经有了基本框架。既然报表是QWeb模板,那么它也可以像其它视图那样进行继承。报表中使用的QWeb模板可使用常规视图继承使用的 XPath 表达式来进行继承。 151 | 152 | 补充:此时点击打印会输出一个空白的 PDF 文件。 153 | 154 | ## 在报表中展示数据 155 | 156 | 与看板视图不同,报表中的QWeb模板在服务端进行渲染,因此使用Python QWeb来实现。我们可以将其看作相同规格的两种实现,需要注意其中的一些区别。 157 | 158 | 首先这里的QWeb表达式由 Python 语法运行,而非JavaScript。对于最简的表达式几乎没有区别,但更为复杂的运算则可能存在差别。表达式运行上下文也不同,对于报表可使用如下变量: 159 | 160 | - docs是要打印记录的可迭代集合 161 | - doc_ids是一个要打印记录的 ID 列表 162 | - doc_model指定记录的模型,如library.book 163 | - time是对Python时间库的引用 164 | - user是运行报表的用户记录 165 | - res_company是当前用户的公司记录 166 | 167 | 可使用t-field来引用字段值,并可使用t-options来进行补充指定渲染字段内容的具体组件。 168 | 169 | > **ℹ️Odoo 11中的修改** 170 | > 在此前的 Odoo 版本中,使用的是t-field-options属性,但在 Odoo 11中淘汰了该属性,改用t-options属性。 171 | 172 | 例如,假设doc表示一条具体记录,代码如下: 173 | 174 | ``` 175 | 177 | ``` 178 | 179 | 现在我们可以开始设计报表的页面内容了。 180 | 181 | > **小贴士:**不幸的是官方文档中并没有涉及QWeb支持的组件及其选项。所以当前对其做进一步的了解只能是通过阅读相应源码。可访问[ir_qweb_fields.py](https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/models/ir_qweb_fields.py),查找继承ir.qweb.field的类,get_available_options() 方法可有助了解支持的选项。 182 | 183 | 报表内容由HTML书写,并且使用了Twitter Bootstrap 4来帮助设计报表布局。在网页开发中大量使用了Bootstrap,有关Bootstrap的完整指南请见[官方网站](https://getbootstrap.com/)。 184 | 185 | 以下为渲染报表头部的 XML 代码,应放在
中并替换掉现有的元素: 186 | 187 | ``` 188 | 189 |
190 |
191 |
Title
192 |
Publisher
193 |
Date
194 |
Publisher Address
195 |
Authors
196 |
197 | 198 |
199 | 200 |
201 |
202 | 203 |
204 | ``` 205 | 206 | 内容的布局使用了Twitter Bootstrap的HTML网格系统。总的来说Bootstrap使用12列的网格布局,此处网格在
元素中。 207 | 208 | > ℹ️**Odoo 12中的修改** 209 | > 现在Odoo使用Bootstrap 4,它对此前 Odoo 版本中使用的Bootstrap 3并没有保持向后兼容。对于从Bootstrap 3改为Bootstrap 4的小技巧,可参照 Odoo 中关于这一话题的 [Wiki 页面](https://github.com/odoo/odoo/wiki/Tips-and-tricks:-BS3-to-BS4)。 210 | 211 | 可使用
来添加行。每行中还有多个单元格,分别占用不同列数,总计应为12列。每个单元格可通过
来进行定义,其中 N 表示占用列的数量。 212 | 213 | > **小贴士:**Bootstrap 4 在其大部分构件中使用了 CSS 弹性盒子布局,已知wkhtmltopdf 对弹性盒子的功能并不都能很好的支持。因此如果有些地方效果不对,请尝试使用其它元素或方法,如 HTML 表格。 214 | 215 | ![Odoo 12报表头部](http://alanhou.org/homepage/wp-content/uploads/2019/01/report-header.jpg) 216 | 217 | 此处我们为头部行添加了标题,然后t-foreach循环遍历每条记录并在各行中进行渲染。因为渲染由服务端完成,记录都是对象,我们可使用点号标记来从关联数据记录中访问字段。这也让关联字段的数据访问变得更为容易。注意这在客户端渲染的QWeb视图中是无法使用的,比如网页客户端的看板视图。 218 | 219 | 以下是在
元素中的记录行内容XML: 220 | 221 | ``` 222 | 223 |
224 |

225 |
226 |
227 | 228 |
229 |
230 | 232 |
233 |
234 | 239 |
240 |
241 | 242 |
243 | ``` 244 | 245 | 可以看到字段可通过t-options属性添加额外的选项,内容为包含带有widget键的 JSON 字典。更为复杂的示例是contact组件,用于格式化地址。我们使用它来渲染出版商地址o.publisher_id。默认contact 组件显示地址时带有图像,类似电话图标。no_marker="true"选项禁用了这一显示。 246 | 247 | ![Odoo 12报表地址](http://alanhou.org/homepage/wp-content/uploads/2019/01/report-address.jpg) 248 | 249 | 补充:no_marker="true"禁用的地址图标如上所示 250 | 251 | ## 渲染图片 252 | 253 | 我们报表最后一列为一组带有头像的作者。我们将通过遍历来渲染出每个作者,并使用Bootstrap媒体对象: 254 | 255 | ``` 256 | 257 |
    258 | 259 |
  • 260 | 262 |
    263 |

    264 | 265 |

    266 |
    267 |
  • 268 |
    269 |
270 | ``` 271 | 272 | 此处我们遍历了author_ids,使用字段图像组件对每个作者的头像进行了渲染,然后还有姓名。 273 | 274 | ![Odoo 12图书项目报表作者头像](http://alanhou.org/homepage/wp-content/uploads/2019/01/report-authors.jpg) 275 | 276 | 注:以上头像来自各位大神 Twitter 的真实头像 277 | 278 | ## 报表汇总 279 | 280 | 报表中经常需要提供汇总。这可借由 Python 表达式来计算总额。在闭合标签之后,我们添加最后一行用于汇总: 281 | 282 | ``` 283 | 284 |
285 |
286 | Count: 287 |
288 |
289 |
290 |
291 |
292 |
293 | ``` 294 | 295 | len() Python函数用于计算集合元素的数量。类似地,汇总也可以使用sum()来对一组值进行求和运算。例如,可使用如下列表推导式来进行总额运算: 296 | 297 | ``` 298 | 299 | ``` 300 | 301 | 可以把这个列表推导式看作一个内嵌的循环。有时我们需要贯穿报表执行一些计算,如流动汇总(running total),汇总至当前记录。这可通过t-set 来定义累加变量在每一行进行更新来实现。为描述这一功能,我们来计算作者数的累加。首先在docs 记录集 t-foreach 循环前初始化变量: 302 | 303 | ``` 304 | 305 | 306 | ``` 307 | 308 | 然后在循环内,将记录的作者数添加到变量中。我们这里显示在书名之后,并在每行打印出当前总数: 309 | 310 | ``` 311 | 312 | 313 | (Accum. authors: ) 314 | ``` 315 | 316 | ![Odoo 12图书项目报表汇总](http://alanhou.org/homepage/wp-content/uploads/2019/01/report-totals.jpg) 317 | 318 | ## 定义纸张样式 319 | 320 | 到这里我们的报表的 HTML 显示没有问题了,但在打印的 PDF 页面中还不够美观。使用横向页面显示结果会更好,因此下面就来添加纸张样式。在报表 XML 文件的最上方添加如下代码: 321 | 322 | ``` 323 | 325 | European A4 Landscape 326 | 327 | A4 328 | 0 329 | 0 330 | Landscape 331 | 40 332 | 23 333 | 7 334 | 7 335 | 336 | 35 337 | 90 338 | 339 | ``` 340 | 341 | 这是对European A4格式的一个拷贝,这在data/report_paperformat_data.xml文件中定义的base 模块中,但这里将排列方向由纵向改为了横向。定义的纸张样式可通过后台Settings > Technical > Reporting > Paper Format菜单进行查看。 342 | 343 | ![Odoo 12菜单查看纸张样式](http://alanhou.org/homepage/wp-content/uploads/2019/01/european-a4.jpg) 344 | 345 | 现在就可在报表中使用它了。默认的纸张样式在公司设置中定义,但我们也可以为特定报表指定纸张样式。这通过在报表操作中的paperfomat属性来实现。下面来编辑打开报表使用的操作,添加这一属性: 346 | 347 | ``` 348 | 351 | ``` 352 | 353 | ![Odoo 12图书项目横向报表](http://alanhou.org/homepage/wp-content/uploads/2019/01/report-landscape.jpg) 354 | 355 | ## 在报表中启用语言翻译 356 | 357 | 要在报表中启用翻译,需要使用带有t-lang属性的元素在模板中调用翻译方法。t-lang需传入一个语言代码来运行,如es或en_US。它需要可以找到所需使用语言的字段名。一种方式是使用当前用户的语言,为此,我们定义一个外层翻译报表来调用要翻译的报表,使用t-lang属性来设置语言来源: 358 | 359 | ``` 360 | 364 | 365 | 369 | ``` 370 | 371 | 本例中,每本书都使用了用户的语言user_id.lang来进行渲染。 372 | 373 | 有些情况下,我们可能需要每条记录以指定语言进行渲染。比如在销售订单中,我们可能要各条记录按照对应合作方/客户的首选语言进行打印。假设我们需要每本书按照对应出版商的语言进行渲染,QWeb模板可以这么写: 374 | 375 | ``` 376 | 384 | ``` 385 | 386 | 以上我们对记录进行了迭代,然后每条记录根据记录上的数据使用相应的语言进行报表模板的调用,本例为出版商的语言publisher_id.lang。 387 | 388 | 补充:以上代码运行时每条记录都会带有一个头部,如需按列表显示还需将头部抽象到循环之外 389 | 390 | ## 使用自定义 SQL 创建报表 391 | 392 | 前面我们所创建的报表都是基于常规记录集,但在有些情况下我们需要执行一些在QWeb模板中不易于处理的数据转换或累加。一种解决方法是写原生 SQL 查询来创建我们所需的数据集,将结果通过特殊的模型进行暴露,然后基于这一数据集来生成报表。 393 | 394 | 我们创建reports/library_book_report.py文件来讲解这一情况,代码如下: 395 | 396 | ``` 397 | from odoo import models, fields 398 | 399 | class BookReport(models.Model): 400 | _name = 'library.book.report' 401 | _description = 'Book Report' 402 | _auto = False 403 | 404 | name = fields.Char('Title') 405 | publisher_id = fields.Many2one('res.partner') 406 | date_published = fields.Date() 407 | 408 | def init(self): 409 | self.env.cr.execute(""" 410 | CREATE OR REPLACE VIEW library_book_report AS 411 | (SELECT * 412 | FROM library_book 413 | WHERE active=True) 414 | """) 415 | ``` 416 | 417 | 要加载以上文件,需要在模块的顶级__init__.py文件中添加from . import reports,并在reports/__init__.py文件中添加from . import library_book_report。 418 | 419 | _auto属性用于阻止数据表的自动创建。我们在模型的init()方法中添加了替代的 SQL。它会创建数据库视图,并提供报表所需数据。以上 SQL 查询非常简单,旨在说明我们为视图可以使用任意有效的 SQL查询,如对额外数据执行累加或计算。 420 | 421 | 我们还需要声明模型字段来让 Odoo 知道如何正确处理每一条记录中的数据。同时不要忘记为新模型添加安全访问规则,否则将无法使用该模型。下面在security/ir.model.access.csv文件中添加下行: 422 | 423 | ``` 424 | access_library_book_report,access_library_book_report,model_library_book_report, 425 | library_group_user,1,0,0,0 426 | ``` 427 | 428 | 还应注意这是一个全新的不同模型,与图书模型的访问规则并不相同。下一步基于该模型我们可以使用reports/library_book_sql_report.xml新增一个报表: 429 | 430 | ``` 431 | 432 | 433 | 438 | 439 | 469 | 470 | ``` 471 | 472 | 对于更为复杂的情况,我们还会需要用户输入参数,这时可以使用不同的方案:向导。为此我们应创建一个临时模型来存储用户的报表参数。因为这是由代码生成的,所以我们可以使用所需的任意逻辑。 473 | 474 | 强烈推荐学习已有的相似报表来获取灵感。一个不错的例子是Leaves菜单选项下的Leaves by Department,相应的临时模型定义可以参见addons/hr_holidays/wizard/hr_holidays_summary_employees.py。 475 | 476 | 补充:原书未对这一部分进行验证,Alan 下面通过添加菜单项的方式来进行验证,还有其它方式,欢迎读者留言讨论 477 | 478 | 首先在views/library_menu.xml文件中添加如下内容: 479 | 480 | ``` 481 | 486 | 491 | ``` 492 | 493 | 在__manifest__.py 文件中引入前述的 XML 文件后更新模块 494 | 495 | ![Odoo 12 Library Book SQL Report](http://alanhou.org/homepage/wp-content/uploads/2019/01/sql-report.jpg) 496 | 497 | ![Odoo 12 SQL报表运行结果](http://alanhou.org/homepage/wp-content/uploads/2019/01/sql-final-report.jpg) 498 | 499 | ## 总结 500 | 501 | 前面一篇文章中,我们学习了QWeb以及如何使用它来设计看板视图。本文中我们学习了QWeb报表引擎,以及使用QWeb模板语言创建报表最为重要的一些技术。 502 | 503 | 下一篇文章中,我们将继续使用QWeb,这次是创建网页。我们将学习书写网页控制器,为我们的网页提供更丰富的功能。 504 | 505 | 506 | 507 | ☞☞☞第十三章 [Odoo 12开发之创建网站前端功能](13.md) 508 | 509 | 510 | 511 | ## 扩展阅读 512 | 513 | 本文所学习课题的补充参考材料有: 514 | 515 | - Odoo官方文档对应专区: 516 | - [报表](https://www.odoo.com/documentation/12.0/reference/reports.html) 517 | - [QWeb语言](https://www.odoo.com/documentation/12.0/reference/qweb.html) 518 | - [Bootstrap样式文档](https://getbootstrap.com/docs/4.1/getting-started/introduction/) -------------------------------------------------------------------------------- /13.md: -------------------------------------------------------------------------------- 1 | # 第十三章 Odoo 12开发之创建网站前端功能 2 | 3 | 本文为[最好用的免费ERP系统Odoo 12开发手册](README.md)系列文章第十三篇。 4 | 5 | Odoo 起初是一个后台系统,但很快就有了前端界面的需求。早期基于后台界面的门户界面不够灵活并且对移动端不友好。为解决这一问题,Odoo 引入了新的网站功能,为系统添加了 CMS(Content Management System)内容管理系统。这使得我们无需集成第三方 CMS 便可创建美观又高效的前端。本文中我们将学习如何利用 Odoo 自带的网站功能开发面向前端的插件模块。 6 | 7 | 本文主要内容有: 8 | 9 | - 学习项目 - 自助图书馆 10 | - 第一个网页 11 | - 创建网站 12 | 13 | 14 | 15 | ## 开发准备 16 | 17 | 我将用第十一章 [Odoo 12开发之看板视图和用户端 QWeb](11.md)中最后编辑的library_checkout插件模块,代码请见[GitHub 仓库](source-code/chapter11)。本文完成后的代码也请参见[GitHub 仓库](source-code/chapter13)。 18 | 19 | ## 学习项目 - 自助图书馆 20 | 21 | 本文中我们将为图书会员添加一个自助服务功能。可供会员分别登录账号来访问他们的借阅请求列表。这样我们就可以学习网站开发的基本技术:创建动态页面、在页面间传递参数、创建表单以及处理表单数据验证。对这些新的图书网站功能,我们要新建一个插件模块library_website。 22 | 23 | 大家应该已经轻车熟路了,首先创建插件的声明文件ibrary_website/__manifest__.py,代码如下: 24 | 25 | ``` 26 | { 27 | 'name': 'Library Website', 28 | 'description': 'Create and check book checkout requests.', 29 | 'author': 'Alan Hou', 30 | 'depends': [ 31 | 'library_checkout' 32 | ], 33 | 'data': [ 34 | 'security/ir.model.access.csv', 35 | 'security/library_security.xml', 36 | 'views/library_member.xml', 37 | ], 38 | } 39 | ``` 40 | 41 | 网站功能将会依赖于library_checkout。我们并没有添加对website核心插件模块的依赖。website插件为创建完整功能的网站提供了有用的框架,但现在我们仅探讨核心框架自带的基础网站功能,尚无需使用website。我们想要图书会员通过登录信息在图书网站上访问自己的借阅请求。为此需要在图书会员模型中添加一个user_id字段,需要分别在模型和视图中添加,下面就开始进行网站的创建: 42 | 43 | 1、添加library_website/models/library_member.py文件 44 | 45 | ``` 46 | from odoo import fields, models 47 | 48 | class Member(models.Model): 49 | _inherit = 'library.member' 50 | user_id = fields.Many2one('res.users') 51 | ``` 52 | 53 | 2、添加library_website/models/__init__.py文件: 54 | 55 | ``` 56 | from . import library_member 57 | ``` 58 | 59 | 3、添加library_website/__init__.py文件: 60 | 61 | ``` 62 | from . import models 63 | ``` 64 | 65 | 4、添加library_website/views/library_member.xml文件: 66 | 67 | ``` 68 | 69 | 70 | 71 | Member Form 72 | library.member 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ``` 82 | 83 | 访问这些网页的都是门户用户,无需访问后台菜单。我们需要为这个用户组设置安全访问权限,否则会在使用图书网站功能时报权限错误。 84 | 85 | 5、添加library_website/security/ir.model.access.csv文件,添加对图书模型的读权限: 86 | 87 | ``` 88 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 89 | access_book_portal,Book Portal Access,library_app.model_library_book,base.group_ 90 | portal,1,0,0,0 91 | access_member_portal,Member Portal Access,library_member.model_library_member,ba 92 | se.group_portal,1,0,0,0 93 | access_checkout_portal,Checkout Portal Access,library_checkout.model_library_che 94 | ckout,base.group_portal,1,0,0,0 95 | ``` 96 | 97 | 6、在library_website/security/library_security.xml文件中添加记录规则来限制门户用户所能访问的记录: 98 | 99 | ``` 100 | 101 | 102 | 103 | 104 | Library Member Portal Access 105 | 106 | 107 | [('user_id', '=', user.id)] 108 | 109 | 110 | 111 | 112 | 113 | Library Checkout Portal Access 114 | 115 | 116 | [('member_id.user_id', '=', user.id)] 117 | 118 | 119 | 120 | 121 | 122 | ``` 123 | 124 | base.group_portal是门户用户组的标识符。在创建门户用户时,应设置他们的用户类型为 Portal,而不是Internal User。这会让他们属于门户用户组并继承我们上面定义的访问权限: 125 | 126 | ![Odoo 12门户用户类型](http://alanhou.org/homepage/wp-content/uploads/2019/01/user-type-portal.jpg) 127 | 128 | 补充:以上内容需开启开发者模式才可见 129 | 130 | 一旦为图书会员创建了一个门户用户,就应在我们会员表单中的用户字段中使用。该登录信息将可以访问相应会员的借阅请求。 131 | 132 | > **小贴士:**在模型中使用 ACL 和记录规则来实现安全权限比使用控制器的逻辑要更为安全。这是因为攻击者有可能跳过网页控制器直接使用RPC 来访问模型 API 。 133 | 134 | 了解了这些,我们就可以开始实现图书网站的功能了。但首先我们来使用简单的Hello World网页简短地介绍下基本网站概念。 135 | 136 | ## 第一个网页 137 | 138 | 要开始了解 Odoo 网页开发的基础,我们将先实现一个Hello World网页来展示基本概念和技术。很有想象空间,是不是? 139 | 140 | 要创建第一个网页,我们需要一个控制器对象。首先来添加controllers/hello.py文件: 141 | 142 | 1、在library_website/__init__.py文件中添加如下行: 143 | 144 | ``` 145 | from . import controllers 146 | ``` 147 | 148 | 2、在library_website/controllers/__init__.py文件中添加如下行: 149 | 150 | ``` 151 | from . import hello 152 | ``` 153 | 154 | 3、添加实际的控制器文件 library_website/controllers/hello.py,代码如下: 155 | 156 | ``` 157 | from odoo import http 158 | 159 | class Hello(http.Controller): 160 | @http.route('/helloworld', auth="public") 161 | def helloworld(self): 162 | return('

Hello World!

') 163 | ``` 164 | 165 | odoo.http模块提供 Odoo 网页相关的功能。我们用于渲染页面的控制器,应该是一个继承了odoo.http.Controller类的对象。实际使用的名称并不是太重要,这里选择了 Hello(),一个常用的选择是 Main()。 166 | 167 | 在控制器类中使用了匹配 URL 路由的方法。这些路由用于做一些处理并返回结果,通常是返回用户网页浏览器的 HTML 页面。odoo.http.route装饰器用于为 URL 路由绑定方法,本例中使用的是/helloworld 路由。 168 | 169 | 安装library_website模块(~/odoo-dev/odoo/odoo-bin -d dev12 -i library_website)就可以在浏览器中打开http://xxx:8069/helloworld,我们应该就可以看到Hello World问候语了。 170 | 171 | 本例中方法执行的处理非常简单,它返回一个带有 HTML 标记的文本字符串,Hello World。 172 | 173 | > ℹ️使用这里的简单 URL 访问按制器,如果同一 Odoo 实例有多个数据库时,在没有指定目标数据库的情况下将会失败。这可通过在启动配置中设置-d或--db-filter来解决,参见第二章 [Odoo 12开发之开发环境准备](2.md)。 174 | 175 | 你可能注意到在路由装饰中使用了auth='public'参数,对于无需登录的用户开放的页面就需要使用它。如果删除该参数,仅有登录用户方可浏览此页面。如果没有活跃会话(session)则会进入登录页面。 176 | 177 | > **小贴士:**auth='public'参数实际表示如果访客未登录则使用public特殊用户运行网页控制器。如果登录了,则使用登录用户来代替public。 178 | 179 | ![Odoo 12 Hello World](http://alanhou.org/homepage/wp-content/uploads/2019/01/hello-world.jpg) 180 | 181 | ### 使用 QWeb 模板的 Hello World 182 | 183 | 使用 Python 字符串来创建 HTML 很快就会觉得乏味。QWeb可用来增添色彩,下面就使用模板来写一个改进版的Hello World网页。QWeb模板通过 XML 数据文件添加,技术层面上它是与表单、列表视图类似的一种视图类型。它们甚至存储在同一个技术模型ir.ui.view中。 184 | 185 | 老规矩,需要在声明文件中添加声明来加载文件,编辑library_website/__manifest__.py文件并添加内容如下: 186 | 187 | ``` 188 | 'data': [ 189 | ... 190 | 'views/helloworld_template.xml', 191 | ], 192 | ``` 193 | 194 | 然后添加实际的数据文件views/helloworld_template.xml,内容如下: 195 | 196 | ``` 197 | 198 | 199 | 202 | 203 | ``` 204 | 205 |