Alan Hou的个人博客

用行动赢得尊重

Alan Hou的个人博客

本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。

一个应用很重要的部分是网络流量的来来回回。服务发现一章中提到,Kubernetes拥有一些能力可让服务暴露到集群之外。对于很多用户的简单用例,这种能力足够用了。

但服务对象在OSI模型的第4层操作。也就是说它只转发TCP和UDP连接,不会深入到连接内部。因此,在集群上托管多个应用使用多个不同的对外暴露的服务。在服务为NodePort类型时,需要让客户端连接到各个服务的具体端口。而在服务类型为LoadBalancer时,要为每个服务分配云资源(通常昂贵或稀少)。但对HTTP(7层)服务,则有更好的方案。

在非Kubernetes场景下解决这类问题,人们常常会想到“虚拟主机”。通过这种机制可以在单IP上托管多个HTTP站点。通常会使用到负载均衡或反射代理在HTTP (80) 和 HTTPS (443)端口上接收进入的网络连接。然后程序会解析HTTP连接,根据请求的Host头和URL路径,将HTTP调用代理至其它程序。这样,负载均衡或反向代理将流量转发去解码或将进入的连接转发到正确upstream服务端。

Kubernetes将其HTTP负载均衡系统称为Ingress。Ingress是Kubernetes中刚刚讨论到的“虚拟主机”的原生实现方式。这种方式更复杂的方面是用户需要管理负载均衡配置文件。对于动态环境,随机虚拟主机的扩充,就会变得很复杂。Kubernetes Ingress系统对其简化的方式有:(a)标准化配置,(b) 将其迁移至Kubernetes对象中,(c) 将多个Ingress对象合并至单个负载均衡配置中。

典型的软件实现如图8-1所示。Ingress控制器是一个由两部分组成的软件系统。第一部分是Ingress代理,使用LoadBalancer类型服务对集群外暴露。这一代码将请求发送给上游服务端。另一个组件是Ingress协调器(reconciler)或operator。Ingress operator负责读取和监控Kubernetes API中的Ingress对象,以及重新配置Ingress代理按Ingress资源所指定的方式路由流量。有多种不同的Ingress实现。其中一些这两个组件位于单个容器中,另一些不同的组件单独部署在Kubernetes集群中。图8-1为Ingress控制器的一个示例。

图8-1 典型软件的Ingress控制器配置

图8-1 典型软件的Ingress控制器配置

Ingress规范与Ingress控制器

虽然概念上不复杂,但在实现层面Ingress与Kubernetes中的其它常规资源对象均不同。尤其是它拆分为了普通资源规范和控制器实现。Kubernetes中没有内置“标准”的Ingress控制器,因而用户需要从多种可选实现中选择一种。

用户可以像其它对象一样创建和修改Ingress对象。但默认没有运行的代码在实际作用于这些对象。由用户(或他们使用的发行版)安装、管理外部控制器。这样控制器才是可插拔的。

Ingress最终这样实现是有一些原因的。首先,没存在可以到处使用的某一个HTTP负载均衡。除了大量的软件负载均衡(开源与自主研发的),还有一些由云厂商提供的负载均衡服务(如AWS的ELB),以及硬件负载均衡。另一个原因是Ingress对象在Kubernetes中添加其它通用扩展能力(参见扩展Kubernetes一章)之前就已经存在了。随着Ingress的发展,很可能会演进到使用这些机制。

安装Contour

存在很多种Ingress控制器,比如这里我们使用的Ingress控制器为Contour。它是一个用于配置开源负载均衡Envoy(CNCF项目)的控制器。Envoy用于动态通过API进行配置。Contour Ingress控制器将Ingress对象翻译成Envoy可以理解的内容。

注:Contour项目由Heptio与真实客户共同创建,已在生产中进行使用,但它现在是一个独立开源项目。

可通过一行命令安装Contour:

1
$ kubectl apply -f https://projectcontour.io/quickstart/contour.yaml

注意这条命令要求执行的用户具有cluster-admin权限。

对于大部分配置都可以使用这条命令。它会创建一个名为projectcontour的命名空间。在命名空间内创建一个部署(两个副本)以及一个对外的LoadBalancer服务。此外它会通过服务配置正确的权限并为一些在Ingress展望一节中讨论的扩展能力安装CustomResourceDefinition(参见扩展Kubernetes一章)。

因其是全局安装,需要保证在所安装的集群上具备管理员权限。安装完成后,可通过如下命令获取Contour的外部地址:

1
2
3
$  kubectl get -n projectcontour service envoy -o wide
NAME CLUSTER-IP EXTERNAL-IP PORT(S) ...
contour 10.106.53.14 a477...amazonaws.com 80:30274/TCP ...

查看EXTERNAL-IP一列。它可能是IP地址(GCP和Azure)或主机名(AWS)。其它云或环境可能会不同。如果你的Kubernetes集群不支持LoadBalancer类型服务,则需要在安装Contour时修改YAML为type: NodePort,将流量通过你的配置所接受的机制路由到集群上的机器。

如果使用的是minikube,在EXTERNAL-IP一列可能不会显示内容。要解决这一问题,需要单独打开一个终端窗口,运行minikube tunnel。这会配置网络路由,为每个type: LoadBalancer的服务分配一个独立 IP。

配置DNS

要让Ingress很好地运行,需要为负载均衡配置指向外部地址的DNS。可以将多个主机名映射到单个外部端点,Ingress控制器会将进入的请求根据主机名转发到相应在的上游服务。

本章中我们假设你有一个域名example.com,需要配置两条DNS:alpaca.example.combandicoot.example.com。如果对外负载均衡有IP地址,应创建A记录。如果所持有的是主机名,应配置CNAME记录。

ExternalDNS项目是一个用于代你管理DNS记录的集群插件。ExternalDNS监控Kubernetes集群并将Kubernetes服务资源的IP地址同步给外部DNS服务商。ExternalDNS支持很多种DNS服务商,包含传统的域名提供商和公有云服务商。

配置本地host文件

如果没有域名或者是使用的本地解决方案,如minikube,可以通过编辑 /etc/hosts文件添加IP地址来完成本地配置。这需要具备主机的admin/root权限。不同平台文件位置也不同,使用修改生效可能也需要做额外操作。例如,Windows上通常位于C:\Windows\System32\drivers\etc\hosts,而在最近版本的macOS上,需要运行sudo killall -HUP mDNSResponder来使修改生效。

编辑该文件添加如下内容:

1
<ip-address> alpaca.example.com bandicoot.example.com

有关<ip-address>,请使用Contour的对外IP地址。如果只有主机名(如AWS),可通过执行host -t a <address> 来获取IP地址(未来可能会发生改变)。

在完成测试后别忘了取消这些修改。

使用Ingress

现在我们已经配置好了Ingress控制器,我们就开始使用它吧。首先,通过如下命令创建一些上游(有时也称为后端)服务以供使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ kubectl create deployment be-default \
--image=gcr.io/kuar-demo/kuard-amd64:blue \
--replicas=3 \
--port=8080
$ kubectl expose deployment be-default
$ kubectl create deployment alpaca \
--image=gcr.io/kuar-demo/kuard-amd64:green \
--replicas=3 \
--port=8080
$ kubectl expose deployment alpaca
$ kubectl create deployment bandicoot \
--image=gcr.io/kuar-demo/kuard-amd64:purple \
--replicas=3 \
--port=8080
$ kubectl expose deployment bandicoot
$ kubectl get services -o wide

NAME CLUSTER-IP ... PORT(S) ... SELECTOR
alpaca 10.115.245.13 ... 8080/TCP ... run=alpaca
bandicoot 10.115.242.3 ... 8080/TCP ... run=bandicoot
be-default 10.115.246.6 ... 8080/TCP ... run=be-default
kubernetes 10.115.240.1 ... 443/TCP ... <none>

最简单的用法

使用Ingress最简单的用法是将其获得的所有内容盲传给上游服务。kubectl中对Ingress的操作命令支持有限,所以我们使用YAML文件(见例8-1)。

例8-1 simple-ingress.yaml

1
2
3
4
5
6
7
8
9
10
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: simple-ingress
spec:
defaultBackend:
service:
name: alpaca
port:
number: 8080

通过kubectl apply创建这个Ingress:

1
2
$ kubectl apply -f simple-ingress.yaml
ingress.extensions/simple-ingress created

可通过kubectl getkubectl describe验证配置是否正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
simple-ingress * 80 13m

$ kubectl describe ingress simple-ingress
Name: simple-ingress
Namespace: default
Address:
Default backend: alpaca:8080
(172.17.0.6:8080,172.17.0.7:8080,172.17.0.8:8080)
Rules:
Host Path Backends
---- ---- --------
* * alpaca:8080 (172.17.0.6:8080,172.17.0.7:8080,172.17.0.8:8080)
Annotations:
...

Events: <none>

这样配置后任何发往Ingress控制器的HTTP请求都会被转发给alpaca服务。现在可以通过服务的任何一个IP/CNAME访问kuardalpaca实例,在本例中即为alpaca.example.combandicoot.example.com。现在type: LoadBalancer服务还没体现出多少价值。下一节中会探索更复杂的配置。

使用主机名

将流量根据请求的属性进行转发时就开始变得有趣了。最常见的例子是让Ingress系统查看HTTP主机头(在原URL中设置为DNS域名),将流量根据头进行转发。我们再添加一个Ingress对象用于将指向alpaca.example.com的流量转发给alpaca服务(见例8-2)。

例8-2 host-ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-ingress
spec:
defaultBackend:
service:
name: be-default
port:
number: 8080
rules:
- host: alpaca.example.com
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: alpaca
port:
number: 8080

通过kubectl apply创建这一Ingress:

1
2
$ kubectl apply -f host-ingress.yaml
ingress.extensions/host-ingress created

通过如下命令验证配置是否正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
host-ingress alpaca.example.com 80 54s
simple-ingress * 80 13m

$ kubectl describe ingress host-ingress
Name: host-ingress
Namespace: default
Address:
Default backend: be-default:8080 (<none>)
Rules:
Host Path Backends
---- ---- --------
alpaca.example.com
/ alpaca:8080 (<none>)
Annotations:
...

Events: <none>

有一些会产生困扰的事。首先,涉及到了default-http-backend。这是一种部分Ingress控制器在没有其它方式时用于处理请求的惯例。这些控制器将请求发送给kube-system命名空间中名为default-http-backend的服务。这一惯例见于kubectl的客户端。其次是alpaca后端服务没有列出端点。这个kubectl的bug 在Kubernetes v1.14中得以修复。

不管怎么说,现在可以通过http://alpaca.example.com 来访问alpaca服务了。如果通过其它访问访问服务端点,会获取到默认服务。

使用路径

另一个有趣的场景是流量转发不仅依赖于主机名,也可取决于HTTP请求的路径。我们可以在paths中指定路径来轻松实现(见例8-3)。本例中,我们将所有来自http://bandicoot.example.com 的流量转发给bandicoot服务,但将http://bandicoot.example.com/a 的服务发送给alpaca服务。这一场景可用于在同一域名的不路径上托管多个服务。

例8-3 path-ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-ingress
spec:
rules:
- host: bandicoot.example.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: bandicoot
port:
number: 8080
- pathType: Prefix
path: "/a/"
backend:
service:
name: alpaca
port:
number: 8080

在同一主机有多个路径位于Ingress系统中时,匹配最长前缀。因此,本例中,以/a/开头的流量会被转发给alpaca服务,而其它的所有流量(以/开头)都会转发到bandicoot服务。

在请求被代理到上游服务时,路径保持不变。也就是bandicoot.example.com/a/请求显示所配置请求主机名和路径的上游服务。上游服务在接收该子路径的流量时要处于就绪状态。kuard有一些特别的测试代码,它使用预定义的子路径(/a//b//c/)和根路径(/)一起提供响应。

清理

执行如下命令完成清理:

1
2
3
$ kubectl delete ingress host-ingress path-ingress simple-ingress
$ kubectl delete service alpaca bandicoot be-default
$ kubectl delete deployment alpaca bandicoot be-default

高级Ingress课题和技巧

Ingress支持一些高级特性。对这些特性的支持根据Ingress控制器的实现不同而不同,并且不同控制器可能对同一特性的实现存在些许差别。

很多扩展我都通过对Ingress对象添加注解进行暴露。注意这些注解很难验证、容易出错。大部分注解应用于整个Ingress对象,因此可能比想象的更通用。要为这些注解添加作用域,可以将单个Ingress对象拆分为多个Ingress对象。Ingress控制器会进行读取及合并。

运行多个Ingress控制器

有很多种Ingress控制器实现,你可能会想对单个集群运行多个Ingress控制器。处理这一状况,有IngressClass资源可让Ingress资源请求具体的实现。在创建Ingress资源时,使用spec.ingressClassName字段指定特定的Ingress资源。

注:Kubernetes 1.18版本之前,还不存在IngressClassName字段,使用的是kubernetes.io/ingress.class注解。虽然很多控制器仍支持它,推荐不要再使用这一注解,因未来很有可能会被控制器所弃用。

如不存在spec.ingressClassName注解,会使用默认Ingress控制器。通过对相应的IngressClass资源添加ingressclass.kubernetes.io/is-default-class注解来进行指定。

多个Ingress对象

如果指定了多个Ingress对象,Ingress控制器应全数读取并将它们合并为一个连续的配置。但如果指定的是重复或冲突的配置,结果就不一定了。不同Ingress控制器处理方式可能不同。即使是单个实现也可能因不易察觉的因素而出现不同。

Ingress和命名空间

Ingress与命名空间的配合有些方式不那么明显。首先,由于大量的安全问题,Ingress对象仅能指向同一命名空间的一个上游服务。也就是说不能使用Ingress对象指向另一个命名空间的子路径服务。

但不同命名空间中的多个Ingress对象可指向同一主机上的子路径。这些Ingress对象会进行合并,组成Ingress控制器的最终配置。

跨命名空间的操作意味着跨集群全局协调Ingress是有必要的。如果不小心,一个命名空间中的Ingress可能会导致另一个命名空间出现问题(或不确定行为)。

通过Ingress控制器不限制允许哪些命名空间指定哪些主机名或路径。高阶用户可以尝试使用自定义的准许控制器强制实现这种策略。在Ingress展望一节中也会讲到处理这问题的一些Ingress演进。

路径重写

有些Ingress控制器实现可支持路径重写。可使用它来修改所代理的HTTP请求路径。通常用Ingress对象上的注解来指定,应用于所有该对象所指定的请求。例如,如果使用NGINX Ingress控制器,可以指定一个nginx.ingress​.kuber⁠netes.io/rewrite-target: /注解。这有时可让上游服务操作子路径,即使原本它不具备这种功能。

很多实现不仅是支持路径重写,还实现了用正则表达式指定路径。例如NGINX控制器允许使用正则表达式捕获路径的一部分,然后在重写时使用这部分捕获的内容。其实现方式(以使用哪种正则表达式模式)则在各实现中并不相同。

但路径重写并不是银弹,有时可能会产生bug。很多web应用都假定可使用绝对路径在内部进行链接。在这种情况下,托管在/subpath的应用可能会在请求/时出现。而接下来可能会将用户发往/app-path。这时就会存在它是该应用的内部链接(这里本应是/subpath/app-path)还是来自另一个应用。出于这一原因,对复杂应用但凡能避免请不要使用子路径。

TLS 服务

在为网站提供服务时,使用TLS和HTTPS来提升服务越来越有必要了。Ingress(及大部分Ingress控制器)提供了这一支持。

首先,用户需要指定TLS的证书文件和密钥(参见例8-4)。也可通过kubectl create secret tls <secret-name> --cert <certificate-pem-file> --key <private-key-pem-file>来创建一个密钥。

例8-4 tls-secret.yaml

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Secret
metadata:
creationTimestamp: null
name: tls-secret-name
type: kubernetes.io/tls
data:
tls.crt: <base64 encoded certificate>
tls.key: <base64 encoded private key>

上传好证书后,可以在Ingress对象中引用它。这会指定一系统证书以及证书所作用的主机名(参见例8-5)。同样如若多个Ingress对象为同一主机名指定证书,结果是不确定的。

例8-5 tls-ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
spec:
tls:
- hosts:
- alpaca.example.com
secretName: tls-secret-name
rules:
- host: alpaca.example.com
http:
paths:
- backend:
serviceName: alpaca
servicePort: 8080

上传、管理TLS密钥并不简单。此外,证书有时会产生大量的费用。要解决这一问题,有一个非营利证书机构Let’s Encrypt可通过API进行操作。因其是API操作,可配置Kubernetes集群来自动获取并安装TLS证书。配置有些麻烦,但一旦生效,使用会很简单。剩下的部分有一家英国创业公司Jetstack创建了开源项目cert-manager,已入驻CNCF。官网cert-manager.ioGitHub仓库上详细讲解了如何安装和使用cert-manager。

其它Ingress实现

有很多种Ingress控制器实现,都是基于Ingress基础对象添加了各种功能。这是一个非常活跃的生态。

首先,各云厂商有Ingress实现为其云暴露特定的7层负载均衡。这些控制器不是配置在Pod中运行的软件负载均衡,而是获取Ingress对象,使用它们通过API来配置云端负载均衡。这减少了集群的负载以及实施者的管理负担,但通常是收费的。

最常用的Ingress控制器可能是开源的NGINX Ingress控制器。也有基于收费的NGINX Plus的商业控制器。开源控制器基本上是读取Ingress对象,将它们合并到一个NGINX配置文件。然后发信号让NGINX进行使用新配置重启。开源的NGINX控制器有大量的特性和通过注解暴露的选项。

EmissaryGloo是另外两个基于Envoy的Ingress控制器,主要聚焦于API网关领域。

Traefik是一个用Go实现的反向代理,也可用作Ingress控制器。它具有很多特性以及对开发者友好的控制面板。

这只是冰山一角。Ingress生态系统非常活跃,有很多基于Ingress对象提供各种功能的新项目和商业方案。

Ingress展望

我们已经学习到,Ingress对象对配置L7负载均衡提供了非常有用的抽象,但它并没有扩展用户需要的所有功能,各种控制器实现进行的补充。Ingress对很多这种特性定义不足。实现的方式八仙过海,这让在实现之间配置的可迁移性变差。

另一个问题是Ingress的配置很容易出错。多对象合并为不同实现采取不同方式处理冲突打开了大门。此外,跨命名空间的合并也打破了命名空间隔离的想法。

Ingress创建的时候还没出现服务网格(以Istio和Linkerd为代表)。Ingress和服务网格的交集仍在定义中。Service mesh在服务网格一章中会进行详细讲解。

Kubernetes未来的HTTP负载均衡应该是网关API,正由Kubernetes致力于网络的特别兴趣小组(SIG)进行开发。Gateway API项目意在开发一个在Kubernetes中路由的更现代的API。虽然更关注HTTP负载均衡,Gateway也包含了控制4层(TCP)负载均衡的资源。Gateway API的开发还远未成熟,因些强烈推荐大家继续使用Kubernetes中现有的Ingress和Service资源。Gateway API现在的进展请见官网

小结

Ingress是Kubernetes的一套特有系统。它只是一种模式,必须要安装这种模式的控制器实现并单独管理。但它是以实用且成本高效地向用户暴露服务的关键系统。Kubernetes还在不断成熟,Ingress也会不断发展。

本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。

我们已学习过变量和函数,下面来快速了解下指针的语法。然后我们通过将Go中的指针与其它语言中的类进行对比来讲清指针的原理。我们还会学习如何以及何时使用指针、Go中如何分配内存以及正确使用指针及值是如何使Go程序变得更快速、更高效的。

指针快速入门

指针其实就一个存储了值所在内存位置的变量。如果读者学过计算机课的话,可能见过表示内存中如何存储变量的图。如下的两个变量可表示为图6-1:

1
2
var x int32 = 10
var y bool = true

图6-1. 在内存中存储两个变量

图6-1. 在内存中存储两个变量

每个变量都存储在一段或多段连续的内存中,称为内存地址。不同类型的变量所占的内存大小可能是不同的。本例中有两个变量x,它是一个32位的整型,以及布尔类型的y。存储32位的整型需要4个字节,因而x存储于4个字节中,从地址1到地址4。布尔类型只占一个字节(只需用一位来表示true或false,但独立寻址的内存大小是一个字节),因而y存储于地址5位的一个字节中,true通过1进行表示。

指针的内容就是存储了其它变量的地址 。图6-2演示了如何在内存中存储指针:

1
2
3
4
5
var x int32 = 10
var y bool = true
pointerX := &x
pointerY := &y
var pointerZ *string

图6-2:在内存中存储指针

图6-2:在内存中存储指针

虽然不同变量类型占用的内存空间不同,但不管是什么类型的指针都占据相同的大小:也即存储数据在内存中空间的字节数相同。x的指针pointerX存储在位置6,值为1,也即x的地址。类似地y的指针pointerY存储于位置10,值为5,即为y的地址。最后一个变量pointerZ,存储于位置14,值为0,因为它没有指向任何变量。

指针的零值为nil。前面的文章中已经多次使用到了nil,用作切片、字典和函数的零值。所有这些类型都通过指针实现。(还有两种类型,通道和接口,也是用接口实现。我们会在类型、方法和接口一章的快速讲解接口以及并发一章中进行讲解)。在复合类型一章中,nil是表示缺少值的某种类型的无类型标识符。与C语言中的NULL不同,nil不是0的别名,它与数字间不可互转。

警告:在代码块,遮蔽和控制结构一章中提到,nil定义于全局代码块中。因其是在全局代码块中定义,可能会被遮蔽。不要将变量或函数命名为nil,除非你是和同事恶作剧,或是完全不care年终评审。

Go指针的语法部分来自C和C++。因Go自带垃圾回收器,大部分内存管理的痛楚都不存在了。此外,C和C++中一些指针黑活,包括指针运算,在Go语言中都不再允许。

注:Go标准库中有一个unsafe包,可对数据结构执行一些底层运算。虽然在C中操作指针是常见运算,但Go开发者极少使用unsafe。在恶龙三剑客:反射、Unsafe 和 Cgo一章中会进行讲解。

&是地址运算符。放在值类型的前面用于返回所存储值的内存地址:

1
2
x := "hello"
pointerToX := &x

*为间接运算符。放在指针类型的变量前可返回其所指向的值。这称为解引用:

1
2
3
4
5
6
x := 10
pointerToX := &x
fmt.Println(pointerToX) // 打印内存地址
fmt.Println(*pointerToX) // 打印10
z := 5 + *pointerToX
fmt.Println(z) // 打印15

在对指针解引用之前,必须确保指针不为nil。对nil指针解引用会崩溃(panic):

1
2
3
var x *int
fmt.Println(x == nil) // 打印true
fmt.Println(*x) // 程序崩溃

指针类型是用于表示指针的类型。写法为在类型名前加*。指针类型可基于任意类型:

1
2
3
x := 10
var pointerToX *int
pointerToX = &x

内置函数new可创建指针变量。它返回指定类型零值实例的指针:

1
2
3
var x = new(int)
fmt.Println(x == nil) // 打印false
fmt.Println(*x) // 打印0

较少使用new函数。对于结构体,可在结构体字面量前加&创建指针实例。不能在原生类型字面量(数字、布尔值和字符串)或常量前加&,因为它们没有内存地址,仅在编译时存在。要用原生类型的指针时,声明一个变量指向它:

1
2
3
x := &Foo{}
var y string
z := &y

不能获取常的地址有时会带来不便。结构体中包含原生类型指针字段时,就无法直接对字段赋字面量:

1
2
3
4
5
6
7
8
9
10
11
type person struct {
FirstName string
MiddleName *string
LastName string
}

p := person{
FirstName: "Pat",
MiddleName: "Perry", // 该行无法通过编译
LastName: "Peterson",
}

编译这段代码返回如下错误:

1
cannot use "Perry" (type string) as type *string in field value

倘若在"Perry"前添加&,会报如下错误:

1
cannot take the address of "Perry"

解决这个问题有两种方法。第一种上面讲到了,引入一个存储常量值的变量。第二种是编译一个接收布尔值、数值或字符串并返回该类型指针的帮助函数:

1
2
3
func stringp(s string) *string {
return &s
}

借助这个函数,可以改写成这样:

1
2
3
4
5
p := person{
FirstName: "Pat",
MiddleName: stringp("Perry"), // 正常运行
LastName: "Peterson",
}

为什么这样就正常了呢?对函数传递常量时,会将常量拷贝到参数变量。因其是一个变量,就在内存中有一段地址。然后这个函数会返回变量的内存地址。

小贴士:帮助函数会将常量值转化为指针。

不要畏惧指针

学习指针的第一条就是不要畏惧。读者如果习惯了使用Java、JavaScript、Python或Ruby,可能会觉得指针很可怕。但指针其实和读者所熟知的类相似。Go语言中非指针结构体才是异类。

在Java和JavaScript中,原生类型和类不同(Python和Ruby中并没有原始值,而是使用不可变实例来进行模拟)。在将原始值赋给另一个变量或传递给函数或方法时,另一个变量对值的修改不会体现在原变量中,见例6-1。

例6-1 Java中原始变量赋值不共享内存

1
2
3
4
int x = 10;
int y = x;
y = 20;
System.out.println(x); // 打印10

我们再来看将类实例赋值给另一个变量或传递给函数或方法的情况(例6-2使用Python编写,Java、JavaScript和Ruby中相应的代码参见GitHub)。

例6-2 将类实例传递给函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Foo:
def __init__(self, x):
self.x = x


def outer():
f = Foo(10)
inner1(f)
print(f.x)
inner2(f)
print(f.x)
g = None
inner2(g)
print(g is None)


def inner1(f):
f.x = 20


def inner2(f):
f = Foo(30)


outer()

运行这段代码会打印:

1
2
3
20
20
True

这是由于Java、Python、JavaScript和Ruby具有如下特征:

  • 如果对函数传递类的实例且修改其字段值,修改会作用于所传递的变量。
  • 如重新赋值参数,修改不会作用于所传入的变量。
  • 如使用nil/null/None传递参数值,将参数设为其它值不会修改调用函数中的变量。

有人在讲解这一行为时,会说这些语言中类实例通过指针传递。这是不对的。如果真是通过指针传递,第二、三种情况会修改调用函数中的变量。这些语言和Go一样都是值传递。

这里看到各种编程语言中的类实例使用指针实现。在将类实例传入函数或方法时,所拷贝的值是实例的指针。因outerinner1指向相同的内存,inner1中对f的修改会体现在outer的变量中。在inner2f赋一个新的类实例时,会单独创建一个实例且不会影响到outer中的变量。

在Go语言中使用指针变量效果相同。Go与其它语言的差别是可以选择使用原生类型和结构体的指针或是值。大部分情况下应使用值。这会更容易理解数据是在何时以及如何发行修改的。使用值的另一个好处是用值会减少垃圾回收器的工作量。在降低垃圾回收器的工作量一节中会再做讨论。

指针表明参数可变

我们已经知道,Go常量可对字面量表达式添加名称并在运行时进行计算。语言中没有其它声明不可变量值的机制。现在软件工程包含不可变量性。MIT的Software Construction课程总结原因为:“不可变量类型不易产生 bug,更易于掌握,也更能应对变化。可变性会使用理解程序变难,强制合约则更难。”

Go语言中不可变声明的匮乏看起来是个问题,但通过允许选择值和指针参数类型解决了这一问题。在软件构造的课程资料中讲到:“如果仅在方法内部使用可变量对象且对向的引用唯一就没有问题。”Go开发者不是将部分变量和参数声明为不可变,而是通过使用指针来表示参数可变。

因Go一种值传递的编程语言,传入函数的值是一份拷贝。对于原生类型、结构体和数组等非指针类型,这意味着调用函数无法修改其原始值。而调用函数中为原始数据的拷贝,进而保障了原始数据的不可变性。

注:我们会在字典和切片的区别一节中讨论对函数传递字典和切片。

但如果将指针传递给函数的话,函数会得到指针的拷贝。它仍会指向原数据,也就意味着调用函数可修改原数据。

这里有两种潜在情况。

第一是如果将nil指针传递给函数,无法将值变为非空。只能对已赋值的指针重新赋值。乍一听让人困扰,但是有道理的。因内容空间是通过值传递传给函数的,我们无法改变其内存地址,就像我们不能修改int参数值一样。可通过如下程序进行演示:

1
2
3
4
5
6
7
8
9
10
func failedUpdate(g *int) {
x := 10
g = &x
}

func main() {
var f *int // f为nil
failedUpdate(f)
fmt.Println(f) // 打印nil
}

代码运行的流程见图6-3。

图6-3 无法更新nil指针
图6-3 无法更新nil指针
刚开始main中的f是一个nil变量。调用failedUpdate后,我们将f的值,也就是nil,拷贝到参数g中。也就是说g被设置成了nil。然后在failedUpdate中声明了一个变量x,值为10。接着修改failedUpdate中的g指向x。这不会修改main中的f,在退出failedUpdate并返回main时,f仍为nil

第二种情况是在退出函数时希望赋值给指针参数的值依然存在,就必须解引用指针并设置值。修改指针改变的是其拷贝而非原始指针。解引用会将新值放入原始指针和拷贝指针共同指向的内存空间。下面是一段简短演示程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func failedUpdate(px *int) {
x2 := 20
px = &x2
}

func update(px *int) {
*px = 20
}

func main() {
x := 10
failedUpdate(&x)
fmt.Println(x) // 打印10
update(&x)
fmt.Println(x) // 打印20
}

其流程见图6-4。

在这个例子中,我们先将main中的x设置为10。调用failedUpdate时,我们将x的地址拷入参数px。然后,我们在failedUpdate中声明了x2,设置为20。接着将failedUpdate中的px指向x2的地址。在返回main时,x的值未发生改变。在调用update时,我们再次将x的地址拷入参数px。但这次修改的是updatepx所指向的值,也即main中的变量x。返回mainx发生了改变。

图6-4 更新指针的错误方式和正确方式
图6-4 更新指针的错误方式和正确方式

指针非首选

在Go中使用指针应谨慎。前面也讨论过,那样会更难理解数据流并会给垃圾回收器带来额外的工作。可以将传入函数的结构体指针改成让函数实例化并返回结构体(参见例6-3和例6-4)。

例6-3 别这么干

1
2
3
4
5
func MakeFoo(f *Foo) error {
f.Field1 = "val"
f.Field2 = 20
return nil
}

例6-4 应当这么干

1
2
3
4
5
6
7
func MakeFoo() (Foo, error) {
f := Foo{
Field1: "val",
Field2: 20,
}
return f, nil
}

使用指针参数修改变量的唯一场景是在函数接收接口时。在操作JSON时会看到这种用法(在讲解Go的标准库时会说到encoding/json对JSON的支持):

1
2
3
4
5
f := struct {
Name string `json:"name"`
Age int `json:"age"`
}{}
err := json.Unmarshal([]byte(`{"name": "Bob", "age": 30}`), &f)

Unmarshal函数使用JSON字节对切片变量赋值。该函数接收字节切片和interface{}参数。传给interface{}参数的值必须为指针。如若不是,则会报错。这种用法是因为最早Go中没有泛型。这也就导致了根据传入类型指向反序列化的方式不方便,并且无法动态地按传入函数的类型指定返回类型。

而JSON的集成非常广泛,有时Go开发者便将这一API看作常规操作,而非什么特例。

注:通过使用reflect包中的Type类型可以用变量表示类型。reflect包预留在没有其它方法的场景中使用。在恶龙三剑客:反射、Unsafe 和 Cgo中会做讲解。

函数返回值应优先值类型。仅在类型中有状态需要做变更时才使用指针作为返回类型。在标准库一章的io及其朋友们一节会讲到I/O,我们会学到读取或写入数据的缓冲。此外,并发所使用数据须以指针传递。这会在并发一章中讨论。

指针提升性能

如果结构体过大,使用结构体指针作为入参或返回值可改善性能。向函数传递任意大小数据的指针耗时是恒定的。这很容易理解,因为所有数据类型的指针大小相同。对函数传值时数据越大耗时越久。在数据达到10 MB时耗时约一毫秒。

返回指针和返回值的效果更有趣。对于小于1 MB 的数据结构,返回指针类型实际上要慢于值类型。例如,返回一个100字节的数据耗费约10纳秒,而返回该数据结构的指针耗时约30纳秒。一旦数据结构大于1 MB,则出现反转。返回10 MB 的数据约耗时2微秒,而返回其指针仅半微秒多点。

这是非常小的时间维度。对于大部分情况,使用指针和值的这点不同并不会影响到程序的性能。但如果在函数间传递数MB 的数据时,即使数据不可变也请考虑使用指针。

以上数据均使用32GB内存i7-8700电脑进行采样。读者可使用GitHub上的代码自行进行性能测试。

零值和无值

Go中另一种指针的常见用途是区分赋零值和未赋值的变量或字段。如果在你的程序中这点很重要,使用nil指针表示未赋值的变量或结构体字段。

因指针同时表示可变,使用时应注意。一般将指针设置为nil供函数返回,而时使用字典所用的逗号ok语法来返回一个值类型和一个布尔值。

记信如果通过参数或参数中的字段向函数传递nil指针,则无法在函数中对其设置值,因为没有存储该值的空间。如果传入的是非nil值,仅在清楚后果时进行修改。

同样的JSON转化是印证这一规则的特例。在数据与JSON进行互转时(在讲解Go的标准库时会说到encoding/json对JSON的支持),经常需要区别零值和未赋值的情况。这时对结构体中可为空的字段使用指针。

在不操作JSON(或其它外部协议)时,抑制信使用指针字段表示无值的诱惑。虽然指针用于表示无值很方便,但在需要修改值时,应当使用值类型配合布尔值。

字典和切片的区别

在前一章我们了解到,对传入函数的字典做任意修改都会体现在原始变量中。既然我们已经学习了指针,就来讲下原理:在Go运行时中,字典通过结构体的指针实现。传入字典也即向函数拷贝指针。

因此,应避免使用字节作为入参或返回值,对外的API尤其如此。在API设计层面,字典是糟糕的选择,因其没有说明其中包含的是什么值,并没有显式定义字典中的键名,因此知晓的唯一方式是追踪代码。从不可变性的角度来看,字典很糟的原因是只要在追踪了所有与其交互过的函数后才知道其结果。这就达不到API自描述的效果了。如果读者习惯了动态语言,请不要因其它语言缺乏结构而使用字典来替代。Go是一种强类型语言,请使用结构体来替代字典传给函数。(在降低垃圾回收器的工作量一节中讨论内存布局时会讲到推荐使用结构体的另一个原因。)

同时,将切片传递给函数情况更为复杂,对切片内容的任何修改都会体现在原变量中,但使用append修改切片的长度不会体现在原变量中,即使切片的容量本身大于这一长度。这是因为切片由三个字段实现:表示长度的int字段,表示容量的int字段以及一段内存块的指针。图6-5演示了其关系。

图6-5 切片的内存布局

图6-5 切片的内存布局

将切片拷贝给其它变量或是传入函数时,拷贝由长度、容量和指针组成。图6-6展示了两个切片变量指向同一块内存。

图6-6 切片及其拷贝的内存布局

图6-6 切片及其拷贝的内存布局

修改切片中的值改变的是指针所指向的内存,因而变化对拷贝和原切片均可见。图6-7中为内存中的状况。

图6-7 修改切片的内容

图6-7 修改切片的内容

对长度和容量的修改不会体现在原切片中,因为这只发生在拷贝上。修改容量表示指针指向了一段新的更大的内存块。图6-8展示了这两个切片变量分别指向了不同的内存块。

图6-8 修改容量改变了存储

图6-8 修改容量改变了存储

如果对切片进行追加时容量足以放下新切片,拷贝的长度发生变化,新值存储于拷贝和原切片共享的内存块中。但原切片不发生改变。也就是说Go运行时不会让原切片看到这些值,因为它们在原切片的长度之外。图6-9表示了这些值在一个切片变量中可见,而在另一个中不可见。

图6-9 修改切片长度对原切片不可见

图6-9 修改切片长度对原切片不可见

结果就是传入函数的切片内容可修改,但无法重置大小。切片是Go中唯一适用的线性数据结构,经常在程序间进行传递。默认应假定切片未由函数修改。应在函数的文档中说明是否修改了切片的内容。

注:可对函数传递任意大小的切片,原因在于传入函数的数据对于任意大小的切片都相同:两个int值和一个指针。而不能编写接收任意大小数组的函数,原因大于传递的是整个数组,而不是数据的指针。

切片作为入参还有另一个用途:它们是可复用缓冲的理想载体。

将切片用作缓冲

在从外部(比如文件或网络连接)读取数据时,很多编程语言的代码如下:

1
2
3
4
5
6
r = open_resource()
while r.has_data() {
data_chunk = r.next_chunk()
process(data_chunk)
}
close(r)

这种方式的问题在于每次进行while循环的迭代时,虽然每一个只用了一次也要重新分配data_chunk。这会产生大量不必要的内存分配。带内存回收的语言会自动处理这些分配,但在完成操作后还是要进行清理。

虽然Go是带垃圾回收的语言,编写地产的Go代码要求避免不必要的内存分配。我们不是在每次从数据源读取时返回新的分配,而是一次性创建一个切片,将其用作读取数据的缓冲:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
file, err := os.Open(fileName)
if err != nil {
return err
}
defer file.Close()
data := make([]byte, 100)
for {
count, err := file.Read(data)
if err != nil {
return err
}
if count == 0 {
return nil
}
process(data[:count])
}

在将切片传递给函数时无法修改其长度或容量,但我们可以修改到当前升度的内容。在以上代码中,我们创建了一个100字节的缓冲,每次循环时,我们将下个字节块(最多100)拷贝入切片。然后将传入的缓冲交给process。在标准库一章的io及其朋友们一节会讲到I/O。

降低垃圾回收器的工作量

使用缓冲只是减少垃圾回收器工作量的一个例子。程序员眼中的“垃圾”是“不再有指针指向的数据”。一旦某一数据不再有指针指向它,数据所占用的内存即可被复用。如果不回收内存,程序的内存占用量会膨胀到内存溢出。内存回收器的任务是自动监测未使用的内存并进行回收以供复用。Go贴心地为我们提供了垃圾回收器,因为几十年的经验表明很难妥善地手动管理内存。但有了垃圾回收器并不表示可以随意制造垃圾。

如果读者花时间研究过编程语言是如何实现的,就会知道堆和栈。对于不熟悉的读者,栈是一段连续的内存块,执行线程的每次函数调用共享相同的栈。栈上分配内存简单快速。栈指针追踪内存分配的最后位置,通过移动栈指针可分配额外的内存。在调用函数时,会为函数数据创建新的栈桢。本地变量以及传入函数的参数存储在栈上。每个新变量会导致栈指针移动该值的大小。函数退出时,返回值会通过栈拷贝回调用函数,栈指针则会移至退出函数的起始栈帧,回收函数本地变量和参数使用的所有栈内存。

注:Go的不寻常在于它可以在程序运行期间增加栈的大小。这是因为每个协助有自己的栈,而协程由Go运行时而非底层操作系统管理(我们会在并发一章中讨论协程)。这有其优势(Go的初始栈很小、占用更少内存)及劣势(栈需要扩容时,所有数据都会被拷贝,这是缓慢的)。这也使得可能会写出栈反复扩容和收缩的糟糕代码。

要在栈上存储内容,需要知道其在编译时的具体大小。在学习Go中的值类型(原生类型、数组和结构体)时,会发现一个共同点:在编译时都能知道占用的具体内存大小。这也是为什么大小是成为了数组的一部分。因为其大小已知,可分配到栈上,不必放到堆上。指针类型的大小也是固定的,同样存储在栈上。

对于指针指向的数据,规则就更为复杂。要在Go中将指针指向的数据分配到栈上,必须满足一些条件。必须为编译时大小已知的本地变量。不能是函数返回的指针。如果是传入函数的指针,必须要保证这些条件仍能满足。如果大小不固定,无法通过移动栈指针来获取空间。如果返回指针变量,指针指向的内存会在函数返回时失效。在编译器决定数据无法存储于栈上时,可以称为指针指向的数据逃逸出了栈,编译器将数据存储于堆上。

堆是由垃圾回收器管理的内存(在C和C++等编程语言中手动管理)。我们不会讨论垃圾回收器算法的实现细节,但远比移动栈帧要复杂。只要能回溯到栈上的指针类型变量,堆所存储的数据就有效。一旦没有指针指向该数据(或指向该数据的数据),这段数据就会成为垃圾,将由垃圾回收器进行清理。

注:C程序中常见的bug是返回本地变量指针。在C中,这会导致指针指向无效内存。Go编译器更为智能。在发现已返回本地变量的指针时,本地变量的值会存储到堆上。

Go编译器的逃逸分配并不完美。有时可存储在栈上的数据逃逸到了堆上。但编译器要要保守些,不能冒需放到堆上的数据存储到栈上的风险,否则对无效数据的引用会导致内存崩溃。Go的新发行版中改良了逃逸分配。

读者可能会想在堆上存储内容有什么坏处呢?有两个性能相关的问题。第一是垃圾回收器执行操作会耗费时间。追踪堆中所有空闲内存的可用块或哪些已用内存堆还持有有效指针消耗并不算小。这会占用程序执行本可使用的宝贵时间。编写了很多种内存回收算法,粗略分为两类:设计用于高吞吐(在单次扫描中发现尽可能多的垃圾)或低延时(尽快完成垃圾扫描)。Jeff Dean,Google工程化成功的幕后大神,作为联合作者于2013年发表了名为The Tail at Scale的论文。其中论述到系统应优化延时,保持低响应时间。Go运行所使用的垃圾回收器更倾向低延时。每次垃圾回收周期被设计为小于500毫秒。但如果你的Go程序创建了大量的垃圾,那么在一个周期中就无法发现发现的垃圾,这会拖慢回收器并增加内存占用。

注:如果读者对实现细节感兴趣,可以听一听Rick Hudson在2018内存管理国际研讨会上的演讲,讲到了Go垃圾回收器的历史和实现

第二个问题与计算机硬件性质有关。RAM虽然是“随机读取内存”,便读取内存最快速的方式是序列化读取。Go中的结构体切片将数据按序放于内存中。这样加载和处理数据都很快。结构体指针(或字段为指针的结构体)的切片的数据RAM中分散存储,读取和处理就会更慢。Forrest Smith写了一篇深入的博文探讨了这会在多大程度上影响性能。他的数据表明通过指针访问随机存储在内存的数据会慢两个数量级。

这种在写软件时考虑其所运行的硬件的方式称为机械同理(mechanical sympathy)。这个词来赛车界,意思是驾驶员熟知赛车可以压榨出其性能极限。2011年,Martin Thompson将这一词用于软件开发。遵守Go的最佳实践可以自动实现机械同理。

比较下Go与Java的方式。Java中,本地变量和参数和Go一样存储于栈中。但前面也提到过,Java中的对象按指针实现。这表示对每个对象变量实例,仅会将指针分配到栈中,对象中的数据位于堆中。仅原生类型值(数字、布尔值、字符)存储于栈上。这就意味着Java的垃圾回收器要完成大量的工作。同时也表示Java中的列表实际上是一个指针数组的指针。虽然它看起来像是线性数据结构,读取时实际也在内存中横跳,效率打折。Python、Ruby和JavaScript中类似。为解决这一低效问题,Java虚拟机内置了一个智能的垃圾回收器,完成大量的工作,有些是优化吞量,有些优化延时,都具有配置项来完成最佳性能调优。Python、Ruby和JavaScript的虚拟机优化则不足,因而性能受到很大影响。

现在读者已经明白Go为什么很少鼓励使用指针。我们尽可能将内容存放在栈上来减轻垃圾回收器的负载。结构体或原生类型切片的数据在内存按序排列以达到快速访问。在垃圾回收器运行时,对快速返回的优化要多于收集更多垃圾。这种方法的核心是一开始就创建尽量少的垃圾。虽然聚焦于优化内存分配可能看上去是不成熟的优化,Go中地道的方式也是最高效的。

如果想学习堆栈以及Go中逃逸分析的更多知识,有一些很好的博客文章,比如Arden Labs上Bill KennedySegment上Achille Roussel和Rick Branson的文章。

小结

本章稍微深入了一下底层来辅助我们理解指针,指针是什么、如何使用指针,以及最重要的,何时使用指针。下一章中,我们会学习Go语言中方法、接口和类型的实现,与其它语言的差别,以及所具备的能力。

本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。

每种编程语言都需要有开发环境,Go自然也不例外。如果读者之前已经写过Go程序,那么一定已经有环境了,但可能会漏掉一些最新技术和工具。如果是第一次在电脑上配置Go,也不必担心,Go及其支持工具非常简单。在配置好环境并验证后,我们会构建一个简单程序,学习几种构建和运行Go的方式,然后涉足一些简化Go开发的工具和技巧。

安装Go工具

要编写Go代码,首先要下载、安装Go开发工具。工具的最新版请见Go官方网站的下载页面。选择所用平台的下载文件并执行安装。Mac的安装包 .pkg及Windows的安装包 .msi可自动在相应的位置安装Go、移除旧的安装版本,并将Go二进制文件放到默认的执行路径下。

小贴士:如果读者是Mac开发者,可以通过brew install go命令来使用Homebrew安装Go。使用Chocolatey的Windows开发者可以通过choco install golang命令安装Go。

各类Linux和FreeBSD安装包是gzip压缩的tar文件,解压为名为go的目录。将该目录拷贝到 /usr/local ,再将 /usr/local/go/bin添加到$PATH,这样就可以访问go命令:

1
2
3
$ tar -C /usr/local -xzf go1.18.4.linux-amd64.tar.gz
$ echo 'export PATH=$PATH:/usr/local/go/bin' >> $HOME/.profile
$ source $HOME/.profile

注:Go程序编译为单个原生二进制,无需安装其它软件即可直接运行。这与Java、Python和JavaScript这样要求安装虚拟机才能运行程序的语言不同。使用单原生二进制利于Go编写语言的分发。

可以打开命令行输入如下命令验证环境配置是否正确:

1
$ go version

如果配置正确应该会打印出如下内容:

1
go version go1.18.4 darwin/arm64

这表明它是在macOS上Go版本为1.18.4。(Darwin是macOS内部的操作系统,arm64是基于ARM的64位芯片。)

排查 Go 安装的问题

如果显示的不是版本信息而是报错了,很可能是go不在执行路径下,或者是路径中有另一个名为go的程序。在macOS或其它类Unix系统中,可使用which go来查看所执行的go命令。如果未返回任何内容,则需要处理执行路径。

如果使用Linux或FreeBSD,则可能是在32位系统中安装了64位开发工具,或者是使用了错误的芯片架构的开发工具。

Go的工具

所有的Go开发工具都可使用go命令来访问。除了go version,还有编译器(go build)、代码格式化工具(go fmt)、依赖管理工具(go mod)、测试运行工具(go test)以及扫描常见代码错误的工具(go vet)等。在后面的文章中会进行详细讲解。现在我们通过第一个程序Hello World来快速查看最常用的一些工具。

注:自Go语言2009年发布以来,代码组织和依赖管理的方式发生过多次变化。因此读者可能在网上看到一些过时的建议,请注意甄别。

对于现在而言,Go语言开发的规则很简单:读者可以自由组织项目并放在任何位置。

注:在中国大陆通常在安装Go之后还应配置代理进行加速,比如使用七牛云的代理:

1
2
$ go env -w GO111MODULE=on
$ go env -w GOPROXY=https://goproxy.cn,direct

第一个Go程序

我们学习一些使用Go编写程序的基础。在编写的过程中会不断了解Go程序的基本组成。读者可能还不能掌握所有知识,但不必担心,后续的文章会有进一步的讲解。

创建Go模块

首先需要创建放程序的目录。命名为ch1。在命令行中进行创建:

1
2
$ mkdir ch1 
$ cd ch1

在该目录中,运行go mod init命令将该目录标记为一个Go模块:

1
2
$ go mod init hello_world
go: creating new go.mod: module hello_world

在第10章中会更深入地讨论模块,但现在读者只需知道Go项目被称为模块,每个模块在根目录下都有一个go.mod文件。运行go mod init会为我们创建该文件。go.mod中内容如下:

1
2
3
module hello_world

go 1.19

go.mod文件声明了模块的名称以及该模块所支持的最小Go版本,还有模块依赖的其它模块。可将其类比Python中的requirements.txt或Ruby中的Gemfile

不应直接编辑go.mod文件。而是使用go getgo mod tidy命令来管理对该文件的修改。

go build

下面开始打码!打开文本编辑器,键入如下内容,保存到ch1目录的hello.go文件中:

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello, world!")
}

注:这里的缩进存在问题,但稍后读者就会明白为什么会这样了。

我们来快速看下创建的Go文件中的各部分。第一行是包声明。在Go模块中,代码由一个或多个包组成。模块中的main包含启动Go程序的代码。

接下来的是导入语句。import语句列出文件中所引用的包。我们这里使用的是标准库中的fmt(读作fumpt)包。不同于其它语言,Go中只导入整包。不能限定包中的具体类型、函数、常量或变量。

所有的Go程序由main包中的main函数启动。我们使用func main() {声明该函数。和Java、JavaScript及C一样,Go使用花括号标记代码块的起始和结束。
函数体由一行代码组成。表明调用fmt包中的Println函数,参数为"Hello, world!"。有过编程经验的读者应该能猜到该函数的作用。

保存好文件后,回到命令行输入如下命令:

1
$ go build

它会在当前目录创建一个名为hello_world的可执行文件(Windows下为hello_world.exe)。运行该命令,会在屏幕中输出Hello, world!

1
2
$ ./hello_world
Hello, world!

二进制文件的名称与模块中声明一致。若想使用其它名称或是存储在其它位置,使用-o参数。例如,我们希望将代码编译为二进制文件hello,可使用:

1
$ go build -o hello

格式化代码

Go的一大主要设计目录是创建一种高效编写代码的语言。这表示语法要简洁、编译要快速。同时也导致Go的作者们重新考虑了代码格式化。大部分语言在代码的格式上留有巨大的灵活性。但Go不是。强制标准的格式让编写规范源代码的工具相当容易。它简化了编译器并允许创建有一些生成代码的智能工具。

还有另一个好处。开发人员过去在格式之争上浪费了大量的时间。因为Go语言定义了代码格式化的标准方式。Go开发者就无需争论花括号样式Tab还是空格。例如,Go使用tab进行缩进,并在起始花括号与声明或代码块起始命令不在同一行时会报语法错误。

注:很多Go开发人员觉得Go团队定义了一套标准格式,是为了避免争论后来才发现了工具的优势。但Go语言开发负责人Russ Cox曾公开说过更好的工具化才是最初的动机。

Go开发工具包含有一个命令go fmt,它会自动修复代码空白匹配标准格式。但是,它无法修复错误行中的花括号。运行命令如下:

1
2
$ go fmt ./...
hello.go

使用./…告诉Go工具对当前目录及其子目录下的所有文件应用该命令。我们在进一步学习Go的工具时它还会出现。

此时如果打开hello.go,会看到fmt.Println已经采用单制表符进行了缩进。

小贴士:在编译代码前请记得运行go fmt,至少在将修改的代码提交到仓库前要运行该命令。如果忘记,单独提交一次只运行了**go fmt ./…**的修改。对使用 IDE 的读者更简单的方法是对 VSCode 和 GoLand 这些编辑器提前做好配置,这样在每次保存代码时都会自动执行该命令。

分号插入规则

go fmt命令不会修复错误行的花括号问题,这背后的原因是分号插入规则。类似C或Java,Go需要在每条语句后添加分号。但Go开发者不需要自己添加这一分号。Go编译器按照一套极简单的规则自动插入,参见《Effective Go》中的描述:

如果新行之前最后的符号(token )是以下中的一个,词法分析器(lexer)会在其后添加一个分号:

  • 标识符(包含int和float64这样的保留字)
  • 基本字面量,如数字或字符串常量
  • 以下符号之一:break、continue、fallthrough、return、++、–、)或}

有了这一简单规则,可以了解到为会花括号放到错误的位置时会出错。如果这样写代码:

1
2
3
4
func main()
{
fmt.Println("Hello, world!")
}

分号插入规则发现在func main()中以“)”结尾,会将其变成:

1
2
3
4
func main();
{
fmt.Println("Hello, world!");
};

这样就不是有效的Go代码了。

分号插入规则及其对花括号的限制让Go编译器变得更简单、快速,同时也限制定了代码风格。这是睿智的做法。

go vet

有一批bug在代码语法上没有问题,但却很可能是错误的。go工具含有一个go vet命令来检测这类错误。我们修改程序来看它是如何检测到的。修改hello.go中的fmt.Println代码行如下:

1
fmt.Printf("Hello, %s!\n")

fmt.Printf非常类似于C、Java、Ruby及其它语言中的printf 。如果之前没见过printf ,这个函数的第一个参数是一个模板,剩余的参数是模板中占位符的值。

go vet可以检测到的是格式化模板中的每个占位符是否有对应的值。对修改后的代码运行go vet,它会发现其中的错误:

1
2
3
$ go vet ./...
# hello_world
./hello.go:6:2: fmt.Printf format %s reads arg #1, but call has 0 args

这里go vet发现了我们的bug,可以很简单地进行修复。修改hello.go中的第6行如下:

1
fmt.Printf("Hello, %s!\n", "world")

虽然go vet可以捕获一些觉的编程错误,但有些问题无法监测到。所幸有一些第三方的Go代码智能工具弥补了这一缺陷。其中一些知名的质量工具会在第11章中进行讲解。

小贴士:就像我们应该运行go fmt来确保代码的格式正确,运行go vet可扫描到有效代码中的一些bug。它是只是保障高质量代码的第一步。除了本系列文章中的建议,所有的Go开发人员都应当读读Effective GoGo语言文档中的代码审核评论来理解地道的Go代码应该是什么样的。

工具选择

虽然我们在这个小程序时只用到了普通的文本编辑器和go命令,但在写更大的项目时最好使用更高级的工具。大部分编辑器和IDE都有优秀的Go开发工具。如果读者没有个人偏好的工具,最主流的两个Go开发环境是VS Code和GoLand。

Visual Studio Code

如果希望使用免费的开发环境,微软所出的Visual Studio Code 是最佳的选择。自2015年发布以来, VS Code已成为最流行开源代码编辑器。它并没有自带对Go的支持,但可以通过在插件库中下载Go插件来变成Go开发环境。

VS Code对Go的支持依赖于第三方工具。包含Go开发工具、Delve调试器gopls,后者是由Go团队开发的Go语言服务端。在安装Go开发工具包时,Go插件会安装Delve和gopls。

注:什么是语言的服务端?这是一种API标准规范,用于实现智能编辑行为,如代码补全、质量检查或查找代码中所有使用变量或函数之处。可以阅读语言服务端协议来了解语言服务端及其能力的详细知识。

一旦配置好的工具,就可以打开项目使用了。图1-1中展示了项目窗口的外观。Getting started with VS Code Go视频中演示了 VS Code的Go插件。

VS Code

图1-1 Visual Studio Code

GoLand

GoLand是JetBrains专门为Go推出的IDE。虽然JetBrains是出了名的以Java为中心的工具,但丝毫不影响GoLand是一款优秀的Go开发环境。参见图1-2中的GoLand用户界面,它和 IntelliJ、PyCharm、RubyMine、WebStorm、Android Studio或其它JetBrains IDE都很像。它对Go的支持有重合名、语法高亮、代码补全、代码导航、文档弹窗、调试器、代码覆盖率等。除了支持Go,GoLand还包含JavaScript/HTML/CSS和SQL数据库工具。不同于VS Code,GoLand不需要用户下载其它工具就可正常使用。

GoLand窗口

图1-2 GoLand
如果已订阅了IntelliJ Ultimate(或是符合免费证书申请),可通过插件来添加对Go的支持。不然的话就需要付费使用GoLand,并没有免费版。

The Go Playground

Go开发还有一个重要的工具,但无需安装。访问The Go Playground 就可以看到类似图1-3中的界面。如果使用过irbnodepython这些命令行环境,会发现The Go Playground的使用体验非常类似。它可用于测试和分享简单程序。在窗口中输入代码,点击Run按钮运行代码。Format按钮对程序运行go fmt同时更新导入包。Share按钮创建一个唯一URL,可发送给其他人查看该程序或是你自己在未来回来查看代码(这些URL验证下来可保存很长时间,但不要把它用成代码仓库)。

Go Playground

图1-3 The Go Playground

在图1-4中可以看到,可以通过在每个文件之间添加-- filename.go --这样的代码来模拟多文件。甚至可以通过在文件名中添加/来模拟子目录,如-- subdir/my_code.go --

注意The Go Playground实际上是别人的电脑(具体来说是Google的电脑),所有自由度受限。它提供了几种Go版本(通常是当前发行版、上一版和最新的开发版)。只能发起对localhost的网络连接,运行太长或占用过多内存时会停止掉进程。如果程序中用到时间的话,需要考虑到时钟设置为November 10, 2009, 23:00:00 UTC(Go首次发布的日期)。虽然有这些限制,Go Playground对于测试新想法很有用,而且不需要在本地新建项目。在本系列文章中,读者会看到很多The Go Playground的链接,可直接运行代码,无需拷贝到本地。

警告:不要在其中使用敏感信息(如个人身份信息、密码或私钥)!如果点击Share按钮,这些信息会保存到Google的服务器,其他有分享链接的人都可以访问到。如果不慎这么干了,写一封邮件把URL和需要删除的原因发送给security@golang.org

Go Playground多文件

图1-4 The Go Playground支持多文件

Makefile

IDE很好使用,不易于自动化。现代软件开发依赖于反复的自动化构建,可在任何地方、任意时间由任何运行。这类工具是很好的软件工程化实践。它避免了一个历史问题,开发者不对构建问题负责并抛出那句经典台词:“在我的电脑上是好的!”实现的方式是通过脚本指定构建步骤。Go开发者使用make 进行解决。开发者通过它指定一系列构建程序所需要的操作以及它们执行的顺序。读者可能不熟悉make ,它1976年就在Unix系统上用于构建程序了。

ch1目录下创建一个文件Makefile ,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
.DEFAULT_GOAL := build

.PHONY:fmt vet build
fmt:
go fmt ./...

vet: fmt
go vet ./...

build: vet
go build

即使读者之前没用过Makefile ,也不难get到其作用。每个操作被称为一个target.DEFAULT_GOAL定义了在没有运行目标时所运行的操作。上例中运行的是build目标命令。接下来就是目标命令的定义,每个冒号(:)前为目标名。目标名后的单词 (build: vet中的vet ) 是在运行该目标之前所需要运行的目标命令。该目标所执行的任务放在之后的缩进行。.PHONYmake 命令在项目中有同名目录时不至于混淆。

运行make 命令后会输出:

1
2
3
4
$ make
go fmt ./...
go vet ./...
go build

通过这条命令,我们格式化了代码、检查了隐藏的错误并执行了编译。我们可以通过make vet来检查代码,或是只运行make fmt来完成格式化。看起来并没有多大改进 ,但构建前代码及检查代码(或是执行持续集成构建服务中的脚本)可以保障不丢失任何步骤。

Makefile的一个缺点是要求有些严格。必须使用制表符来缩进这些步骤。并且在Windows系统中没有原生支持。如果在Windows电脑上进行Go开发,需要先安装make。最简单的安装方式是先安装包管理工具如 Chocolatey,然后使用它来安装make (对于Chocolatey,命令为choco install make)。

如果想要学习更多有关Makefile的知识,Chase Lambert有一个很好的教程,但使用了一小部分C讲解了相关概念。

保持更新

像其它编程语言一样,Go开发工具会做定期更新。Go程序编译为单独的原生二进制文件,无需担心开发环境的升级引起已部署程序的崩溃。可以在同一台电脑或虚拟机上同时运行多个Go版本编译的程序。

从Go 1.2开始,大约每6个月会发布一个大版本。还有一些按需发布的bug和安全问题修复的小版本。因快速的开发周期以及Go团队对于向后兼容的承诺,Go的发行版倾向于递增而不是扩散的做法。Go兼容性承诺详细描述了Go团队如何规划避免引起崩溃的Go代码。其中说到不会从1开始Go版本不会出现任何语言和标准库层面的向后不兼容,除非涉及到bug或安全问题修复。这一承诺不适用于go命令。go命令的参数和功能有过向后不兼容的修改,且很有可能同样在未来出现。

准备好在电脑上更新Go开发工具时,Mac和Windows用户拥有快速的方法。安装了brewchocolatey的用户可以使用该工具升级。使用安装包安装的用户可在https://go.dev/dl/下载最新版本,它会删除掉老版本并安装新版本。

Linux和BSD用户需要下载最新版本,将老版本移至备份目录,解压新版本,然后再删除老版本:

1
2
3
$ mv /usr/local/go /usr/local/old-go
$ tar -C /usr/local -xzf go1.18.4.linux-amd64.tar.gz
$ rm -rf /usr/local/old-go

注:从技术上来说,无需移动已有安装,只需要删除它再安装新版本即可。但这不能保证“万无一失”。如果在安装新版本时出现问题,最好还有旧版本可以使用。

小结

本文中,我们学习了如何安装、配置Go开发环境。同时讨论了构建Go程序及保障代码质量的一些工具。至此我们已准备好了环境,下一篇文章中我们就开始探讨Go语言的内置类型以及如何声明变量。

现在我们已经有了开发环境并且知道如何管理Odoo服务实例和数据库,可以学习如何创建Odoo插件模块了。

本章我们的主要目标是理解一个插件模块的结构是什么样的以及对其进行补充的典型增量工作流。本章中各节所讨论的各种组件会在后续章节中进行扩充讲解。

本章中,我们将讲解如下内容:

  • 创建和安装一个新的插件模块
  • 完成插件模块的声明
  • 组织插件模块文件结构
  • 添加模型
  • 添加菜单项和视图
  • 添加访问权限
  • 使用脚手架命令来创建模块

技术准备

本章中,我们将预设你已安装了Odoo并且按照第一章 安装Odoo开发环境中各节进行了操作。读者还应熟悉第二章 管理Odoo服务端实例中所描述的发现和安装额外插件模块的知识。

本章中所使用的代码可以从如下GitHub仓库中进行下载:https://github.com/alanhou/odoo14-cookbook/tree/main/Chapter03

Odoo的插件模块是什么?

除框架代码以外,Odoo的所有基础代码都以模块的形式组合在一起。这些模块可以随时从数据库中安装或卸载。这些模块有两大目的。要么是添加新应用/业务逻辑,要么是修改已有应用。简言之,Odoo中的一切都始于模块也终于模块。

Odoo由不同规模的公司所使用,每个公司都有不同的业务流和要求。处理这一问题,Odoo将应用的功能拆分到了不同的模块中。这些模块可按需在数据库中进行加载。基本上,用户可以在任何时间点启用/禁用这些功能。因此,同一软件可以按不同的要求进行调整。查看下面Odoo模块的截屏;该列中第一个模块是主应用,其它的模块为该应用添加功能而设计。让模块列表按应用分类进行分组,进入Apps菜单并按Category分组:

图3.1 – 按category对应用分组

图3.1 – 按category对应用分组

如果计划在Odoo中开发新应用,应为不同功能设置边界。这有助于将你的应用切分为不同的插件模块。既然你已经知道了Odoo中插件模块的用途,我们可以开始构建自己的插件模块了。

创建和安装一个新的插件模块

这一节中,我们将新建一个模块,让其在我们的Odoo实例中可用并进行安装。

准备工作

我们需要准备好一个Odoo实例来开始我们的开发。

如果按照第一章 安装Odoo开发环境从源码轻松安装Odoo一节进行操作的话,Odoo应该在~/odoo-dev/odoo下。为方便讲解,我们假定Odoo安装在该路径下,但是你可以使用其它你自己喜欢的路径。

我们还需要一个位置来安装自己的Odoo模块。就本节而言,我们将使用odoo的同级目录:~/odoo-dev/local-addons。

如何操作…

本章的示例中,我们将创建一个小型的插件模块来管理图书馆中的一系列图书。

如下步骤将创建并安装一个新的插件模块:

  1. 进入到工作目录即你要操作并放置新建的自定义模块的插件目录中:

    1
    2
    $ cd ~/odoo-dev
    $ mkdir local-addons
  2. 为新模块选择一个技术名称并使用该名称作为模块名创建目录。本例中,我们使用my_library:

    1
    $ mkdir local-addons/my_library

    ℹ️模块的技术名称必须是有效的Python标识符,需以字母开头,仅包含字母、数字和下划线。建议在模块名称中只使用小写字母。

  3. 通过添加__init__.py文件来让Python模块可导入:

    1
    $ touch local-addons/my_library/__init__.py
  4. 为Odoo添加一个最小化的模块声明来让其成为一个插件模块。在my_library文件夹下创建__manifest__.py文件并添加如下行:

    1
    {'name': 'My Library'}
  5. 启动Odoo实例,将我们的插件目录添加到插件路径中:

    1
    $ odoo/odoo-bin --addons-path=odoo/addon/,local-addons/

    ℹ️如果在该Odoo命令中添加了 –save 选项,插件路径会被保存到配置文件中。下次你启动服务时,如未提供插件路径选项的话,就会使用它。

  6. 让这个新模块在Odoo中可用;使用管理员登录Odoo,启动开发者模式,然后在Apps顶级菜单中选择Update Apps List。现在Odoo应该就识别到了我们的模块了:
    图3.2 – 更新应用列表对话框
    图3.2 – 更新应用列表对话框

  7. 选择顶部的Apps菜单,在右上方的搜索栏中,删除默认的应用过滤器并搜索my_library,点击它的Install按钮,就完成了安装。

运行原理…

Odoo模块是一个包含代码文件及其它资源的目录。所使用的目录名为模块的技术名称。模块声明中的 name 键对应其标题。

manifest.py是模块的声明文件。它包含一个带有模块元数据的Python字典,有分类、版本、所依赖的模块以及一系列它要加载的数据文件。在这一节中,我们使用了最简化的声明文件,但在真实的模块中,我们需要使用其它的重要键名。这将在下一节完成插件模块的声明是进行讨论。

模块目录必须是Python可导入的,因此即便内容为空也要添加一个__init__.py 文件。要载入模块,Odoo服务要导入它。这会导致__init__.py文件中的代码被执行,它作为运行该模块Python代码的一个入口。因此,它通常会包含加载模块Python文件及子模块的一些导入语句。

已识别模块可通过命令行使用–init或-i选项直接安装。例如,如果希望安装crm和website应用,可使用-i crm,website。这一模块列表在提供插件路径新建数据库时获取到的模块进行初始化设置。可通过Update Module List菜单更新已有数据库。

完成插件模块的声明

声明文件对于Odoo模块非常重要。它包含插件模块重要的元数据并声明了应加载的数据文件。

准备工作

我们应当有一个包含__manifest__.py声明文件的模块来进行操作。你可能要按前一节的步骤来提供一个可以操作的模块。

如何操作…

我们为这个插件模块添加声明文件和一个图标:

  1. 以最相关的键名创建一个声明文件,编辑模块的__manifest__.py文件至如下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    'name': "My library",
    'summary': "轻松管理图书",
    'description': """
    Manage Library
    ==============
    Description related to library.
         """,
    'author': "Alan Hou",
    'website': "https://alanhou.org",
    'category': 'Uncategorized',
    'version': '14.0.1',
    'depends': ['base'],
    'data': ['views/views.xml'],
    'demo': ['demo.xml'],
    }
  2. 为该模块添加一个图标,选择一张PNG图片并将其拷贝至static/description/icon.png

译者注:

1、此时如直接进行更新操作会出现报错,因为 data 和 demo 相关文件尚未添加

2、description 下面的内容无需进行缩进,否则显示样式会存在问题

运行原理…

声明文件中的内容是一个常规的Python字典,包含键和值。我们的示例声明中包含了最为相关的一些键名:

  • name:这是该模块的标题。
  • summary:这是一个单行描述的副标题。
  • description:这是一个以普通文本或重构文本(RST)格式编写的长描述。通常放在三个引号中,Python中使用三个引号来界定多行文本。RST的快速入门指南请见http://docutils.sourceforge.net/docs/user/rst/quickstart.html
  • author:是一个作者姓名的字符串。如果有多个作者的话,一般使用逗号来进行分隔,但注意它仍应是一个字符串,而非Python列表。
  • website:这个 URL 可供人们访问来了解模块或作者的更多信息。
  • category:用于按照兴趣领域组织模块。标准的分类名称列表请见https://github.com/odoo/odoo/blob/14.0/odoo/addons/base/data/ir_module_category_data.xml 。但也可以定义这些名称以外的新分类名称。
  • version:这是该模块的版本号。可供Odoo应用商店用于检测已安装模块的新版本。如果版本号没有以Odoo目标版本号(如14.0)开头,会进行自动添加。但是,如果你显式的声明Odoo目标版本号信息量会更充足,比如用14.0.1.0.0或14.0.1.0来替代1.0.0或1.0。
  • depends:这是该模块所直接依赖的模块技术名称列表。如果你的模块不依赖于任何其它插件模块,那么应至少添加一个base模块。别忘记包含这个模块所引用的XML ID、视图或模块的定义模型。那样可确保它们以正确的顺序进行加载,避免难以调试的错误。
  • data:这是在模块安装或升级时需加载数据文件的相对路径列表。这些路径相对于模块的根目录。通常,这些是XML和CSV文件,但也可以使用YAML格式的数据文件。这些内容会在第六章 管理模块数据中深入讨论。
  • demo:这是加载演示数据文件的相对路径列表。仅在创建数据库时启用了Demo Data标记时才会进行加载。

模块图标使用的图像是位于static/description/icon.png的一个PNG文件。

小贴士: Odoo的大版本一般会有较大的变化,因此如不进行转化或迁移操作的话,一个大版本中构建的模块不大可能与下一个版本进行兼容。因此,在安装模块前确定Odoo的目标版本号就非常的重要了。

扩展内容

也可以使用一个单独的描述文件来替代模块声明中的长描述。自8.0版起,可通过后缀名为 .txt、.rst或.md(Markdown)的README文件来进行替换。此外,可在模块中包含一个description/index.html文件。

这一HTML描述会覆盖掉声明文件中所定义的描述。

还有一些常用的其它键名:

  • licence:默认值为LGPL-3。这一标识符用于模块对外使用的证书。其它可用的证书有AGPL-3、Odoo自有证书v1.0(多用于付费应用)以及其它OSI核准的证书。
  • application:如果为True,模块作为应用列出。通常这用于一个功能区的中心模块。
  • auto_install:若为True,表示这是一个胶水模块,在它所有的依赖模块安装后会被自动安装。
  • installable:若为True(默认值),表示该模块可以进行安装。
  • external_dependencies:有些Odoo模块内部使用了Python/bin库。如果你的模块使用了这些库,需要在这里进行添加。如果所列模块在主机上没有安装的话则会停止该模块的安装。
  • {pre_init, post_init, uninstall}_hook:这是在安装/卸载时调用的Python函数钩子。更多详细示例,请见第八章 高级服务端开发技巧

还有一些应用商店列表使用到的特殊键:

  • price: 这个键用于对插件模块设置价格。值应为整型值。如未设置价格,则表示应用为免费的。
  • currency: 这是价格对应的币种。可用的值有USD 和 EUR。该键的默认值为EUR。
  • live_test_url: 如果想为应用提供一个在线测试URL,可以使用该键来在应用商店中显示在线预览按钮。
  • iap: 如果模块用于提供IAP服务的话,需设置IAP开发者密钥。
  • images: 给出图片路径。该图片用作Odoo应用商店的封面图。

组织插件模块文件结构

一个插件模块包含代码及其它资源文件,如XML文件和图像。大多数这些文件,我们都可以自由选择其在插件目录中的存放位置。

但是,Odoo使用了一些模块结构的惯例,建议遵循这些惯例。

准备工作

我们应当有一个插件模块目录,其中仅包含__init__.py和__manifest__.py文件。本节中,我们假定这个目录为local-addons/my_library。

如何操作…

执行如下步骤来为该插件模块创建基本骨架结构:

  1. 为代码文件创建目录:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    $ cd local-addons/my_library
    $ mkdir models
    $ touch models/__init__.py
    $ mkdir controllers
    $ touch controllers/__init__.py
    $ mkdir views
    $ touch views/views.xml
    $ mkdir security
    $ mkdir wizard
    $ touch wizard/__init__.py
    $ mkdir report
    $ mkdir data
    $ mkdir demo
    $ mkdir i18n
  2. 编辑模块的顶级 init.py文件,这样子目录中的代码会被加载到:

    1
    2
    3
    from . import models
    from . import controllers
    from . import wizard

    这会给我们一个包含最常用入手目录结构,类似下面这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    my_library
    ├── __init__.py
    ├── __manifest__.py
    ├── controllers
    │   └── __init__.py
    ├── data
    ├── demo
    ├── i18n
    ├── models
    │   └── __init__.py
    ├── report
    ├── security
    ├── static
    │   └── description
    │   └── icon.png
    ├── views
    │   └── views.xml
    └── wizard
    └── __init__.py

运行原理…

为大家普及一下背景,Odoo插件模块可以有三种类型文件:

  • Python代码init.py加载,通过该文件导入.py文件及代码子目录。子目录中包含的Python代码,再由其内部的__init__.py导入。
  • 模块声明文件__manifest__.py中data和demo键名所声明供加载的数据文件,通常为用户界面、fixture数据和演示数据中会使用到的XML和CSV文件。还可使用YAML文件,可以包含一些模块加载时运行的过程指令,例如,通过程序生成或更新记录而非在XML文件中加入数据。
  • 网页资源,如JavaScript代码和库文件、CSS、SASS及QWeb/HTML模板文件。这些文件用于在 UI 元素中构建UI组成部分及管理用户动作。它们通过继承主模板的XML文件来进行声明,用于为网页客户端或网站页面添加这些资源。

插件文件在如下目录中进行组织:

  • models/ 包含后端代码文件,用于创建模型及其业务逻辑。推荐每个模型一个文件并使用与模型相同的名称,例如 library.book模型的对应文件为library_book.py。这些在第四章 应用模型中会进行深入讲解。
  • views/ 包含用于用户界面的XML文件,包含动作、表单、列表等等。类似于模型,建议每个模型一个文件。网站模板的文件名通常以_template后缀进行结尾。后端视图在第九章 后端视图中讲解,网站视图在第十四章 CMS网站开发中进行讲解。
  • data/ 包含模块初始数据的其它数据文件。数据文件在第六章 管理模块数据中进行讲解。
  • demo/ 包含带演示数据的数据文件,对于测试、培训或模块评测都非常有用。
  • Odoo会在i18n/ 中查找.pot及.po翻译文件。更多详情参见第十一章 国际化。这些文件无需在声明文件中声明。
  • security/ 包含定义访问控制列表的数据文件,通常是一个ir.model.access.csv文件,也可以是一个XML文件,用于定义权限组及行级权限的记录规则。参见第十章 权限安全来获取更多内容。
  • controllers/ 包含网站控制器的代码文件,用于为模块提供各种功能。网页控制器在第十三章 Web服务端开发中进行讲解。
  • static/ 用于放置所有的网页资源。和其它目录不同,该目录名不只是一种惯例。这一目录中的文件无需用户登录即可对外提供访问。该目录多包含JavaScript、样式表、图像等文件。它们无需在模块声明文件中进行声明,但需要在网页模板中引用。这会在第十四章 CMS网站开发中进行讨论。
  • wizard/ 包含所有与向导有关的文件。在Odoo中,向导用于存放中间数据。在第八章 高级服务端开发技巧中将会进一步学习到向导的
  • report/ : Odoo提供了为诸如销售订单和发票生成PDF文档的功能。该目录存放与PDF报告相关的所有文件。我们在第十二章 自动化、工作流、Email和打印件中会进一步学习 PDF报告的相关知识。

📝在向模块添加新文件时,不要忘记在__manifest__.py(数据文件)或__init__.py(代码文件)文件中进行声明,否则会忽略这些文件而不进行加载。

添加模型

模型中定义的数据结构会用于我们的业务应用。这一节向读者展示如何为模块添加基本模型。

在我们的示例中,希望对图书馆的书籍进行管理。那么,我们需要创建一个代表书籍的模型。每本书包含一个书名及一名或多名作者。

准备工作

我们需要有一个模块来进行操作。如果你按照本章第一节创建和安装一个新的插件模块操作的话,则会有一个名为my_library的空模块。我们将使用它来做进一步讲解。

如何操作…

要添加新模型,我们需要添加一个Python文件来描述它,然后升级插件模块(如未安装则执行安装)。以下使用的路径为我们的插件模块目录中的相对路径(例如,~/odoo-dev/local-addons/my_library/):

  1. 为模块添加一个 Python 文件models/library_book.py,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from odoo import models, fields


    class LibraryBook(models.Model):
    _name = 'library.book'
    name = fields.Char('Title', required=True)
    date_release = fields.Date('Release Date')
    author_ids = fields.Many2many(
    'res.partner',
    string='Authors'
    )
  2. 在模块中添加一个Python初始化文件models/init.py来加载代码文件,内容如下:

    1
    from . import library_book
  3. 编辑模块的Python初始化文件来在模块中加载models/目录:

    1
    from . import models
  4. 从命令行或用户界面中的Apps菜单中升级该Odoo模块。如果你在升级模块时仔细查看服务日志的话,会看到如下行:

    1
    odoo.modules.registry: module my_library: creating or updating database table

然后,就可以在我们的Odoo实例中使用这一新的library.book模型了。有两种方式来查看我们的模型是否在数据库中进行了添加。

第一种方式是可以在用户界面中进行查看。激活开发者工具,然后打开菜单Settings>Technical>Database Structure>Models。然后在那里搜索library.book模型。

第二种方式是查看PostgreSQL数据库中的表数据。可以在数据库中搜索library_book数据表。在下面的代码示例中,我们使用了test-14.0作为数据库。但是你可以修改为你自己的数据库名:

1
2
$ psql test-14.0
test-14.0# \d library_book;

运行原理…

第1步中我们在新建的模块中创建一个了一个Python文件。

Odoo框架有着其自己的ORM框架。这一ORM框架对PostgreSQL数据库进行了抽象。通过继承Odoo Python类Model,我们可以创建自己的模型(数据表)。在定义新模型时,也将其加入到了中央模型仓库中。这让其它模块在之后对其进行修改变得更为容易。

模型有一些以下划线为前缀的通用属性。最重要的一个是 _name,它提供了一个唯一内部标识符来在整个Odoo实例中进行使用。ORM会根据这个属性来生成数据表。本节中,我们使用了 _name = ‘library.book’ 。基于这一属性,该ORM框架会创建一个名为library_book的新数据表。注意ORM在创建新表的名称时会将_name属性中的. 替换为 _ 。

模型字段以类属性的方式进行定义。我们首先以Char类型定义了name字段。模型中有了这一字段会非常方便,因为默认在其它模型中引用该模型时,它会用作记录描述。

我们还使用了一个关联字段的示例author_ids。这在图书和其作者之前建立了一个多对多的关联。一本书可以有多个作者,而每个作者也可以编著多本书。

有关模型有很多可以讨论的地方,我们将在第四章 应用模型中进行深度讲解。

接下来,我们必须让我们的模块可以识别到这一新的Python文件。通过__init__.py文件来实现。因为将代码放在了models/子目录内,我们需要前述的__init__.py 文件来导入该目录,其中又包含另一个__init__.py文件,在其中导入那里的每一个代码文件(本例中只有一个文件)。

对Odoo模型的修改通过升级该模块来实现。Odoo服务会处理由模型类到数据库结构变化的转换。

虽然此处没有提供示例,业务逻辑也可以在这些Python文件中进行添加,或通过对模型类添加新的方法,或继承已有方法,如create() 或 write()。这在第五章 基本服务端开发中进行讨论。

添加菜单项和视图

在有了我们的数据结构所需的模型之后,我们希望用户可以通过用户界面与它们进行交互。本节基于上一节的图书模型进行创建,添加菜单项来显示一个包含列表和表单视图的用户界面。

准备工作

需要有实现library.book模型的插件模块,这在上一节中已经提供到。使用的路径为相对插件模块所处位置的相对路径(如~/odoo-dev/local-addons/my_library/)。

如何实现…

要添加视图,我们将添加一个XML文件,其中包含对于模块的定义。因其是一个新模型,我们还应添加一个菜单选项来让用户可以访问它。

注意下述步骤的顺序也是很重要的,因为其中一些会引用到在之前步骤中定义的ID:

  1. 创建一个XML文件views/library_book.xml来添加描述用户界面的数据记录:

    1
    2
    3
    4
    <?xml version="1.0" encoding="utf-8"?> 
    <odoo>
    <!-- Data records go here -->
    </odoo>
  2. 在插件模块声明文件__manifest__.py中添加新的数据文件,通过views/library_book.xml来进行添加:

    1
    2
    3
    4
    5
    6
    {
    'name': "My Library",
    'summary': "Manage books easily",
    'depends': ['base'],
    'data': ['views/library_book.xml'],
    }
  3. 在library_book.xml文件中添加打开视图的动作:

    1
    2
    3
    4
    5
    <record id='library_book_action' model='ir.actions.act_window'>
    <field name="name">Library Books</field>
    <field name="res_model">library.book</field>
    <field name="view_mode">tree,form</field>
    </record>
  4. 在library_book.xml文件中添加菜单项,让其对用户可见:

    1
    2
    <menuitem name="My Library" id="library_base_menu" />
    <menuitem name="Books" id="library_book_menu" parent="library_base_menu" action="library_book_action"/>
  5. 在library_book.xml文件中添加一个自定义表单视图:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <record id="library_book_view_form" model="ir.ui.view">
    <field name="name">Library Book Form</field>
    <field name="model">library.book</field>
    <field name="arch" type="xml">
    <form>
    <group>
    <group>
    <field name="name"/>
    <field name="author_ids" widget="many2many_tags"/>
    </group>
    <group>
    <field name="date_release"/>
    </group>
    </group>
    </form>
    </field>
    </record>
  6. 在library_book.xml文件中添加自定义树状(列表)视图:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <record id="library_book_view_tree" model="ir.ui.view">
    <field name="name">Library Book List</field>
    <field name="model">library.book</field>
    <field name="arch" type="xml">
    <tree>
    <field name="name"/>
    <field name="date_release"/>
    </tree>
    </field>
    </record>
  7. 在library_book.xml文件中添加自定义搜索选项:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <record id="library_book_view_search" model="ir.ui.view">
    <field name="name">Library Book Search</field>
    <field name="model">library.book</field>
    <field name="arch" type="xml">
    <search>
    <field name="name"/>
    <field name="author_ids"/>
    <filter string="No Authors" name="without_author" domain="[('author_ids','=',False)]"/>
    </search>
    </field>
    </record>

在Odoo中新增模型时,用户默认没有访问权限。必须要为新模型定义访问权限才能进行访问。本例中我们还没有定义访问权限,因而用户无法访问到这个新模型。没有权限菜单和视图也无法展示。所幸的是有一个快捷方式。通过切换为超级用户模式,可以无需访问权限查看这些菜单。

注: 从版本号12开始, admin 用户必须要获取相应的访问权限才能在用户界面中访问我们的模型。

以超级用户访问 Odoo

通过将admin转换为超级用户superuser,你就可以不受访问权限的限制,因此无需授予访问权限即可访问所有菜单和视图。要将admin用户转换为superuser,先激活开发者模式。然后在开发者工具选项中,点击Become Superuser选项。

以下截图供您参考:

成为超级管理员

图3.3 – 启动超级用户模式

在成为超级用户之后,你的菜单会显示一个条状背景,如下图所示:

超级管理员条状图案

图3.4 – 超级用户模式

这时如果你试着升级模块,则会看到一个新的菜单选项(可能会需要刷新浏览器)。点击Books菜单会打开图书模型的列表视图,如下图所示:

图书菜单

图3.5 – 访问图书的菜单

运行原理…

在底层中,用户界面由存储在特定模型中的记录所定义。前两步中创建了一个空的XML文件用于定义待加载的记录,然后将它们添加到模型的数据文件列表中来供安装。

数据文件可以放在该模型目录中的任意位置,但按照习惯用户界面的数据文件在views/子目录中定义。通常,这些文件的名称是基于模型的名称的。本例中,我们为library.book模型创建用户界面,因此我们创建了views/library_book.xml文件。

下一步是定义在客户端用户界面主区域中显示的窗口动作。该动作有一个由 res_model定义的目标模型,name属性用于在用户打开动作时向用户所显示的标题。这些都是基本属性。窗口动作还支持其它属性,让我们对视图渲染方式拥有更多的控制,比如显示什么视图,为可用的记录添加过滤器或设置默认值。这些会在第九章 后端视图中进行讨论。

通常,数据记录使用标签定义,在本例中我们为ir.actions.act_window模型创建了一条记录。这会创建窗口动作。

类似地,菜单项存储在ir.ui.menu模型中,我们可以使用标签来进行创建。但是在Odoo中有一个名为的快捷标签,因此我们在本例中进行了使用。

以下是菜单项的主要属性:

  • name:这是要显示的菜单项文本。
  • action:这是要执行的动作的标识符。我们使用前一步中所创建的窗口动作的ID。
  • sequence:这用于设置同级菜单项的显示顺序。
  • parent:这是父级菜单项的标识符。本例中的菜单项没有父级,即它会显示在顶级菜单中。
  • web_icon: 这一属性用于显示菜单的图标。图标仅在Odoo企业版中才显示。

至此,我们还没有在模型中定义任何视图。但是,如果这时你升级了该模型,Odoo会自动地实时为你创建视图。可是,我们一定会想要控制视图的展示内容,因此,在接下来的两步中创建了一个表单视图和一个树状视图。

这两个视图以 ir.ui.view 模型上的记录进行定义。我们所使用的属性如下:

  • name:这是标识视图的标题。在Odoo的源码中,你会发现这里重复使用了XML ID,但是你完全可以添加一个更易于阅读的名称作为标题。

    小贴士: 如果省略了name字段,Odoo会使用模型名称及视图类型来生成一个。对于新模型的标准视图这完全没有问题。在继承视图时建议使用一个更具说明性的名称,因为这会让你在Odoo用户界面上查找具体视图时更为方便。

  • model:这是目标模型的内部标识符,和_name属性中的所定义的名称一致。

  • arch:这是视图架构,实际定义结构的地方。这里不同类型的视图会有不同。

表单视图在顶级

元素中定义,它的画布是一个两列网格。在表单内,元素用于在垂直方向上编排字段。两个组会生成包含元素所定义字段的两列。字段根据数据类型使用其默认组件,但可以借助widget属性来指定具体使用的组件。

树状视图要简单些,它们以包含用于各列中显示的元素的顶级元素定义。

最后,我们添加了搜索视图来在右上角的搜索框中扩展搜索选项。在顶级的标签中,可以包含元素。字段元素是在搜索视图中可用于搜索的其它字段。过滤器元素为可通过点击激活的预置过滤条件。这些话题会在第九章 后端视图中进行讨论。

添加访问权限

在添加新的数据模型时,需要定义谁可以创建、读取、更新和删除记录。在创建一个全新的应用时,这可能还包含新用户组的定义。因此,如果用户没有访问权限的话,Odoo应不会显示该菜单和视图。在上一节中,我们将admin用户转换为超级用户来访问这个菜单。在学完本节后,你就可以直接以admin用户来访问我们图书模块的菜单和视图了。

这一节使用上一节中定义的图书模型,会定义一个用户安全组来控制谁能够访问或修改图书记录。

准备工作

我们会需要上一节中实现了library.book模型的插件模块,因为在这一节我们会为其添加权限规则。使用的是插件模块所在位置(例如~/odoo-dev/local-addons/my_library/)的相对路径。

如何操作…

我们要在本节中添加的安全规则如下:

  • 所有人均可阅读图书记录
  • 名为Librarians的新用户组拥有创建、阅读、更新和删除书籍记录的权限

要进行实现,我们需要执行如下步骤:

  1. 创建一个名为security/groups.xml的文件并添加如下内容:

    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="utf-8" ?>
    <odoo>
    <record id="group_librarian" model="res.groups">
    <field name="name">Librarians</field>
    <field name="users" eval="[(4, ref('base.user_admin'))]" />
    </record>
    </odoo>
  2. 添加一个名为security/ir.model.access.csv的文件并加入如下内容:

    1
    2
    3
    id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
    acl_book,library.book_default,model_library_book,,1,0,0,0
    acl_book_librarian,library.book_librarian,model_library_book,group_librarian,1,1,1,1
  3. 在__manifest__.py中添加这两个数据文件:

    1
    2
    3
    4
    5
    6
    7
    # ...
    'data': [
    'security/groups.xml',
    'security/ir.model.access.csv',
    'views/library_book.xml'
    ],
    # ...

在实例中更新插件后即可使用新定义的权限规则了。

运行原理…

我们提供了两个新数据文件并添加到了插件模块的声明文件中,这样安装或升级该模块时会在数据库中加载它们:

  • security/groups.xml文件通过创建一条res.groups记录来定义一个新权限组。我们还通过admin用户的引用ID base.user_admin为其授予了Librarians的权限,这样admin用户将拥有library.book模型的权限。
  • ir.model.access.csv通过组来关联模型的权限。第一行group_id:id列为空,表示该规则适用于所有人。最后一行授予了我们刚刚创建的组的成员所有权限:

小贴士: 声明文件中data版块内文件的顺序非常重要,创建权限组的文件必须要在列出安全权限的文件之前加载,因为安全权限的定义依赖于组的存在。因为视图可具体到权限组,我们推荐将组的定义文件放在该列表中会更为保险。

其它内容

使用脚手架命令来创建模块

在新建 Odoo 模块时,需要设置一些范本代码。为帮助大家快速新建模块,Odoo提供了scaffold(脚手架)命令。

本节展示如何使用scaffold命令新建一个模块,它会创建一个目录和文件结构来供使用。

准备工作

我们将在一个自定义模块目录中新建一个插件模块,因此需要已安装Odoo并且给自定义模块一个目录。假定Odoo安装的位置为/odoo-dev/odoo,我们的自定义模块会放置在/odoo-dev/local-addons目录中。

如何操作…

我们将使用scaffold命令来创建范本代码。按照如下步骤来使用scaffold命令新建一个模块:

  1. 切换工作目录到想要放置模块的地方。它可以是你所选择的任意目录,但需要在一个插件路径中才可进行使用。按照我们在前面小节中所选择的目录,应该是这样的:

    1
    $ cd ~/odoo-dev/local-addons
  2. 为这个新模块选择一个技术名称,并使用scaffold命令来创建它。本例中,我们选择的是my_module:

    1
    $ ~/odoo-dev/odoo/odoo-bin scaffold my_module
  3. 编辑默认模块声明文件__manifest__.py并修改相应的值。肯定想要改的有name键中的模块标题。

以下是生成的插件模块的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ tree my_module/
my_module/
├── controllers
│   ├── controllers.py
│   └── __init__.py
├── demo
│   └── demo.xml
├── __init__.py
├── __manifest__.py
├── models
│   ├── __init__.py
│   └── models.py
├── security
│   └── ir.model.access.csv
└── views
├── templates.xml
└── views.xml

5 directories, 10 files

现在应编辑生成的各文件来适应你对新模块的需求。

运行原理…

scaffold命令创建基于一个模板的新模块结构。

默认,这个新模块在当前工作目录中进行创建,但我们也可以指定一个目录来创建该模块,将其作为一个额外的参数进行传递。

参考如下示例:

1
$ ~/odoo-dev/odoo/odoo-bin scaffold my_module ~/odoo-dev/local-addons

此处使用了default模板,但也可以为网站主题编写的theme模板。可以使用-t选项来选择一个指定的模板。我们也可以使用包含模板的一个目录来作为路径。

这表示我们可以通过scaffold命令来使用自己的模板。内置的模板位于./odoo/cli/templates这个Odoo子目录中。用类似下面的命令来使用我们自己的模板:

1
$ ~/odoo-dev/odoo/odoo-bin scaffold -t path/to/template my_module

默认,在/odoo/cli/templates目录中有两个模板。一个是default模板,另一个是theme模板。但可以创建我们自己的模板,像以上命令那样用**-t**来使用它。

第一章 安装Odoo开发环境中,我们学习了如何使用源码中所带的标准核心插件配置Odoo实例。本章主要讲解如何对Odoo实例添加非核心或自定义插件。在Odoo中,可以通过多个目录加载插件。此外,推荐使用单独的目录加载第三方插件或自定义的插件,以避免与Odoo核心模块产生冲突。甚至Odoo企业版也是一种类型的插件目录,需要像加载普通插件目录那样对其进行加载。

本章中,我们将讲解如下内容:

  • 配置插件路径
  • 标准化你的实例目录布局
  • 安装及升级本地插件模块
  • 通过GitHub安装插件模块
  • 对插件应用修改
  • 应用及尝试建议的拉取请求

📝有关用语

本书中,我们会交叉使用插件(add-on)、模块(module)、应用(app)或插件模块(add-on module)。它们都是指可通过用户界面在Odoo中安装的Odoo应用或扩展应用。

配置插件路径

通过addons_path参数的配置,可以在 Odoo 中加载自己的插件模块。在Odoo初始化一个新数据库时,它会在addons_path配置参数中给定的这些目录中搜索插件模块。addons_path会在这些目录中搜索潜在的插件模块。

addons_path中所列出的目录预期应包含子目录,每个子目录是一个插件模块。在数据库初始化完成后,将能够安装这些目录中所给出的模块。

准备工作

这一部分假定你已经准备好了实例并生成了配置文件,如在第一章 安装Odoo开发环境在一个文件中存储实例配置一节所描述。Odoo的源码存放在/odoo-dev/odoo中,而配置文件存放在/odoo-dev/myodoo.cfg中。

如何配置…

按如下步骤在实例的addons_path中添加~/odoo-dev/local-addons目录:

  1. 编辑你的实例配置文件,即 ~/odoo-dev/myodoo.cfg。

  2. 定位到以addons_path =开头的一行,默认应该会看到如下内容:

    1
    addons_path = ~/odoo-dev/odoo/addons

    译者注: 当前默认生成的配置文件中为绝对路径

  3. 修改该行,添加一个逗号(英文半角),并接你想要添加为addons_path的目录名称,如以下代码所示:

    1
    addons_path = ~/odoo-dev/odoo/addons,~/odoo-dev/local-addons
  4. 在终端中重启实例

    1
    $ ~/odoo-dev/odoo/odoo-bin -c my-instance.cfg

运行原理…

在重启 Odoo 时,会读取配置文件。addons_path变量的值应为一个逗号分隔的目录列表。可接受相对路径,但它们是相对于当前工作目录的,因此应在配置文件中尽量避免。

至此,我们仅在Odoo中列出了插件目录,但~/odoo-dev/local-addons中尚不存在插件模块。即使在该目录中新增了插件模块,Odoo也不会在用户界面中显示这一模块。为此,你需要执行一个额外的操作,在下一部分更新插件模块列表中会进行讲解。

📝这背后的原因是在初始化新数据库时,Odoo在可用模块中自动列举了自定义模块,但如若在数据库初始化之后新增模块,就需要像更新插件模块列表一节中那样手动更新可用模块列表。

扩展知识…

在首次调用 odoo-bin脚本来初始化新数据库时,可以传递一个带逗号分隔目录列表的–addons-path命令行参数。这会以所提供插件路径中所找到的所有插件来初始化可用插件模块列表。这么做时,要显式地包含基础插件目录(odoo/odoo/addons)以及核心插件目录(odoo/addons)。与前面稍有不同的是本地插件目录不能为空(译者注: 请先阅读下面的小贴士),它必须要至少包含一个子目录,并包含插件模块的最小化结构。

第三章 创建Odoo插件模块中,我们会来看如何编写你自己的模块。同时,这里有一个生成内容来满足Odoo要求的快捷版黑科技:

1
2
3
4
$ mkdir -p ~/odoo-dev/local-addons/dummy
$ touch ~/odoo-dev/local-addons/dummy/__init__.py
$ echo '{"name": "dummy", "installable": False}' > \
~/odoo-dev/local-addons/dummy/__manifest__.py

你可以使用–save选项来保存路径至配置文件中:

1
2
3
$ odoo/odoo-bin -d mydatabase \
--addons-path="odoo/odoo/addons,odoo/addons,~/odoo-dev/local-addons" \
--save -c ~/odoo-dev/my-instance.cfg --stop-after-init

本例中,使用相对路径不会有问题,因为它们会在配置文件中转化为绝对路径。

📝注: 因为Odoo仅当从命令行中设置路径时在插件路径的目录中查看插件,而不是在从配置文件中加载路径的时候,dummy已不再必要。因此,你可以删除它(或保留到你确定不需要新建一个配置文件时)。

标准化你的实例目录布局

我们推荐你在开发和生产环境都使用相似的目录布局。这一标准化会在你要执行运维时体现出用处,它也会缓解你日常工作的压力。

这一部分创建将相似生命周期或相似用途的文件分组放在标准化子目录中的目录结构。

📝仅在希望以相似的文件结构管理开发和生产环境时才需要学习本节。如果不需要,可以跳过本节。

此外,不必严格按照本节中相同的目录结构。请自由按照自己的需求来调整这一结构。

如何标准化…

创建所推荐实例布局,需要执行如下步骤:

  1. 为每个实例创建一个目录:

    1
    2
    $ mkdir ~/odoo-dev/projectname
    $ cd ~/odoo-dev/projectname
  2. 在名为env/的子目录中创建一个Python虚拟环境对象:

    1
    $ python3 -m venv env
  3. 创建一些子目录,如下:

    1
    $ mkdir src local bin filestore logs

    这些子目录的功能如下:

    • src/:包含Odoo本身的一个拷贝,以及一些第三方插件项目(我们在下一步中添加了Odoo源码)
    • local/:用于保存你针对具体实例的插件
    • bin/:包含各类帮助可执行shell脚本
    • filestore/:用于文件存储
    • logs/(可选):用于存储服务日志文件
  4. 克隆Odoo并安装所需依赖包(参见第一章 安装Odoo开发环境获取更多内容):

    1
    2
    $ git clone -b 14.0 --single-branch --depth 1 https://github.com/odoo/odoo.git src/odoo
    $ env/bin/pip3 install -r src/odoo/requirements.txt
  5. 以bin/odoo保存如下shell脚本:

    1
    2
    3
    4
    5
    6
    #!/bin/sh
    ROOT=$(dirname $0)/..
    PYTHON=$ROOT/env/bin/python3
    ODOO=$ROOT/src/odoo/odoo-bin
    $PYTHON $ODOO -c $ROOT/projectname.cfg "$@"
    exit $?
  6. 让该脚本可执行:

    1
    $ chmod +x bin/odoo
  7. 创建一个空的本地模块dummy:

    1
    2
    3
    4
    $ mkdir -p local/dummy
    $ touch local/dummy/__init__.py
    $ echo '{"name": "dummy", "installable": False}' >\
    local/dummy/__manifest__.py
  8. 为你的实例生成配置文件:

    1
    2
    3
    $ bin/odoo --stop-after-init --save \
    --addons-path src/odoo/odoo/addons,src/odoo/addons,local \
    --data-dir filestore
  9. 添加一个.gitignore文件,用于告诉GitHub排除这些给定目录,这样Git在提交代码时就会忽略掉这些目录,例如 filestore/, env/, logs/和src/:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # dotfiles, with exceptions:
    .*
    !.gitignore
    # python compiled files
    *.py[co]
    # emacs backup files
    *~
    # not tracked subdirectories
    /env/
    /src/
    /filestore/
    /logs/
  10. 为这个实例创建一个Git仓库并将已添加的文件添加到Git中:

    1
    2
    3
    $ git init
    $ git add .
    $ git commit -m "initial version of projectname"

运行原理…

我们生成了一个有明确标签目录和独立角色的干净目录结构。我们使用了不同的目录来存储如下内容:

  • 由其它人所维护的代码(src/中)
  • 本地相关的具体代码
  • 实例的文件存储(filestore)

通过为每个项目建一个virtualenv环境,我们可以确保该项目的依赖文件不会与其它项目的依赖产生冲突,这些项目你可能运行着不同的Odoo版本或使用了不同的第三方插件模块,这将需要不同版本的Python依赖。当然也会带来一部分磁盘空间的开销。

以类似的方式,通过为我们不同的项目使用不同的Odoo拷贝以及第三方插件模块,我们可以让每个项目单独的进行演化并仅在需要时在这些实例上安装更新,因此也减少了引入回退的风险。

bin/odoo允许我们不用记住各个路径或激活虚拟环境就可以运行服务。这还为我们设置了配置文件。你可以在其中添加其它脚本来协助日常工作。例如,可以添加一个脚本来检查运行实例所需的第三方项目。

有关配置文件,我们仅展示了这里需要设置的最小化选项,但很明显可以做更多设置,例如数据库名、数据库过滤器或项目所监听的端口。有关这一话题的更多信息,请参见第一章 安装Odoo开发环境

最后,通过在Git仓库中管理所有这些,在不同的电脑上复制这一设置及在团队中分享开发内容变得相当容易。

📝加速贴士

要加速项目的创建,可以创建一个包含空结构的模板仓库,并为每个新项目复制(fork)该仓库。这会省却你重新输入bin/odoo脚本、.gitignore及其它所需模板文件(持续集成配置、README.md、ChangeLog等等)所花费的时间。

参见内容

如果你喜欢这种方法,我们建议你尝试第三章 服务器部署中的使用 Docker 运行 Odoo 一部分的内容。

扩展知识…

复杂模块的开发要求有各类配置选项,在想要尝试任何配置选项时都会要更新配置文件。更新配置文件常常是一件头痛的事,避免它的一种方式是通过命令行传递所有配置选项,如下:

  1. 手动激活虚拟环境:

    1
    $ source env/bin/activate
  2. 进入Odoo源代码目录:

    1
    $ cd src/odoo
  3. 运行服务:

    1
    ./odoo-bin --addons-path=addons,../../local -d test-14 -i account,sale,purchase --log-level=debug

第3步中,我们直接通过命令行传递了一些参数。第一个是–addons-path,它加载Odoo的核心插件目录addons,以及你自己的插件目录local,在其中你可以放自己的插件模块。选项-d会使用test-14数据库或者在该数据库不存在时新建一个数据库。选项-i 会安装会计、销售和采购模块。接着,我们传递了log-level选项来将日志级别提升为debug,这样日志中会显示更多的信息。

📝通过使用命令行,你可以快速地修改配置选项。也可以在Terminal中查看实时日志。所有可用选项可参见第一章 安装Odoo开发环境,或使用-help命令来查看所有的选项及各个选项的描述。

安装并升级本地插件模块

Odoo 功能的核心来自于它的插件模块。Odoo自带的插件是你所拥有的财富,同时你也可以从应用商店下载一些插件模块或者自己写插件。

这一节中,我们将展示如何通过网页界面及命令行来安装和升级插件模块。

对这些操作使用命令行的主要好处有可以同时作用于一个以上的插件以及在安装或升级的过程中可以清晰地浏览到服务端日志,对于开发模式或编写脚本安装实例时都非常有用。

准备工作

确保你有一个运行中的 Odoo 实例,且数据库已初始化、插件路径已进行恰当地设置。在这一部分中,我们将安装/升级一些插件模块。

如何安装升级…

安装或升级插件有两种方法-可以使用网页界面或命令行。

通过网页界面

可按照如下步骤来使用网页界面安装新的插件模块到数据库中:

  1. 使用管理员账户连接实例并打开Apps菜单
    Apps 页面图2.1 – Odoo应用列表

  2. 使用搜索框来定位你想要安装的插件。这里有一些帮助你完成该任务的操作指南:

    • 激活Not Installed过滤器
    • 如果你要查找一个具体的功能插件而不是广泛的功能插件,删除Apps过滤器
    • 在搜索框中输入模块名的一部分并使用它来作为模块过滤器
    • 你会发现使用列表视图可以阅读到更多的信息
  3. 点击卡片中模块名下的Install按钮。

注意有些Odoo插件模块具有外部Python依赖,如果你的系统中未安装该Python依赖,那么 Odoo 会中止安装并显示如下的对话框:

pyldap 安装依赖图2.2 – 外部库依赖的警告
译者注: 按正常安装不会出现一错误,需通过 pip uninstall pyldap 才能复现这一错误

修复这一问题,仅需在你的系统中安装相关的Python依赖即可。

要升级已安装到数据库的模块,使用如下步骤:

  1. 使用管理员账户连接到实例

  2. 打开Apps菜单

  3. 点击Apps:
    图2.3 – Odoo应用列表
    图2.3 – Odoo应用列表

  4. 使用搜索框来定位你所安装的插件。有如下的小贴士:

    • 激活Installed过滤器
    • 如果你要查找一个具体的功能插件而不是广义的功能插件,删除Apps过滤器
    • 在搜索框中输入部分插件模块的名称并按下 Enter 来使用它作为模块过滤器。例如,输入CRM并按下 Enter 来搜索CRM应用
    • 你会发现使用列表视图可以阅读到更多的信息
  5. 点击卡片右上角的的三个点,然后点击Upgrade选项:

图2.4 – 升级模块的下拉链接 

图2.4 – 升级模块的下拉链接

激活开发者模式来查看模块的技术名称。如果你不知道如何激活开发者模式,请参见第一章 安装Odoo开发环境

查看模块技术名称

图2.5 – 应用的技术名称

在激活开发者模式之后,它会以红色显示模块的技术名称。如果你使用的是Odoo社区版,会看到一些带有Upgrade按钮的应用。这些是Odoo企业版的应用,要想安装/使用它们,需要购买一个证书。

通过命令行

在数据库中安装新插件,可执行如下步骤:

  1. 查找插件的名称。这是包含__manifest__.py文件的目录名,不带前面的路径。

  2. 停止实例。如果你在操作生产数据库,请进行备份。

  3. 运行如下命令:

    1
    $ odoo/odoo-bin -c instance.cfg -d dbname -i addon1,addon2 --stop-after-init

    如果在配置文件中进行过设置可以省略掉-d dbname。
    译者注: 请将addon1,addon2替换为你所要安装的插件名

  4. 重启实例

升级数据库中已安装的插件,可执行如下步骤:

  1. 查找待更新的插件模块名称。这是包含__manifest__.py文件的目录名,不带前面的路径。

  2. 停止实例。如果你在操作生产数据库,请进行备份。

  3. 运行如下命令:

    1
    $ odoo/odoo-bin -c instance.cfg -d dbname - u addon1 --stop-after-init

    如果在配置文件中进行过设置可以省略掉-d dbname。

  4. 重启实例

运行原理…

插件模块的安装和升级是两个紧密关联的操作,但有一些重要的区别,在下面两部分中进行了强调:

插件安装

在安装插件时,Odoo以提供的名称检查它的可用插件列表中未安装插件。它还会检查该插件的依赖,并且如果有依赖的话,它会在安装插件前递归安装这些依赖。

单个模块的安装包含如下步骤:

  1. 如果存在,运行插件preinit钩子
  2. 从Python源代码中加载模型定义并在必要时更新数据库结构(参见第四章 应用模型了解更多信息)
  3. 加载插件的数据文件并在必要时更新数据库内容(参见第六章 管理模块数据了解更多信息)
  4. 如果实例中启用了演示数据则安装插件演示数据
  5. 如果存在,运行插件postinit钩子
  6. 运行对插件视图定义的验证
  7. 如果启用了演示数据及测试,运行该插件的测试(参见第十八章 自动化测试用例了解更多信息)
  8. 在数据库中更新模块状态
  9. 从插件的翻译文件中更新数据库中的翻译(参见第十一章 国际化了解更多信息)

📝preinit和postinit钩子分别使用pre_init_hook和post_init_hook键名在__manifest__.py文件中定义。这些钩子用于在插件模块的安装之前及之后触发Python函数。参见第三章 创建Odoo插件模块了解更多有关 init 钩子的知识。

插件升级

升级插件时,Odoo以给定的名称在可用的插件模块列表中检查已安装插件。它还会检查该插件的反向依赖(即依赖于所升级插件的那些插件)。如果存在,则也会对它们进行递归升级。

单个插件模块的升级过程包含如下步骤:

  1. 如果存在,先运行插件模块的预迁移步骤(参见第六章 管理模块数据了解更多信息)
  2. 从Python源码中加载模型定义并在必要时更新数据库结构(参见第四章 应用模型了解更多信息)
  3. 加载插件的数据文件并在必要时更新数据库内容(参见第六章 管理模块数据了解更多信息)
  4. 如果实例中启用了演示数据更新插件演示数据
  5. 如果模块有任何迁移方法的话,运行插件模块的后置迁移步骤(参见第六章 管理模块数据了解更多信息)
  6. 运行对插件视图定义的验证
  7. 如果启用了演示数据并启用了测试,运行该插件的测试(参见第十八章 自动化测试用例了解更多信息)
  8. 在数据库中更新模块状态
  9. 从插件的翻译文件中更新数据库中的翻译(参见第十一章 国际化了解更多信息)

📝注意更新未安装的插件模块时什么也不会做。但是安装已安装的插件模块会重新安装该插件,这会通过一些包含数据的数据文件产生一些预期外的问题,这些文件应由用户进行更新而非在常规的模块升级处理时进行更新(参见第六章 管理模块数据中使用noupdate和forcecreate标记部分的内容)。通过用户界面不存在错误的风险,但通过命令行时则有可能发生。

扩展知识…

要当心依赖的处理。假定有一个实例你想要对其安装sale、sale_stock和sale_specific插件,sale_specific依赖于sale_stock,而sale_stock依赖于sale。要安装这三者,你只需要安装sale_specific,因为它会递归安装sale_stock和sale这两个依赖。要升级这三者,需要升级sale,因为这样会递归升级其反向依赖,sale_stock和sale_specific。

管理依赖另一个比较搞的地方是在你向已经安装了一个版本的插件添加依赖的时候。我们继续通过前例来理解这一问题。想像一下在sale_specific中添加了一个对stock_dropshipping的依赖。更新sale_specific插件不会自动安装新的依赖,也不会要求安装sale_specific。在这种情况下,你会收到非常糟糕的错误消息,因为插件的Python代码没有成功加载,而插件的数据和模型表则已存在于数据库中。要解决这一问题,你需要停止该实例并手动安装新的依赖。

从GitHub安装插件模块

GitHub是第三方插件一个很好的来源。很多Odoo合作伙伴使用GitHub来分享他们内部维护的插件,而Odoo社区联盟(OCA)在GitHub上共同维护着几百个插件。在你开始编写自己的插件之前,确保查看是否已有可直接使用的插件或者作为初始以继续扩展的插件。

这一部分向你展示如何从GitHub上克隆OCA的partner-contact项目并让其中所包含的插件模块在我们实例中可用。

准备工作

假设你希望对客户(partner) 表单添加新的字段。默认Odoo客户模型不包含gender字段。如果要添加gender字段,需要新建一个模块。所幸邮件列表中有人告诉你有partner_contact_gender这么一个插件模块,由OCA作为partner-contact项目的一部分进行维护。

本部分中所使用的路径反映了我们在标准化你的实例目录布局一节中所推荐的布局。

如何安装…

按照如下步骤来安装partner_contact_gender:

  1. 进入项目目录:

    1
    $ cd ~/odoo-dev/my-odoo/src
  2. 在src/目录中克隆partner-contact项目的14.0分支:

    1
    2
    $ git clone --branch 14.0 \
    https://github.com/OCA/partner-contact.git src/partner-contact
  3. 修改插件路径来包含该目录并更新你的实例中的插件列表(参见本章中的配置插件路径更新插件模块列表小节)。instance.cfg中的addons_path一行应该是这样的:

    1
    2
    3
    4
    addons_path = ~/odoo-dev/my-odoo/src/odoo/odoo/addons, \
    ~/odoo-dev/my-odoo/src/odoo/addons, \
    ~/odoo-dev/my-odoo/src/, \
    ~/odoo-dev/local-addons
  4. 安装partner_contact_gender插件(如果你不知道如何安装该模块,参见前面的小节,安装并升级本地插件模块

运行原理…

Odoo社区联盟的所有代码仓库都将他们自己的插件放在单独的子目录中,这与Odoo对插件路径中目录的要求是一致的。因此,只需复制某处的仓库并将其添加到插件路径中就够了。

扩展知识…

有些维护者遵循不同的方法,每个插件模块一个仓库,放在仓库的根目录下。这种情况下,需要新建一个目录,在这个目录中添加插件路径并克隆所需维护者的插件到该目录中。记住在每次添加一个新仓库克隆时要更新插件模块列表。

对插件应用修改

GitHub上可用的大部分插件需要进行修改并且不遵循Odoo对其稳定发行版所强制的规则。它们可能进行漏洞修复或改善,包含你提交的问题或功能请求,这些修改可能会带来数据库模式的修改或数据文件和视图中的更新。这一部分讲解如何安装升级后的版本。

准备工作

假定你对partner_contact_gender报告了一个问题并收到通知说该问题已在partner-contact项目14.0分支的最近一次修订中得以解决。这种情况下,你可以使用最新版本来更新实例。

如何修改…

要对来自GitHub的插件进行源的变更,需执行如下步骤:

  1. 停止使用该插件的实例。

  2. 如果是生产实例请进行备份(参见第一章 安装Odoo开发环境管理Odoo服务端数据库一节)。

  3. 进入克隆了partner-contact的目录:

    1
    $ cd ~/odoo-dev/my-odoo/src/partner-contact
  4. 为该项目创建一个本地标签,这样万一出现了崩溃还可以进行回退:

    1
    2
    $ git checkout 14.0
    $ git tag 14.0-before-update-$(date --iso)
  5. 获取源码的最新版本:

    1
    $ git pull --ff-only
  6. 在数据库中更新partner_contact_gender插件(参见安装并升级本地插件模块一节)

  7. 重启实例

运行原理…

通常,插件模块的开发者不时会发布插件的最新版本。这一更新一般包含漏洞修复及新功能。这里,我们将获取一个插件的新版本并在我们的实例中更新它。

如果git pull –ff-only失败的话,可以使用如下命令回退到前一个版本:

1
$ git reset --hard 14.0-before-update-$(date --iso)

然后,可以尝试git pull(不添加–ff-only),它会产生一个合并,但这表示你对插件做了本地修改。

其它内容…

如果更新这一步崩溃了,参见第一章 安装Odoo开发环境从源码更新Odoo一节获取恢复的操作指南。记住要保持首先在生产数据库的拷贝上进行测试。

应用及尝试建议的拉取请求

在GitHub的世界中,拉取请求(PR)是由开发者所提交的请求,这样项目维护人员可以添加一些新的开发。比如一个 PR 可能包含漏洞修复或新功能。这些请求在拉取到主分支之前会进行审核和测试。

这一部分讲解如何对你的 Odoo 项目应用一个PR来测试漏洞修复的改进。

准备工作

在前一节中,假定你对partner_contact_gender 报告了一个问题并收到一条通知在拉取请求中问题已修复,尚未合并到项目的14.0分支中。开发人员要求你验证PR #123中的修复状况。你需要使用这一分支更新一个测试实例。

不应在生产数据库直接使用该分支,因此先创建一个带有生产数据库拷贝的测试环境(参见第一章 安装Odoo开发环境)。

如何操作…

应用并测试一个插件的GitHub拉取请求,需要执行如下步骤:

  1. 停止实例

  2. 进入partner-contact所被克隆的目录:

    1
    $ cd ~/odoo-dev/my-odoo/src/partner-contact
  3. 为该项目创建一个本地标签,这样万一出现崩溃时你可以回退:

    1
    2
    $ git checkout 14.0
    $ git tag 14.0-before-update-$(date --iso)
  4. 拉取pull请求的分支。这么做最容易的方式是使用PR编号,在开发者与你沟通时你应该可以看到。在本例中,这个拉取请求编号是123:

    1
    $ git pull origin pull/123/head
  5. 在你的数据库中更新partner_contact_gender1插件模块并重启该实例(如果你不知道如何更新该模块的话请参见安装并升级本地插件模块一节)

  6. 测试该更新 - 尝试重现问题,或测试你想要的功能。

如果这不能运行,在GitHub的PR页面进行评论,说明你做了什么以及什么不能运行,这样开发者可以更新这个拉取请求。

如果它没有问题,也在PR页面说下;这是PR验证流程中非常重要的一部分;这会加速主分支中的合并。

运行原理…

我们在使用一个GitHub功能,使用pull/nnnn/head分支名称来通过编号进行拉取请求的拉取,其中nnnn是PR的编号。Git pull命令会合并远程分支到我们的分支,在我们基础代码中应用修改。在这之后,我们更新插件模块、对其测试并向作者报告修改是成功或是失败。

扩展知识…

如果你想要同步测试它们,你可以针对相同仓库的不同拉取请求重复本节中的第4步。如果你对结果很满意,可以创建一个分支来保留对应用了改变的结果的引用:

1
$ git checkout -b 14.0-custom

使用一个不同的分支会有助于记住你没有使用GitHub的版本,而是一个自定义的版本。

📝git branch命令可用于列出你仓库中的所有本地分支。

从这开始,如果需要应用来自GitHub中14.0分支的最近一个审核版本,需要在拉取时不使用–ff-only:

1
$ git pull origin 14.0

配置Odoo开发环境有很多种方式。本章中提供了其中的一种,你肯定可以在网上找到其它的教程讲解其它方法。请记住本章中所讲解的是开发环境,与生产环境的要求是不同的。

如果你是一个Odoo开发新手,必须要了解Odoo生态的方方面面。第一部分会给出这些方面的简短介绍,然后我们就会进入到Odoo开发所需的安装。

本章中,我们将讲解如下主题:

  • 了解Odoo生态系统
  • 源码轻松安装Odoo
  • 管理Odoo服务端数据库
  • 在文件中存储实例配置
  • 启用Odoo开发者工具
  • 更新插件模块列表

参考安装脚本:Github

了解Odoo生态系统

Odoo为开发者提供了开箱即用的模块结构。它强大的框架有助于开发者很快地构建项目。在开启成为成功的Odoo开发者之旅以前,应该要熟悉Odoo生态中的一些特性。

Odoo版本

Odoo有两个版本。第一个是社区版,完全开源,另一个是企业版,需要支付授权证书费用。不同于其它软件供应商,Odoo企业版仅仅是在社区版基础上添加了一些附加特性或新应用的高级应用。基本上,企业版运行于社区版之上。社区版采用Lesser General Public License v3.0 (LGPLv3)许可证书,并带有企业版企业资源计划(ERP)的所有基础应用,如销售、客户关系管理(CRM)、发票、采购、网站构建器等等。而企业版采用 Odoo 企业版许可证书,这是一个自有证书。Odoo 企业版带有很多高级功能如完整的财务、studio、基于IP的语音传输(VoIP)、移动端响应式设计、电子签名、营销自动化、快递与银行的集成以及IoT等。企业版还为你提供无限的漏洞修复支持。下图显示了企业版依赖于社区版,这也是为什么使用企业版时需要用到社区版:

图1.1 – 社区版和企业版的差别

 

图1.1 – 社区版和企业版的差别

读者可以访问https://www.odoo.com/page/editions查看这两个版本的完整对比。

📝Odoo有数量庞大的社区开发人员,这也是你在应用商店中看到有大量的第三方应用(模块)的原因。有些免费应用使用Affero General Public License version 3 (AGPLv3)许可证书。如果你的应用依赖于这些应用就不能使用其自有证书。Odoo自有证书的应用仅能在拥有LGPL或其它自有证书的模块基础上进行开发。

Git仓库

Odoo的完整代码托管在GitHub上。可以在这里对稳定版提交漏洞/问题。还可以通过提交拉取请求(Pull Request - PR)来提议添加新功能。Odoo有许多个仓库,参见下表来获取更多信息:

仓库 用途
https://github.com/odoo/odoo 这是 Odoo 的社区版。对公众开放。
https://github.com/odoo/enterprise 这是 Odoo 的企业版。仅对Odoo 官方合作伙伴开放。
https://github.com/odoo-dev/odoo 这是不断开发中的仓库。对公众开放。(已废弃)

每年,Odoo会发布一个大版本(长期支持(LTS)版本)和数个小版本。小版本多用于Odoo的在线SaaS服务,也就是说Odoo的SaaS用户可以更早地使用到这些功能。GitHub 上大版本分支的名称像14.0, 13.0和12.0,而小版本分支名称有saas-14.1和saas-14.2。小版本多用于Odoo 的SaaS平台。master分支处于开发中,不稳定,因此,不建议在生产环境中使用它,因为它可能导致数据库的崩溃。

Runbot

Runbot是Odoo的自动化测试环境。在Odoo的Github 分支中有新提交时,Runbot会拉取最新的修改并并创建最近4个提交的构建。这里,你可以测试所有的稳定版和开发中的分支。甚至可以使用到企业版并测试它的开发分支。

每个构建有不同的背景色,表明测试用例的状态。绿色背景表示所有的测试用例成功运行,用户可以测试该分支,而红色背景表示在这个分支上有些测试用例出错了,有些功能在该构建上可能出现崩溃。可以查看到所有测试用例的日志,会显示在安装过程中所发生的具体问题。每个构建有两个数据库。数据库all安装了所有的模块,而数据库base仅安装了Odoo的基础模块。每个构建均安装了基本演示数据,因此可以快速进行测试而无需额外的配置。

ℹ️使用如下 URL 来访问runbot:http://runbot.odoo.com/runbot

以下账户信息可用于访问任一runbot构建:

  • 登录ID: admin 密码: admin
  • 登录ID:demo 密码: demo
  • 登录ID: portal 密码: portal

📝这是公共测试环境,因此有时可能会有其它用户使用/测试你所测试的相同分支。

Odoo应用商店

Odoo在几年前发布了应用商店,当即大热。现在,那里托管着22,000多个不同的应用。在应用商店中,可以找到大量的针对不同版本的免费和付费应用。包含不同垂直业务领域的具体解决方案,如教育、食品行业和医药业。它还包含一些继承了已有Odoo应用或添加了新功能的应用。应用商店还为Odoo网站构建器提供了大量的美观的主题。在第三章 创建Odoo插件模块中,我们将学习如何为你的自定义模块设置价格和币种。

可以通过如下链接访问Odoo应用商店:https://www.odoo.com/apps。

📝Odoo开源了很版本13和14的主题。在此前的版本均为付费主题。也就是在 Odoo 13和14中无需花费额外费用即可下载、使用这些漂亮的主题。

Odoo社区联盟(OCA)

Odoo社区联盟(OCA)是一个开发/管理Odoo社区模块的非营利组织。所有的OCA模块都开源并由Odoo社区成员维护。在OCA的GitHub账户下,可以找到针对不同Odoo应用的多个仓库。除Odoo模块外,它还包含很多工具、迁移库、会计本地化等等。

以下是OCA官方GitHub账号的URL:https://github.com/OCA。

Odoo官方帮助论坛

Odoo 拥有一个非常强大的框架,大量的操作只需通过使用/激活选项或遵循指定的模式即可实现。因此,如果你碰到了一些技术问题或是对一些复杂用例不确定,那么就可以在Odoo官方帮助论坛上询问。这个论坛上有大量活跃的开发人员,包含一些Odoo官方的员工。

在如下 URL 上可以搜索或提交你的问题:https://help.odoo.com/。

Odoo的eLearning平台

最近Odoo发布了一个全新的eLearning平台。该平台上有大量的视频讲解如何使用各类Odoo应用。在编写本书时,平台上还没有技术相关视频,只是一些介绍功能的视频。

eLearning平台的网址为:https://www.odoo.com/slides。

通过源码轻松安装Odoo

强烈推荐使Linux Ubuntu操作系统来安装Odoo,因为这是Odoo测试、调试和安装Odoo企业版所使用的操作系统,此外大部分的Odoo开发人员都使用GNU/Linux,使用GNU/Linux而非Windows或macOS会更有机会获取操作系统相关问题的社区支持。

也推荐使用和生产环境相同的环境(相同发行版和版本号)进行开发。这样可以避免讨厌的“彩蛋”,比如在部署当天发现有个库有一个预料外的版本,会有一些不同和不兼容的问题。如果你的工作站使用不同的操作系统,一种推荐的方式是在工作站上配置虚拟机(VM)并在 VM 上安装 GNU/Linux发行版。

📝Ubuntu已成为微软商店中的一个应用,如果不希望切换操作系统的话也可以使用它。

本书假定读者使用的是Ubuntu 18.04 LTS版,但也可以使用其它的GNU/Linux系统。不论你选择哪个Linux发行版,都应该要有一个从命令行使用它的概念,有系统运维相关知识显然会更好。

相关文章:Odoo 14安装简明教程(CentOS)

准备工作

假设读者已运行了Ubuntu 18.04并已拥有 root 密码或配置了sudo拥有root访问权限。下面的部分中,我们将安装Odoo的相关依赖并通过GitHub下载Odoo源码。

ℹ️有些配置需要使用到系统登录用户,这里我们在需要用到用户名时在命令行中使用$(whoami)。这个shell命令可以在所键入命令中替换为登录用户。

如果你有GitHub账户的话有些操作一定会更容易。如果还没有GitHub账户,请访问https://github.com并创建账户。

如何安装…

使用源码安装 Odoo,需要按照如下的步骤:

  1. 运行如下命令来安装主要依赖:

    1
    2
    $ sudo apt-get update
    $ sudo apt install git python3-pip build-essential wget python3-dev python3-venv python3-wheel libxslt-dev libzip-dev libldap2-dev libsasl2-dev python3-setuptools libpq-dev -y
  2. 下载并安装wkhtmltopdf:

    1
    2
    $ wget https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.bionic_amd64.deb
    $ sudo dpkg -i wkhtmltox_0.12.5-1.bionic_amd64.deb

    如果以上命令出现了报错,通过如下命令可强制安装依赖:

    1
    $ sudo apt-get install -f
  3. 此时安装PostgreSQL数据库

    1
    $ sudo apt install postgresql -y
  4. 配置PostgreSQL

    1
    $ sudo -u postgres createuser --superuser $(whoami)

    译者注: 如果报错说明服务未启动createuser: could not connect to database template1: could not connect to server: No such file or directory

    1
    sudo systemctl start postgresql
  5. 配置git(以下信息请自行修改):

    1
    2
    $ git config --global user.name "Your Name"
    $ git config --global user.email youremail@example.com
  6. 克隆 Odoo 基础代码:

    1
    2
    3
    $ mkdir ~/odoo-dev
    $ cd ~/odoo-dev
    $ git clone -b 14.0 --single-branch --depth 1 https://github.com/odoo/odoo.git
  7. 创建一个odoo-14.0 虚拟环境并启用:

    1
    2
    $ python3 -m venv ~/venv-odoo-14.0
    $ source ~/venv-odoo-14.0/bin/activate
  8. 在venv中安装Odoo的Python依赖:

    1
    2
    $ cd ~/odoo-dev/odoo/
    $ pip3 install -r requirements.txt
  9. 创建并启动第一个Odoo实例:

    1
    2
    $ createdb odoo-test
    $ python3 odoo-bin -d odoo-test -i base --addons-path=addons --db-filter=odoo-test$
  10. 在浏览器中访问http://localhost:8069(虚拟机请修改为对应的 IP 地址),并使用admin账户和密码admin来进行登录

    📝如需RTL(文字从右向左)的支持,请使用如下命令安装node 和 rtlcss :

    1
    2
    sudo apt-get install nodejs npm -y
    sudo npm install -g rtlcss

运行原理…

第1步中,我们安装了一些核心依赖。这些依赖包含各类工具,如git, pip3, wget, Python设置工具等。这些核心工作帮助我们使用简单命令安装其它Odoo依赖。

第2步中,我们下载并安装了wkhtmltopdf包,用于在Odoo中将销售订单、发票等报告打印为PDF文档。Odoo 14.0要求使用wkhtmltopdf的0.12.5版本,这一版本可能在当前Linux发行版中并未包含。所幸wkhtmltopdf的维护者在http://wkhtmltopdf.org/downloads.html为我们提供了针对各发行版的预构建包,通过该URL可进行下载及安装。

PostgreSQL配置

第3步中,我们安装了PostgreSQL数据库。

第4步中,我们通过系统登录用户名新建了一个数据库。$(whoami) 用于获取系统用户名,-s选项用于授予超级用户权限。我们来了解下为什么需要这些配置。

Odoo使用psycopg2 Python库来与PostgreSQL数据库建立连接。通过psycopg2库访问PostgreSQL数据库。Odoo使用如下的默认值:

  • 默认psycopg2尝试使用本地连接的当前用户相同的用户名来连接数据库,这会启动无密码认证(在开发环境中这样很好)
  • 本地连接使用Unix域套接字
  • 数据库服务监听5432端口

这样就好了!PostgreSQL现在就做好了与Odoo建立连接的准备。

因为这个是开发服务器,我们对用户授予了–superuser的权限。对生产实例,可以在命令行中使用–createdb来代替–createdb以进行权限限制。在生产服务上–superuser权限会给予黑客在一些部署代码中找到更多漏洞的机会。

如果想使用不同的数据库用户,则需提供该用户的密码。通过在创建用户时在命令行传递–pwprompt标记来实现,此时命令行会提示你输入密码。

如果用户已存在而你又想要为其设置密码(或修改已忘记的密码),可以使用如下命令:

1
$ psql -c "alter role $(whoami) with password 'newpassword'"

如果这个命令执行报错提示数据库不存在,那是因为你没按照操作步骤的第4步创建一个与当前登录名相同的数据库。不必担心,仅需使用 –dbname选项来添加已有数据库名,例如 –dbname template1。

Git配置

开发环境我们使用GitHub上的Odoo源代码。借助于git可以轻松地在各个Odoo版本间进行切换。同时可以使用git pull命令拉取最新的修改。

在第5步中,我们配置了git用户信息。

第6步中,我们从Odoo官方GitHub仓库下载了源代码。使用的是git clone命令来下载Odoo源代码。指定了单分支这样只需要下载14.0版本的分支即可。同时还使用了–depth 1来避免下载该分支的完整历史提交。这些选项会让源代码下载变得快速,你也可以在需要的时候省略掉这些选项。

Odoo开发者还推荐nightly构建,以tar文件和发行包的形式出现。使用git clone最主要的优势是在源代码树中提交了新的bug修复时可以对仓库直接进行更新。还能够轻松地测试所推荐的修复并追踪回退,这样会让bug报告更为精确、对开发者也更有帮助。

📝如果你可以访问企业版源代码,也可以将其下载到~/odoo-dev目录下面单独的文件夹中。

虚拟环境

Python虚拟环境或简称virtualenv,是隔离的Python工作空间。这些对于Python开发者非常有用,因为它们允许在Python解释器版本上安装不同版本的Python库。

你可以使用python3 -m venv ~/newvenv命令来创建所需数量的环境。这会在指定位置创建一个newenv目录,其中包含一个bin/子目录和一个lib/python3.6(译者注: 或你安装的其它 Python 3版本)子目录。

在第7步中,我们在~/venv-odoo-14.0目录下新建了一个虚拟环境。这就是我们为Odoo准备的独立Python环境。Odoo的所有Python依赖都在这一环境中进行安装。

要启用虚拟环境,我们需要使用source命令。通过source ~/venv-odoo-14.0/bin/activate,我们激活了该虚拟环境。

安装 Python包

Odoo的源代码中有一系列Python依赖,位于requirements.txt中。第8步中,我们通过pip3 install命令安装了所有这些依赖。

这样就可以了,此时即可运行Odoo实例。

启动实例

到了你期待已久的时刻了。要启动我们的第一个实例,第9步中首先我们新建了一个空数据库,然后使用odoo-bin以及如下命令行参数启动Odoo实例:

1
python3 odoo-bin -d odoo-test -i base --addons-path=addons --db-filter=odoo-test$

可以通过在odoo-bin的前面添加./来省略掉python3,因其是一个可执行的Python脚本,如下:

1
./odoo-bin -d odoo-test –i base --addons-path=addons --db-filter=odoo-test$

odoo-bin后使用了如下命令行参数:

  • -d database_name:默认使用这一数据库。
  • –db-filter=database_name$:仅尝试连接匹配所提供正则表达式的数据库。一个Odoo安装可以为使用不同数据库的多个实例提供服务,通过这一参数限制可用的数据库。最后的那个$很重要,因为在匹配模式中使用了正则表达式,这会避免选择以相同的指定字符串开头的名称。
  • –addons-path=directory1,directory2,…:Odoo通过这一逗号分隔列表中的目录来查找插件(add-on)。在实例创建的时候扫描该列表来添加实例中可用的插件模块列表。如果希望使用Odoo企业版,请在这一选项中添加其目录。
  • -i base: 用于安装base模块。通过命令行创建数据库时需要使用到。

如果你使用了与Linux当前登录用户不同的数据库用户,则需要再传递如下的参数:

  • –db_host=localhost: 使用TCP连接数据库服务
  • –db_user=database_username: 使用指定的数据库登录用户
  • –db_password=database_password: 这是用于认证PostgreSQL服务的密码

使用–help可获取所有可用选项的一个总览。我们在本章后面还会来了解odoo-bin脚本更多的知识。

Odoo在一个空数据库上启动时,它会首先创建一个支持其操作所需的数据库结构。还会扫描插件路径来查找可用的插件模块,并将一些内容插入到数据库的初始记录中。这包括admin用户及默认admin密码,在登录时将使用到。

在浏览器中访问http://localhost:8069/( 译者注: 非本地请自行修改为 IP 地址)会访问到你所新创建的实例的登录页面,如下图所示:

图1.2 - Odoo实例的登录页面

图1.2 - Odoo实例的登录页面

这是因为Odoo内置有HTTP服务器。默认,它监听TCP端口8069上的所有本地网络接口。

管理Odoo服务端数据库

在使用Odoo时,你的实例所有的数据都存储在PostgreSQL数据库中。可以使用你习惯的任意标准数据库管理工具,但Odoo为一些常用操作提供了一个网页界面。

准备工作

我们假定你的工作环境已配置好,并且已运行了一个实例。

如何管理…

Odoo数据库管理界面提供创建、复制、删除、备份和恢复数据库的工具。还有一种修改主控密码(master密码)的方式,用于保护对数据库管理界面的访问。

访问数据库管理界面

需要执行如下步骤来访问数据库:

  1. 进入实例的登录页面(如果已登录请先登出)。
  2. 点击Manage Databases链接。这会导航至http://localhost:8069/web/database/manager(也可以在浏览器中直接访问这个URL)。

图1.3 – 数据库管理器

图1.3 – 数据库管理器

设置或修改主控密码

如果已经以默认值设置了实例且尚未像下面讲解的那样做过修改,数据库管理页面会显示一条警告,告诉你还没有设置主控密码并建议你通过直接点击链接来进行设置:

图1.4 – Master密码警告

图1.4 – 主控密码警告

需要执行如下步骤来设置master:

  1. 点击Set Master Password按钮。会弹出一个对话框来要求你提供新的主控密码:
    设置 Master 密码对话框图1.5 – 设置新主控密码对话框
  2. 输入一个复杂的新密码并点击Continue

如果已设置了主控密码,点击页面底部的Set Master Password按钮来进行修改。在弹出的对话框中输入老的主控密码再输入新密码,然后点击Continue。

📝主控密码是在admin_password键下的服务端配置。如果启动服务时没有指定配置文件,会在~/.odoorc中生成新的配置文件。查看下一部分获取更多有关配置文件的信息。

创建新数据库

这个对话框用于创建一个由当前Odoo服务处理的新数据库实例:

  1. 在数据库管理窗口中,点击页面底部的Create Database按钮。会弹出如下对话框:
    创建数据库页面图1.6 – 新建数据库对话框

  2.  填写表单,如下:

    • Master Password:这是这一实例的主控密码。
    • Database Name:输入所想要创建的数据库名称。
    • Email: 在此处添加email地址;在稍后用作用户名。
    • Password:输入你想为新实例所设置的admin用户密码。
    • Phone Number:设置电话号码(可选)。
    • Language:在下拉列表中选择你希望新数据库默认安装的语言。Odoo会自动加载所选语言的翻译。
    • Country:在下拉列表中选择主租户的国家。选择这一项后会自动做一些配置,如公司的币种。
    • Demo data:勾选获取演示数据。这对于运行交互式测试或为用户设置演示项目非常有用,但针对包含生产数据的数据库设计时则不应勾选。

    ℹ️如果想要使用该数据库来运行模块的自动化测试(参见第七章 调试),则需要有演示数据,因为Odoo中的大多数自动化测试依赖于这些记录来成功运行。

  3. 点击Continue按钮并等待新数据库初始化完成。然后会被重定向到该实例并以管理员进行连接。

问题处理:如果被重定向到了登录页,这可能是因为向Odoo传递了–db-filter选项并且与新数据库名不匹配造成的。注意odoo-bin start会默默地进行这一操作,仅让当前数据库可用。解决这一问题,只需不使用start命令来对Odoo进行重启,在本章中的通过源码轻松安装 Odoo 一节展示过这一做法。如果你有一个配置文件(参见本章后面的在一个文件中存储实例配置一节),那么请检查db_filter未进行设置或设置一个与新数据库名称匹配的值。

复制数据库

通常你已经有一个数据库了,会希望使用它做一存储过程的试验或运行测试,但又不修改已有数据。这里的方案很简单:复制该数据库并在拷贝上运行测试。可以按需重复多次:

  1. 在数据库管理页面,点击Duplicate Database:
    Duplicate Database图1.7 – 复制数据库对话框

  2. 填写表单如下:

    • Master Password:这是Odoo服务的主控密码
    • New Name:给予拷贝的名称
  3. 点击Continue按钮。

  4. 可以在数据库管理页面点击新创建的数据库名称来访问该数据库的登录页面。

删除数据库

在完成测试之后,如想要清理掉所复制的数据库。通过如下步骤来进行清理:

  1. 在数据库管理页面,点击想删除的数据库名称旁的Delete Database链接,会出现下图中的对话框:
    Delete Database图1.8 – 删除数据库弹窗
  2. 填写表单并输入 Master Password,即Odoo服务的主控密码。
  3. 点击Delete按钮。

📝小心!数据丢失!

如果你选择了错误的数据库,并且没有备份,则无法恢复损失的数据。

备份数据库

需执行如下步骤来创建备份:

  1. 在数据库管理页面,点击想备份数据库名称旁的Backup按钮,会弹出下图这样的对话框:
    Backup Database图1.9 – 备份数据库弹窗

  2. 填写表单

    • Master Password:Odoo服务的主控密码。
    • Backup Format:对生产数据库请保持使用zip,因为这是唯一真正的全量备份格式。仅在备份不关心文件存储的开发数据库时使用pg_dump格式。
  3. 点击Backup。然后浏览器会下载备份文件。

还原数据库备份

如果需要还原一个备份,需要按如下步骤操作:

  1. 在数据库管理页面,点击页面底部的Restore Database按钮,会弹出如下的对话框:
    Restore Database图1.10 – 还原数据库对话框

  2. 填写表单:

    • Master Password:这是Odoo服务的主控密码。
    • File:这是之前所下载的Odoo备份
    • Database Name:提供你需进行备份还原的数据库名称。该数据库在服务器上一定不能存在。
    • 数据库可能进行了迁移或拷贝:如果原数据库在另一台服务器上或是为从当前服务器删除则选择This database was moved。否则选择This database is a copy,这也是安全的默认选项。
  3. 点击Continue按钮。

📝不能在数据库自身之上还原数据库。这么做会得到一条错误消息(Database restore error: Database already exists)。需要先删除该数据库。

运行原理…

除Change master password以外的这些页面上的功能,是在服务器上运行PostgreSQL运维命令并通过网页界面报告结果。

主控密码是非常重要的信息,仅存储在Odoo服务的配置文件中,从不在数据库中进行存储。曾经有一个admin默认值,但使用这个值是一个广为人知的安全问题。在Odoo v9及之后的版本中,这被识别为一个未设置的主控密码,并在访问数据库管理页面时会敦促你修改密码。虽然这在配置文件中以admin_passwd进行存储,它与admin的密码是不同的,它们是两个不同的密码。主控密码是为Odoo服务进程设置的,进程本身可以处理多个数据库实例,每个实例都有一个独立的admin用户及其自己的密码。

📝安全考虑:记住本章中我们所考虑的是开发环境。Odoo数据库管理界面在我们运行生产服务时是需要进行安全保护的,因为这里给到了过多敏感信息的访问权限,尤其是在服务器托管了多个不同客户端的Odoo实例时。

Odoo使用PostgreSQL的createdb工具来新建数据库,它通过和以空数据库启动Odoo时相同的方式调用内部的Odoo函数来初始化新数据库。

Odoo使用createdb的–template选项传递原数据库作为参数来复制数据库。这基本上使用内部优化的PostgreSQL例行程序在新数据库中复制模板数据库的结构,这比创建备份和还原备份的速度会快很多(尤其是在使用网页界面时,还要求你下载备份文件然后再重新上传)。

备份和还原操作分别使用pg_dump和pg_restore工具。在使用zip格式时,备份还包含文件存储的拷贝,其中为配置Odoo不保存在数据库中的文档的拷贝,这是14.0中的默认选项。如果没做过修改的话,这些文件存放在~/.local/share/Odoo/filestore中。

📝如果备份很大,下载时会失败。这可能是因为Odoo服务本身无法在内存中处理这么大的文件或者是因为服务在反向代理之后运行,而这个代理设置了HTTP响应大小的限制。反过来,出于某些原因,你可能会在还原数据库的操作中遇到问题。在碰到这些问题时,应当投入时间建立更健壮的外部备份方案。

更多内容…

有经验的Odoo开发者通常不使用数据库管理界面,而在命令行执行相关操作。比如使用演示数据初始化新数据库,可以使用如下的一行代码:

1
$ createdb testdb && odoo-bin -d testdb

命令行的另一个彩蛋是可以在使用时要求安装一些插件,比如 -i sale,purchase,stock。

停止服务并运行如下这些命令来复制数据库:

1
2
3
4
$ createdb -T dbname newdbname
$ cd ~/.local/share/Odoo/filestore # 如果你修改了data_dir请调整此处
$ cp -r dbname newdbname
$ cd -

注意在开发的上下文中,文件存储通常会被省略。

📝createdb -T 仅在数据库没有活跃会话时方能使用,这表示在通过命令行复制数据库之前你需要关闭Odoo服务。

可运行如下命令来删除一个实例:

1
2
$ dropdb dbname
$ rm -rf ~/.local/share/Odoo/filestore/dbname

可运行如下命令来创建一个备份(假设PostgreSQL服务在本地运行):

1
2
$ pg_dump -Fc -f dbname.dump dbname
$ tar cjf dbname.tgz dbname.dump ~/.local/share/Odoo/filestore/dbname

可运行如下命令来还原备份:

1
2
$ tar xf dbname.tgz
$ pg_restore -C -d dbname dbname.dump

📝当心!

如果你的Odoo实例使用了另一个用户连接数据库,需要传递-U username来使用正确的用户作为还原数据库的所有者。

在文件中存储实例配置

odoo-bin脚本有几十个选项,记住所有这些以及记得在启动服务时适当地进行配置会非常单调费力。所幸可以将它们存储在一个配置文件中,只需对想要修改的选项进行手动修改,比如为开发环境做修改。

如何配置

对于本小节可执行如下步骤:

  1. 运行如下命令来为你的Odoo实例生成一个配置文件:

    1
    $ ./odoo-bin --save --config myodoo.cfg --stop-after-init
  2. 还可以添加其它选项,它们的值会被保存到所生成的文件中。所有未设置的值都会以默认值进行保存。使用如下命令来获取可用的选项列表:

    1
    $ ./odoo-bin --help | less

    这会提供一些不同选项所执行内容的帮助文档。

  3. 要从命令行形式转化为配置形式,使用长选项名,删除前面的中间杠,并将中间的中间杠转换为下划线。–without-demo就变成了without_demo。对大多数选项都是如此,但有一些例外,在下一部分中会列出。

  4. 编辑myodoo.cfg文件(使用下一部分中的表格来查看所要修改的参数)。然后运行如下命令来以所保存的选项启动服务:

    1
    $ ./odoo-bin -c myodoo.cfg

    📝-config选项通常简写为-c。

运行原理

启动时,Odoo通过三个步骤来加载它的配置。首先,所有选项的一组默认值会从源码中进行初始化,然后解析配置文件,该文件中所定义的任意值会覆盖默认值。最后,会分析命令行选项,它们的值会覆盖前面步骤中所获取的配置。

前面我们已提到,配置变量的名称可通过删除命令行选项的前置中间杠以及将中间的连接符转换为下划线来获取。其中有一些例外,特别是下面这些:

命令行 配置文件
–db-filter dbfilter
–no-http http_enable = True/False
–database db_name
–dev dev_mode
–i18n-import/–i18n-export 不可用

表1.2

以下是通过配置文件设置的常用选项列表:

选项 格式 用途
without_demo 逗号分隔的模块名列表 该选项阻止模块演示数据被加载。 设置为all取消所有模块的演示数据,设为False为所有模块启用演示数据。对具体模块禁用演示数据,应提供模块名,如 sale,purchase,crm。
addons_path 逗号分隔的路径列表 这是一个服务查找插件的路径名列表。
admin_passwd 文本 这是 master 密码(参见前面部分的内容)
data_dir 一个目录路径 这个目录中服务会存储session信息、从网上下载的插件以及在启用了文件存储时存放文档。
http_interface 网络接口的 IP 地址 默认为0.0.0.0,表示服务监听所有接口。
http_port longpolling_port 端口号 这些是 Odoo 服务所会监听的端口。你需要指定这两者来在同一台主机上运行多个 Odoo 服务;longpolling_port仅在workers不为0时使用。 http_port默认值为8069,longpolling_port默认为8072。
logfile 文件路径 Odoo 写入日志的文件。
log_level 日志信息级别 指定日志的级别。可接受的值(内容逐渐增加)包括critical, error, warn, info, debug, debug_rpc, debug_rpc_answer, debug_sql。
workers 整数 worker进程的数量,更多信息参见第三章 服务器部署
proxy_mode True/False 激活反向代理WSGI封装。仅在运行于可信任的 web 代理后启用它。

表1.3

以下是与数据库相关的配置选项列表:

选项 格式 用途
db_host 主机名 这是运行PostgreSQL服务的服务器名。使用 False 来使用本地 Unix 域套接字,以及 localhost 来使用本地 TCP 套接字。
db_user 数据库登录用户 在db_host为 False 时这通常为空。这将是用于连接数据库的用户。
db_password 数据库用户密码 在db_host为 False以及 db_user 与运行服务的用户相同时通常为空。阅读pg_hba.conf的主页面来获取更多相关信息。
db_name 数据库名 用于设置一些默认执行命令操作的数据库名。这不会限制服务所操作的数据库。参照下面的 dbfilter 参数。
db_sslmode 数据库SSL模式 用于指定数据库SSL连接模式。
dbfilter 一个正则表达式 该表达式应匹配服务所使用的数据库名。如果你运行网站,应该匹配单个数据库,类似^databasename$。更多相关信息请参见第三章 服务器部署
list_db True/False 设置为 True 来取消列出数据库。更多信息请参见第三章 服务器部署

表1.4

译者注: 表中的pg_hba.conf文件位置:/etc/postgresql/xxx/main/pg_hba.conf,另表中的第三章 服务器部署为上一版中的内容,在本书中已不再包含。

Odoo对配置文件的解析现在使用Python的ConfigParser模块。但是在Odoo 11.0中的实现发生了变化,它不再支持使用变量插值。因此,如果你习惯了使用%(section.variable)s表达式通过其它变量的值定义变量值的话,需要改变这一习惯并恢复使用显式的值。

有些选项不在配置文件使用,但广泛用于开发之中:

选项 格式 用途
-i或–init 逗号分隔的模块名列表 它会在初始化数据库时默认安装给定的模块
-u 或-update 逗号分隔的模块名列表 它会在重启服务时更新给定的模块。多在修改了源代码或从 git 更新了分支时使用
–dev all, reload, qweb, werkzeug, xml 这会启用开发者模式及自动重新加载功能。

表1.5

激活Odoo开发者工具

开发人员在使用Odoo时,应当知道如何在网页界面激活开发者模式,这样你就可以访问技术设置菜单及开发者信息。启动调试模式会暴露出一些高级配置项及字段。Odoo隐藏这些选项和字段来实现更好的易用性,因为日常不会使用到它们。

如何激活

按照如下步骤来在网页界面中激活开发者模式:

  1. 连接到你的实例并以 admin 登录
  2. 访问Settings菜单
  3. 滚动至页面底部,找到Developer Tools版块
    图1.11 – 启用不同开发者模式的链接
    图1.11 – 启用不同开发者模式的链接
  4.  点击Activate the developer mode链接
  5. 等待用户界面重载

ℹ️其它方式: 也可以通过编辑 URL 来激活开发者模式。在链接的#号前,插入?debug=1。例如,如果你的链接是http://localhost:8069/web#menu_id=102&action=94,那么你需要将其修改为http://localhost:8069/web?debug=1#menu_id=102&action=94。此外,如果你想要使用带静态文件的调试模式,则将 URL修改为http://localhost:8069/web?debug=assets#menu_id=102&action=94

译者注: 加(with assets)的模式会将静态文件(css, js)分拆每一个具体文件,这将有助于调试,但相对于合并的静态文件而言会损失一些加载速度

通过如下其中一种方式可退出开发者模式:

  • 编辑URL并在查询字符串中写入?debug=0
  • 通过使用Settings菜单相同位置下的Deactivate the developer mode链接
  • 点击顶部调试小虫图标,在下拉菜单中点击Leave Developer Tools选项

很多开发者使用浏览器插件来切换调试者模式。通过使用插件,可以无需访问settings菜单快速地切换调试模式。这些插件可在Firefox和Chrome浏览器中使用。参见如下截图,它有助于你在Chrome商店中找到该插件:

Odoo Debug Chrome 插件

图1.12 – 调试模式的浏览器插件

📝调试模式的行为从Odoo v13开始发生了改变。从v13开始,调试模式的状态存储在会话中,那么即使在URL中删除掉?debug,调试模式依然处于启动状态。

运行原理

开发者模式中,会发生两件事情:

  • 鼠标在表单视图的字段上或列表视图的列名上悬浮时会给出提示信息,提供该字段的技术信息(内部名称、类型等)。
  • 调试图标下拉菜单会显示在右上角用户菜单旁,给到显示的模型相关技术信息的访问,有各种关联的视图定义、工作流、自定义过滤管理等等。

开发模式有一个变体:Developer mode (with assets)。这一模式和普通的开发者模式相似,但除此之外,发送到浏览器的JavaScript 和 CSS没有做最小化处理,这表示你浏览器的web开发者工具可以方便地用于调试JavaScript代码(更多内容请见第十五章 网页客户端开发)。

📝注意!

使用非开发者模式及开发者模式来测试你的插件,因JavaScript库的非最小化版本会隐藏最小化版本中伤你至深的 bug。

更新插件模块列表

在新增模块时,Odoo并不知道新模块的存在。为在Odoo中列出该模块,需要更新模块列表。本小节中学习如何更新应用列表。

准备工作

启动实例并使用Administrator账号连接实例。然后启用开发者模式(如尚不知道如何启用开发者模式,参见第一章 安装Odoo开发环境)。

如何实现…

在实例中更新已有的插件模块列表,执行如下步骤:

  1. 打开Apps菜单
  2. 点击Update Apps List。
    图1.13 – 更新应用列表的菜单项
    图1.13 – 更新应用列表的菜单项
  3. 在弹出的对话框中点击Update按钮:
    图1.14 – 更新应用列表对话框
    图1.14 – 更新应用列表对话框
  4. 在更新结束时,可以点击Apps查看更新后的可用插件模块列表。需要删除掉Apps搜索框中默认的过滤器来查看所有模块。

运行原理…

在点击Update按钮时,Odoo会读取插件路径配置变量。对于列表中的每个路径,它会查找插件声明文件包含的直接子目录,声明文件__manifest__.py存储在插件模块目录下。Odoo读取声明内容,查找其中的Python字典。只要声明中键为installable的实例不设置为False,就会将插件模块元数据存储到数据库中。如果模块已存在,会更新信息。如不存在,会新建一条记录。如果此前可用的插件模块查找不到,也不会从列表中进行删除。

📝仅在初始化数据库后新增插件路径时才需要更新应用列表。如果在初始化数据库之前在配置文件中新增了插件路径,则无需手动更新模块列表。

总结一下我们目前所学到的知识,在完成安装后,我们使用如下命令启动Odoo服务(如果使用了虚拟环境,需要先激活该环境):

1
python3 odoo-bin -d odoo-test -i base --addons-path=addons --db-filter=odoo-test

运行后,可以通过http://localhost:8069 来访问Odoo。

还可以使用配置文件来运行Odoo,如下:

1
./odoo-bin -c myodoo.cfg

启动好Odoo服务后,可以在Apps 菜单下安装/更新其中的模块。

常见问题

1、Running setup.py install for psycopg2 … error
ERROR: Command errored out with exit status 1

1
apt install libpq-dev -y

本文为最好用的免费ERP系统Odoo 12开发手册系列文章第十四篇暨完结篇。

本文中将学习将 Odoo 服务器作为生产环境的基本准备。安装和维护服务器是一个复杂的话题,应该由专业人员完成。本文中所学习的不足以保证普通用户创建应对包含敏感数据和服务的健壮、安全环境。

本文旨在介绍 Odoo 部署的重要配置和最佳实践,这样系统管理员可以更好地准备 Odoo 服务器主机。通过本文的学习,我们将能创建一个相对安全的 Odoo 服务器,足够应对普通的生产环境使用。本文所述并非部署 Odoo 的唯一方法,其它的方法也会同样适用。

本文主要内容有:

  • 通过源码安装 Odoo,主要包含:

    • 安装依赖
    • 准备一个独立的系统用户
    • 通过源码安装
    • 设置配置文件
    • 多进程
  • 设置 Odoo 为系统服务,主要包含:

    • 创建 systemd 服务
    • 创建自启动或 sysvinit 服务
    • 通过命令行查看 Odoo 服务
  • 设置 Nginx 反向代理

  • 配置HTTPS安全服务, 主要包含 :

    • 创建自签名SSL证书
    • 在 Nginx 上配置 HTTPS访问
    • 缓存静态内容
  • 服务器和模块更新,主要包含:

    • 创建模拟环境
    • 更新 Odoo 源码

开发准备

本章无需使用前面开发的代码,相关代码和脚本请见 GitHub 仓库

通过源码安装 Odoo

Odoo 有 Debian 或 Ubuntu的安装包,使用它可以实现工作服务进程以及在系统启动时自动开启服务。安装过程按步骤即可,在https://nightly.odoo.com/上可以找到相关信息。上面还有CentOS的rpm文件以及 Windows 的.exe 安装包。

虽然这样安装 Odoo 很容易也很方便,大多数人倾向于部署和运行版本控制的源码来进行集成。这样能更好地控制部署内容,并且在生产环境中也更易于管理代码的变更和修复。

安装依赖

使用Debian发行版时,默认登录用户为带有管理员权限的 root,这时命令行显示的为#。在Ubuntu系统中,禁用了 root 账号,在安装时配置的初始用户可通过 sudo 来运行 root 权限的命令。首先我们应更新包索引,然后执行升级来确保所有安装的程序是最新的,命令如下:

1
2
sudo apt update
sudo apt upgrade -y

下一步,我们将安装PostgreSQL数据库,并让当前用户成为数据库超级用户,命令如下:

1
2
sudo apt install postgresql -y
sudo su -c "createuser -s $USER" postgres

我们将通过源码运行 Odoo,但在那之前,我们需要安装所需依赖。以下是所需的Debian包:

1
2
sudo apt-get install git python3-dev python3-pip -y
sudo apt install build-essential libxslt-dev libzip-dev libldap2-dev libsasl2-dev libssl-dev -y

还应记得安装打印报表所需的wkhtmltox,命令如下:

1
2
3
wget "https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.xenial_amd64.deb" -O /tmp/wkhtml.deb
sudo dpkg -i /tmp/wkhtml.deb
sudo apt-get -fy install # 处理依赖错误

注意安装包时可能报依赖错误,像这里最一条命令会强制这些依赖的安装并正确完成安装。

1
2
3
4
5
6
# 错误信息
...
dpkg: error processing package wkhtmltox (--install):
dependency problems - leaving unconfigured
Errors were encountered while processing:
wkhtmltox

现在我们就只缺少 Odoo 所需要的 Python 包了,它们大多数都有Debian或Ubuntu系统包。官方Debian安装包使用到这些,可在Odoo 源码debian/control文件中找到这些包名。但是这些 Python 依赖也可以通过PyPI(Python Package Index)直接安装。所要求的包和通常基于 Python 的项目一样在 Odoo 的requirements.txt文件中。我们可以使用如下命令安装这些包:

1
2
wget https://raw.githubusercontent.com/odoo/odoo/12.0/requirements.txt
sudo -H pip3 install -r requirements.txt

在Ubuntu 18.04中,最后一条命令可能会打印了红色的警告,内容是PyYAML 和pySerial的卸载,这在系统中通过包安装了老版本时会发生。这种警告可以安全的略过。

既然我们有了所有的依赖,安装了数据库服务、系统包和 Python 包,我们就可以安装 Odoo 了。

准备独立的系统用户

从安全角度建议使用独立的用户运行 Odoo,这一用户不带有任何系统的特权。为此我们需要创建系统和数据库用户,使用命令如下:

1
2
3
sudo adduser --disabled-password --gecos "Odoo" odoo
sudo su -c "createuser odoo" postgres
createdb --owner=odoo odoo-prod

以上odoo 为用户名,odoo-prod用于运行 Odoo 实例的数据库名。odoo用户成为了odoo-prod数据库的所有者。也就说它对该数据库有创建和删除的权限,包括删除整个数据库的权限。如果你运行的是多租户服务器,应为每个租户创建一个类似 odoo 的指定系统用户。

小贴士: Odoo的设计即使在系统用户不是数据库所有者时也可以正确运行。但这可能会让设置变复杂,并且固化安全的好习惯是有一个主系统用户作为数据库的所有者、为每个实例创建一个指定的不带有超级用户权限的系统用户来运行服务。

注意这些是没有管理权限的普通用户,新建系统用户时会自动创建一个家目录。比如/home/odoo,用户可通过~快捷符号来表示自己的家目录。我们在用户 Odoo 对应的配置和文件中会使用到它。我们可以使用如下命令来以该用户打开一个会话:

1
2
sudo su odoo
exit

exit命令终止会话并回到原用户。

源码安装

要不了多久,你的服务就会需要升级、打补丁。这时版本控制仓库会很有帮助。我们使用 git来从仓库中获取代码,就像我们在安装开发环境时的操作一样。下面我将使用 odoo 用户并将代码下载家目录中:

1
2
sudo su odoo
git clone https://github.com/odoo/odoo.git /home/odoo/odoo-12 -b 12.0 --depth=1

-b 选项确保获取的是正确的分支,–depth=1选项会忽略修改历史并仅获取最新修订的代码,这样下载内容更精简,速度也更快。

小贴士: git在管理 Odoo 部署版本时是一个非常有价值的工具。注意本系列文章中仅仅涉及到代码版本管理的冰山一角。想要更加熟悉 git,值得花时间进一步学习,可从http://git-scm.com/doc开始。

到此为止,我们应该已经拥有源码安装 Odoo 的所有内容。可通过如下命令以指定用户会话检查是否正确启动并退出:

1
2
3
$ /home/odoo/odoo-12/odoo-bin --version
Odoo Server 12.0
$ exit

下面我们将设置一些在系统中使用的系统级别的文件和目录。

设置配置文件

在启动 Odoo 服务时添加–save参数会将配置保存到~/.odoorc文件中。我们将以这个文件作为服务配置的初始文件,将其保存到/etc/odoo下,使用命令如下:

1
sudo su -c "~/odoo-12/odoo-bin -d odoo-prod --db-filter='^odoo-prod$' --without-demo=all -i base --save --stop-after-init" odoo

这行命令中也会包含服务实例所使用的配置参数。

小贴士: 老版本中的.openerp_serverrc配置文件还被支持,找到后会进行使用。如果安装 Odoo 10或之后版本的服务器上同时还安装了老版本的 Odoo,可能会引起混淆。这时你会发现–save选项没有更新.odoorc,而是更新了.openerp_serverrc文件。

下一步我们需要将配置文件放到系统的配置文件目录/etc 下,命令如下:

1
2
3
4
sudo mkdir /etc/odoo
sudo cp /home/odoo/.odoorc /etc/odoo/odoo.conf
sudo chown -R odoo /etc/odoo
sudo chmod u=r,g=rw,o=r /etc/odoo/odoo.conf # 安全加固使用

以上命令最后一行是可选的,但它提升了系统的安全性。它确保运行 Odoo 进程的用户可以读取但无法修改配置文件。这时你将无法修改数据库主密码,但在生产服务下这不是什么问题,因为应使用list_db=False服务配置来禁用网页数据库管理员。我们还需为 Odoo 服务创建一个存储日志文件的目录。这通常放在/var/log目录下,命令如下:

1
2
sudo mkdir /var/log/odoo
sudo chown odoo /var/log/odoo

现在让我们通过如下命令编辑配置文件并确保已配置了一些重要参数:

1
sudo nano /etc/odoo/odoo.conf

以下是大部分重要参数的推荐值:

1
2
3
4
5
6
7
8
9
10
11
[options]
addons_path = /home/odoo/odoo-12/odoo/addons,/home/odoo/odoo-12/addons
admin_passwd = False
db_name = odoo-prod
dbfilter = ^odoo-prod$
http_port = 8069
list_db = False
logfile = /var/log/odoo/odoo-server.log
proxy_mode = True
without_demo = all
workers = 6

下面逐一讲解:

  • addons_path是一组逗号分隔的用于查找插件模块的路径。读取顺序为从左到右,最左边目录的优先级最高。
  • admin_passwd是访问网页客户端数据库管理功能的主密码。一定要设置复杂的密码,或者最好是设为 False来关闭这一功能。
  • db_name是在服务启动时初始化的数据库实例。
  • dbfilter用于过滤可访问的数据库,它是一个 Python 解释的正则表达式。为使用户无需弹出窗口选择数据库,并使未经身份验证的 URL 可正常运作,应设置为^dbname$,比如dbfilter=^odoo-prod$。它支持%h和%d占位符,由 HTTP 请求主机名和子域名进行替换。
  • http_port是服务器监听的端口号,默认使用的是8069
  • list_db = False在 RPC级别和 UI 上屏蔽数据库列表,并屏蔽数据库管理界面以及相应的 RPC 功能。
  • logfile是服务日志写入的位置。对于系统服务,一般位于/var/log文件夹内。如果留空,日志会转而在标准输出中打印。
  • proxy_mode在需要反向代理访问时应设为True,我们需要用到反向代理。
  • without_demo在生产环境中应进行设置,这样新建的数据库中不会带有演示数据。
  • workers的值在大于等于2时启用多进程,一会儿我们会进一步的讨论。

ℹ️Odoo 10中引入http_port参数来替代老版本中使用但现在已淘汰了的xmlrpc_port参数。

从安全角度看,admin_passwd=False和list_db=False选项尤为重要。它们屏蔽掉对数据库管理功能的网页端访问,在生产环境和面向外网的 Odoo 服务中都应进行设置。

以下也是会用到的参数:

  • data_dir是会话数据和附件存储的路径,记住将备份放在这里
  • http_interface设置监听的地址。默认监听0.0.0.0,但在使用反向代理时应设置为127.0.0.1来仅响应本地请求。Odoo 11中引入它来代替淘汰了的xmlrpc_interface参数。

我们可通过-c或–config选项来检查运行服务的设置:

1
sudo su -c "~/odoo-12/odoo-bin -c /etc/odoo/odoo.conf" odoo

通过上述设置运行 Odoo 不会在终端中有任何输出,因为修改都写到了配置文件中定义的日志文件中了。要追踪服务的操作,我们需要在终端中运行如下命令:

1
sudo tail -f /var/log/odoo/odoo-server.log

Odoo 12通过-c 检查配置文件

有时可能会需要将日志输出到标准输出中,最好的方法是复制一份不带logfile选项的配置文件。

小贴士: 要在同一个终端窗口中运行多个终端会话,可以使用tmux这样的应用或者GNU Screen。可以使用Byobu,它基于GNU Screen或Tmux提供了好看的用户界面。

多进程工作进程

生产实例可能会处理大量的工作负载。默认服务运行单进程并只能使用CPU 的一个核心来处理请求,这是因为Python语言的全局解释器锁(GIL)。但是可使用多进程模式来处理并发请求,来充分利用多核的优势。workers=N选项设置使用的工作进程数,作为一个参照,可设置为1+2*P,其中 P为处理器的核数。最佳设置需要根据具体情况进行调优,因为这取决于服务器的负载以及多少其它像PostgreSQL这样大负载服务在运行。

为负载将workers设置高比低好,最低值应为6,因为这是大多数浏览器的并行连接数,最大值通常受服务器的 RAM 所限。普通使用模式的经验是,Odoo服务应能处理(1+2*P)*6个并发用户。

还有一些limit-配置参数可用于调优工作进程。在达到这些值时工作进程会被回收,相应的进程会停止并启动一个新进程。这可以防止服务器内存溢出以及防止某一进程过度使用服务器资源。

官方文档中对工作进程参数调优也给出了一些很好的建议,欲知详情,请参照官方文档Odoo 部署

设置 Odoo 为系统服务

现在我们需要将 Odoo 设为系统服务,并在系统启动时自动开启。

在Ubuntu或Debian中,init程序负责服务的启动。Debian或其分支操作系统曾使用过sysvinit,Ubuntu曾使用过兼容的名为Upstart的启动程序。但最近都进行了修改,最新的Debian和Ubuntu发行版的init程序都使用了systemd。这表示现在有两种方式来安装系统服务,你需要根据操作系统的版本来选择正确的方法。Ubuntu 16.04或之后的版本中应使用systemd,但是很多云服务商还在使用更早的版本,所以很有可能你也会使用到这些老版本。要检查系统中是否使用systemd,可使用如下命令:

1
man init

这条命令会打印当前使用的 init 程序的文档,然后就可以查看所使用的启动服务。

ℹ️Window的子系统(WSL) 中的 Ubuntu环境仅适用于开发,它有可能发生些异常问题,完全不适合用于运行生产环境。在写本文时,man init显示启动服务为systemd,但并不能安装systemd服务,反而安装sysvinit服务时是正常的。

补充:如出现-bash: man: command not found,则通过sudo apt install man -y来执行安装

创建systemd服务

如果你使用的是较近的操作系统,如Debian 8或Ubuntu 16.04,你的启动服务就应该是systemd。要在系统中添加服务,只需创建一个描述服务的文件。我们创建/lib/systemd/system/odoo.service文件并加入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=Odoo
After=postgresql.service

[Service]
Type=simple
User=odoo
Group=odoo
ExecStart=/home/odoo/odoo-12/odoo-bin -c /etc/odoo/odoo.conf

[Install]
WantedBy=multi-user.target

小贴士: Odoo源码中在debian/下包含一个odoo.service文件示例。你可以不用新建方件,直接把拷贝该文件然后进行所需修改。至少需要根据设置来修改ExecStart选项。

下一步我们需要使用如下命令来注册这个新服务:

1
sudo systemctl enable odoo.service

使用如下命令启动该服务:

1
sudo systemctl start odoo

使用如下命令检查该服务状态:

1
sudo systemctl status odoo

Odoo 12服务运行状态

最后,如需停止服务,请使用如下命令:

1
sudo systemctl stop odoo

创建Upstart或sysvinit 服务

如果你使用更老的操作系统,如Debian 7或Ubuntu 15.04,那么很有可能需要使用sysvinit或Upstart启动服务。就创建系统服务而言,两者的作用一致。一些虚拟专用服务器(VPS)服务还在使用老的Ubuntu镜像,所以在碰到这种情况时可以通过以下方式部署Odoo服务。

Odoo源码中有一个init脚本用于Debian发行包,我们仅需做一些小修改来使用它创建启动服务:

1
2
sudo cp /home/odoo/odoo-12/debian/init /etc/init.d/odoo
sudo chmod +x /etc/init.d/odoo

现在你可能需要查看一下init脚本的内容,主要的参数都在文件上方定义的变量中,如下例所示:

1
2
3
4
5
6
7
8
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
DAEMON=/usr/bin/odoo
NAME=odoo
DESC=odoo
CONFIG=/etc/odoo/odoo.conf
LOGFILE=/var/log/odoo/odoo-server.log
PIDFILE=/var/run/${NAME}.pid
USER=odoo

这些变量应该足够使用了,接下来我们将使用它们的默认值进行设置,但你可以根据自己的需要进行修改。变量USER是运行服务的系统用户,我们前面已经创建了一个odoo用户。变量DAEMON是可执行服务的路径,我们启动 Odoo 的文件在不同的路径下,但可创建以下软链接:

1
2
sudo ln -s /home/odoo/odoo-12/odoo-bin /usr/bin/odoo
sudo chown -h odoo /usr/bin/odoo

变量CONFIG是我们需要使用的配置文件。在前面一节中,我们在默认配置的路径/etc/odoo/odoo.conf下创建了配置文件。最后变量LOGFILE是存储日志文件的路径。配置的路径/var/log/odoo我们在定义配置文件时进行了创建。

现在我们应该可以像下面这样来启动和关闭 Odoo 服务了:

1
2
$ sudo /etc/init.d/odoo start
Starting odoo: ok

关闭服务方法相似,命令如下:

1
2
$ sudo /etc/init.d/odoo stop
Stopping odoo: ok

Ubuntu中可使用 service 命令:

1
2
3
sudo service odoo start
sudo service odoo status
sudo service odoo stop

现在我们需要在系统启动时自动开启服务,通过如下命令实现:

1
sudo update-rc.d odoo defaults

这时,重启服务器 Odoo 服务就会正常地自动启动了。是时候测试下一切是否都如预期一样了。

使用命令行检查 Odoo 服务

现在我们可以确定Odoo实例是否运行以及是否能正常对请求进行响应。如果Odoo正常运行,我们应该可以得到响应并且日志文件中不会报错。在服务器上通过如下命令可检测 Odoo 是否对HTTP请求进行响应:

1
2
curl http://localhost:8069
<html><head><script>window.location = '/web' + location.hash;</script></head></html>

此外通过如下命令可查看日志文件的内容:

1
sudo less /var/log/odoo/odoo-server.log

你还可以使用tail -f 来实时查看日志文件中新增的内容:

1
sudo tail -f /var/log/odoo/odoo-server.log

设置 Nginx 反向代理

虽然 Odoo 自身可以输出网页,但强烈建议在其上加一层反向代理。反向代理作为一个中间层来管理客户端所发送请求以及 Odoo 服务作出响应之间的数据流。使用反向代理有诸多好处。

从安全角度考虑,有以下几点:

  • 处理并加固HTTPS协议来对数据流加密
  • 隐藏内部网络特征
  • 作为应用防火墙,限制所接受处理的 URL

然后从性能角度考虑,反向代理可提供如下显著的改进:

  • 缓存静态内容,因而降低 Odoo 服务器的负载
  • 压缩内容来加快加载时间
  • 作为负载均衡器,在多台服务间分配负载

Apache是在考虑反向代理时的一个常用选择,Nginx 是近期在技术圈被热议的对其的替代。此处我们使用Nginx作为反向代理,并展示如何使用它来实现上述讨论的安全和性能方面的功能。

首先,我们应当安装Nginx,我们需要它监听默认的HTTP端口,所以需要确保没有被其它服务所占用。执行如下命令应该会报错:

1
2
$ curl http://localhost
curl: (7) Failed to connect to localhost port 80: Connection refused

如果没有收到错误,应当禁用相应服务来让Nginx使用该端口。例如,关闭已有的Apache服务,使用sudo service apache2 stop。更好的选择是从服务器上删除该服务或重新配置让其监听其它端口,这样Nginx就可以正常使用HTTP和HTTPS端口(80 and 443) 了。

一旦完成上述操作,就可以安装Nginx了:

1
2
sudo apt-get install nginx -y
sudo service nginx start # 如尚未启动,启动Nginx服务

要确定Nginx是否正确运行,通过浏览器访问或在服务上执行curl http://localhost应该可以得到一个Welcome to nginx页面。

Nginx 欢迎页面

Nginx配置文件和Apache的方式基本相同,存储在/etc/nginx/available-sites/中,并可通过在/etc/nginx/enabled-sites/中添加软链接来激活。注意应同时关闭Nginx安装时默认带有的配置:

1
2
3
sudo rm /etc/nginx/sites-enabled/default
sudo touch /etc/nginx/sites-available/odoo
sudo ln -s /etc/nginx/sites-available/odoo /etc/nginx/sites-enabled/odoo

使用nano或vi等编辑器来编辑 Nginx配置文件:

1
sudo nano /etc/nginx/sites-available/odoo

一个基本的针对Odoo服务的Nginx配置文件如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
upstream odoo {
server 127.0.0.1:8069;
}
upstream odoochat {
server 127.0.0.1:8072;
}
server {
listen 80;
# Add Headers for odoo proxy mode
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;

# log
access_log /var/log/nginx/odoo.access.log;
error_log /var/log/nginx/odoo.error.log;
# Redirect longpoll requests to odoo longpolling port
location /longpolling {
proxy_pass http://odoochat;
}
# Redirect requests to odoo backend server
location / {
proxy_redirect off;
proxy_pass http://odoo;
}
# common gzip
gzip_types text/css text/scss text/plain text/xml application/xml application/json application/javascript;
gzip on;
}

补充:添加域名请在server 配置区内添加对 server_name 的配置

首先为Odoo服务添加了upstream配置,监听了默认端口8069和8072。8069用于网页客户端和RPC请求,8072用于多进程时 Odoo 实时消息所需的长轮询(long polling)请求。

Nginx应在默认HTTP端口80上接收访问流量,然后重定向到upstream odoo服务中。这在server配置区中进行了定义。/longpolling 地址的访问流量会传递到upstream odoochat,剩余的流量则传递到upstream odoo。这里我们还添加了一些请求头的信息,这样 Odoo 后台服务就会知道这些是经过代理的流量。

出于安全考虑,应确保proxy_mode参数设为True。这是因为在Nginx作为代理时,所有的请求都会认为是来自本地而不是远程 IP 地址。在代理中设置X-ForwardedFor头以及启动–proxy-mode可解决这一问题。但是,如果不在代理级别强制header就启用–proxy-mode 会让其他人可以伪装远程地址。

在配置文件的最后,可以看到两条gzip相关的命令,它们用于对一些文件进行压缩,提升性能。可通过如下命令测试配置是否正确:

1
2
3
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

如果出现错误,请检查配置文件中输入是否正确。常见的问题是默认的HTTP被其它服务所占用,如Apache或默认的Nginx网站,所以在重启Nginx服务前先通过本文中的命令确保并没有这种问题。在完成处理后,可使用如下命令重新加载新的配置:

1
sudo /etc/init.d/nginx reload

如果操作系统使用的是systemd,上述命令正确的版本应该是:

1
sudo systemctl reload nginx

通过如下命令可确认 Nginx 是否将访问流量重定向到了后台Odoo服务中:

1
2
$ curl http://localhost
<html><head><script>window.location = '/web' + location.hash;</script></head></html>

Odoo 12登录页面

配置HTTPS安全服务

网站数据不应在因特网中以普通文件进行传输,在将Odoo网页服务暴露在网络中时,我们应使用HTTPS协议来对数据进行加密。有时可使用自签署证书。但注意自签署证书可能会带来安全风险,比如中间人攻击(Man-in-the-Middle Attack),因此有些浏览器会不接受该证书。

更健壮的解决方案是使用认证的证书机构所签署的证书,在运行商业或电商网站时这就尤为重要了。

小贴士: Let’s Encrypt服务提供免费的证书。Odoo 有现存插件模块处理对Odoo服务SSL证书的自动请求,但在写本文时,还未移植到Odoo 12中,可访问 GitHub 进一步了解。

创建自签署 SSL 证书

下一步,我们应安装证书来启用SSL。创建一个自签署证书,可使用如下命令:

1
2
3
4
$ sudo mkdir /etc/ssl/nginx && cd /etc/ssl/nginx
$ sudo openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 365 -nodes
$ sudo chmod a-wx * # make files read only
$ sudo chown www-data:root * # access only to www-data group

上述命令创建一个/etc/ssl/nginx目录以及不带密码的自签署SSL证书。在运行openssl命令时,会要求用户输入其它信息,然后会生成一个证书和密钥文件。最后,将这些文件的所有权赋予用于运行网页服务的www-data用户。

在 Nginx上配置HTTPS访问

既然我们已经有了SSL证书,就可以配置 Nginx 来使用它了。要强制使用HTTPS,需要将所有的 HTTP 访问重定向到HTTPS。将前面的server区中替换为如下内容:

1
2
3
4
server {
listen 80;
rewrite ^(.*) https://$host$1 permanent;
}

现在,如果重新加载Nginx配置并在浏览器中访问服务的话,将会看到http://地址被转换成了https:// 地址。但该地址不会返回任何内容,我们需要正确地配置HTTPS服务,可通过添加如下服务器配置来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
server {
listen 443;
# Add Headers for odoo proxy mode
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;

# SSL parameters
ssl on;
ssl_certificate /etc/ssl/nginx/server.crt;
ssl_certificate_key /etc/ssl/nginx/server.key;
ssl_session_timeout 30m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCMSHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSAAES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-
SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-
SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-
SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHEDSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-
SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-
SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-
SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-
SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;

# log
access_log /var/log/nginx/odoo.access.log;
error_log /var/log/nginx/odoo.error.log;
# Redirect longpoll requests to odoo longpolling port
location /longpolling {
proxy_pass http://odoochat;
}

# Redirect requests to odoo backend server
location / {
proxy_redirect off;
proxy_pass http://odoo;
}

# common gzip
gzip_types text/css text/scss text/plain text/xml application/xml application/json application/javascript;
gzip on;
}

以上配置代码会监听HTTPS端口并使用/etc/ssl/nginx/ 证书文件来对数据进行加密。这与我们在设置 Nginx 反向代理 中看到的server 配置区相似。如果重新加载配置,我们的 Odoo 服务将通过HTTPS进行运作,如以下命令所示:

1
2
3
4
5
6
7
8
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo service nginx reload # or: sudo systemctl reload nginx
* Reloading nginx configuration nginx
...done.
$ curl -k https://localhost
<html><head><script>window.location = '/web' + location.hash;</script></head></html>

最后部分的输入可用于确认Odoo客户端正通过HTTPS进行访问。

小贴士: 在更早的Odoo镜像中,PosBox仅在HTTP模式下生效,这就需要在 Nginx 中添加对/pos/ 链接的例外处理。Odoo 10及之后的镜像中包含了自签署证书来让PosBOx和IoT Box可通过HTTPS进行通讯,这一修改在 GitHub 中引入。

Odoo 12自签署证书 Nginx

缓存静态内容

我们可以配置 Nginx 来缓存服务端静态文件,这样再次请求时就可以访问Nginx 中的缓存,而无需将请求传递到upstream odoo服务中。启用静态内容缓存可带来更快的响应时间并减少 Odoo 服务的工作负载。要启用这一设置,在location /longpolling区之前加入如下代码:

1
2
3
4
5
6
7
# cache static data
location ~* /web/static/ {
proxy_cache_valid 200 60m;
proxy_buffering on;
expires 864000;
proxy_pass http://odoo;
}

通过这一些命令,静态数据就可以缓存60分钟了。在这个期间的其它请求Nginx 会直接使用缓存进行响应。

服务和模块更新

一旦 Odoo 服务运行了一段时间,就会需要对其进行升级。这包括两个步骤:获取服务或模块的新版本、执行安装。

创建分阶环境

如果你按照通过源码安装 Odoo一节正确地进行了安装,应该就可以在暂存区仓库中获取并测试新版本源码。强烈建议创建一个生产环境数据库的拷贝,并使用它进行升级测试。如果odoo-prod是我们的生产环境数据库,可通过如下命令创建一个拷贝odoo-stage:

1
2
3
4
5
6
7
dropdb odoo-stage
createdb --owner=odoo odoo-stage
pg_dump odoo-prod | psql -d odoo-stage
sudo su odoo
cd ~/.local/share/Odoo/filestore/
cp -al odoo-prod odoo-stage # create filestore hardlinks
exit

在使用以上数据库拷贝之前,应进行清理,比如停止计划动作、关闭 email 服务(包含发送和接收消息)。根据你的设置来执行这些指定步骤,但通常可使用自动化脚本来执行。记住psql可用于在命令行直接执行SQL命令,如psql -d odoo-stage -c “<SQL命令>”。

 小贴士: 可通过createdb命令来更快地创建拷贝:createdb –owner=odoo –template=odoo-prod odoo-stage。但需要说明的是要运行该命令,不能有任何对odoo-prod数据库的连接,因此需要停止Odoo生产环境的服务。

更新 Odoo 源码

我们使用git pull 命令来从GitHub仓库获取最新的Odoo源码。在那之前,我们可以使用git tag命令来为当前使用的提交创建一个标签,这样可以可容易的对更新进行撤销,命令如下:

1
2
3
4
sudo su odoo
cd ~/odoo-12
git tag --force 12-last-prod
git pull

要让代码修改生效,我们需要重启Odoo服务。而要使用数据文件的修改生效,需要对模块进行升级。通常对Odoo稳定版本的修改都是代码的修复,因此无需冒险执行模块升级。如果需要对模块升级,可使用-u 附加参数,或者是-u base,它将升级所有模块。

现在可以启动Odoo的分阶服务了,它将使用在分阶数据库上使用升级代码:

1
2
~/odoo-12/odoo-bin -d odoo-stage --http-port=8080 -c /etc/odoo/odoo.conf # optionally add: -u base
exit

Odoo 分阶服务通过在8080端口上进行配置。可通过浏览器访问http://xxx:8080来检查升级代码是否正确运作。如果出现了错误,可通过如下命令来返回上一个版本:

1
2
3
4
sudo su odoo
cd ~/odoo-12
git checkout 12-last-prod
exit

如果一切运行都如预期,则可安全地执行生产环境服务的升级,通常是通过重启来实现。如果想要执行实际的模块升级,建议的方法是停止服务、运行升级、再重启服务,命令如下:

1
2
3
sudo service odoo stop
sudo su -c "~/odoo-12/odoo-bin -c /etc/odoo/odoo.conf -u base --stop-after-init" odoo
sudo service odoo start

记住对在用Git 版本进行记录,可通过 git checkout回到修改前,这让我们可以在需要的时候进行回滚。强烈推荐在执行数据库升级前保存备份。在完成之后,可使用 Git 拉取新的版本到生产仓库并完成升级:

1
2
3
4
5
6
sudo su odoo
cd ~/odoo-12
git tag --force 12-last-prod
git pull
exit
sudo service odoo restart # or: sudo systemctl restart odoo

无需频繁进行升级,但也不建议等上一年再进行升级。每几个月进行一次升级。还要记得重启服务来启用代码升级,但对模块升级则并非如此。但如果需要进行指定的漏洞修复,可以更早的进行升级。还应关注公开渠道对 Odoo 安全漏洞的披露,发 GitHub 上 Odoo 的Issues,具体可查看Security标签,或者是官方的邮件列表,可通过https://www.odoo.com/groups进行订阅。

作为一项服务,企业版用户会更早地收到邮件通知来警报这一类问题。

总结

在本文中,我们学习了在Debian系生产服务器中设置和运行 Odoo 所需的其它步骤。我们一起了解了配置文件中最重要的设置项,并学习了如何擅用多进程模式。为提升安全性和扩展性,我们还学习了如何使用 Nginx 来作为 Odoo 服务处理之上的反向代理。

本文涵盖了运行 Odoo 服务并向用户提供稳定、安全服务的基础知识。要更多地了解 Odoo,可参考官方文档。其中更深入的讲解了一些课题,并包含了一些本系列文章未涉及的课题。

还有一些有关 Odoo 的出版图书还助于你的学习。Packt Publishing中有一些相关书籍,具体来说Odoo Development Cookbook包含了本系列文章未讨论到的高级课题。

最后,Odoo是一个拥有活跃社区的开源产品。参与、提问并回馈社区不仅有助于学习,还有助于建立人脉。说到这,就不能不提 Odoo 社区联盟(OCA),它倡导协作并提供高质量的开源代码,可访问https://odoo-community.org/做更进一步了解。

扩展阅读

以下Odoo官方文档的内容可作为本文讨论课程的补充材料,来帮助获取额外的参考:

本文首发地址:Alan Hou 的个人博客

本文为最好用的免费ERP系统Odoo 12开发手册系列文章第十三篇。

Odoo 起初是一个后台系统,但很快就有了前端界面的需求。早期基于后台界面的门户界面不够灵活并且对移动端不友好。为解决这一问题,Odoo 引入了新的网站功能,为系统添加了 CMS(Content Management System)内容管理系统。这使得我们无需集成第三方 CMS 便可创建美观又高效的前端。本文中我们将学习如何利用 Odoo 自带的网站功能开发面向前端的插件模块。

本文主要内容有:

  • 学习项目 - 自助图书馆
  • 第一个网页
  • 创建网站

开发准备

我将用第十一章 Odoo 12开发之看板视图和用户端 QWeb中最后编辑的library_checkout插件模块,代码请见GitHub 仓库。本文完成后的代码也请参见GitHub 仓库

学习项目 - 自助图书馆

本文中我们将为图书会员添加一个自助服务功能。可供会员分别登录账号来访问他们的借阅请求列表。这样我们就可以学习网站开发的基本技术:创建动态页面、在页面间传递参数、创建表单以及处理表单数据验证。对这些新的图书网站功能,我们要新建一个插件模块library_website。

大家应该已经轻车熟路了,首先创建插件的声明文件ibrary_website/manifest.py,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
'name': 'Library Website',
'description': 'Create and check book checkout requests.',
'author': 'Alan Hou',
'depends': [
'library_checkout'
],
'data': [
'security/ir.model.access.csv',
'security/library_security.xml',
'views/library_member.xml',
],
}

网站功能将会依赖于library_checkout。我们并没有添加对website核心插件模块的依赖。website插件为创建完整功能的网站提供了有用的框架,但现在我们仅探讨核心框架自带的基础网站功能,尚无需使用website。我们想要图书会员通过登录信息在图书网站上访问自己的借阅请求。为此需要在图书会员模型中添加一个user_id字段,需要分别在模型和视图中添加,下面就开始进行网站的创建:

1、添加library_website/models/library_member.py文件

1
2
3
4
5
from odoo import fields, models

class Member(models.Model):
_inherit = 'library.member'
user_id = fields.Many2one('res.users')

2、添加library_website/models/init.py文件:

1
from . import library_member

3、添加library_website/init.py文件:

1
from . import models

4、添加library_website/views/library_member.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<odoo>
<record id="view_form_member" model="ir.ui.view">
<field name="name">Member Form</field>
<field name="model">library.member</field>
<field name="inherit_id" ref="library_member.view_form_member" />
<field name="arch" type="xml">
<field name="card_number" position="after">
<field name="user_id" />
</field>
</field>
</record>
</odoo>

访问这些网页的都是门户用户,无需访问后台菜单。我们需要为这个用户组设置安全访问权限,否则会在使用图书网站功能时报权限错误。

5、添加library_website/security/ir.model.access.csv文件,添加对图书模型的读权限:

1
2
3
4
5
6
7
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_book_portal,Book Portal Access,library_app.model_library_book,base.group_
portal,1,0,0,0
access_member_portal,Member Portal Access,library_member.model_library_member,ba
se.group_portal,1,0,0,0
access_checkout_portal,Checkout Portal Access,library_checkout.model_library_che
ckout,base.group_portal,1,0,0,0

6、在library_website/security/library_security.xml文件中添加记录规则来限制门户用户所能访问的记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0"?>
<odoo>
<data noupdate="1">
<record id="member_portal_rule" model="ir.rule">
<field name="name">Library Member Portal Access</field>
<field name="model_id" ref="library_member.model_library_member" />
<field name="domain_force">
[('user_id', '=', user.id)]
</field>
<field name="groups" eval="[(4,ref('base.group_portal'))]" />
</record>

<record id="checkout_portal_rule" model="ir.rule">
<field name="name">Library Checkout Portal Access</field>
<field name="model_id" ref="library_checkout.model_library_checkout" />
<field name="domain_force">
[('member_id.user_id', '=', user.id)]
</field>
<field name="groups" eval="[(4,ref('base.group_portal'))]" />
</record>
</data>
</odoo>

base.group_portal是门户用户组的标识符。在创建门户用户时,应设置他们的用户类型为 Portal,而不是Internal User。这会让他们属于门户用户组并继承我们上面定义的访问权限:

Odoo 12门户用户类型

补充:以上内容需开启开发者模式才可见

一旦为图书会员创建了一个门户用户,就应在我们会员表单中的用户字段中使用。该登录信息将可以访问相应会员的借阅请求。

小贴士: 在模型中使用 ACL 和记录规则来实现安全权限比使用控制器的逻辑要更为安全。这是因为攻击者有可能跳过网页控制器直接使用RPC 来访问模型 API 。

了解了这些,我们就可以开始实现图书网站的功能了。但首先我们来使用简单的Hello World网页简短地介绍下基本网站概念。

第一个网页

要开始了解 Odoo 网页开发的基础,我们将先实现一个Hello World网页来展示基本概念和技术。很有想象空间,是不是?

要创建第一个网页,我们需要一个控制器对象。首先来添加controllers/hello.py文件:

1、在library_website/init.py文件中添加如下行:

1
from . import controllers

2、在library_website/controllers/init.py文件中添加如下行:

1
from . import hello

3、添加实际的控制器文件 library_website/controllers/hello.py,代码如下:

1
2
3
4
5
6
from odoo import http

class Hello(http.Controller):
@http.route('/helloworld', auth="public")
def helloworld(self):
return('<h1>Hello World!</h1>')

odoo.http模块提供 Odoo 网页相关的功能。我们用于渲染页面的控制器,应该是一个继承了odoo.http.Controller类的对象。实际使用的名称并不是太重要,这里选择了 Hello(),一个常用的选择是 Main()。

在控制器类中使用了匹配 URL 路由的方法。这些路由用于做一些处理并返回结果,通常是返回用户网页浏览器的 HTML 页面。odoo.http.route装饰器用于为 URL 路由绑定方法,本例中使用的是/helloworld 路由。

安装library_website模块(~/odoo-dev/odoo/odoo-bin -d dev12 -i library_website)就可以在浏览器中打开http://xxx:8069/helloworld,我们应该就可以看到Hello World问候语了。

本例中方法执行的处理非常简单,它返回一个带有 HTML 标记的文本字符串,Hello World。

ℹ️使用这里的简单 URL 访问按制器,如果同一 Odoo 实例有多个数据库时,在没有指定目标数据库的情况下将会失败。这可通过在启动配置中设置-d或–db-filter来解决,参见第二章 Odoo 12开发之开发环境准备

你可能注意到在路由装饰中使用了auth=’public’参数,对于无需登录的用户开放的页面就需要使用它。如果删除该参数,仅有登录用户方可浏览此页面。如果没有活跃会话(session)则会进入登录页面。

小贴士: auth=’public’参数实际表示如果访客未登录则使用public特殊用户运行网页控制器。如果登录了,则使用登录用户来代替public。

Odoo 12 Hello World

使用 QWeb 模板的 Hello World

使用 Python 字符串来创建 HTML 很快就会觉得乏味。QWeb可用来增添色彩,下面就使用模板来写一个改进版的Hello World网页。QWeb模板通过 XML 数据文件添加,技术层面上它是与表单、列表视图类似的一种视图类型。它们甚至存储在同一个技术模型ir.ui.view中。

老规矩,需要在声明文件中添加声明来加载文件,编辑library_website/manifest.py文件并添加内容如下:

1
2
3
4
'data': [
...
'views/helloworld_template.xml',
],

然后添加实际的数据文件views/helloworld_template.xml,内容如下:

1
2
3
4
5
6
<?xml version="1.0"?>
<odoo>
<template id="helloworld" name="Hello World Template">
<h1>Hello again World!</h1>
</template>
</odoo>

<template>实际上是一种简写形式,它声明<record>将数据以type=”qweb”类型加载到ir.ui.view模型中。现在,我们需要修改控制器方法来使用这个模板:

1
2
3
4
5
6
7
8
from odoo import http
from odoo.http import request

class Hello(http.Controller):

@http.route('/helloworld', auth="public")
def helloworld(self, **kwargs):
return request.render('library_website.helloworld')

模板的渲染是通过render()函数的 request 对象来实现的。

小贴士: 注意我们添加了**kwargs方法参数。使用该参数,HTTP 请求中的任意附加参数,如GET 或 POST 请求参数,可通过 kwargs 字典捕获。这会让我们的方法更加健壮,因为即便添加了未预期的参数也不会产生错误。

Odoo 12 使用 QWeb 模板的 Hello World

HelloCMS!

下面我们来增加点趣味性,创建我们自己的简单 CMS。为此我们可以通过 URL在路由中使用模板名(一个页面),然后对其进行渲染。然后就可以动态创建网页,通过我们的 CMS 来提供服务。实现方法很简单:

1
2
3
@http.route('/hellocms/<page>', auth='public')
def hello(self, page, **kwargs):
return http.request.render(page)

以上page 参数应匹配一个模板的外部ID,如果在浏览器中打开http://xxx:8069/hellocms/library_website.helloworld,应该又可以看到熟悉的Hello World 页面了。实际上内置的website模块提供了CMS功能,在 /page路径(endpoint)下还包含更为健壮的实现。

Odoo 12 CMS示例

ℹ️在werkzeug的行话中,endpoint是路由的别名,由其静态部分(不含占位符)来表示。比如,CMS 示例中的 endpoint为/hellocms。

大多数情况下,我们要将页面集成到 Odoo 网站中,因此接下来的示例将使用website插件模块。

创建网站

前面的示例并未集成到 Odoo 网站中,并有页面 footer 和网站菜单。Odoo 的website插件模板为方便大家提供这些功能。

要使用网站功能,我们需要在工作实例中安装website插件模块。应当在library_website插件模块中添加这一依赖,修改__manifest__.py的 depends 内容如下:

1
2
3
4
'depends': [
'library_checkout',
'website',
],

要使用网站功能,我们需要对控制器和 QWeb模板进行一些修改。控制器中可在路由上添加一个额外的website=True参数:

1
2
3
@http.route('/helloworld', auth="public", website=True)
def helloworld(self, **kwargs):
return request.render('library_website.helloworld')

集成website模块并非严格要求website=True参数,不添加它也可以在模板视图中添加网站布局。但是通过添加可以让我们在网页控制器中使用一些功能:

  • 路由会自动变成支持多语言并且会从网站安装的语言中自动检测最接近的语言。需要说明这可能会导致重新路由和重定向。
  • 控制器抛出的任何异常都会由网站代码进行处理,这会将默认的错误码变成更友好的错误页面向访客展示。
  • 带有当前网站浏览记录的request.website变量,可在请求中进行使用。
  • auth=public路由的 public用户将是由后台网站配置中选择的用户。这可能会和本地区、时区等相关。

如果在网页控制器中无需使用上述功能,则可省略website=True参数。但大多数网站QWeb模板需要使用website=True开启一些数据,比如底部公司信息,所以最好还是添加上。

ℹ️传入QWeb运行上下文语言的网站数据由website/model/ir_ui_view.py文件中的_prepare_qcontext方法设定。

要在模板中添加网站的基本布局,应为QWeb/HTML包裹一个t-call=”website.layout”指令,如下所示:

1
2
3
4
5
<template id="helloworld" name="Hello World Template">
<t t-call="website.layout">
<h1>Hello World!</h1>
</t>
</template>

t-call运行QWeb模板website.layout并向其传递 XML 内的tcall 节点。website.layout设计用于渲染带有菜单、头部和底部的完整网页,交将传入的内容放在对应的主区域内。这样,我们的Hello World!示例内容就会显示在 Odoo 网站页面中了。

Odoo 12依赖 website 后的 Hello World

添加 CSS 和 JavaScript 资源

我们的网站页面可能需要一些其它的 CSS 或JavaScript资源。这方面的网页由website 管理,因此需要一个方式来告诉它使用这些文件。我们将使用 CSS 来添加一个简单的删除线效果,创建library_website/static/src/css/library.css文件并添加如下内容:

1
2
3
.text-strikeout {
text-decoration: line-through;
}

接下来需要在网站页面中包含该文件。通过在website.assets_frontend模板中添加来实现,该模板用于加载网站相关的资源。添加library_website/views/website_assets.xml数据文件来继承该模板:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<odoo>
<template id="assets_frontend"
name="library_website_assets"
inherit_id="website.assets_frontend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/css"
href="/library_website/static/src/css/library.css" />
</xpath>
</template>
</odoo>

很快我们就会使用text-strikeout这个新的样式类。当然,可以使用相似的方法来添加JavaScript资源。

借阅列表控制器

既然我们已经过了一遍基础知识,就来一起实现借阅列表吧。我们需要使用/checkout URL来显示借阅列表的网页。为此我们需要一个控制器方法来准备要展示的数据,以及一个QWeb模板来向用户进行展示。

在模块中添加library_website/controllers/main.py文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
from odoo import http
from odoo.http import request

class Main(http.Controller):
@http.route('/checkouts', auth='user', website=True)
def checkouts(self, **kwargs):
Checkout = request.env['library.checkout']
checkouts = Checkout.search([])
return request.render(
'library_website.index',
{'docs': checkouts})

控制器获取要使用的数据并传给渲染的模板。本例中控制器需要一个登录了的会话,因为路由中有一个auth=’user’属性。这是默认行为,推荐明确指出需要用户会话。登录了的用户存储在环境对象中,通过 request.env来使用。search()语句使用它来过滤出相应的借阅记录。

对于无需登录即可访问的控制器,所能读取的数据也是非常有限的。这种情况下,我们经常需要对部分代码采用提权上下文运行。这时我们可使用sudo()模型方法,它将权限上下文权限修改为内部超级用户,突破大部分限制。权力越大,责任越大,我们要小心这种操作带来的安全风险。需要特别注意在提权时输入的参数以及执行的操作的有效性。建议将sudo() 记录集操作控制在最小范围内。

回到我们的代码,它以request.render()方法收尾。和之前一样,我们传入了QWeb模板渲染的标识符,和模板运行用到的上下文字典。本例中我们向模板传入 docs 变量,该变量包含要渲染借阅记录的记录集。

借阅 QWeb 模板

QWeb模板使用数据文件来添加,我们可以使用library_website/views/checkout_template.xml文件并添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0"?>
<odoo>
<template id="index" name="Checkout List">
<t t-call="website.layout">
<div id="wrap" class="container">
<h1>Checkouts</h1>

<!-- List of Checkouts -->
<t t-foreach="docs" t-as="doc">
<div class="row">
<input type="checkbox" disabled="True"
t-att-checked="'checked' if doc.stage_id.fold else None" />
<a t-attf-href="/checkout/{{slug(doc)}}">
<h3 t-field="doc.request_date"
t-att-class="'text-strikeout' if doc.stage_id.fold else ''" />
</a>
</div>
</t>
</div>
</t>
</template>
</odoo>

以上代码使用t-foreach指令来迭代 docs 记录集。我们使用了复选框 input 并在借阅完成时保持为已选状态。在 HTML 中,复选框是否被勾选取决于是否有 checked 属性。为此我们使用了t-att-NAME指定来根据表达式动态渲染 checked 属性。当表达式运行结果为 None(或任意其它 false 值)时,QWeb会忽略该属性,本例用它就非常方便了。

在渲染任务名时,t-attf指令用于动态创建打开每个指定任务的明细表单的URL。我们使用一个特殊函数slug()来为每条记录生成易于阅读的 URL。该链接目前尚无法使用,因为我们还没有创建对应的控制器。

在每条借阅记录上,我们还使用了t-att 指令来在借阅为最终状态时应用text-strikeout样式。

Odoo 12图书项目 checkouts

借阅明细页面

借阅列表中的每一项都有一个相应明细页面的链接。我们就为这些链接实现一个控制器,以及实现一个QWeb模板来用于展示。说到这里应该已经很明朗了。

在library_website/controllers/main.py文件中添加如下方法:

1
2
3
4
5
6
7
8
9
10
class Main(http.Controller):
...

@http.route('/checkout/<model("library.checkout"):doc>',
auth='user', # 默认值,但此处明确指定
website=True)
def checkout(self, doc, **kwargs):
return http.request.render(
'library_website.checkout',
{'doc': doc})

注意这里路由使用了带有model(“library.checkout”)转换器的占位符,会映射到方法的 doc 变量中。它从 URL 中捕获借阅标识符,可以是简单的 ID 数值或链接别名,然后转换成相应的浏览记录对象。

对于QWeb模板,应在library_website/views/checkout_template.xml数据文件中添加如下代码:

1
2
3
4
5
6
7
8
9
<template id="checkout" name="Checkout Form">
<t t-call="website.layout">
<div id="wrap" class="container">
<h1 t-field="doc.request_date" />
<h5>Member: <span t-field="doc.member_id" /></h5>
<h5>Stage: <span t-field="doc.stage_id" /></h5>
</div>
</t>
</template>

这里值得一提的是使用了元素。和在后台中一样,它处理字段值的相应展示。比如,它正确地展示日期值和many-to-one值。

Odoo 12借阅明细页面

补充:controllers/init.py和__mainfest__.py 中请自行添加控制器文件和数据文件的引用

总结

读者现在应该对网站功能的基础有了不错的掌握。我们学习了如何使用网页控制器和QWeb模板来动态渲染网页。然后学习了如何使用website插件并使用它来创建我们自己页面。最后,我们介绍了网站表单插件来帮助我们来创建网页表单。这些都是创建网站功能的核心能技巧。

我们已经学习了Odoo 主要构件的开发,是时候学习如何将Odoo 服务部署到生产环境了。

 

☞☞☞第十四章 Odoo 12开发之部署和维护生产实例

 

扩展阅读

Odoo 官方文档中有一些对本文讲解课题的补充参考材料:

本文首发地址:Alan Hou 的个人博客

本文为最好用的免费ERP系统Odoo 12开发手册系列文章第十一篇。

QWeb 是 Odoo 使用的模板引擎,它基于 XML 来生成 HTML 片断和页面。通过 QWeb可生成内容丰富的看板(Kankan)视图、报表和 CMS 网页。本文中我们将学习QWeb 语法以及如何使用 QWeb 来创建我们自己的看板视图和自定义报表。

本文主要内容有:

  • 看板是什么?
  • 设计看板视图
  • QWeb 模板语言
  • 看板视图的继承
  • 添加自定义 CSS 和 JavaScript

开发准备

我们将继续使用第十章 Odoo 12开发之后台视图 - 设计用户界面完成的library_checkout插件模块。相应代码请见 GitHub仓库。本章完成后的代码也请参见GitHub仓库

了解看板

Kanban 是一个日语词汇,字面意思榜单,与精益制造和准时化生产相关联,由丰田工业工程师大野耐一(Taiichi Ohno)引入。最近看板的概念应用于更多领域,并且随着敏捷方法的施行在软件工业内流行起来。

看板让我们能够可视化工作队列,它以列来进行组织,每列代表工作进程的一个阶段。工作项以放在看板对应列的卡片来表示。新的工作项从最左边的列开始,并开始向右移动直至最右边列,代表工作完成。

看板的简单化或视觉效果让其对简单的业务流程有着优异的支持。一个基本的看板示例包含三列,如下图所示:待办、在办和完成。当然它可以扩展为你需要的其它指定流程:

看板示例

对许多业务用例,看板都是管理相应流程的更有效方式,与 Odoo 11之前的更重的工作流引擎形成鲜明对比。Odoo 在支持经典的列表和表单视图的同时还支持看板视图,这易于我们实施这种类型的视图。下面就让我们一起来学习如何使用看板视图。

看板视图

现在我们要为借阅模型添加一个看板视图。每个借阅是一个卡片,看板将会被组织成阶段列。在前面的文章中,我们已经添加了stage_id阶段字段。

此前在表单视图我们大部分时候使用 Odoo 独有的 XML 元素,比如<field><group>,有时也会使用 HTML 元素,如<h1><div>,但用得较少。在看板视图中则恰恰相反,展示模板基于 HTML,仅支持两个 Odoo 独有的元素:<field><button>

最终呈现在网页客户端中的内容是由 QWeb 模板动态生成的。QWeb 引擎处理特殊的 XML 标签和属性来进行生成。这样可以很好地控制如何渲染内容,但也让视图设计更为复杂。看板视图设计灵活性很强,我们将尽力以直接易懂地方式介绍快速创建看板视图的知识。查看与所需相似的看板视图来获取创意然后创建自己的看板是一种不错的方法。

我们将学习两种使用看板视图的方式。一种是卡片列表,它用于联系人、产品、雇员通讯录或应用等。联系人看板视图长这样:

Odoo 12联系人看板视图

但这不是真正的看板,看板应是一个组织成不同列的卡片,当然看板视图也支持这种布局。可能过 CRM 或项目应用来查看示例。访问CRM > Sales > My Pipeline可得到如下结果:

Odoo 12 CRM看板视图

这两种布局的最大区别是卡片按列的组织方式。这通过 Group By 功能实现,与列表视图中相似。通常分组是通过stage字段实现。看板视图的一个非常有用的功能是可以在列之间拖放卡片,自动分配分组视图字段的对应值。从两个示例中的卡片我们可以看到一些分别。其实它们的设计非常灵活,设计看板卡片不只有一种方式。这两个示例为我们提供设计的一些基础。

联系人卡片基本组成有左侧的图像,主区域的加粗标题和紧随其后的一系列值。CRM 管道卡片结构更为复杂些。卡片主区域也有一个标题以及相关信息紧随其后,还有 footer 区。在该区域中,可看到左侧有一个优先级组件,后面带有一个活动指示,在右侧是一个负责用户的头像。上图中看不到,在鼠标悬停在右上角时还会有一个选项菜单。这个菜单让我们可以修改卡片的颜色提示等。

我们将使用这种更复杂的结构来作为借阅看板卡片的参照。

设计看板视图

我们将改进一直以来开发的library_checkout模型,为图书借阅添加看板视图。为此我们使用一个新文件library_checkout/views/checkout_kanban_view.xml。需要在__manifest__.py文件的 data 键最下方添加这个文件。在library_checkout/views/library_menu.xml文件中,可以看到借阅菜单项使用的窗口操作。需要对其修改来启用本文中添加的视图类型:

1
2
3
4
<act_window id="action_library_checkout"
name="Checkouts"
res_model="library.checkout"
view_mode="kanban,tree,form,activity,calendar,graph,pivot" />

这里我们修改了菜单操作来在view_mode列表的最前面添加了kanban,来让它成为默认的视图模式。然后我们来添加kanban视图记录。与其它视图基本相同,除了 arch 字段内,最外层 XML元素为。下一步创建实际使用的 XML 文件library_checkout/views/checkout_kanban_view.xml来放置这个惊艳的看板视图:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<odoo>
<record id="library_checkout_kanban" model="ir.ui.view">
<field name="model">library.checkout</field>
<field name="arch" type="xml">
<kanban>

</kanban>
</field>
</record>
</odoo>

在使用看板视图前,我们需要为图书借阅模型添加几个字段。

优先级、看板状态和颜色

除阶段外,看板中还有一些常用和有用的字段:

  • priority让用户组织他们的工作项,标记什么应优先处理
  • kanban_state标记是否应移向下一阶段或因某种原因原地不动。在模型定义层中两者都是选择项字段。在视图层,对它们有特别的组件用于表单和看板视图。
  • color用于存储看板卡片显示的颜色,并可通过看板视图中的颜色拾取器菜单设置

编辑library_checkout/models/library_checkout.py文件来在我们的模型中添加这些字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Checkout(models.Model):
...
priority = fields.Selection(
[('0', 'Low'),
('1', 'Normal'),
('2', 'High')],
'Priority',
default='1')
kanban_state = fields.Selection(
[('normal', 'In Progress'),
('blocked', 'Blocked'),
('done', 'Ready for next stage')],
'Kanban State',
default='normal')

我们还应该在表单视图中添加这些字段,使用各自的特别组件。kanban_state字段就加在<div class="oe_title">之前并在按钮框之后:<field name="kanban_state" widget="state_selection" />。priority应添加在name 字段之前,包裹在<h1>元素中:<field name="priority" widget="priority" />。color字段一般不出现在表单视图中。

既然借阅模型已有我们所需使用的所有字段,我们可以来写看板视图了。

看板卡片元素

看板视图框架包含一个<kanban>外层元素和以下基础结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<kanban default_group_by="stage_id" class="o_kanban_small_column">
<!-- Fields -->
<field name="stage_id" />
<field name="id" />
<field name="color" />
<field name="kanban_state" />
<field name="priority" />
<field name="message_partner_ids" />

<!-- Optional progress bar -->
<progressbar
field="kanban_state"
colors='{"done": "success", "blocked": "danger"}' />
<!-- Templates with HTML snippets to use -->
<templates>
<t t-name="kanban-box">
<!-- HTML Qweb template -->
</t>
</template>
</kanban>

注意在元素中使用了default_group_by=”stage_id”属性,我们用它来让看板默认以 stage 分组,这也是看板通常的分组方式。在简单卡片列表的看板中,如联系人,我们不需要添加该属性,只需使用<kanban>标签即可。<kanban>元素支持以下属性:

  • default_group_by设置默认列分组使用的字段
  • default_order设置看板项默认使用的排序
  •  quick_create=”false”禁用了每列顶部的快速创建选项(大的加号符号),快速创建只需提供标题描述即可创建新项。false是 JavaScript 的语法,必须是小写字母。
  • class为渲染看板视图的根元素添加 CSS 类。相关类是_kanban_small_column,让列比默认的更加紧湊。其它类可由我们模块的 CSS 文件来进行提供。
  •  group_create, group_edit, group_delete和quick_create_view可设置为 false 来禁用看板列上对应的操作。如group_create=”false”删除右侧添加新列的按钮。
  • on_create用于创建用户点击左上角 Create 按钮时弹出的自定义简单表单视图窗口。应为相应的表单视图添加<module>.<xml_id>值。

然后我们的模板中使用了一组字段。确切地说,只有在 QWeb 表达式中明确使用的字段才需要在这里声明,用以保证从服务端抓取它们的数据。QWeb引擎在处理模板前,仅会在视图中查找 <field name="...">来从模型中获取数据。QWeb的属性通常使用不会被检测到的record.field引用方式。正因为如此,需在<templates>之前包含这些字段来让模板处理时有相应字段值可以使用。

ℹ️Odoo 11中的修改
引入了进度条组件。使用的时候在看板列的上方会出现一个颜色条,来提供该列各项的状态数据。在本文前面CRM Pipeline的示例图中可以查看。

<progressbar>有如下属性:

  • field是对列中各项进行颜色分组的字段名
  • colors是一个字典,将分组字段值与以下三种颜色分别进行映射:danger (红色), warning (黄色)或success (绿色)。
  • sum_field是一个可选项,用于选取整列汇总的字段名。如未设置,会使用各项的计数值。

然后我们的<templates>元素包含一个或多个QWeb模板来生成要使用的 HTML 片断。必须要有一个名为kanban-box的模板,它渲染看板卡片。还可以添加其它模板,通常用于定义主模板中复用到的 HTML 片断。这些模板使用标准的 HTML 和 QWeb 模板语言。QWeb提供了一些特殊指令,用于处理动态生成最终展示的 HTML。

ℹ️Odoo 12中的修改
Odoo 现在使用 Twitter Bootstrap 4,此前版本中使用Bootstrap 3。这些样式在渲染 HTML 的地方通常都可使用,有关Bootstrap更多知识请见官方网站

下面就来详细了解看板视图中所使用的QWeb模板设计。

看板卡片布局

看板卡片主内容区域在kanban-box模板内定义。这个内容区也可以有一个 footer 底部子容器。卡片右上角还可以添加按钮,点击后打开操作菜单的功能。对于footer区域,应在看板盒子模型底部使用

并添加oe_kanban_bottom CSS 类。还可以通过oe_kanban_bottom_left和oe_kanban_bottom_right CSS 类进一步分割为左、右 footer 区。此外,可通过Bootstrap的pull-left和pull-right类在卡片的任意位置(包括oe_kanban_bottom底部区域)添加向左或向右对齐元素。

以下是对看板卡片中QWeb模板的第一次迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<t t-name="kanban-box">
<!-- Set the Kanban Card color -->
<div t-attf-class="
oe_kanban_color_#{kanban_getcolor(record.color.raw_value)}
oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown">
<!-- Top-right drop down menu here... -->
</div>
<div class="oe_kanban_body">
<!-- Content elements and fields go here... -->
</div>
<div class="oe_kanban_footer">
<div class="oe_kanban_footer_left">
<!-- Left hand footer... -->
</div>
<div class="oe_kanban_footer_right">
<!-- Right hand footer... -->
</div>
</div>
<div class="oe_clear" />
</div>
</t>

这就是看板卡片的整体结构。你可能注意到了在顶部<div>元素中使用了color字段来动态设置卡片颜色。在后面的部分中我们会讲解t-attf QWeb指令的细节。现在来为主内容区域添加内容:

1
2
3
4
5
6
7
8
9
10
11
<div class="oe_kanban_body">
<div>
<strong>
<a type="open"><field name="member_id" /></a>
</strong>
</div>
<ul>
<li><field name="user_id" /></li>
<li><field name="request_date" /></li>
</ul>
</div>

这个模板中大部分都是常规 HTML,但也有渲染字段值的<field>元素和在常规表单视图按钮中使用的 type 属性,此处用在锚文本标签中。

在左部 footer 中插入优先级组件:

1
2
3
4
5
6
7
                            <div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<field name="priority" widget="priority" />
<field name="activity_ids" widget="kanban_activity" />
</div>
...
</div>

这里像我们在表单视图中做的那样添加了priority字段。还添加了一个计划活动的字段,使用kanban_activity特殊组件来显示即将开始活动的指示。

在右部footer中,放入看板状态组件和请求借阅的会员头像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="oe_kanban_bottom_right">
<field name="kanban_state"
widget="kanban_state_selection" />
<img t-att-src="kanban_image(
'library.checkout',
'member_image',
record.id.raw_value)"
t-att-title="record.member_id.value"
t-att-alt="record.member_id.value"
width="24"
height="24"
class="oe_kanban_avatar"
/>
</div>

补充:原文件使用的 CSS 类oe_kanban_footer,oe_kanban_footer_left和oe_kanban_footer_right经测试不会进行左右对齐,参照 CRM 进行了如上修改

看板状态通过<field>元素和kanban_state_selection组件来进行添加。用户头像使用 HTML <img>标签插入。图像内容使用QWeb t-att-命令动态生成,后面会详细讲解。这里使用了kanban_image()帮助函数来获取src属性的值。kanban_image() Javascript函数从 Odoo 模型中获取表单并在网页中渲染。有以下属性:

  • 获取图像的模型
  • 包含图像的字段
  • 获取的记录 ID

Odoo 12看板 footer 添加

为看板卡片添加选项菜单

看板卡片可在右上角带有一个选项菜单。通常的操作有编辑或删除记录,但也可以为其添加和按钮调用的同样操作。还有一个设置卡片颜色的组件。以下是oe_kanban_content顶部添加的选项菜单的基础代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="o_dropdown_kanban dropdown">
<a class="dropdown-toggle btn"
data-toggle="dropdown" role="button"
aria-label="Dropdown menu"
title="Dropdown menu"
href="#">
<span class="fa fa-ellipsis-v" />
</a>
<div class="dropdown-menu" role="menu">
<!-- Edit and Delete actions, if available: -->
<t t-if="widget.editable">
<a role="menuitem" type="edit" class="dropdown-item">Edit</a>
</t>
<t t-if="widget.deletable">
<a role="menuitem" type="delete" class="dropdown-item">Delete</a>
</t>
<!-- Color picker option -->
<ul class="oe_kanban_colorpicker" data-field="color" />
</div>
</div>

下拉菜单基本上是由带有<a>标签的<li> HTML 列表元素组成。Edit 和 Delete 这类选项需要满足指定条件下才会出现。这通过QWeb的t-if命令来实现。本文后续会详细讲解QWeb的命令。widget全局变量表示一个KanbanRecord()  JS 对象,负责渲染当前看板卡片。有两个非常有用的属性:widget.editable和widget.deletable,让我们可以检查相应的操作是否可用。

可以看到如何根据记录字段值来显示或隐藏选项,Set as Done仅在未设置is_done 字段时才会显示。最后一个选项添加颜色拾取器组件来使用 color 数据字段选择或修改卡片背景色。因此,除<button>元素外,<a>也可用于运行Odoo 操作。

Odoo 12看板视图下拉选项

看板视图中的操作

在QWeb模板中,用于超链的<a>标签可带有一个 type 属性。它设置链接执行的操作类型,这样链接和常规表单中的按钮可进行同样的操作。和表单视图一样,操作类型可以是action或object,并应带有一个 name 属性来标识所要执行的具体操作。此外,还有以下操作类型可以使用:

  • open打开相应的表单视图
  • edit在编辑模式下直接打开相应的表单视图
  • delete删除记录并从看板视图中删除该项

QWeb 模板语言

QWeb会查找模板中的特殊指令并替换为动态生成的 HTML。这些指令是 XML 元素属性,可以用在<div>, <span><field>等有效标签或元素中。有时我们要使用QWeb指令但不希望放在模板的 XML 元素中。对这种情况,可以使用能带有 QWeb 指令(如t-if或t-foreach)的特殊元素<t>,该元组不会在最终产生的XML/HTML有任何输出。

QWeb指令常使用运算的表达式来根据当前记录值生成不同的结果。有两种不同的QWeb实现:客户端JavaScript和服务端Python。报表和网页使用服务端QWeb的 Python 实现。看板视图使用客户端JavaScript实现。也就是说看板视图中的QWeb表达式应使用JavaScript语法书写,而不是 Python。

在显示看板视图时,内部的步骤大致如下:

  1. 获取模板的XML进行渲染
  2. 调用服务端read()方法来获取模板中所涉及的字段数据
  3. 定位kanban-boxs模板并使用QWeb解析它来输出最终的HTML片断
  4. 在浏览器显示(DOM)中注入 HTML

以上在技术上并不精确,仅是用于理解看板视图中如何运作的脑图。下面我们将学习QWeb表达式运行并探讨可用的QWeb指令,通过示例改进借阅看板卡片。

QWeb JavaScript 运行上下文

许多QWeb指令使用表达式的运行来生成结果。在看板视图这类客户端的应用中,表达式应使用JavaScript书写。表达式在带有几个有用变量的上下文中进行运行。可用record 对象带有从服务端请求的字段来表示当前记录。字段值可通过raw_value或value属性来获取:

  • raw_value是由服务端read()方法返回的值,因此在条件表达式中更适用
  • value根据用户设置来格式化,用于在用户界面中的显示。常用于date/datetime, float/monetary和关联字段。

QWeb运行上下文还可在JavaScript网页客户端中引用。要擅用这些需要对网页客户端结构有很好的理解,但这里我们不会进行深入介绍。要进行引用 ,QWeb表达式运行中有以下标识符可以使用:

  • widget是对当前KanbanRecord() 组件对象的引用 ,用于在看板卡片中渲染当前记录。它会暴露一些帮助函数供我们使用。
  • record是widget.record的简写形式,使用点号标记来提供对可用字段的访问。
  • read_only_mode表示当前视图是否为读模式(而非编辑模式)。它是widget.view.options.read_only_mode的简写形式。
  • instance是对全部网页客户端实例的一个引用 。

值得一提的是有些字符是不能在表达式中使用的,比如小于号(<) 。这是因为在 XML 标准中,这些字符具有特殊含义,不应在 XML 内容中使用。反向的>=是一个有效替代方式,但通常是使用以下替代符号来进行不等式运算:

  • lt是小于
  • lte是小于等于
  • gt是大于
  • gte是大于等于

ℹ️前述的比较符号仅用于 Odoo,是引入来解决 XML 格式中的限制的。它们不是 XML 标准的一部分。

字符串替换动态属性– t-attf

我们的看板卡片使用t-attf QWeb指令来为顶级<div>元素动态设置一个类,这样卡片可根据 color 字段值来显示颜色。为此使用了t-attf- QWeb指令。t-attf-指令使用字符串替换动态生成标签属性。这让像 URL 地址或 CSS 类名这类较大字符串中的部分内容可动态生成。

该指令查找表达式代码块进行运行并替换结果。它们通过 或#{和}来进行分隔。代码块的内容可以是任意JavaScript表达式并使用QWeb表达式中的任意可用变量,如record和widget。本例中我们还使用了专门提供的kanban_color() JS 函数,用于映射索引值到类颜色名。

作为一个更复杂的示例,我们使用这个指令来动态生成用户的颜色,红色字体表示优先级很高。下面替换看板卡片中的相应代码:

1
2
3
4
5
<li t-attf-class="oe_kanban_text_{{
record.priority.raw_value lt '2'
? 'black' : 'red'}}">
<field name="user_id" />
</li>

这将会根据借阅优先级的值生成class=”oe_kanban_text_red”或class=”oe_kanban_text_black”。请注意看板视图中是有oe_kanban_text_red这个 CSS 类的,但oe_kanban_text_black仅用于演示,实际并不存在。

ℹ️注意JavaScript表达式中使用的lt符号,是<的转义表达式,并不能在XML中使用。

Odoo 12字符串替换动态属性

表达式动态属性 - t-att

t-att- QWeb指令通过运行表达式动态生成属性值。我们的看板卡片中使用它来为<img>标签动态生成属性,title 属性使用以下表达式动态渲染:

1
t-att-title="record.member_id.value"

.value字段返回在屏幕上显示的值。对于many-to-one字段,这通常是相关记录的 name 值。对于用户则是用户名。运行之后在鼠标悬停于图像上时会显示相应的用户名。

在表达式运行的结果值为假时,就不会渲染该属性。这对于特殊的 HTML 属性非常重要,比如 input 字段中的 checked,即便在没有属性值时也会有显示效果。

Odoo 12表达式动态属性

循环 - t-foreach

通过循环遍历来重复同一 HTML 代码块。我们可使用它来添加记录 follower 的头像。让我们先来仅渲染记录的partner ID:

1
2
3
<t t-foreach="record.message_partner_ids.raw_value" t-as="rec">
<t t-esc="rec" />;
</t>

t-foreach指令接收一个JavaScript表达式,运行来遍历集合。在大多数情况下,这会是 一个to-many关联字段的名称。与t-as指令一同使用来设置用于引用遍历各项的名称。下面使用的t-esc指令运行所提供的表达式,本处仅为 rec 变量名,将其渲染为已转译的安全 HTML。

在上例中,我们遍历了存储在message_partner_ids 字段中的 follower。因为在看板卡片上的空间有限,我们使用JS 的slice()函数来限定所显示的follower数量,如下所示:

1
t-foreach="record.message_partner_ids.raw_value.slice(0, 3)"

rec变量存储每个遍历值,本例中为partner ID。这样我们可以将循环改写为:

1
2
3
4
<t t-foreach="record.message_partner_ids.raw_value.slice(0,3)" t-as="rec">
<img t-att-src="kanban_image('res.partner', 'image_small', rec)"
class="oe_avatar" width="24" height="24" alt="" />
</t>

比如可将其添加在右侧 footer 的用户头像旁。还包含一些帮助变量,它们的名称以t_as 中定义的变量名为前缀。本例中使用了rec,因此可用的帮助变量如下:

  • rec_index是迭代索引,从0开始
  • rec_size是集合中的元素数量
  • rec_first在迭代的第一个元素中为真
  • rec_last在迭代的最后一个元素中为真
  • rec_even在索引为偶数时为真
  • rec_odd在索引为奇数时为真
  • rec_parity根据当前索引为odd或even
  • rec_all表示进行迭代的对象
  • rec_value在迭代{key:value} 字典时,存储value (rec存储键名)

例如可通过如下代码去除ID 列表最后逗号:

1
2
3
4
5
<t t-foreach="record.message_parter_ids.raw_value.slice(0, 3)"
t-as="rec">
<t t-esc="rec" />
<t t-if="!rec_last">;</t>
</t>

Odoo 12 循环遍历

条件判断 - t-if

我们的看板视图在卡片选项菜单中使用了t-if指令来根据不同条件显示不同选项。t-if指令在客户端渲染看板视图时需传入在 JS 中运行的表达式。标签和其内容仅在条件运行值为true 时才会渲染。作为示例,仅在借出有值时显示图书借出数量,在request_date字段后加入如下代码:

1
2
3
<t t-if="record.num_books.raw_value gt 0">
<li><field name="num_books" /> books</li>
</t>

我们使用了<t t-if="...">元素,这样在条件为 false 时,元素不会有任何输出。在为 true 时,仅会渲染其所包含的<li>元素来进行输出。注意条件表达式中使用gt符号来替代>以表示大于运算符。可通过t-elif和t-else来支持else if和else条件语句,使用示例如下:

1
2
3
4
5
6
7
8
9
<t t-if="record.num_books.raw_value == 0">
<li>No books.</li>
</t>
<t t-elif="record.num_books.raw_value gt 9">
<li>A lot of books!</li>
</t>
<t t-else="">
<li><field name="num_books" /> books.</li>
</t>

Javascript表达式中,AND和OR的运算符分别为&&和 ||。但在 XML 中不支持&符号,我们可以使用 and 和 or 运算符来规避这一问题。

Odoo 12 QWeb 条件判断

渲染值 - t-esc和t-raw

我们使用了<field>元素来渲染值,但也可以无需<field>标签直接显示字段值。t-esc指令运行表达式并将其渲染为转义后的 HTML 值,如下所示:

1
<t t-esc="record.message_partner_ids.raw_value" />

有些情况下,如果确定源数据是安全的,可以无需转义使用t-raw 来渲染原始值,如下例所示:

1
<t t-raw="record.message_partner_ids.raw_value" />

小贴士: 出于安全考虑,应尽量避免使用t-raw。它应严格用于输出特别准备不包含用户数据的HTML 数据,或者是已明确对 HTML 特殊字符转义的用户数据。

为变量设置值 - t-set

对于更复杂的逻辑,我们可以将表达式结果存储在变量中,在模板中随后使用。这通过t-set指令来实现,它设置变量名,紧接着使用t-value指令来添加表达式计算分配的值。作为示例,以下代码将优先级较高的和前面一节一样渲染为红色,但使用red_or_black 变量来作为 CSS 类使用的变量,如下所示:

1
2
3
4
5
<t t-set="red_or_black"
t-value="record.priority.raw_value gte '2' ? 'oe_kanban_text_red' :''" />
<li t-att-class="red_or_black">
<field name="user_id" />
</li>

变量中也可分配 HTML内容,示例如下:

1
2
3
4
<t t-set="calendar_sign">
<i class="fa fa-calendar" />
</t>
<t t-raw="calendar_sign" />

 

Odoo 12为变量设置值

调用和复用其它模板 - t-call

QWeb模板可作为可复用的 HTML 片段插入到其它模板中。我们无需重复相同的 HTML 代码块,可以设计构成部分来组成更为复杂的用户界面视图,可复用的模板在<templates>标签中定义,通过顶级元素中 kanban-box 以外的 t-name值进行标识。这些模板可通过t-call来进行包含,在当前看板视图、相同模块的其它地方以及其它插件模块中均可。

follower头像列表可以通过可复用代码段来进行分离,下面通过子模板重写代码。首先应在 XML 文件中添加另一个模板,在<templates>元素内,<t t-name="kanban-box">节点之后,添加如下代码:

1
2
3
4
5
6
7
8
9
<t t-name="follower_avatars">
<div>
<t t-foreach="record.message_partner_ids.raw_value.slice(0,3)"
t-as="rec">
<img t-att-src="kanban_image('res.partner', 'image_small', rec)"
class="oe_avatar" width="24" height="24" alt="" />
</t>
</div>
</t>

在kanban-box主模板调用它就简单明了了,将原来包含 for each 指令的

元素修改为如下代码:

1
<t t-call="follower_avatars" />

调用其它插件模块中定义的模板,和视图类似,我们需要使用完整的module.name标识符。比如,以上代码片断可使用library_checkout.follower_avatars完整标识符来进行引用 。调用的模板和调用者运行在同一上下文中,所以调用方中的变量名在处理调用模板时同样可用。

一种更优雅的实现方式是向调用模板传递参数,这通过在 t-call 标签中设置变量来完成。这些仅在子模板上下文中运行和使用,在调用方上下文中并不存在。我们将使用这个方法来让调用方设置follower 头像的最大数,而不是在子模板中硬编码。首先,我们将原固定值3修改为一个变量 arg_max:

1
2
3
4
5
6
7
8
9
<t t-name="follower_avatars">
<div>
<t t-foreach="record.message_partner_ids.raw_value.slice(0, arg_max)"
t-as="rec">
<img t-att-src="kanban_image('res.partner', 'image_small', rec)"
class="oe_avatar" width="24" height="24" alt="" />
</t>
</div>
</t>

然后像下面这样在执行子模板调用时定义该变量:

1
2
3
<t t-call="follower_avatars">
<t t-set="arg_max" t-value="3" />
</t>

t-call元素内的整个内容可通过0(数字零)这个魔法变量在子模板中使用。不使用参数变量,我们还可以定义代码片断并在子模板中通过<t t-raw="0" />使用。这对以模块化的方式创建布局、合并/嵌套 QWeb 模板尤为有用。

字典和列表动态属性

我们已经学习最重要的那些QWeb指令,但还有一部分我们也应该了解。下面简短地进行讲解。

前面我们看到t-att-NAME和t-attf-NAME样式的动态标签属性,此外还可以使用固定的t-att指令。它接收键值对字典或pair(两个元素的列表)。

使用如下映射:

1
<p t-att="{'class': 'oe_bold', 'name': 'Hello'}" />

将生成如下结果:

1
<p class="oe_bold" name="Hello" />

使用如下 pair:

1
<p t-att="['class', 'oe_bold']" />

将生成如下结果:

1
<p class="oe_bold" />

看板视图的继承

看板视图和报表中使用的模板可通过视图相同的常规方法来进行继承,例如,使用XPath表达式,参见第四章 Odoo 12 开发之模块继承

常见的情况是使用<field>元素作为选择器,然后在其前或后添加其它元素。对于看板视图,同一字段可声明多次,例如在模板前和模板内分别声明。这时,选择器将匹配第一个字段元素,不会将修改我们希望修改的模板内的字段。要规避这一问题,我们需使用XPath来确保匹配的是模板内的字段,例如:

1
2
3
4
5
6
7
8
9
10
11
<record id="res_partner_kanban_inherit" model="ir.ui.view">
<field name="name">Contact Kanban modification</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.res_partner_kanban_view" />
<field name="arch" type="xml">
<xpath expr="//t[@t-name='kanban-box']//field[@name='display_name']"
position="before">
<span>Name:</span>
</xpath>
</field>
</record>

在上例中,XPath 查找<t t-name="kanban-box">元素内的<field name="display_name">元素。这条规则会排除掉<templates>版块之外的相同字段元素。对于更 复杂的XPath表达式,我们可以使用命令行工具来研究出正确的语法。你的 Linux 系统中可能已有安装了xmllint工具(sudo apt install libxml2-utils),它有一个–xpath 选项可对 XML 文件执行查询。

另一个输出更好看的选项是Debian/Ubuntu包中的libxml-xpath-perl,带有一个xpath 命令:

1
2
$ sudo apt-get install libxml-xpath-perl
$ xpath -e "//record[@id='res_partner_kanban_view']" -e "//field[@name='display_name']]" /path/to/myfile.xml

自定义 CSS 和 JavaScript

如前所见,看板视图大多数为 HTML 并重度使用了 CSS 类。我们介绍了标准产品中提供的一些常用 CSS 类,但要实现最佳效果,我们还可以为模块添加自己的 CSS。我们这里不会详细讲解 CSS 代码的写法,但相应地需要讲解如何为模块添加自己的 CSS (JavaScript)这些前端资源。Odoo 中后台的前端资源在assets_backend模块中声明。要在模块中添加前端资源,需要对模块进行继承。进行这一操作的 XML 文件通常放在views/ 模块子目录内。

以下是在library_checkout模块中添加一个 CSS 和 JavaScript文件的示例,对应文件为library_checkout/views/checkout_kanban_assets.xml:

1
2
3
4
5
6
7
8
9
10
11
12
<odoo>
<template id="assets_backend" inherit_id="web.assets_backend"
name="Library Checkout Kanban Assets">
<xpath expr="." position="inside">
<link rel="stylesheet"
href="/library_checkout/static/src/css/checkout_kanban.css" />
<script type="text/javascript"
src="/library_checkout/static/src/js/checkout_kanban.js">
</script>
</xpath>
</template>
</odoo>

和平常一样,需要在__manifest__.py描述文件中对其进引用,注意这些前端文件放在/static/src 子目录中,这不是强制要求,但是约定俗成如此。

总结

我们学习了看板和如何创建看板视图来实现这些看板。我们还介绍了QWeb模板以及使用它来设计看板卡片。QWeb同时还是 CMS 网站的渲染引擎,因此它在 Odoo 工具集中的重要性越来越高。

看板视图可通过其它视图中使用的相同XML语法来进行继承。看板的 XML 结构可能会更为复杂,我们经常需要使用XPath表达式来定义需继承的元素。

最后,高级看板视图可以使用独有的 CSS 和 JavaScript 文件。可作为模块文件来进行添加,然后应在web.assets_backend QWeb模板中添加这些文件,以在客户端页面中包含。

在下一篇文章中,我们将继续使用QWeb,但是是在服务端创建自定义报表 。

 

☞☞☞第十二章 Odoo 12开发之报表和服务端 QWeb

 

扩展阅读

以下参考材料是对本文所学习课题的补充:

本文首发地址:Alan Hou 的个人博客

本文为最好用的免费ERP系统Odoo 12开发手册系列文章第十一篇。

QWeb 是 Odoo 使用的模板引擎,它基于 XML 来生成 HTML 片断和页面。通过 QWeb可生成内容丰富的看板(Kankan)视图、报表和 CMS 网页。本文中我们将学习QWeb 语法以及如何使用 QWeb 来创建我们自己的看板视图和自定义报表。

本文主要内容有:

  • 看板是什么?
  • 设计看板视图
  • QWeb 模板语言
  • 看板视图的继承
  • 添加自定义 CSS 和 JavaScript

开发准备

我们将继续使用第十章 Odoo 12开发之后台视图 - 设计用户界面完成的library_checkout插件模块。相应代码请见 GitHub仓库。本章完成后的代码也请参见GitHub仓库

了解看板

Kanban 是一个日语词汇,字面意思榜单,与精益制造和准时化生产相关联,由丰田工业工程师大野耐一(Taiichi Ohno)引入。最近看板的概念应用于更多领域,并且随着敏捷方法的施行在软件工业内流行起来。

看板让我们能够可视化工作队列,它以列来进行组织,每列代表工作进程的一个阶段。工作项以放在看板对应列的卡片来表示。新的工作项从最左边的列开始,并开始向右移动直至最右边列,代表工作完成。

看板的简单化或视觉效果让其对简单的业务流程有着优异的支持。一个基本的看板示例包含三列,如下图所示:待办、在办和完成。当然它可以扩展为你需要的其它指定流程:

看板示例

对许多业务用例,看板都是管理相应流程的更有效方式,与 Odoo 11之前的更重的工作流引擎形成鲜明对比。Odoo 在支持经典的列表和表单视图的同时还支持看板视图,这易于我们实施这种类型的视图。下面就让我们一起来学习如何使用看板视图。

看板视图

现在我们要为借阅模型添加一个看板视图。每个借阅是一个卡片,看板将会被组织成阶段列。在前面的文章中,我们已经添加了stage_id阶段字段。

此前在表单视图我们大部分时候使用 Odoo 独有的 XML 元素,比如<field><group>,有时也会使用 HTML 元素,如<h1><div>,但用得较少。在看板视图中则恰恰相反,展示模板基于 HTML,仅支持两个 Odoo 独有的元素:<field><button>

最终呈现在网页客户端中的内容是由 QWeb 模板动态生成的。QWeb 引擎处理特殊的 XML 标签和属性来进行生成。这样可以很好地控制如何渲染内容,但也让视图设计更为复杂。看板视图设计灵活性很强,我们将尽力以直接易懂地方式介绍快速创建看板视图的知识。查看与所需相似的看板视图来获取创意然后创建自己的看板是一种不错的方法。

我们将学习两种使用看板视图的方式。一种是卡片列表,它用于联系人、产品、雇员通讯录或应用等。联系人看板视图长这样:

Odoo 12联系人看板视图

但这不是真正的看板,看板应是一个组织成不同列的卡片,当然看板视图也支持这种布局。可能过 CRM 或项目应用来查看示例。访问CRM > Sales > My Pipeline可得到如下结果:

Odoo 12 CRM看板视图

这两种布局的最大区别是卡片按列的组织方式。这通过 Group By 功能实现,与列表视图中相似。通常分组是通过stage字段实现。看板视图的一个非常有用的功能是可以在列之间拖放卡片,自动分配分组视图字段的对应值。从两个示例中的卡片我们可以看到一些分别。其实它们的设计非常灵活,设计看板卡片不只有一种方式。这两个示例为我们提供设计的一些基础。

联系人卡片基本组成有左侧的图像,主区域的加粗标题和紧随其后的一系列值。CRM 管道卡片结构更为复杂些。卡片主区域也有一个标题以及相关信息紧随其后,还有 footer 区。在该区域中,可看到左侧有一个优先级组件,后面带有一个活动指示,在右侧是一个负责用户的头像。上图中看不到,在鼠标悬停在右上角时还会有一个选项菜单。这个菜单让我们可以修改卡片的颜色提示等。

我们将使用这种更复杂的结构来作为借阅看板卡片的参照。

设计看板视图

我们将改进一直以来开发的library_checkout模型,为图书借阅添加看板视图。为此我们使用一个新文件library_checkout/views/checkout_kanban_view.xml。需要在__manifest__.py文件的 data 键最下方添加这个文件。在library_checkout/views/library_menu.xml文件中,可以看到借阅菜单项使用的窗口操作。需要对其修改来启用本文中添加的视图类型:

1
2
3
4
<act_window id="action_library_checkout"
name="Checkouts"
res_model="library.checkout"
view_mode="kanban,tree,form,activity,calendar,graph,pivot" />

这里我们修改了菜单操作来在view_mode列表的最前面添加了kanban,来让它成为默认的视图模式。然后我们来添加kanban视图记录。与其它视图基本相同,除了 arch 字段内,最外层 XML元素为。下一步创建实际使用的 XML 文件library_checkout/views/checkout_kanban_view.xml来放置这个惊艳的看板视图:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<odoo>
<record id="library_checkout_kanban" model="ir.ui.view">
<field name="model">library.checkout</field>
<field name="arch" type="xml">
<kanban>

</kanban>
</field>
</record>
</odoo>

在使用看板视图前,我们需要为图书借阅模型添加几个字段。

优先级、看板状态和颜色

除阶段外,看板中还有一些常用和有用的字段:

  • priority让用户组织他们的工作项,标记什么应优先处理
  • kanban_state标记是否应移向下一阶段或因某种原因原地不动。在模型定义层中两者都是选择项字段。在视图层,对它们有特别的组件用于表单和看板视图。
  • color用于存储看板卡片显示的颜色,并可通过看板视图中的颜色拾取器菜单设置

编辑library_checkout/models/library_checkout.py文件来在我们的模型中添加这些字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Checkout(models.Model):
...
priority = fields.Selection(
[('0', 'Low'),
('1', 'Normal'),
('2', 'High')],
'Priority',
default='1')
kanban_state = fields.Selection(
[('normal', 'In Progress'),
('blocked', 'Blocked'),
('done', 'Ready for next stage')],
'Kanban State',
default='normal')

我们还应该在表单视图中添加这些字段,使用各自的特别组件。kanban_state字段就加在<div class="oe_title">之前并在按钮框之后:<field name="kanban_state" widget="state_selection" />。priority应添加在name 字段之前,包裹在<h1>元素中:<field name="priority" widget="priority" />。color字段一般不出现在表单视图中。

既然借阅模型已有我们所需使用的所有字段,我们可以来写看板视图了。

看板卡片元素

看板视图框架包含一个<kanban>外层元素和以下基础结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<kanban default_group_by="stage_id" class="o_kanban_small_column">
<!-- Fields -->
<field name="stage_id" />
<field name="id" />
<field name="color" />
<field name="kanban_state" />
<field name="priority" />
<field name="message_partner_ids" />

<!-- Optional progress bar -->
<progressbar
field="kanban_state"
colors='{"done": "success", "blocked": "danger"}' />
<!-- Templates with HTML snippets to use -->
<templates>
<t t-name="kanban-box">
<!-- HTML Qweb template -->
</t>
</template>
</kanban>

注意在元素中使用了default_group_by=”stage_id”属性,我们用它来让看板默认以 stage 分组,这也是看板通常的分组方式。在简单卡片列表的看板中,如联系人,我们不需要添加该属性,只需使用<kanban>标签即可。<kanban>元素支持以下属性:

  • default_group_by设置默认列分组使用的字段
  • default_order设置看板项默认使用的排序
  •  quick_create=”false”禁用了每列顶部的快速创建选项(大的加号符号),快速创建只需提供标题描述即可创建新项。false是 JavaScript 的语法,必须是小写字母。
  • class为渲染看板视图的根元素添加 CSS 类。相关类是_kanban_small_column,让列比默认的更加紧湊。其它类可由我们模块的 CSS 文件来进行提供。
  •  group_create, group_edit, group_delete和quick_create_view可设置为 false 来禁用看板列上对应的操作。如group_create=”false”删除右侧添加新列的按钮。
  • on_create用于创建用户点击左上角 Create 按钮时弹出的自定义简单表单视图窗口。应为相应的表单视图添加<module>.<xml_id>值。

然后我们的模板中使用了一组字段。确切地说,只有在 QWeb 表达式中明确使用的字段才需要在这里声明,用以保证从服务端抓取它们的数据。QWeb引擎在处理模板前,仅会在视图中查找 <field name="...">来从模型中获取数据。QWeb的属性通常使用不会被检测到的record.field引用方式。正因为如此,需在<templates>之前包含这些字段来让模板处理时有相应字段值可以使用。

ℹ️Odoo 11中的修改
引入了进度条组件。使用的时候在看板列的上方会出现一个颜色条,来提供该列各项的状态数据。在本文前面CRM Pipeline的示例图中可以查看。

<progressbar>有如下属性:

  • field是对列中各项进行颜色分组的字段名
  • colors是一个字典,将分组字段值与以下三种颜色分别进行映射:danger (红色), warning (黄色)或success (绿色)。
  • sum_field是一个可选项,用于选取整列汇总的字段名。如未设置,会使用各项的计数值。

然后我们的<templates>元素包含一个或多个QWeb模板来生成要使用的 HTML 片断。必须要有一个名为kanban-box的模板,它渲染看板卡片。还可以添加其它模板,通常用于定义主模板中复用到的 HTML 片断。这些模板使用标准的 HTML 和 QWeb 模板语言。QWeb提供了一些特殊指令,用于处理动态生成最终展示的 HTML。

ℹ️Odoo 12中的修改
Odoo 现在使用 Twitter Bootstrap 4,此前版本中使用Bootstrap 3。这些样式在渲染 HTML 的地方通常都可使用,有关Bootstrap更多知识请见官方网站

下面就来详细了解看板视图中所使用的QWeb模板设计。

看板卡片布局

看板卡片主内容区域在kanban-box模板内定义。这个内容区也可以有一个 footer 底部子容器。卡片右上角还可以添加按钮,点击后打开操作菜单的功能。对于footer区域,应在看板盒子模型底部使用

并添加oe_kanban_bottom CSS 类。还可以通过oe_kanban_bottom_left和oe_kanban_bottom_right CSS 类进一步分割为左、右 footer 区。此外,可通过Bootstrap的pull-left和pull-right类在卡片的任意位置(包括oe_kanban_bottom底部区域)添加向左或向右对齐元素。

以下是对看板卡片中QWeb模板的第一次迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<t t-name="kanban-box">
<!-- Set the Kanban Card color -->
<div t-attf-class="
oe_kanban_color_#{kanban_getcolor(record.color.raw_value)}
oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown">
<!-- Top-right drop down menu here... -->
</div>
<div class="oe_kanban_body">
<!-- Content elements and fields go here... -->
</div>
<div class="oe_kanban_footer">
<div class="oe_kanban_footer_left">
<!-- Left hand footer... -->
</div>
<div class="oe_kanban_footer_right">
<!-- Right hand footer... -->
</div>
</div>
<div class="oe_clear" />
</div>
</t>

这就是看板卡片的整体结构。你可能注意到了在顶部<div>元素中使用了color字段来动态设置卡片颜色。在后面的部分中我们会讲解t-attf QWeb指令的细节。现在来为主内容区域添加内容:

1
2
3
4
5
6
7
8
9
10
11
<div class="oe_kanban_body">
<div>
<strong>
<a type="open"><field name="member_id" /></a>
</strong>
</div>
<ul>
<li><field name="user_id" /></li>
<li><field name="request_date" /></li>
</ul>
</div>

这个模板中大部分都是常规 HTML,但也有渲染字段值的<field>元素和在常规表单视图按钮中使用的 type 属性,此处用在锚文本标签中。

在左部 footer 中插入优先级组件:

1
2
3
4
5
6
7
                            <div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<field name="priority" widget="priority" />
<field name="activity_ids" widget="kanban_activity" />
</div>
...
</div>

这里像我们在表单视图中做的那样添加了priority字段。还添加了一个计划活动的字段,使用kanban_activity特殊组件来显示即将开始活动的指示。

在右部footer中,放入看板状态组件和请求借阅的会员头像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="oe_kanban_bottom_right">
<field name="kanban_state"
widget="kanban_state_selection" />
<img t-att-src="kanban_image(
'library.checkout',
'member_image',
record.id.raw_value)"
t-att-title="record.member_id.value"
t-att-alt="record.member_id.value"
width="24"
height="24"
class="oe_kanban_avatar"
/>
</div>

补充:原文件使用的 CSS 类oe_kanban_footer,oe_kanban_footer_left和oe_kanban_footer_right经测试不会进行左右对齐,参照 CRM 进行了如上修改

看板状态通过<field>元素和kanban_state_selection组件来进行添加。用户头像使用 HTML <img>标签插入。图像内容使用QWeb t-att-命令动态生成,后面会详细讲解。这里使用了kanban_image()帮助函数来获取src属性的值。kanban_image() Javascript函数从 Odoo 模型中获取表单并在网页中渲染。有以下属性:

  • 获取图像的模型
  • 包含图像的字段
  • 获取的记录 ID

Odoo 12看板 footer 添加

为看板卡片添加选项菜单

看板卡片可在右上角带有一个选项菜单。通常的操作有编辑或删除记录,但也可以为其添加和按钮调用的同样操作。还有一个设置卡片颜色的组件。以下是oe_kanban_content顶部添加的选项菜单的基础代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="o_dropdown_kanban dropdown">
<a class="dropdown-toggle btn"
data-toggle="dropdown" role="button"
aria-label="Dropdown menu"
title="Dropdown menu"
href="#">
<span class="fa fa-ellipsis-v" />
</a>
<div class="dropdown-menu" role="menu">
<!-- Edit and Delete actions, if available: -->
<t t-if="widget.editable">
<a role="menuitem" type="edit" class="dropdown-item">Edit</a>
</t>
<t t-if="widget.deletable">
<a role="menuitem" type="delete" class="dropdown-item">Delete</a>
</t>
<!-- Color picker option -->
<ul class="oe_kanban_colorpicker" data-field="color" />
</div>
</div>

下拉菜单基本上是由带有<a>标签的<li> HTML 列表元素组成。Edit 和 Delete 这类选项需要满足指定条件下才会出现。这通过QWeb的t-if命令来实现。本文后续会详细讲解QWeb的命令。widget全局变量表示一个KanbanRecord()  JS 对象,负责渲染当前看板卡片。有两个非常有用的属性:widget.editable和widget.deletable,让我们可以检查相应的操作是否可用。

可以看到如何根据记录字段值来显示或隐藏选项,Set as Done仅在未设置is_done 字段时才会显示。最后一个选项添加颜色拾取器组件来使用 color 数据字段选择或修改卡片背景色。因此,除

0%