2023年3月

今天看到个有趣的网站,给大家分享一下。

该网站的功能很神奇,可以实现编程语言的转化。感觉在一些场景之下还是有点作用的,比如你原来跟我一样是做Java的,因为工作需要突然转Go。这个时候用你Java的经验 + 这个工具,或许可以起到一定的帮助作用。

工具的使用也很简单,只需要在左侧黏贴你想转换的原始代码,然后点击
CONVERT CODE
,右侧输入框就会转换成目标代码:

像下面这样更加复杂的代码转换也是不在话下:

const fs = require('fs');
 const AWS = require('aws-sdk');
 
 const s3 = new AWS.S3({
     accessKeyId: process.env.AWS_ACCESS_KEY,
     secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
 });
 
 const fileName = 'contacts.csv';
 
 const uploadFile = () => {
   fs.readFile(fileName, (err, data) => {
       if (err) throw err;
       const params = {
           Bucket: 'testBucket', // pass your bucket name
           Key: 'contacts.csv', // file will be saved as testBucket/contacts.csv
           Body: JSON.stringify(data, null, 2)
       };
       s3.upload(params, function(s3Err, data) {
           if (s3Err) throw s3Err
       });
   });
 };
 
 uploadFile();

可以看到其中还包含了aws的内容,居然也能顺利的转成Java

import java.io.File;
import java.io.IOException;
import java.util.Scanner;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.SdkClientException;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;

public class UploadObject {

    public static void main(String[] args) throws IOException {
        String clientRegion = "*** Client region ***";
        String bucketName = "*** Bucket name ***";
        String stringObjKeyName = "*** String object key name ***";
        String fileObjKeyName = "*** File object key name ***";
        String fileName = "*** Path to file to upload ***";

        try {
            AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
                    .withRegion(clientRegion)
                    .withCredentials(new ProfileCredentialsProvider())
                    .build();

            // Upload a text string as a new object.
            s3Client.putObject(bucketName, stringObjKeyName, "Uploaded String Object");

            // Upload a file as a new object with ContentType and title specified.
            PutObjectRequest request = new PutObjectRequest(bucketName, fileObjKeyName, new File(fileName));
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setContentType("plain/text");
            metadata.addUserMetadata("x-amz-meta-title", "someTitle");
            request.setMetadata(metadata);
            s3Client.putObject(request);
        }
        catch(AmazonServiceException e) {
            // The call was transmitted successfully, but Amazon S3 couldn't process 
            // it, so it returned an error response.
            e.printStackTrace();
        }
        catch(SdkClientException e) {
            // Amazon S3 couldn't be contacted for a response, or the client
            // couldn't parse the response from Amazon S3.
            e.printStackTrace();
        }
    }
}

根据官方说明,该工具也是通过AI实现的,是不是很神奇呢?这个到底是怎么实现的呢?有了解的小伙伴留言区一起探讨下吧!

欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源

子集

力扣题目链接

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:

输入:nums = [0]
输出:[[],[0]]

思路

如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,
那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点

其实这题就是模板题,与
组合总和

分割回文串
不同的是,这题需要全部收集遍历到的值,包括
最开始path为空也要算进结果数组中

那就套用回溯问题的解题模板即可,真的就是直接套

代码

class Solution {
private:
    //定义结果数组
    vector<int> path;
    vector<vector<int>> res;
    //确定回溯函数的参数和返回值
    void backtracking(vector<int>& nums, int beginIndex){
        res.push_back(path);//放在这里,不会把path为空的情况漏掉
        //确定停止条件
        if(beginIndex >= nums.size()){//写不写都行,因为当满足条件时for循环也到头了,也会自己停止
            return;
        }
        //确定单层处理逻辑
        //直接遍历获取树结构中的所有值
        for(int i = beginIndex; i < nums.size(); ++i){
            path.push_back(nums[i]);
            backtracking(nums, i + 1);//因为子集不能有重复的,因此要跳过本次的值
            path.pop_back();
        }
    }

public:
    vector<vector<int>> subsets(vector<int>& nums) {
        backtracking(nums, 0);
        return res;
    }
};

关于backtracking(nums, i + 1)为什么要跳,对比看->
组合总和思路部分单层处理逻辑
来理解

子集II

力扣题目链接(opens new window)

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

  • 输入: [1,2,2]
  • 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]

思路

相较
子集I
,这题题目加了“
可能包含重复元素,但不能包含重复子集

这意味着什么?又得
去重

经过
组合总合II
的训练,现在你知道了我们可以使用一个布尔数组used来标记用过的元素,并通过该数组的状态以及相邻相同元素来进行去重操作

本题的其他部分代码与
子集I
区别不大,在其基础之上添加去重逻辑就行了

正好这里可以很明显的体现出去重逻辑应该写在哪些地方

代码

class Solution {
private:
    //定义结果数组
    vector<int> path;
    vector<vector<int>> res;
    //确定回溯函数的参数和返回值
    void backtracking(vector<int>& nums, int beginIndex, vector<bool>& used){
        res.push_back(path);//放在这里,不会把path为空的情况漏掉
        //确定停止条件
        if(beginIndex >= nums.size()){//写不写都行,因为当满足条件时for循环也到头了,也会自己停止
            return;
        }
        //确定单层处理逻辑
        //直接遍历获取树结构中的所有值
        for(int i = beginIndex; i < nums.size(); ++i){
            if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == 0){//去重逻辑详见 组合总和II
                continue;//在单层遍历时有重复值就跳过
            }
            path.push_back(nums[i]);
            used[i] = true;//记录使用的元素
            backtracking(nums, i + 1, used);//因为子集不能有重复的,因此要跳过本次的值
            used[i] = false;//回溯
            path.pop_back();
        }
    }

public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        //排序
        sort(nums.begin(), nums.end());
        backtracking(nums, 0, used);
        return res;
    }
};

与上一题的代码对比来看不难发现

新增的去重逻辑大部分都在单层处理部分

这也和之前我们在组合总和II中分析的一致,即:在被视为树结构的回溯问题中,“树层”中出现重复是不行的,但是纵向的“树枝”中可以出现重复

下面总结一下目前使用的回溯算法模板中的去重问题

去重操作总结

本题与
组合总和II
都应用了“去重”这一操作

这两个问题都在题干中给出了类似的要求

(本题)“给定一个可能包含重复元素的整数数组 nums...解集不能包含重复的组合”

(组合)“candidates 中的每个数字在每个组合中只能使用一次...解集不能包含重复的组合”

发现什么了吗?

这类题目都会给一个
有重复值
(或者
暗示有重复值
)的数据,然后让你
找出所有组合,并且组合不能有重复

以后看见这些字眼要想到去重操作

再回顾一下出现重复值的本质

重复值的本质

当我们对一个
已经排序
了的数据进行回溯遍历时,当
相邻的两个重复值
中的前一个已经通过递归遍历完树结构的一个分支后,此时因为回溯我们会回到最开始触发递归的位置,然后
继续从相邻重复值的后一个值再次开启递归遍历

那之后开启的递归遍历中的
所有组合(或者是需要获取的某种结果)一定都在前一次递归遍历中出现过了

这就是重复值的本质

处理方法
逻辑

为了处理上述问题,我们引入一个布尔数组used来记录当前使用过的元素

used数组的大小会和题目给的数组大小一样
(为了一一对应数组中的元素)

当我们使用递归
到单层处理部分
时,此时我们会记录组合结果,同时我们也在used中记录当前组合使用到的元素

等到下一次再记录组合结果之前,就判断一下used的状态

关键点来了,也就是
重复的条件

重复的条件

重复的条件是,判断当前遍历到的元素(也就是要用到组合中的元素)
的前一个元素与其本身是否构成相邻重复值

也就是当前这个元素在上次遍历时是否也出现过

如果确实出现过,那么
再看当前used的前一个元素位置是否有记录

如果
有记录
(used[i - 1] != 0),那么意味着
本次递归是处于树结构的一条枝干中
,可以使用重复值

如果
没有记录
(used[i - 1] == 0),说明
本次递归的上一次递归已经使用过相邻重复值
了,那么
本次递归再进行下去的结果将全部被包含于上次的结果中
,因此
要跳过本次for循环
,使用下一个值进行递归遍历寻找新的组合

可见,
used数组
是负责判断当前处于"树枝"还是"树层"的一个
辅助工具

而关键点还是得有
相邻重复值
,这是
造成重复值出现的本质核心

步骤总结

1、在回溯函数中
引入used布尔数组

2、在
单层处理逻辑中加入对于相邻重复值和used状态的判断

3、在
主函数
中对用来寻找组合的
数组进行提前排序
(使用sort)

以后有新理解再补充

什么是IP的rdns信息?

过去很多人,将IP的rDNS信息理解为解析到IP的反查域名信息。IP的rDNS信息和IP反查域名信息完全是两个不同的信息。IP的rdns信息被称之为反向DNS解析(rDNS),即通过DNS解析系统来将IP地址解析为域名。根据RFC1912标准“每一个互联网可以访问的主机都应该有一个名字”,所以rDNS信息更多是对应到一个IP地址的名字。IP的反查域名是指根据域名解析到IP的记录来反查出哪些域名解析到这个IP地址上。下面以8.8.8.8来说明IP的反查信息和rDNS信息的区别。

通过命令nslookup可以直接获取到8.8.8.8的rDNS信息”dns.google.”,注意此处被标识为名称(name),即表示为IP的名称。

通过virustotal平台查询8.8.8.8的IP反查信息,可以得到下图红框中10个域名是解析到8.8.8.8上的。IP反查的结果是域名A记录指向该IP的域名信息集合。

IP的rDNS是如何工作的?

PTR记录

IP的rDNS信息本质上是IP地址的一个name信息。那这个信息是由谁在哪里设置的呢?在通过nslookup获取一个IP的rDNS信息请求时,本质上是发起了一个DNS请求,来获取一个IP的PTR解析记录。我们都知道DNS协议是域名解析协议,是用来解析协议的,那么如何能用来解析IP呢?这里要提到一个很牛的设计,为了能够通过DNS协议来解析到IP的rDNS信息,ICANN组织新增了一个tld(通用顶级域)“.arpa.”用来负责将IP映射为域名,支持rDNS协议和其他网络基础构架的管理工作。

IP地址”4.3.2.1”映射转换之后的域名为1.2.3.4.in-addr.arpa.,IP地址114.114.115.115映射转换之后的域名为115.115.114.114.in-addr.arpa。然后通过DNS协议查询该域名的PTR记录就能够获得该IP的rDNS值,这个查询出来的值和nslookup命令查询出来的值是一样的。

IP rDNS的权威DNS

我们都知道DNS协议中,每个域名都有对应的权威DNS服务器,用来维护这个域名的A记录、CNAME记录值等。在IP rDNS记录也是一样存在着权威DNS服务器。IP地址经过转换之后就是一个特殊的域名。下面我们来分别看一下各级域名对应的权威DNS服务器。

下面各图就IP rDNS转换域名之后的各级权威DNS服务器进行了解析尝试发现,114.114这样的B段就配置了中国的DNS服务器,前面114这样的A段地址展示了全球五大RIR的权威DNS服务器,再往前的特殊域arpa.和in-addr.arpa.都在icann手上进行维护。
这里可以做一个有趣的推论,全球的IP地址分配单位可能是按照B段来进行分配的
。另外114.114.115这样的C段是没有查询到权威NS服务器的,由此也可以做一个有趣的推论,实际注册了IP网段的单位设定的权威DNS服务器可能不是严格按照C段来设定的。从方便的管理角度来看,按照C段更方便做    IP转化为域名的映射的管理,但实际上并没有这么分配,也就是国内注册IP的单位也不是严格按照C段来进行注册的。

arpa.的权威DNS服务器:

in-addr.arpa.的权威DNS服务器:

114.in-addr.arpa.的权威DNS服务器:

114.114.in-addr.arpa.的权威DNS服务器:


114.114.115.in-addr.arpa.的权威DNS服务器:

如何快速获得所有IPv4的rDNS数据:

向公共DNS发起全网IP的PTR DNS解析

将所有的IP转化为域名之后,对公共DNS发起PTR的解析记录请求查询,这样的方法理论上是可行的。但是全球IPv4的地址大概有42亿,去除掉保留地址和一些特殊的IP地址之后,应该还有35亿左右,这个量可能会因为网络解析请求量过大,导致请求被拒绝,以致无法全面和准确的获得IP的rDNS信息。

向权威DNS发起PTR解析

由于公共DNS的数量和承载性能有限制,首先获得一个IP地址的rDNS域名对应的权威DNS,然后在向这个权威DNS服务器发起查询请求,这样能够将请求分散到不同的权威DNS服务器,降低并发太高的问题。但是仍然还会面临实际的请求数据量过于庞大的问题。下图展示了获取114.114.114.114的权威DNS服务器的过程:

上级域名“114.114.114.in-addr.arpa.”没有配置权威DNS服务器:

上上级域名“114.114.in-addr.arpa”配置了权威DNS服务器:

向上上级域名的权威DNS服务器发起PTR解析请求,注意看此处返回了授权区域(“AUTHORITY SECTION”)没有返回具体的ANSWER。这规定了该域名的rDNS记录应该去对应的权威DNS服务器(ns100.114dns.net)上去查询。

再次向该域名的权威DNS服务器发起PTR记录查询请求,成功获取到rDNS记录:

根据上面这几个步骤来看,向权威DNS发起请求,获取rDNS记录的过程比直接向公共dns发起解析请求慢很多,因为中间可能会多了一步或者多步。整体上来看也不能很好的解决快速获取rDNS数据的问题。

按需积累IP的rDNS解析数据

无论哪种方式,要瞬间获得42亿IP的rdns信息都会比较困难。所以按照实际的业务需求对出现的部分业务优先级高的IP通过网络发包获得rdns信息,然后不断的积累,预期能够获得更多的rdns数据。

使用开源的rDNS数据。

网络安全公司Rapid7下属的一个sonar实验室,会定期对全网进行IPv4 的rdns信息探测收集。收集完成之后,会对外提供下载地址。这也是一个重要的数据来源通道。

情报生产应用

生产白名单

所有IP映射为域名之后的各级权威DNS服务器,都是可以用来转化为DNS服务器、用来做白名单数据的;

识别IP用途

很多机构为了让外部知道自己IP是做什么用途的,会在IP的rDNS信息上声明相关的信息。比如百度的爬虫就是根据rDNS信息来判断是否为百度的爬虫。


根据rDNS信息判断IP是否为家庭宽带IP

判断IP是否属于动态IP

该IP属于河南电信的动态IP:

该IP属于上海电信IP:

参考链接:

https://opendata.rapid7.com/sonar.rdns_v2/

https://help.baidu.com/question?prod_id=99&class=476&id=2996

环境

odoo-14.0.post20221212.tar

ORM API学习总结/学习教程

模型(Model)

Model字段被定义为model自身的属性

from odoo import models, fields
class AModel(models.Model):
    _name = 'a.model.name'

    field1 = fields.Char()

警告

字段的名称和方法的名称不能相同,最后定义的方法、函数名称会覆盖前面定义的相同名称。

默认的,字段的标签(Lable,即用户可见字段名称)为对应字段名称开头字母改成大写后的值,可通过
string
字段属性改成修改字段Label

field2 = fields.Integer(string="Field Label")

可通过
default
,定义默认值:

name = fields.Char(default="a value")

默认值也可以通过函数获取:

    def _default_name(self):
        return 'Title'

name = fields.Char(default=lambda self: self._default_name())

API

BaseModel

class odoo.models.BaseModel
[
源代码]

Odoo模型的基类。Odoo mode可通过继承一下类来创建Model:

  • Model
    用于常规数据库持久化模型
  • TransientModel
    用于临时数据,存储在数据库中,但每隔一段时间就会自动清空
  • AbstractModel
    用于多继承模块共享的抽象父类,不会在数据库中创建模型表

系统为每个数据库自动实例化每个模型一次。这些实例表示每个数据库上的可用模型,取决于该数据库上安装的模块。每个实例的实际类都是从创建和继承相应模型的Python类构建的。

每个模型实例都是一个“记录集(recordset)”,即模型记录的有序集合。记录集由
browse()
,
search()
或字段访问等方法返回。记录没有显式的表示:单条记录表示为一条记录的记录集。

要创建不需要实例化的类,可以将
_register
属性设置为
False

  • _auto= False

    是否应该创建数据库表。如果设置为
    False
    , 应该重写
    init()
    来创建数据库表。默认设。针对
    Model

    TransientModel
    自动设置为
    False
    ,针对
    AbstractModel
    自动设置为
    False
    。可通过继承
    AbstractModel
    来创建不需要任何数据表的模型

  • _log_access

    ORM是否自动生成和更新
    Access Log fields
    。默认
    _auto
    的值。

  • _table= None

    模型对应的数据库表的名称。如果
    _auto
    设置为
    True
    的话。

  • _sequence= None

    用于ID字段的SQL序列

  • _sql_constraints= []

    sql约束,格式:
    [(name, sql_def, message)]

  • _register= True

    registry visibility

  • _abstract= True

    是否为抽象模型

  • _transient= False

    是否为
    transient
    模型

  • _name= None

    模型名称(以 点分式命名的模块名称,比如
    estate.users

  • _description= None

    模块描述,非整数名称

  • _inherit= None

    继承的Python模型:需要继承模型的名称(
    _name
    属性值)或者名称列表(
    list
    类型)

  • _inherits= {}
    (不太理解)

    dictionary {‘parent_model’: ‘m2o_field’} mapping the _name of the parent business objects to the names of the corresponding foreign key fields to use:

    _inherits = {
        'a.model': 'a_field_id',
        'b.model': 'b_field_id'
    }
    

    implements composition-based inheritance: the new model exposes all the fields of the inherited models but stores none of them: the values themselves remain stored on the linked record.

    警告

    if multiple fields with the same name are defined in the
    _inherits
    -ed models, the inherited field will correspond to the last one (in the inherits list order).

  • _rec_name= None

    用于标记记录的字段,默认值:
    name

  • _order= 'id'

    用于搜索结果的默认排序字段

  • _check_company_auto= False

    执行
    write

    create
    , 对拥有
    check_company=True
    属性的关联字段调用
    _check_company
    以确保公司一致性

  • _parent_name= 'parent_id'

    用作父字段的many2one字段

  • _parent_store= False

    设置为
    True
    以计算
    parent_path
    字段。与
    parent_path
    字段一起,设置记录树结构的索引存储,以便使用
    child_of

    parent_of
    域运算符对当前模型的记录进行更快的分层查询

  • _date_name= 'date'

    用于默认日历视图的字段

  • _fold_name= 'fold'

    用于确定看板视图中折叠组的字段

AbstractModel

odoo.models.AbstractModel
[
源代码]

odoo.models.BaseModel
的别名

Model

class odoo.models.Model
[
源代码]

常规数据库持久化Odoo模型的主要父类。

通过继承此类来创建Odoo模型的:

class user(Model):
    ...

系统将为安装了该类模块的每个数据库实例化一次类

  • _auto= True

    是否应该创建数据库表。如果设置为
    False
    , 应该重写
    init()
    来创建数据库表。默认设。针对
    Model

    TransientModel
    自动设置为
    False
    ,针对
    AbstractModel
    自动设置为
    False
    。可通过继承
    AbstractModel
    来创建不需要任何数据表的模型

  • _abstract= False

    是否为抽象模型

  • _transient= False

    是否为
    transient
    模型

TransientModel

class odoo.models.TransientModel
[
源代码]

用于临时记录的父类模型,旨在暂时保持,并定期进行清理

TransientModel
具有简化的访问权限管理,所有用户都可以创建新记录,并且只能访问他们创建的记录。超级用户可以无限制地访问所有
TransientModel
记录。

  • _auto= True

    是否应该创建数据库表。如果设置为
    False
    , 应该重写
    init()
    来创建数据库表。默认设。针对
    Model

    TransientModel
    自动设置为
    False
    ,针对
    AbstractModel
    自动设置为
    False
    。可通过继承
    AbstractModel
    来创建不需要任何数据表的模型

  • _abstract= False

    是否为抽象模型

  • _transient= False

    是否为
    transient
    模型

字段(Fields)

class odoo.fields.Field
[
源代码]

字段拥有以下属性

  • string
    (str) – 用户看到的字段的标签;如果未设置,ORM将采用类中的字段名开头字母改成大写后的

  • help
    (str) – 用户看到的字段的提示条(设置该属性后,当鼠标悬停在字段标签上方时,会自动浮现提示条,显示该属性的文字内容)。

  • invisible
    – 字段是否可见。默认为
    False
    ,即可见

  • readonly
    (bool) – 字段在用户界面是否只读,默认值
    False
    ,仅对UI起作用

  • required
    (bool) – 字段在用户界面是否必填,默认
    False
    。这通过在数据库层面为列添加
    NOT NULL
    约束来实现

  • index
    (bool) – 是否为字段添加索引。注意:对不存储、虚拟字段不起作用。默认值:
    False

  • default
    (值或者可调用对象) – 设置字段的默认值。可以是静态值,或者以结果集为入参,返回某个值的函数。使用
    default=None
    舍弃该字段的默认值。

  • states
    (dict) –将
    state
    值映射到UI属性-值对列表的字典映射,简单说就是允许用户界面依据
    state
    字段的值来动态设置对应字段的UI属性,因此,它要求存在一个
    state
    字段并在视图中使用(即使是隐藏的),
    state
    属性的名称是在odoo硬编码且不允许修改的,可用属性有:
    readonly
    ,
    required
    ,
    invisible
    。例如
    states={'done':[('readonly',True)]}
    ,表示当
    state
    值为
    done
    时,将用户界面
    states
    所在字段在设置为只读(仅针对UI层面)

    用法举例:

    state = fields.Selection([
            ('draft', 'To Submit'),
            ('cancel', 'Cancelled'),
            ('confirm', 'To Approve'),
            ('refuse', 'Refused'),
            ('validate1', 'Second Approval'),
            ('validate', 'Approved')
            ], string='Status', readonly=True, copy=False, default='confirm')
    date_from = fields.Datetime(
            'Start Date', readonly=True, index=True, copy=False,
            states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
    
  • groups
    (str) – 值为逗号分隔的组XML ID列表,如
    groups='base.group_user,base.group_system'
    ,可限制字段只能被给定组用户访问。

  • company_dependent
    (bool) –

    字段值是否依赖于当前公司,如果设置为
    True
    ,则表示依赖当前公司,即字段值和公司绑定。这个属性的作用就是让同一字段,可以根据不同公司,存储不同的值,假设一个用户属于多个公司,他在不同公司的职务也不一样,此时就可以设置该属性为
    True

    该值未存储在当前模型表中。它注册为
    ir.property
    ,也就是说它的值存储在
    ir_property
    表中,通过查询该表来获取该字段的值。

  • copy
    (bool) – 当记录重复时,该字段值是否被拷贝(在使用 ORM
    copy()
    方法复制并生成新记录时,不复制该字段的值)。 (针对普通字段,默认值为:
    True
    ,针对
    one2many
    和计算字段,包括属性字段(
    property fields
    ,个人理解注册
    ir.property
    的字段)和关系字段,默认值为
    False

  • store
    (bool) – 该字段是否存储到数据库,针对计算字段,默认值为
    False
    ,其它字段默认为
    True

  • group_operator
    (str) –

    在当前字段上分组时,供
    read_group()
    使用的聚合函数

    支持的聚合函数:


    • array_agg
      : 值,包括空值,连接成一个数组
    • count
      : 记录数
    • count_distinct
      : 不重复记录数
    • bool_and
      : 如果所有值都为真,则为真,否则为假
    • bool_or
      : 如果至少有一个值为真,则为真,否则为假
    • max
      : 所有值的最大值
    • min
      : 所有值的最小值
    • avg
      :所有值的平均值(算术平均值)
    • sum
      : 所有值的总和
  • group_expand
    (str) –

    用于在当前字段上分组时用于扩展
    read_group
    结果的函数

    @api.model
    def _read_group_selection_field(self, values, domain, order):
        return ['choice1', 'choice2', ...] # available selection choices.
    
    @api.model
    def _read_group_many2one_field(self, records, domain, order):
        return records + self.search([custom_domain])
    

基础字段

class odoo.fields.Boolean
[
源代码]

bool
的封装

class odoo.fields.Char
[
源代码]

基本字符串字段,长度有限,通常在客户端显示为单行字符串

参数:

  • size
    (int) – 为该字段可存储最大值

  • trim(bool)
    – 说明该值是否被修剪(默认情况下,
    True
    )。请注意,修剪操作仅由 Web 客户端应用。

  • translate
    (bool 或者可调用对象)
    – 启用字段值的翻译;用于
    translate=True
    整体翻译字段值;
    translate
    也可以是可调用的,从而使得
    translate(callback,value)
    通过使用
    callback(term)
    来检索术语的翻译来翻译
    value`

class odoo.fields.Float
[
源代码]

float
的封装

精度数字由可选的
digitals
属性给出。

参数

  • digits
    (
    tuple
    (
    int
    ,
    int
    ), 或者
    str
    ) – 一个元组
    (total, decimal)
    或者引用
    DecimalPrecision
    记录的字符串

    digits=(8,2) 表示总的8位,小数点占2位
    

Float类为此提供了一些静态方法:

round()
以给定精度对浮点值进行舍入。
is_zero()
检查浮点值在给定精度下是否等于零。
compare()
按给定精度比较两个浮点值。

例子:

fields.Float.round(self.product_uom_qty, precision_rounding=self.product_uom_id.rounding)
fields.Float.is_zero(self.product_uom_qty, precision_rounding=self.product_uom_id.rounding)
field.Float.compare(self.product_uom_qty, self.qty_done, precision_rounding=self.product_uom_id.rounding)

比较助手出于历史目的使用
__cmp_
语义,因此使用此助手的正确惯用方式如下:

如果result==0,则第一个和第二个浮点数相等,如果result<0,第一个浮点数小于第二个,如果result>0,第一个浮动点数大于第二个浮动点数

class odoo.fields.Integer
[
源代码]

int
的封装

高级字段

class odoo.fields.Binary
[
源代码]

封装二进制内容(比如一个文件)。

参数:

  • attachment
    (bool)
    – 字段是否存储为
    ir_attachment
    还是该model表的一列(默认为:
    True`,即存储为前者。
class odoo.fields.Html
[
源代码]

html代码内容的封装

参数:略

class odoo.fields.Image
[
源代码]

图片的封装,扩展
Binary

如果图像大小大于像素的
max_width/max_height
限制,则通过保持纵横比将图像大小调整到该限制。

参数:

  • max_width
    (int ) – 图像的最大宽度(默认值:
    0
    ,无限制)

  • max_height
    ( int) – 图像的最大高度(默认值:
    0
    ,无限制)

  • verify_resolution
    ( bool) – 是否应验证图像分辨率以确保它不会超过最大图像分辨率(默认值:
    True
    。最大图像分辨率请参阅
    odoo.tools.image.ImageProcess
    (默认值:
    50e6
    )。

    参数

如果没有指定
max_width
/
max_height
或者设置为0,且
verify_resolution

False
,则不会验证字段内容,此时应该使用
Binary
字段。

class odoo.fields.Monetary
[
源代码]

封装以给定
res_currency
表示的浮点值。

小数精度和货币符号取自
currency_field
属性。

参数:

  • currency_field
    (str) –拥有表示该货币字段的
    res_currency

    Many2one
    字段名称(默认:
    'currency_id'
    )
class odoo.fields.Selection
[
源代码]

封装不同值之间的互斥选择。

说明:
Selection
字段的可选值,存储在
public.ir_model_fields_selection
表中,通过
field_id
字段通过
public.ir_model_fields
表进行

-- 查询Selection字段ID
SELECT id FROM public.ir_model_fields
where model = 'stock.quality' and name='state' 

-- 查询Selection字段可选值
select * from public.ir_model_fields_selection where field_id = 13028; -- 13028为Selection字段ID

参数:

  • selection
    (list(tuple(str, str)) 或者可调用对象 或者 str)) – 指定字段的可选值。其值为包含2元组的列表,或者返回前者模型方法,或者方法名称

  • selection_add
    (list(tuple(str, str)) –

    在重写字段的情况下,提供
    selection
    的扩展。它是一个包含二元组
    (value, label)
    或者单元组
    (value,)
    的列表,其中,单元组中的
    value
    必须作为
    value
    出现在
    selection
    列表中的元组中。新值插入顺序和原有
    selection
    中元组顺序保持一致:

    selection = [('a', 'A'), ('b', 'B')]
    selection_add = [('c', 'C'), ('b',)]
    > result = [('a', 'A'), ('c', 'C'), ('b', 'B')]
    
  • ondelete

    为带有
    selection_add
    的任何重写字段提供回退机制。这是一个将
    selection_add
    中的每个选项映射到回退操作的dict。

    此回退操作将应用于其
    selection_add
    选项映射到该操作的所有记录。

    这些操作可以是以下任一操作:


    • set null
      默认情况下,具有此选项的所有记录的选择值都将设置为False。
    • cascade
      –具有此选项的所有记录将与选项本身一起删除。
    • set default
      -具有此选项的所有记录都将设置为字段定义的默认值
    • <callable>
      -一个可调用对象,其第一个也是唯一的参数将是包含指定的Selection选项的记录集,用于自定义处理

selection
属性选择是强制性的,除非是
related
或扩展的字段

class odoo.fields.Text
[
源代码]

类似
Char
,用于更长的内容,没有大小,通常展示为多行文本框。

参数:

translate
(bool 或者可调用对象) – 同
Char

Date(time) 字段

当将一个值赋值给
Date
/
Datetime
字段时,以下选择是合法的:

  • date

    datetime
    对象.
  • 正确格式的字符:
    • Date
      字段采用
      YYYY-MM-DD
    • Datetime
      字段采用
      YYYY-MM-DD HH:MM:SS
  • False
    或者
    None
    .

Date

Datetime
字段类拥有以下辅助函数,用于尝试转换为兼容类型:

示例

解析来自外部的日期/日期时间:

fields.Date.to_date(self._context.get('date_from'))

Date
/
Datetime
比较最佳实践:

  • Date
    字段只能和date对象比较
  • Datetime
    字段只能和datetime对象比较

Datetime
字段在数据库中存储为不带时区的时间戳,并以UTC时区存储。因为这样可使Odoo数据库独立于托管服务器系统的时区。时区转换完全由客户端管理。

Common operations with dates and datetimes such as addition, subtraction or fetching the start/end of a period are exposed through both
Date
and
Datetime
. These helpers are also available by importing
odoo.tools.date_utils
.

class odoo.fields.Date
源代码

Python
date
对象的封装

  • static add(value, *args, **kwargs)

    返回
    value

    relativedelta
    之和


    • 参数

      value
      – 初始
      date

      datetime

      args
      – 传递给
      relativedelta
      的位置参数

      kwargs
      – 传递给
      relativedelta
      的关键词参数

    • 返回

      date/datetime结果对象


    示例:

    from odoo.fields import Date
    
    print(Date.add(datetime.now(), years=1)) # 输出形如:2024-01-03
    # 常见参数:
    # years, months, days, leapdays, weeks, hours, minutes, seconds, microseconds
    
  • static subtract(value, *args, **kwargs)
    [
    源代码]

    返回
    value

    relativedelta
    之差


    • 参数

      value
      – 初始
      date
      或者
      datetime

      args
      – 传递给
      relativedelta
      位置参数

      kwargs
      – 传递给
      relativedelta
      的关键词参数

    • 返回

      date/datetime
      结果对象

  • static context_today(record, timestamp=None)
    [
    源代码]

    按客户端时区以适合
    date
    字段的格式返回当前日期


    注解

    该方法可能用于计算默认值


    • 参数

      record
      – 从中获取时区的记录集

      timestamp
      (
      datetime
      ) – 替代当前日期时间(datetime)的可选的
      datetime
      对象

    • 返回类型

      date

  • static end_of(value, granularity)
    [
    源代码]

    从日期或日期时间获取时间段的结束


    • 参数

      value
      – 初始
      date

      datetime

      granularity
      – 字符串表示的时间段类型, 可以是
      year
      ,
      quarter
      ,
      month
      ,
      week
      ,
      day
      或者
      hour

    • 返回

      与指定时段的起始对应的
      date/datetime
      对象


    示例:

    print(datetime.now()) # 2023-01-03 10:12:32.332208
    print(Date.end_of(datetime.now(), 'year')) # 输出形如:2023-12-31 23:59:59.999999
    print(Date.end_of(datetime.now(), 'month')) # 输出形如:2023-01-31 23:59:59.999999
    
  • static start_of(value, granularity)
    [
    源代码]

    从日期或日期时间获取时间段的开始


    • 参数

      value
      – 初始
      date

      datetime

      granularity
      – 字符串表示的时间段类型, 可以是
      year
      ,
      quarter
      ,
      month
      ,
      week
      ,
      day
      或者
      hour

    • 返回

      与指定时段的起始对应的
      date/datetime
      对象


    示例:

    print(datetime.now()) # 2023-01-03 10:18:57.071276
    print(Date.start_of(datetime.now(), 'year')) # 输出形如:2023-01-01 00:00:00
    print(Date.start_of(datetime.now(), 'month')) # 输出形如:2023-01-01 00:00:00
    print(Date.start_of(datetime.now(), 'hour')) # 输出形如:2023-01-03 10:00:00
    
  • static to_date(value)
    [
    源代码]

    尝试转换
    value

    date
    对象


    警告

    如果value为
    datetime
    对象,它将被转换为
    date
    对象,且所有日期时间特定信息(HMS, TZ, …)都会丢失。


    • 参数

      value
      (str 或 date 或 datetime) –需要转换的值

    • 返回

      代表
      value
      的对象

    • 返回类型

      date
      类型或者
      None

  • static to_string(value)
    [
    源代码]


    date
    或者
    datetime
    对象转为字符串


    • 参数

      value
      – 需要转换的日期或者日期时间对象

    • 返回

      以服务器日期格式返回代表
      value
      的字符串。如果
      value

      datetime
      类型,自动舍弃小时,分,秒,时区信息。

    • 返回类型:str


    示例:

    print(Date.to_string(datetime.now())) # 输出形如:2023-01-03
    
  • static today(*args)
    [
    源代码]

    返回当前日期

    示例:

    print(Date.today()) # 格式形如:2023-01-03
    
class odoo.fields.Datetime
[
源代码]

Python
datetime
对象的封装

  • static context_timestamp(record, timestamp)
    [
    源代码]

    返回转换为客户端时区的给定时间戳。


    注解

    此方法不是用作默认初始值设定项,因为
    datetime
    字段在客户端显示时会自动转换。对于默认值,应使用
    now()


    • 参数

      record
      – 从中获取时区的记录集。

      timestamp
      (
      datetime
      ) – 待转换为客户端时区的naive
      datetime
      值 (
      UTC
      表示的)

    • 返回

      按上下文时区转换为时区敏感的
      datetime

    • 返回类型

      datetime

  • static add(value, *args, **kwargs)
    [
    源代码]

    参考
    Date.add

  • static subtract(value, *args, **kwargs)
    [
    源代码]

    参考
    Date.subtract

  • static end_of(value, granularity)
    [
    源代码]

    参考
    Date.end_of

  • static start_of(value, granularity)
    [
    源代码]

    参考
    Date.start_of

  • static to_string(value)
    [
    源代码]

    参考
    Date.to_string

  • static today(args)
    [
    源代码]

    返回当天,午夜 (00:00:00)

    示例:

    from odoo.fields import Datetime
    
    print(Datetime.today()) # 输出形如:2023-01-03 00:00:00
    print(Datetime.now()) # 输出当前时间 2023-01-03 12:33:00
    
  • static to_datetime(value)
    [
    源代码]

    将ORM
    value
    转为
    datetime


    • 参数

      value
      (str 或者
      date
      或者 datetime) – 需要转换的值

    • 返回

      代表
      value
      的对象

    • 返回类型

      datetime
      或者
      None

关系字段(Relational Fields)

class odoo.fields.Many2one
[
源代码]

Many2one
字段的值是大小为0(无记录)或1(单个记录)的记录集。

参数:

  • comodel_name
    (str) – 目标模型的名称,
    comodel_name
    是必选参数,除非是相关或扩展字段(不太理解,原文:name of the target model
    Mandatory
    except for related or extended fields)
  • domain
    – 用于设置客户端侧候选值的可选 domain (domain 或者字符串)
  • context
    (dict) – 处理该字段时供客户端使用的上下文
  • ondelete
    (str) – 当引用的记录被删除时,怎么处理:可选值有:
    'set null'
    ,
    'restrict'
    ,
    'cascade'
  • auto_join
    (bool) – 是否在搜索该字段时生成
    JOIN
    (默认:
    False
    )
  • delegate
    (bool) – 将其设置为
    True
    以标记可通过当前模型访问目标模型的字段(对应
    _inherits
    )
  • check_company
    (bool) – 标记需要在
    _check_company()
    中校验的字段。取决于字段属性,添加一个默认的公司domain
class odoo.fields.One2many
[
源代码]

One2many
字段的值为
comodel_name
中所有满足条件的记录的结果集,而目标模型中的
inverse_name
则等价于当前记录。

参数:

  • comodel_name
    (str) – 目标模型的名称
  • inverse_name
    (str) – 目标模型中反向
    Many2one
    字段名称,根据该字段反向查询记录
  • domain
    – 用于设置客户端候选值的条件 (domain 或者字符串),可选
  • context
    (dict) – 处理该字段时供客户端使用的上下文
  • auto_join
    (bool) – 是否在搜索该字段时生成
    JOIN
    (默认:
    False
    )
  • limit
    (int) – 读取时用的可选限制

comodel_name

inverse_name
参数是必选参数,除非是相关或者扩展字段

class odoo.fields.Many2many
[
源代码]

Many2many
字段的值为一个结果集。

参数:

  • comodel_name
    – 目标模型的名称,必选参数,除非是关联或者扩展字段
  • relation
    (str) – 数据库中存储关系的表名,可选参数。
  • column1
    (str) –
    relation
    表中引用"这些"记录的列名,可选参数
  • column2
    (str) –
    relation
    表中引用"那些"记录的列名,可选参数

relation
,
column1

column2
参数可选。 如果未给定,自动根据模型名称生成,提供的不同的
model_name

comodel_name

注意,ORM不支持在给定模型,使用同样的
comodel
,创建多个省略了
relation
参数的字段,因为这些字段将使用相同的表。ORM阻止两个
Many2many
字段使用相同的
relation
参数,除非:

  • 两个字段都使用相同的模型,
    comodel
    并显示指定
    relation
    参数,否则
  • 至少有一个字段属于携带
    _auto = False
    的模型

参数:

  • domain
    – 用于设置客户端候选值的条件 (domain 或者字符串),可选
  • context
    (dict) – 处理该字段时供客户端使用的上下文
  • check_company
    (bool) – 标记需要在
    _check_company()
    中校验的字段。取决于字段属性,添加一个默认的公司条件
  • limit
    (int) – 读取时用的可选限制

注意:
odoo不会在当前模型对应表中为
One2many

Many2many
类型的属性建立对应的表字段,但会为
Many2one
类型的属性建立对应表字段,针对
Many2many
类型的属性,odoo会建立一张辅助表,表名默认格式为
model1_table_name_model2_table_name_rel
,该表拥有两列,一列为当前模型表主键ID(
model1_table_name_id
),一列为关系字段关联模型表的主键ID(
model2_table_name_id
),这样通过两表记录ID就可以查询所需记录了

伪关系字段

  • class odoo.fields.Reference
    [
    源代码]

    伪关系字段(数据库中没有FK)。该字段值存储为数据库中遵循模式
    "res_model,res_id"
    的字符串。

  • class odoo.fields.Many2oneReference
    [
    源代码]

    该字段的值存储为数据库中的一个整数。与
    odoo.fields.Reference
    字段相反,必须在
    Char
    类型字段中指定模型,其中,该字段的名称必须在当前
    Many2oneReference
    字段中的
    model_field
    属性中指定

    参数:
    model_field
    (str) – 存储模型的字段名称。

计算字段

可以使用
compute
参数计算字段(而不是直接从数据库中读取)
它必须将计算值分配给字段
。如果它使用其他字段的值,则应使用
depends()
指定这些字段

from odoo import api
total = fields.Float(compute='_compute_total')

@api.depends('value', 'tax')
def _compute_total(self):
    for record in self:
        record.total = record.value + record.value * record.tax
  • 当使用子字段时,依赖可使用分点路径:

    @api.depends('line_ids.value')
    def _compute_total(self):
        for record in self:
            record.total = sum(line.value for line in record.line_ids)
    
  • 默认情况下,不存才计算字段。他们在请求时被计算并返回。 设置
    store=True
    将在数据库中存储计算及字段并启动开启字段搜索。

  • 也可以通过设置
    search
    参数开启在计算字段上的搜索。该参数值为一个返回
    搜索条件
    的方法名称 。

    upper_name = field.Char(compute='_compute_upper', search='_search_upper')
    
    def _search_upper(self, operator, value):
        if operator == 'like':
            operator = 'ilike'
        return [('name', operator, value)]
    

    在对模型进行实际搜索之前处理domain时调用该搜索方法。它必须返回与条件
    field operator value
    等效的domain

  • 计算字段默认值。为了允许对计算字段进行设置,使用
    inverse
    参数。该参数值为反向计算并设置相关字段的函数的名称:

    document = fields.Char(compute='_get_document', inverse='_set_document')
    
    def _get_document(self):
        for record in self:
            with open(record.get_document_path) as f:
                record.document = f.read()
    def _set_document(self):
        for record in self:
            if not record.document: continue
            with open(record.get_document_path()) as f:
                f.write(record.document)
    
  • 可以用同一方法同时计算多个字段,只需对所有字段使用同一方法并设置所有字段

    discount_value = fields.Float(compute='_apply_discount')
    total = fields.Float(compute='_apply_discount')
    
    @api.depends('value', 'discount')
    def _apply_discount(self):
        for record in self:
            # compute actual discount from discount percentage
            discount = record.value * record.discount
            record.discount_value = discount
            record.total = record.value - discount
    

警告

虽然可以对多个字段使用相同的计算方法,但不建议对
reverse
方法使用相同的方法。


reverse
的计算过程中,所有使用所述inverse的字段都受到保护,这意味着即使它们的值不在缓存中,也无法计算它们。

如果访问了这些字段中的任何一个字段,且并且其值不在缓存中,ORM将简单的为这些字段返回默认值
False
。这意味着这些
inverse
字段的值(触发
inverse
方法的值除外)可能不会给出正确的值,这可能会破坏
inverse
方法的预期行为

相关字段(Related fields)

计算字段的一种特殊情况是
相关
(代理)字段,它提供当前记录上子字段的值。它们是通过设置
related
参数来定义的,与常规计算字段一样,它们可以存储:

nickname = fields.Char(related='user_id.partner_id.name', store=True)

related
字段的值是通过遍历一系列关系字段并读取所访问模型上的字段来给出的。要遍历的字段的完整序列由
related
属性指定

如果未重新定义某些字段属性,则会自动从源字段中复制这些属性:
string

help

required
(仅当序列中的所有字段都是必需的时)、
groups

digits

size

translate

cleaning”、“selection

comodel_name

domain

context
。所有无语义属性都从源字段复制。

默认的, related字段:

  • 不被存储
  • 不被复制
  • 只读
  • 超级用户模式下被计算

像计算字段那样,添加
store=True
以存储
related
字段。当其依赖被修改时,会自动重新计算
related
字段。

小技巧

如果不希望在任何依赖项更改时重新计算
related
字段,则可以指定精确的字段依赖项:

nickname = fields.Char(
    related='partner_id.name', store=True,
    depends=['partner_id'])
# nickname仅在partner_id被修改时才会被重新计算,而不会在partner名称被修改时重新计算

警告

不可以在
related
字段依赖项中包含
Many2many
或者
One2many
字段

related
可以用于引用另一个模型中的
One2many

Many2many
字段,前提是通过当前模型的一个
Many2one
关系来实现的。
One2many

Many2many
不被支持,无法正确的汇总结果:

m2o_id = fields.Many2one()
m2m_ids = fields.Many2many()
o2m_ids = fields.One2many()

# Supported
d_ids = fields.Many2many(related="m2o_id.m2m_ids")
e_ids = fields.One2many(related="m2o_id.o2m_ids")

# Won't work: use a custom Many2many computed field instead
f_ids = fields.Many2many(related="m2m_ids.m2m_ids")
g_ids = fields.One2many(related="o2m_ids.o2m_ids")

自动生成的字段

  • odoo.fields.id

    ID字段

    如果当前记录集长度为1,返回记录集中唯一记录的ID。否则抛出一个错误

访问日志字段

如果启用
_log_access
,自动设置并更新这些字段。当未用到这些字段时,以禁用它以阻止创建或更新表中这些字段。

默认的
_log_access
被设置为
_auto
的值。

  • odoo.fields.create_date

    创建记录时存储创建时间,
    Datetime
    类型

  • odoo.fields.create_uid

    存储记录创建人,
    Many2one
    to a
    res.users

  • odoo.fields.write_date

    存储记录最后更新时间,
    Datetime
    类型

  • odoo.fields.write_uid

    存储记录最后更新人,
    Many2one
    to a
    res.users
    .

警告

必须对
odoo.models.TransientModel
模型开启
_log_access

保留字段名称

除了自动字段之外,还有一些字段名是为预定义行为保留的。当需要相关行为时,应在模型上定义它们:

  • odoo.fields.name

    _rec_name
    的默认值,用于在需要代表性“命名”的上下文中显示记录。
    odoo.fields.Char
    类型

  • odoo.fields.active

    切换记录的全局可见性,如果
    active
    设置为
    False
    ,则记录在大多数搜索和列表中不可见。
    odoo.fields.Boolean
    类型

  • odoo.fields.state

    对象的声明周期阶段,供
    fields.[Selection

    states
    属性使用

  • odoo.fields.parent_id

    _parent_name
    的默认值,用于以树结构组织记录,并在domain中启用
    child_of

    parent_of
    运算符。
    Many2one
    字段。

  • odoo.fields.parent_path


    _parent_store
    设置为
    True
    时,用于存储反映[
    _parent_name
    ]树结构的值,并优化搜索domain中的
    child_of

    parent_of
    运算符。必须使用
    index=True
    声明才能正确操作。
    odoo.fields.Char
    类型

  • odoo.fields.company_id

    用于Odoo多公司行为的主字段名。供
    :meth:~Odoo.models._check_company
    用于检查多公司一致性。定义记录是否在公司之间共享(没有值)还是仅由给定公司的用户访问。
    Many2one
    :类型:
    res_company

记录集(Recordset)

与模型和记录的交互是通过记录集执行的,记录集是同一模型的记录的有序集合。

警告

与名称所暗示的相反,记录集当前可能包含重复项。这在未来可能会改变。

在模型上定义的方法是在记录集上执行的,方法的
self
是一个记录集:

class AModel(models.Model):
    _name = 'a.model'
    def a_method(self):
        # self can be anything between 0 records and all records in the
        # database
        self.do_operation()

对记录集进行迭代将产生新的单条记录的记录集,这与对Python字符串进行迭代产生单个字符的字符串非常相似:

def do_operation(self):
    print(self) # => a.model(1, 2, 3, 4, 5)
    for record in self:
        print(record) # => a.model(1), then a.model(2), then a.model(3), ...

字段访问

记录集提供了一个“Active Record” 接口:模型字段可直接作为记录的属性直接读取和写入。

注解

当访问潜在多条记录的记录集上的非关系字段时,使用
mapped()
,该函数返回一个列表:

total_qty = sum(self.mapped('qty')) # mapped返回一个列表,形如[2,4,5]

字段值也可以像字典项一样访问。设置字段的值会触发对数据库的更新:

>>> record.name
Example Name
>>> record.company_id.name
Company Name
>>> record.name = "Bob"
>>> field = "name"
>>> record[field]
Bob

警告

  • 尝试读取多条记录上的字段将引发非关系字段的错误。
  • 访问一个关系字段(
    Many2one

    One2many

    Many2many
    ),总是返回记录集,如果未设置字段的话,则返回空记录集。

记录缓存和预取

Odoo为记录的字段维护一个缓存,这样,不是每个字段的访问都会发出数据库请求。

以下示例仅为第一条语句查询数据库:

record.name             # 第一次访问从数据库获取值
record.name             # 第二次访问从缓存获取值

为了避免一次读取一条记录上的一个字段,Odoo会按照一些启发式方法预取个记录和字段,以获得良好的性能。一旦必须在给定记录上读取字段,ORM实际上会在更大的记录集上读取该字段,并将返回的值存储在缓存中以供后续使用。预取的记录集通常是通过迭代获得记录的记录集。此外,所有简单的存储字段(布尔值、整数、浮点值、字符、文本、日期、日期时间、选择、many2one)都会被提取;它们对应于模型表的列,并在同一查询中高效地获取。

考虑以下示例,其中
partners
为包含1000条记录的记录集。如果不进行预取,循环将对数据库进行2000次查询。使用预取,只进行一次查询

for partner in partners:
    print partner.name          # first pass prefetches 'name' and 'lang'
                                # (and other fields) on all 'partners'
    print partner.lang

预取也适用于辅助记录:当读取关系字段时,它们的值(即记录)将被订阅以供将来预取。访问这些辅助记录之一将预取同一模型中的所有辅助记录。这使得以下示例仅生成两个查询,一个用于合作伙伴,另一个用于国家/地区:

countries = set()
for partner in partners:
    country = partner.country_id        # first pass prefetches all partners
    countries.add(country.name)         # first pass prefetches all countries

方法修饰器

Odoo API模块定义了Odoo环境和方法修饰符

  • odoo.api.autovacuum(method)
    [
    源代码]

    修饰一个方法,使其由日常vacuum cron作业(模型
    ir.autovacuum
    )调用。这通常用于垃圾收集之类的不需要特定cron作业的任务

  • odoo.api.constrains(*args)
    [
    源代码]

    装饰一个约束检查器

    每个参数必须是校验使用的字段名称:

    @api.constrains('name', 'description')
    def _check_description(self):
        for record in self:
            if record.name == record.description:
                raise ValidationError("Fields name and description must be different")
    

    当记录的某个命名字段被修改时调用装饰器函数。

    如果校验失败,应该抛出
    ValidationError

    警告

    @constrains
    仅支持简单的字段名称,不支持并忽略点分名称(关系字段的字段,比如
    partner_id.customer
    )

    @constrains
    仅当修饰方法中声明的字段包含在
    create

    write
    调用中时才会触发。这意味着视图中不存在的字段在创建记录期间不会触发调用。必须重写
    create
    ,以确保始终触发约束(例如,测试是否缺少值)

  • odoo.api.depends(*args)
    [
    源代码]

    返回一个装饰器,该装饰器指定
    compute
    方法的字段依赖关系(对于新型函数字段)。参数支持是由点分隔的字段名序列组成的字符串:

    pname = fields.Char(compute='_compute_pname')
    
    @api.depends('partner_id.name', 'partner_id.is_company')
    def _compute_pname(self):
        for record in self:
            if record.partner_id.is_company:
                record.pname = (record.partner_id.name or "").upper()
            else:
                record.pname = record.partner_id.name
    

    有的也可能传递一个函数作为参数,这种情况下,依赖通过调用 在这种情况下,通过使用字段的模型调用函数来提供依赖项

  • odoo.api.depends_context(*args)
    [
    源代码]

    返回一个修饰符,该修饰符指定非存储的“compute”方法的上下文依赖项。每个参数都是上下文字典中的键:

    price = fields.Float(compute='_compute_product_price')
    
    @api.depends_context('pricelist')
    def _compute_product_price(self):
        for product in self:
            if product.env.context.get('pricelist'):
                pricelist = self.env['product.pricelist'].browse(product.env.context['pricelist'])
            else:
                pricelist = self.env['product.pricelist'].get_default_pricelist()
            product.price = pricelist.get_products_price(product).get(product.id, 0.0)
    

    所有依赖项都必须是可哈希的。以下键具有特殊支持:


    • company
      (上下文中的值或当前公司id),
    • uid
      (当前用户ID和超级用户标记),
    • active_test
      (
      env.context
      或者
      field.context
      中的值).
  • odoo.api.model(method)
    [
    源代码]

    修饰一个record-style的方法,其中
    self
    是一个空记录集,但其内容不相关,只有模型相关,可以理解为不会创建对应数据库记录的模型对象。模型层面的操作需要添加此修饰器,相当于类静态函数

    @api.model
    def method(self, args):
        ...
    
  • odoo.api.model_create_multi(method)
    [
    源代码]

    修饰一个以字典列表为参数,并创建多条记录的方法。可能仅通过一个字典或者字典列表调用该方法:

    record = model.create(vals)
    records = model.create([vals, ...])
    
  • odoo.api.onchange(*args)
    [
    源代码]

    返回一个修饰器来修饰给定字段的onchange方法。

    在出现字段的表单视图中,当修改某个给定字段时,将调用该方法。在包含表单中存在的值的伪记录上调用该方法。该记录上的字段赋值将自动返回客户端。

    每个参数必须是字段名:

    @api.onchange('partner_id')
    def _onchange_partner(self):
        self.message = "Dear %s" % (self.partner_id.name or "")
        return {
            'warning': {'title': "Warning", 'message': "What is this?", 'type': 'notification'},
        }
    

    如果类型设置为通知(
    notification
    ),则警告将显示在通知中。否则,它将作为默认值显示在对话框中

    警告

    @onchange
    仅支持简单的字段名称,不支持并自动忽略点分名称(关系字段的字段,比如
    partner_id.tz
    )

    危险

    由于
    @onchange
    返回伪记录的记录集,对上述记录集调用任何一个CRUD方法(
    create()
    ,
    read()
    ,
    write()
    ,
    unlink()
    )都是未定义的行为,因为它们可能还不存在于数据库中。相反,只需像上面的示例中所示那样设置记录的字段或调用
    update()
    方法

    警告

    one2many
    或者
    many2many
    字段不可能通过
    onchange
    修改其自身。这是客户端限制 - 查看
    #2693

  • odoo.api.returns(model, downgrade=None, upgrade=None)
    [
    源代码]

    为返回
    model
    实例的方法返回一个修饰器


    • 参数

      model
      – 模型名称,或者表示当前模型的
      'self'

      downgrade
      – 一个用于转换record-style的
      value
      为传统风格输出的函数
      downgrade(self, value, *args, **kwargs)

      upgrade
      – 一个用于转换传统风格(traditional-style)的
      value
      为record-style的输出的函数
      upgrade(self, value, *args, **kwargs)


    参数
    self
    ,
    *args

    **kwargs
    以record-style方式传递给方法

    修饰器将方法输出适配api风格:
    id
    ,
    ids
    或者
    False
    对应传统风格,而记录集对应记录风格:

    @model
    @returns('res.partner')
    def find_partner(self, arg):
        ...     # return some record
    
    # output depends on call style: traditional vs record style
    partner_id = model.find_partner(cr, uid, arg, context=context)
    
    # recs = model.browse(cr, uid, ids, context)
    partner_record = recs.find_partner(arg)
    

    注意,被修饰的方法必须满足那约定。

    这些修饰器是自动
    继承的
    :重写被修饰的现有方法的方法将被相同的
    @return(model)修饰

环境(Environment)

Environment
存储ORM使用的各种上下文数据:数据库游标(用于数据库查询)、当前用户(用于访问权限检查)和当前上下文(存储任意元数据)。环境还存储缓存。

所有记录集都有一个环境,它是不可变的,可以使用
env
访问,并提供对以下的访问:

  • 当前用户 (
    user
    )
  • 游标 (
    cr
    )
  • 超级用户标识(
    su
    )
  • 或者上下文 (
    context
    )
>>> records.env
<Environment object ...>
>>> records.env.user
res.user(3)
>>> records.env.cr
<Cursor object ...)
>>> self.env.context # 返回字典数据,等价于 self._context
{'lang': 'en_US', 'tz': 'Europe/Brussels'}
>>> self._context
{'lang': 'en_US', 'tz': 'Europe/Brussels'}

从其他记录集创建记录集时,将继承环境。环境可用于获取其他模型中的空记录集,并查询该模型:

>>> self.env['res.partner']
res.partner()
>>> self.env['res.partner'].search([['is_company', '=', True], ['customer', '=', True]])
res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)

Environment.ref(xml_id, raise_if_not_found=True)
[
源代码]

返回与给定
xml_id
对应的记录。

Environment.lang

返回当前语言代码。返回类型str

Environment.user

返回当前用户(作为一个实例)。返回类型
res_users

Environment.company

返回当前公司(作为一个实例)

如果未在上下文 (
allowed_company_ids
)中指定, 返回当前用户的主公司(If not specified in the context(
allowed_company_ids
), fallback on current user companies)

  • 引发

    AccessError
    – 非法或者为授权
    allowed_company_ids
    上下文key内容

  • 返回

    当前公司(默认值=
    self.user.company_id
    )

  • 返回类型

    res.company

警告

在sudo模式下没有应用健康检查!在sudo模式下,用户可以访问任何公司,即使不是在他允许的公司。

这允许触发公司间修改,即使当前用户无权访问目标公司

Environment.companies

返回用户启用的公司的记录集。

如果未在上下文 (
allowed_company_ids
)中指定, 返回当前用户的主公司(If not specified in the context(
allowed_company_ids
), fallback on current user companies)

  • 引发

    AccessError
    – 非法或者为授权
    allowed_company_ids
    上下文key内容

  • 返回

    当前公司(默认值=
    self.user.company_id
    )

  • 返回类型

    res.company

警告

在sudo模式下没有应用健康检查!在sudo模式下,用户可以访问任何公司,即使不是在他允许的公司。

这允许触发公司间修改,即使当前用户无权访问目标公司

修改环境

  • Model.with_context([context][, **overrides])
    -> records[
    源代码]

    返回附加到扩展上下文的此记录集的新版本。

    扩展上下文是提供的合并了
    overrides

    context
    ,或者是合并了
    overrides
    当前
    context

    # current context is {'key1': True}
    r2 = records.with_context({}, key2=True)
    # -> r2._context is {'key2': True}
    r2 = records.with_context(key2=True)
    # -> r2._context is {'key1': True, 'key2': True}
    

需要注意的是,上下文是和记录集绑定的,修改后的上下文并不会在其它记录集中共享。

  • Model.with_user(user)
    [
    源代码]

    以非超级用户模式返回附加到给定用户的此记录集的新版本,即传入一条用户记录并返回该用户的环境,除非
    user
    是超级用户(按照约定,超级用户始终处于超级用户模式)

  • Model.with_company(company)
    [
    源代码]

    返回具有已修改上下文的此记录集的新版本,这样:

    result.env.company = company
    result.env.companies = self.env.companies | company
    

    • 参数

      company
      (
      res_company
      或者 int) – 新环境的主公司


    警告

    当当前用户使用未经授权的公司时,如果不是在sudoed环境中访问该公司,则可能会触发
    AccessError

  • Model.with_env(env)
    [
    源代码]

    返回附加到所提供环境的此记录集的新版本。


    • 参数

      env
      (
      Environment
      ) –


    警告

    新环境将不会从当前环境的数据缓存中受益,因此稍后的数据访问可能会在从数据库重新获取数据时产生额外的延迟。返回的记录集具有与
    self
    相同的预取对象。

  • Model.sudo([flag=True])
    [
    源代码]

    根据
    flag
    ,返回启用或禁用超级用户模式的此记录集的新版本。超级用户模式不会更改当前用户,只是绕过访问权限检查。

    警告

    使用
    sudo
    可能会导致数据访问跨越记录规则的边界,可能会混淆要隔离的记录(例如,多公司环境中来自不同公司的记录)。

    这可能会导致在多条记录中选择一条记录的方法产生不直观的结果,例如获取默认公司或选择物料清单。

    注解

    因为必须重新评估记录规则和访问控制,所以新的记录集将不会从当前环境的数据缓存中受益,因此以后的数据访问可能会在从数据库重新获取时产生额外的延迟。返回的记录集具有与
    self
    相同的预取对象。

SQL执行

环境上的
cr
属性是当前数据库事务的游标,允许直接执行SQL,无论是对于难以使用ORM表达的查询(例如复杂join),还是出于性能原因

self.env.cr.execute("some_sql", params)

由于模型使用相同的游标,并且
Environment
保存各种缓存,因此当在原始SQL中更改数据库时,这些缓存必须失效,否则模型的进一步使用可能会变得不连贯。在SQL中使用
CREATE

UPDATE

DELETE
,但不使用
SELECT
(只读取数据库)时,必须清除缓存。

注解

可以使用
invalidate_cache()
执行缓存的清理

  • Model.invalidate_cache(fnames=None, ids=None)
    [
    源代码]

    修改某些记录后,使记录缓存无效。如果
    fnames

    ids
    都为
    None
    ,则清除整个缓存。

    参数:

    fnames
    –已修改字段的列表,
    None
    表示所有字段

    ids
    –修改的记录ID的列表,
    None
    表示所有记录

警告

执行原始SQL绕过ORM,从而绕过Odoo安全规则。请确保在使用用户输入时对查询进行了清洗,如果确实不需要使用SQL查询,请使用ORM实用程序。

常用ORM方法Common ORM methods

创建/更新(Create/update)

  • Model.create(vals_list)
    → records[
    源代码]

    为模型创建新记录

    使用字典列表
    vals_list
    中的值初始化新记录,如果需要,使用
    default_get()
    中的值


    • 参数

      vals_list
      (list) --模型字段的值,作为字典列表:
      [{'field_name':field_value,…},…]
      为了向后兼容,
      vals_list
      可以是一个字典。它被视为单个列表
      [vals]
      ,并返回一条记录。有关详细信息请参见
      write()

    • 返回

      创建的记录

    • 引发

      AccessError


      • 如果用户对请求的对象没有创建权限

      • 如果用户尝试绕过访问规则在请求的对象上创建


      ValidationError
      – 如果用户尝试为字段输入不在选择范围内的无效值

      UserError
      –如果将在对象层次结构中创建循环,操作的一个结果(例如将对象设置为其自己的父对象)

  • Model.copy(default=None)
    [
    源代码]

    使用默认值更新拷贝的记录
    self


    • 参数

      default
      (dict) – 用于覆盖复制记录的原始值的字段值的字典,形如:
      {'field_name': overridden_value, ...}

    • 返回

      新记录

  • Model.default_get(fields_list)
    → default_values[
    源代码]

    返回
    fields_list
    中字段的默认值。默认值由上下文、用户默认值和模型本身决定


    • 参数

      fields_list
      (list) – 需要获取其默认值的字段名称

    • 返回

      将字段名映射到相应的默认值(如果它们具有的话)的字典。

    • 返回类型

      dict


    注解

    不考虑未请求的默认值,不需要为名称不在
    fields_list
    中的字段返回值。

  • Model.name_create(name)
    → record[
    源代码]

    通过调用
    create()
    创建新记录,调用时
    create()
    时只提供一个参数值:新记录的显示名称。

    新记录将使用适用于此模型的任何默认值初始化,或通过上下文提供。
    create()
    的通常行为适用


    • 参数

      name
      – 要创建记录的显示名称

    • 返回类型

      元组

    • 返回

      创建的记录的
      name_get()
      成对值

  • Model.write(vals)
    [
    源代码]

    使用提供的值更新当前记录集中的所有记录

    参数:

    vals
    (dict) –需要更新的字段及对应的值,比如:
    {'foo': 1, 'bar': "Qux"}
    ,将设置
    foo
    值为
    1

    bar

    "Qux"
    ,如果那些为合法的话,否则将触发错误。
    需要特别注意的是,需要更新的字段越多,更新速度越慢(笔者实践时发现的,但是没验证是否和字段类型有关,特别是关系字段,关系字段的更新可能会调用对应模型的
    write
    方法,该方法如果被重写了,也可能会导致耗时的增加,总的来说,遵守一个原则,仅更新需要更新的字段)


    • 引发

      AccessError


      • 如果用户对请求的对象没有创建权限

      • 如果用户尝试绕过访问规则在请求的对象上创建


      ValidationError
      – 如果用户尝试为字段输入不在选择范围内的无效值

      UserError
      –如果将在对象层次结构中创建循环,操作的一个结果(例如将对象设置为其自己的父对象)(官方原文:if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent)

  • 对于数字型字段(
    odoo.fields.Integer
    ,
    odoo.fields.Float
    ) ,值必须为对应类型

  • 对于
    odoo.fields.Boolean
    , 值必须为
    bool
    类型

  • 对于
    odoo.fields.Selection
    , 值必须匹配选择值(通常为
    str
    ,有时为
    int
    )

  • 对于
    odoo.fields.Many2one
    ,值必须为记录的数据库标识

  • 其它非关系字段,使用字符串值

    危险

    出于历史和兼容性原因,
    odoo.fields.Date

    odoo.fields.Datetime
    字段使用字符串作为值(写入和读取),而不是
    date

    datetime
    。这些日期字符串仅为UTC格式,并根据
    odoo.tools.misc.DEFAULT_SERVER_DATE_FORMAT

    odoo.tools.miisc.DEFAULT_SERVER _DATETIME_FORMAT
    进行格式化

  • odoo.fields.One2many

    odoo.fields.Many2many
    使用特殊的“命令”格式来操作存储在字段中/与字段关联的记录集。

    这种格式是一个按顺序执行的三元组列表,其中每个三元组都是要对记录集执行的命令。并非所有命令都适用于所有情况。可能的命令有:


    • (0, 0, values)

      从提供的
      values
      字典创建新记录,形如
      (0, 0, {'author': user_root.id, 'body': 'one'})

    • (1, id, values)

      使用
      values
      字典中的值更新id值为给定
      id
      值的现有记录。不能在
      create()
      中使用。

    • (2, id, 0)

      从记录集中删除id为指定
      id
      的记录,然后(从数据库中)删除它

      不能在
      create()
      中使用。

    • (3, id, 0)

      从记录集中删除id为指定
      id
      的记录,但不删除它。不能在
      create()
      中使用。

    • (4, id, 0)

      添加一条id为指定
      id
      的已存在记录到记录集

    • (5, 0, 0)

      从结果集移除所有记录, 等价于显示的对每条记录使用命令
      3
      。 不能在
      create()
      中使用。

    • (6, 0, ids)

      根据
      ids
      列表,替换所有已存在记录, 等价于使用命令
      (5, 0, 0)
      ,随后对
      ids
      中的每个id使用命令
      (4, id, 0)
      。实践发现,针对One2many字段,如果
      ids
      对应记录的
      Many2one
      字段没存储当前模型主键ID值时,无法使用该命令。


    实际使用时,这些命令可以组合使用,如下,给
    fieldName
    设置值时,会先指定命令
    5
    ,在执行命令
    0

    Model.write({'fieldName': [(5, 0, 0), (0, 0, dict_value)]})
    
  • Model.flush(fnames=None, records=None)
    [
    源代码]

    处理所有待定的计算(在所有模型上),并将所有待定的更新刷新到数据库中(Process all the pending computations (on all models), and flush all the pending updates to the database)。


    • 参数

      fnames
      – 需要刷新的字段名称列表。如果给定,则将处理范围限制为当前模型的给定字段。

      records
      – 如果给定 (协同
      fnames
      ), 限制处理范围为给定的记录

搜索/读取(Search/Read)

  • Model.browse([ids])
    → records[
    源代码]

    在当前环境中查询
    ids
    参数指定的记录并返回记录结果集,如果为提供参数,或者参数为
    []
    ,则返回空结果集

    self.browse([7, 18, 12])
    res.partner(7, 18, 12)
    

    • 参数

      ids
      (int 或者 list(int) 或 None) – id(s)

    • 返回

      recordset

  • Model.search(args[, offset=0][, limit=None][, order=None][, count=False])
    [
    源代码]

    基于
    args
    搜索域
    搜索记录


    • 参数

      args

      搜索域
      。使用
      []
      代表匹配所有记录。

      offset
      (int) – 需要忽略的结果记录数 (默认: 0)

      limit
      (int) – 最大返回记录数 (默认返回所有)

      order
      (str) – 排序字符串

      count
      (bool) – 如果为
      True
      ,仅计算并返回匹配的记录数 (默认: False)

    • 返回

      最多
      limit
      条符合搜索条件的记录

    • 引发

      AccessError
      –如果用户尝试绕过访问规则读取请求的对象

  • Model.search_count(args)

    int
    [
    源代码]

    返回当前模型中匹配提供的搜索域
    args
    的记录数.

  • Model.name_search(name='', args=None, operator='ilike', limit=100)
    → records[
    源代码]

    搜索比较显示名称与给定
    name
    匹配(匹配方式为给定
    operator
    ),且匹配搜索域
    args
    的记录

    例如,这用于基于关系字段的部分值提供建议。有时被视为
    name_get()
    的反函数,但不能保证是。

    此方法等效于使用基于
    display_name
    的搜索域调用
    search()
    ,然后对搜索结果执行
    “name_get()”
    关于搜索结果


    • 参数

      name
      (str) – 需要匹配的名称

      args
      (list) – 可选的搜索域, 进一步指定限制

      operator
      (str) – 用于匹配
      name
      的域操作,比如
      'like'
      或者
      '='

      limit
      (int) – 可选参数,返回最大记录数

    • 返回类型

      list

    • 返回

      所有匹配记录的对值
      (id, text_repr)
      列表

  • Model.read([fields])
    [
    源代码]

    读取
    self
    中记录的指定字段, 低阶/RPC方法。Python代码中,优选
    browse()
    .


    • 参数

      fields
      – 需要返回的字段名称(默认返回所有字段)

    • 返回

      字典的列表,该字典为字段名称同其值映射,每条记录一个字典

    • 引发

      AccessError
      – 如果用户没有给定记录的读取权限

  • Model.read_group(domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True)
    [
    源代码]

    获取列表视图中按给定
    groupby
    字段分组的记录列表。


    • 参数

      domain
      (list) –
      搜索域
      。使用
      []
      表示匹配所有

      fields
      (list) – 对象上指定的列表视图中存在的字段列表。每个元素要么是“field”(字段名,使用默认聚合),要么是“field:agg”(使用聚合函数“agg”聚合字段),要么就是“name:agg(field)”(使用“agg'聚合字段并将其当做“name”返回)。可能的聚合函数为PostgreSQL提供的函数(
      https://www.postgresql.org/docs/current/static/functions-aggregate.html
      ),且“count_distict”,具有预期含义。

      groupby
      (list) – 记录分组依据的分组依据描述列表。groupby描述要么是字段(然后将按该字段分组),要么是字符串“field:groupby_function”。目前,唯一支持的函数是
      day

      week

      month

      quarter

      year
      ,它们只适用于
      date/datetime
      字段

      offset
      (int) – 需要跳过的记录数,可选参数。

      limit
      (int) – 需要返回的最大记录数,可选参数

      orderby
      (str) – 排序字符串(当前仅支持
      Many2one
      字段)。可选参数。

      lazy
      (bool) – 如果为
      True
      ,则结果只按第一个groupby分组,其余groupby放入
      __context
      键中。如果为
      False
      ,则在一个调用中完成所有groupby。

    • 返回

      字典列表(每条记录一个字典)。包含:按
      groupby
      参数中指定字段分组后的字段的值

      __domain
      : 指定搜索条件的元组的列表

      __context
      : 拥有类似
      groupby
      参数的字典

    • 返回类型

      [{‘field_name_1’: value, …]

    • 引发

      AccessError

      如果用户对所请求的对象没有读取权限,

      如果用户尝试绕过对访问规则读取所请求对象

  • Model.copy_data()

    拷贝当前模型记录的数据,返回一个字典,字典key为模型字段名称,key值为对应的字段值。注意:返回字典key不包含Odoo系统自动生成的模型表字段:
    create_uid

    create_date

    write_date

    write_uid

    id

字段/视图(Fields/Views)s
  • Model.fields_get([fields][, attributes])
    [
    源代码]

    返回每个字段的定义

    返回的值是包含字典的字典(按字段名索引)。包括继承字段。将转换string、help和selection(如果存在)属性


    • 参数

      fields
      – 字段列表, 如果未提供或者为
      []
      则表示所有

      attributes
      – 每个字段需要返回的属性描述列表。 如果未提供或者为
      []
      则表示所有

  • Model.fields_view_get([view_id | view_type='form'])
    [
    源代码]

    获取所请求视图的详细组成,如字段、模型、视图架构


    • 参数

      view_id
      (int) – 视图的ID或者None

      view_type
      (str) – 返回视图的类型,如果
      view_id

      None
      的话(‘form’, ‘tree’, …)

      toolbar
      (bool) – 设置为
      True
      以包含上下文操作

      submenu
      – 已弃用

    • 返回

      请求视图的组成(包括继承的视图和扩展)

    • 返回类型

      dict

    • 引发

      AttributeError

      如果继承的视图具有除“before”、“after”、“inside”、“replace”以外的未知位置

      则如果在父视图中找到除“position”以外的标记

      Invalid ArchitectureError
      – 如果框架中有定义form, tree, calendar, search 等以外的视图

搜索域(Search domains)

域是一个标准列表,每个标准都是
(field_name,operator,value)
的三元组(一个“列表”或“元组”),其中:

  • field_name
    (
    str
    )

    当前模块的字段名称 或通过
    Many2one
    ,使用点符号的关系遍历,例如
    'street'
    或者
    'partner_id.country'

  • operator
    (
    str
    )

    用于比较
    field_name

    value
    的运算符。有效运算符为:


    • =

      等于

    • !=

      不等于

    • >

      大于

    • >=

      大于等于

    • <

      小于

    • <=

      小于等于

    • =?

      未设置或者等于(如果
      value

      None
      或者
      False
      则返回
      True
      ,否则与
      =
      一样)

    • =like


      field_name

      value
      模式匹配。模式中的下划线
      _
      匹配任何单个字符;百分号
      %
      匹配任何零个或多个字符的字符串

    • like


      field_name

      %value%
      模式匹配。类似
      =like
      ,但是匹配前使用
      %
      包装
      value

    • not like

      不匹配
      %value%
      模式

    • ilike

      大小写敏感的
      like

    • not ilike

      大小写敏感的
      not like

    • =ilike

      大小写敏感的
      =like

    • in

      等于
      value
      中的任意项,
      value
      应该为项列表

    • not in

      不等于
      value
      中的任意项

    • child_of


      value
      记录的child(后代)(
      value
      可以是一个项或一个项列表)。考虑模型的语义(即遵循由
      _parent_name
      命名的关系字段)。

    • parent_of


      value
      记录的parent(祖先)(
      value
      可以是一个项或一个项列表)。考虑模型的语义(即遵循由
      _parent_name
      命名的关系字段)

  • value

    变量类型,必须可同命名字段比较(通过
    operator
    )

可以使用前缀形式的逻辑运算符组合域条件:

  • '&'

    逻辑
    AND
    , 默认操作,以将条件相互结合。Arity 2 (使用下2个标准或组合)

  • '|'

    逻辑
    OR
    arity 2

  • '!'

    逻辑 *NOT * arity 1

例子:

搜索来自比利时或德国名为
ABC
,且语言不为英语的合作伙伴:

[('name','=','ABC'),
 ('language.code','!=','en_US'),
 '|',('country_id.code','=','be'),
     ('country_id.code','=','de')]

该域被解释为:

    (name is 'ABC')
AND (language is NOT english)
AND (country is Belgium OR Germany)
  • Model.unlink()
    [
    源代码]

    删除当前记录集中的记录

    引发

    AccessError

    如果用户没有所请求对象的unlink权限

    如果用户尝试绕过访问规则对请求对象执行unlink

    UserError
    –如果记录为其它记录的默认属性

记录(集)信息

  • Model.ids

    返回与
    self
    对应的真实记录ID

  • odoo.models.env

    返回给定记录集的环境。类型
    Environment

  • Model.exists()
    → records[
    源代码]

    返回self中存在的记录子集并将删除的记录标记为缓存中的记录. 可用作对记录的测试:

    if record.exists():
        ...
    

    按约定,将新记录作为现有记录返回

  • Model.ensure_one()
    [
    源代码]

    验证当前记录集只拥有一条记录

    引发
    odoo.exceptions.ValueError

    len(self) != 1

  • Model.name_get()
    → [id, name, ...][
    源代码]

    返回
    self
    中记录的文本表示形式。默认情况下,为
    display_name
    字段的值。


    • 返回

      每个记录的
      (id, text_repr)
      对值列表

    • 返回类型

      list(tuple)

  • Model.get_metadata()
    [
    源代码]

    返回关于给定记录的元数据


    • 返回

      每个请求记录的所有权字典列表 list of ownership dictionaries for each requested record

    • 返回类型

      具有以下关键字的字典列表:


      • id: 对象ID
      • create_uid: 创建记录的用户
      • create_date: 创建记录的日期
      • write_uid: 上次更改记录的用户
      • write_date: 上次更改记录的日期
      • xmlid: 用于引用此记录的XML ID(如果有),格式为
        module.name
      • noupdate: 一个布尔值,指示记录是否将被更新

操作

记录集是不可变的,但可以使用各种集合操作组合同一模型的集合,从而返回新的记录集

  • record in set
    返回
    record
    (必须为只包含一个元素的记录集) 是否在
    set
    中。
    record not in set
    则刚好相反
  • set1 <= set2
    and
    set1 < set2
    返回
    set1
    是否是
    set2
    的子集
  • set1 >= set2
    and
    set1 > set2
    返回
    set1
    是否是
    set2
    的超集
  • set1 | set2
    返回两个记录集的并集。一个包含出现在两个源记录集中的所有记录的记录集
  • set1 & set2
    返回两个记录集的交集。一个只包含同时存在两个源记录集中的记录的记录集。
  • set1 - set2
    返回一个包含仅出现在
    set1
    中的记录的记录集

记录集是可迭代的,因此通常的Python工具可用于转换(
map()

sorted()

ifilter()
,…),然后这些函数返回
list

iterator
,删除对结果调用方法或使用集合操作的能力。

因此,记录集提供以下返回记录集本身的操作(如果可能):

Filter
  • Model.filtered(func)
    [
    源代码]


    • 参数

      func
      (可调用对象 或者 str) – 一个函数或者点分字段名称序列

    • 返回

      满足func的记录集,可能为空。


    # only keep records whose company is the current user's
    records.filtered(lambda r: r.company_id == user.company_id)
    
    # only keep records whose partner is a company
    records.filtered("partner_id.is_company")
    
  • Model.filtered_domain(domain)
    [
    源代码]

Map
  • Model.mapped(func)
    [
    源代码]


    self
    中的所有记录应用
    func
    ,并将结果作为列表或记录集返回(如果
    func
    返回记录集)。后者返回的记录集的顺序是任意的。


    • 参数

      func
      (可调用对象 或 str) – 一个函数或者点分字段名称序列

    • 返回

      如果
      func

      False
      则返回
      self
      作用于所有
      self
      中记录的
      func
      的返回结果

    • 返回类型

      list 或 recordset


    # returns a list of summing two fields for each record in the set
    records.mapped(lambda r: r.field1 + r.field2)
    

    提供的函数可以是获取字段值的字符串:

    # returns a list of names
    records.mapped('name')
    
    # returns a recordset of partners
    records.mapped('partner_id')
    
    # returns the union of all partner banks, with duplicates removed
    records.mapped('partner_id.bank_ids')
    

注解

V13开始, 支持多多关系字段访问,像mapped调用那样工作:

records.partner_id  # == records.mapped('partner_id')
records.partner_id.bank_ids  # == records.mapped('partner_id.bank_ids')
records.partner_id.mapped('name')  # == records.mapped('partner_id.name')
Sort
  • Model.sorted(key=None, reverse=False)
    [
    源代码]

    返回按
    key
    排序的记录集
    self


    • 参数

      key
      (可调用对象或者str 或者
      None
      ) – 一个参数的函数,为每个记录返回一个比较键,或字段名,或
      None
      ,如果为
      None
      ,记录按照默认模型的顺序排序

      reverse
      (bool) – 如果为
      True
      , 返回逆序排序的结果


    # sort records by name
    records.sorted(key=lambda r: r.name)
    

继承与扩展(Inheritance and extension)

Odoo提供三种不同的机制,以模块化方式扩展模型:

  • 从现有模型创建新模型,向副本中添加新信息,但保留原始模块
  • 扩展其他模块中定义的模型,替换以前的版本
  • 将模型的一些字段委派给它包含的记录

经典继承

当同时使用
_inherit

_name
属性时,Odoo使用现有模型(通过
_inherit
提供)作为base创建新模型。新模型从其base中获取所有字段、方法和元信息(默认值等)。

class Inheritance0(models.Model):
    _name = 'inheritance.0'
    _description = 'Inheritance Zero'

    name = fields.Char()

    def call(self):
        return self.check("model 0")

    def check(self, s):
        return "This is {} record {}".format(s, self.name)

class Inheritance1(models.Model):
    _name = 'inheritance.1'
    _inherit = 'inheritance.0'
    _description = 'Inheritance One'

    def call(self):
        return self.check("model 1")

使用它们:

a = env['inheritance.0'].create({'name': 'A'})
b = env['inheritance.1'].create({'name': 'B'})

a.call()
b.call()

输出:

“This is model 0 record A” “This is model 1 record B”

第二个模型继承了第一个模型的
check
方法及其
name
字段,但重写了
call
方法,就像使用标准Python继承一样。

说明:

  • 以上为官方文档给出的案例,笔者实践发现是无法直接运行的。

  • 模型继承会继承父类中的所有属性,会拷贝字段、属性和方法。

  • 可以同时继承多个模型,比如:

    _inherit = ['res.partner', 'md.status.mixin']
    

扩展

当使用
_inherit
但省略
_name
时,新模型将替换现有模型,实质上就是在原有模型上扩展。这对于将新字段或方法添加到现有模型(在其他模块中创建)或自定义或重新配置它们(例如更改其默认排序顺序)非常有用:

class Extension0(models.Model):
    _name = 'extension.0'
    _description = 'Extension zero'

    name = fields.Char(default="A")
    
    def func():
        print('test a')

class Extension1(models.Model):
    _inherit = 'extension.0'

    description = fields.Char(default="Extended")
    
    def func(): # 重写函数
        print('test b')
record = env['extension.0'].create({})
record.read()[0]

返回:

{'name': "A", 'description': "Extended"}

注解

它还会返回各种自动生成的字段,除非它们被禁用了。

env['extension.0'].func({})

返回:

test b

注意:

如果同时继承抽象模块和非抽象模块,并把
_name
配置为非抽象模块,抽象模块的字段也会添加到非抽象模块对应的表

委托(Delegation)

第三种继承机制提供了更大的灵活性(可以在运行时更改),但威力更小:使用
_inherits
模型,将当前模型中未找到的任何字段的查找委托给“children”模型。委托通过
Reference
执行在父模型上自动设置的字段。

主要区别在于意义。使用委托时,模型
has one
而不是
is one
,从而将关系转换为组合而不是继承:

class Screen(models.Model):
    _name = 'delegation.screen'
    _description = 'Screen'

    size = fields.Float(string='Screen Size in inches')

class Keyboard(models.Model):
    _name = 'delegation.keyboard'
    _description = 'Keyboard'

    layout = fields.Char(string='Layout')

class Laptop(models.Model):
    _name = 'delegation.laptop'
    _description = 'Laptop'

    _inherits = {
        'delegation.screen': 'screen_id',
        'delegation.keyboard': 'keyboard_id',
    }

    name = fields.Char(string='Name')
    maker = fields.Char(string='Maker')

    # a Laptop has a screen
    screen_id = fields.Many2one('delegation.screen', required=True, ondelete="cascade")
    # a Laptop has a keyboard
    keyboard_id = fields.Many2one('delegation.keyboard', required=True, ondelete="cascade")

record = env['delegation.laptop'].create({
    'screen_id': env['delegation.screen'].create({'size': 13.0}).id,
    'keyboard_id': env['delegation.keyboard'].create({'layout': 'QWERTY'}).id,
})
record.size
record.layout

将产生结果:

13.0
'QWERTY'

可以直接修改委托字段:

record.write({'size': 14.0})

警告

使用委托继承时,方法不是被继承的,只有字段

警告

  • _inherits
    或多或少已实现,如果可以的话避免用它(
    _inherits
    is more or less implemented, avoid it if you can)
  • 链式的
    _inherits
    基本上没有实现,我们不对最终行为做任何保证。(chained
    _inherits
    is essentially not implemented, we cannot guarantee anything on the final behavior)

字段增量定义

字段定义为模型类的类属性。如果扩展了模型,还可以通过在子类上重新定义具有相同名称和类型的字段来扩展字段定义。在这种情况下,字段的属性取自父类,并由子类中给定的属性覆盖。

例如,下面的第二个类仅在
state
字段上添加工具提示:

class First(models.Model):
    _name = 'foo'
    state = fields.Selection([...], required=True)

class Second(models.Model):
    _inherit = 'foo'
    state = fields.Selection(help="Blah blah blah")

入门实践

模型定义

odoo14\custom\estate\models\estate_property_tag.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from odoo import models, fields

class EstatePropertyTag(models.Model):
    _name = 'estate.property.tag'
    _description = 'estate property tag'
    _order = 'name'

    name = fields.Char(string='tag', required=True)
    color = fields.Integer(string='Color')

odoo14\custom\estate\models\estate_property_offer.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from odoo import models, fields

class EstatePropertyOffer(models.Model):
    _name = 'estate.property.offer'
    _description = 'estate property offer'

    property_id = fields.Many2one('estate.property', required=True)
    price = fields.Integer()

odoo14\custom\estate\models\estate_property_type.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from odoo import models, fields

class EstatePropertyType(models.Model):
    _name = 'estate.property.type'
    _description = 'estate property type'

    name = fields.Char(string='name', required=True)

odoo14\custom\estate\models\estate_property.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from odoo import models, fields

class EstateProperty(models.Model):
    _name = 'estate.property'
    _description = 'estate property table'
    _order = 'id desc'

    name = fields.Char(required=True)
    property_type_id = fields.Many2one("estate.property.type", string="PropertyType")
    tag_ids = fields.Many2many("estate.property.tag")
    offer_ids = fields.One2many("estate.property.offer", "property_id", string="PropertyOffer")

ORM操作实践

>>> self.env['estate.property.type']
estate.property.type()

# 创建单条记录
>>> self.env['estate.property.type'].create({'name':'house'})
estate.property.type(1,)

# 按id查询记录
>>> self.env['estate.property.type'].browse([1])
estate.property.type(1,)
# 未给定id列表,或者未提供参数的情况下,返回空记录集
>>> self.env['estate.property.type'].browse()
estate.property.type()
>>> self.env['estate.property.type'].browse([])
estate.property.type()

# 复制记录
>>> self.env['estate.property.type'].browse([1]).copy({'name':'garden'})
estate.property.type(2,)

# 针对仅获取单条记录的记录集,可通过 records.fieldName 的方式引用对应字段(读取字段值,或者给字段赋值)
>>> self.env['estate.property.type'].browse([2]).name 
'garden'

# 更新记录
>>> self.env['estate.property.type'].browse([1]).name
'house'
>>> self.env['estate.property.type'].browse([1]).write({'name':'garden'})
True
>>> self.env['estate.property.type'].browse([1]).name
'garden'
# 针对仅获取单条记录的记录集,可通过 records.fieldName 的方式引用对应字段(读取字段值,或者给字段赋值)
>>> self.env['estate.property.type'].browse([1]).name = 'house'
>>> self.env['estate.property.type'].browse([1]).name
'house'
# 不能直接通过以下方式,试图在write函数指定id的方式来更新记录 # 不会修改任何记录,也未新增任何记录
>>> self.env['estate.property.type'].write({'id':1, 'name':'apartment'}) 
True
>>> self.env['estate.property.type'].browse([1]).name
'house'


# 通过search api查询记录集
>>> self.env['estate.property.type'].search([])
estate.property.type(1, 2)

# 批量创建记录
# 创建测试用数据
>>> self.env['estate.property.tag'].create([{'name': 'tag1', 'color': 1}, {'name': 'tag1', 'color': 2}, {'name': 'tag1', 'color': 3}])
estate.property.tag(1, 2, 3)

# 注意:Many2one类型字段的值,必须设置为对应记录的主键id
>>> self.env['estate.property'].create({'name': 'house in beijing', 'property_type_id': 1, 'tag_ids':[(0,0, {'name': 'tag1', 'color': 3})]})
estate.property(1,)
>>> self.env['estate.property'].search([]) 
estate.property(1,)

# 查询关系字段值
>>> self.env['estate.property'].browse([1]).property_type_id # Many2one
estate.property.type(1,)
>>> self.env['estate.property'].browse([1]).tag_ids  # Many2many
estate.property.tag(4,)

# 更新Many2many关系字段值
>>> self.env['estate.property'].browse([1]).tag_ids.write({'name': 'tag4', 'color': 4})
True
>>> self.env['estate.property'].browse([1]).tag_ids.color
4

>>> self.env['estate.property.tag'].search([])
estate.property.tag(1, 2, 3, 4)

# 查询关系字段值
>>> self.env['estate.property'].browse([1]).offer_ids # One2many
estate.property.offer()

## 更新One2many关系字段值
# 为关系字段创建关联记录
# (0, 0, values)
# 从提供的`values`字典创建新记录。
>>> self.env['estate.property'].browse([1]).offer_ids = [(0, 0, {'property_id':1})]
>>> self.env['estate.property'].browse([1]).offer_ids
estate.property.offer(1,)
>>> self.env['estate.property'].browse([1]).offer_ids.property_id
estate.property(1,)

# 更新关系字段所代表记录对象的属性值
# (1, id, values)
# 使用 values 字典中的值更新id值为给定 id 值的现有记录。不能在create()中使用。
>>> self.env['estate.property'].browse([1]).offer_ids = [(1, 1, {'price': 30000})]
>>> self.env['estate.property'].browse([1]).offer_ids.price
30000

# 删除关系字段关联记录
# (3, id, 0)
# 从记录集中删除id为id的记录,但不从数据库中删除它,可以理解为仅解除关联。不能在create()中使用。
>>> self.env['estate.property'].browse([1]).offer_ids = [(3,1,0)]
>>> self.env['estate.property'].browse([1]).offer_ids
estate.property.offer()

# 将已存在记录同关系字段关联
# (4, id, 0)
# 添加一条id为id已存在记录到记录集
>>> self.env['estate.property.offer'].browse([1])
estate.property.offer(1,)
>>> self.env['estate.property'].browse([1]).offer_ids = [(4,1,0)]
>>> self.env['estate.property'].browse([1]).offer_ids
estate.property.offer(1,)

# 为关系字段一次创建多条关联记录
>>> self.env['estate.property'].browse([1]).offer_ids = [(0, 0, {'property_id':1, 'price': 100000}),(0, 0, {'property_id':1, 'price': 200000}), (0, 0, {'property_id':1, 'price': 200000}), (0, 0, {'property_id':1, 'price': 300000})]
>>> self.env['estate.property'].browse([1]).offer_ids
estate.property.offer(1, 2, 3, 4, 5)

# 替换关系字段关联的记录
# (6, 0, ids) 
# 根据ids列表,替换所有已存在记录, 等价于使用命令(5, 0, 0),随后对ids中的每个id使用命令(4, id, 0)。
>>> self.env['estate.property'].browse([1]).offer_ids = [(3,1,0),(3,2,0)]
>>> self.env['estate.property'].browse([1]).offer_ids
estate.property.offer(3, 4, 5)
>>> self.env['estate.property'].browse([1]).offer_ids = [(6, 0, [1,2])] # 报错, 因为ID 1,2 对应的记录,其Many2one字段值为null

# 为Many2many关系字段创建多条关联记录
>>> self.env['estate.property'].create({'name': 'house in shanghai'})
estate.property(2,)
>>> self.env['estate.property'].browse([2])
estate.property(2,)
>>> self.env['estate.property'].browse([2]).tag_ids
estate.property.tag()
>>> self.env['estate.property'].browse([2]).tag_ids = [(0, 0, {'name': 'tag5', 'color': 5}), (0, 0, {'name': 'tag6', 'color': 6}), (0, 0, {'name': 'tag7', 'color': 7})]
>>> self.env['estate.property'].browse([2]).tag_ids
estate.property.tag(5, 6, 7)

# 删除关系字段关联的记录
# (2, id, 0)
# 从记录集中删除id为id的记录,然后(从数据库中)删除它,不能在create()中使用
>>> self.env['estate.property'].browse([2]).tag_ids = [(2, 5, 0)]
2023-01-29 08:48:25,491 15984 INFO odoo odoo.models.unlink: User #1 deleted estate.property.tag records with IDs: [5]
>>> print( self.env['estate.property.tag'].browse([5]).exists())
estate.property.tag()
>>> if self.env['estate.property.tag'].browse([5]).exists():
...     print('exists record with id equal 5')
...
>>>

# 创建测试用数据
>>> self.env['estate.property.tag'].create({'name': 'tag8', 'color': 8})
estate.property.tag(8,)
>>> self.env['estate.property.tag'].create({'name': 'tag9', 'color': 9})
estate.property.tag(9,)
>>> self.env['estate.property'].browse([2])
estate.property(2,)

# 替换关系字段关联的记录
# (6, 0, ids) 
# 根据ids列表,替换所有已存在记录, 等价于使用命令(5, 0, 0),随后对ids中的每个id使用命令(4, id, 0)。
>>> self.env['estate.property'].browse([2]).tag_ids
estate.property.tag(6, 7)
>>> self.env['estate.property'].browse([2]).tag_ids = [(6, 0 , [8, 9])]
>>> self.env['estate.property'].browse([2]).tag_ids
estate.property.tag(8, 9)
>>>

# 通过mapped获取记录字段值(关联记录的属性值)列表
>>> self.env['estate.property'].browse([2]).tag_ids.mapped('name')
['tag8', 'tag9']
>>> self.env['estate.property'].browse([2]).mapped('tag_ids')
estate.property.tag(8, 9)
>>> self.env['estate.property'].browse([2]).mapped('tag_ids').mapped('id')) 
[8, 9]
              

# search api 应用
# 搜索域
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)])
estate.property.tag(6, 7, 8, 9)

# 偏移
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)], offset=1)
estate.property.tag(7, 8, 9)

# 限制返回记录集中的最大记录数
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)], offset=1, limit=2)
estate.property.tag(7, 8)

# 返回记录集中的记录排序
# 降序
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)], offset=1, limit=2, order = 'id desc')
estate.property.tag(8, 7)
# 升序
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)], offset=1, limit=2, order = 'id')
estate.property.tag(7, 8)
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)], offset=1, limit=2, order = 'id asc')
estate.property.tag(7, 8)

# 仅返回记录数
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)], count=True)
4
# 利用search_count api实现等价效果
>>> self.env['estate.property.tag'].search_count(args=[('id', '>', 5)])
4

# 搜索域条件组合
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5),('color', '<', 8)])
estate.property.tag(6, 7)


# 获取记录(集)信息
# ids
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)]).ids
[6, 7, 8, 9]
# env
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)]).env
<odoo.api.Environment object at 0x0000020E31C80080>
# name_get api 使用
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)]).name_get()
[(6, 'tag6'), (7, 'tag7'), (8, 'tag8'), (9, 'tag9')]
# get_metadata api 使用
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)]).get_metadata()
[{'id': 6, 'create_uid': (1, 'OdooBot'), 'create_date': datetime.datetime(2023, 1, 29, 8, 41, 10, 551001), 'write_uid': (1, 'OdooBot'), 'write_date': datetime.datetime(2023, 1, 29, 8,41, 10, 551001), 'xmlid': False, 'noupdate': False}, {'id': 7, 'create_uid': (1, 'OdooBot'), 'create_date': datetime.datetime(2023, 1, 29, 8, 41, 10, 551001), 'write_uid': (1, 'OdooBot'), 'write_date': datetime.datetime(2023, 1, 29, 8, 41, 10, 551001), 'xmlid': False, 'noupdate': False}, {'id': 8, 'create_uid': (1, 'OdooBot'), 'create_date': datetime.datetime(2023,1, 29, 8, 41, 10, 551001), 'write_uid': (1, 'OdooBot'), 'write_date': datetime.datetime(2023, 1, 29, 8, 41, 10, 551001), 'xmlid': False, 'noupdate': False}, {'id': 9, 'create_uid': (1, 'OdooBot'), 'create_date': datetime.datetime(2023, 1, 29, 8, 41, 10, 551001), 'write_uid': (1, 'OdooBot'), 'write_date': datetime.datetime(2023, 1, 29, 8, 41, 10, 551001), 'xmlid': False, 'noupdate': False}]


# 利用 read_group 实现按组读取
>>> self.env['estate.property.tag'].create({'name': 'tag10', 'color': 9})
estate.property.tag(10,)
>>> self.env['estate.property.tag'].read_group([], fields=['color'], groupby=['color'])
[{'color_count': 1, 'color': 6, '__domain': [('color', '=', 6)]}, {'color_count': 1, 'color': 7, '__domain': [('color', '=', 7)]}, {'color_count': 1, 'color': 8, '__domain': [('color', '=', 8)]}, {'color_count': 2, 'color': 9, '__domain': [('color', '=', 9)]}]


# 获取字段定义
>>> self.env['estate.property.tag'].fields_get(['name'])
{'name': {'type': 'char', 'change_default': False, 'company_dependent': False, 'depends': (), 'manual': False, 'readonly': False, 'required': True, 'searchable': True, 'sortable': True
, 'store': True, 'string': 'tag', 'translate': False, 'trim': True}}

# 回滚
>>> self.env.cr.rollback()
>>> self.env['estate.property.tag'].search(args=[('id', '>', 5)], offset=1, limit=2, order = 'id')
estate.property.tag()

# 执行 sql
self.env.cr.execute('TRUNCATE TABLE estate_property_tag_test CASCADE;')
self.env.cr.commit()
# 重置自增主键ID 为1(每个表的主键ID存在名为 tableName_id_seq 的序列中)
self.env.cr.execute('ALTER SEQUENCE estate_property_tag_test_id_seq RESTART WITH 1;')
self.env.cr.commit()

>>> self.env['estate.property.tag'].create([{'name': 'tag1', 'color': 1}, {'name': 'tag2', 'color': 2}, {'name': 'tag3', 'color': 3}])
estate.property.tag(1, 2, 3)

# 批量更新记录字段值 #记录集存在多条记录的情况下,不能通过 records.fieldName = 目标值 实现批量更新
>>> self.env['estate.property.tag'].browse([1,3]).write({'color':1})  
True
>>> self.env['estate.property.tag'].browse([1,3]).mapped('color')
[1, 1]

# 修改查询记录集context
>>> self.env['estate.property.tag'].browse([]).env.context
{'lang': 'en_US', 'tz': 'Europe/Brussels'}
>>> self.env['estate.property.tag'].with_context(is_sync=False).browse([]).env.context
{'lang': 'en_US', 'tz': 'Europe/Brussels', 'is_sync': False}

# with_context和sudo共存时的使用方式
>>> self.env['estate.property.tag'].with_context(is_sync=False).sudo().browse([]).env.context
{'lang': 'en_US', 'tz': 'Europe/Brussels', 'is_sync': False}
>>> self.env['estate.property.tag'].sudo().with_context(is_sync=False).browse([]).env.context
{'lang': 'en_US', 'tz': 'Europe/Brussels', 'is_sync': False}

# 修改创建记录时返回记录的context(更新记录(write)也是一样的用法)
# 如此,可以通过重写对应模型的create或者write方法,并在方法中通过self.env.context获取目标key值,进而执行需求实现需要采取的动作,参见下文
>>> self.env['estate.property.tag'].with_context(is_sync=False).create({'name': 'tag4', 'color': 4}).env.context
{'lang': 'en_US', 'tz': 'Europe/Brussels', 'is_sync': False}


# 删除记录
>>> self.env['estate.property.tag'].search([])
estate.property.tag(1, 2, 3, 4)
>>> self.env['estate.property.tag'].search([('id', '>', 2)]).unlink()
2023-01-29 09:55:47,796 15984 INFO odoo odoo.models.unlink: User #1 deleted estate.property.tag records with IDs: [3, 4]
True

# 遍历记录集
>>> for record_set in self. self.env['estate.property.tag.test'].search([]):
...     print(record_set)
...
estate.property.tag.test(1,)
estate.property.tag.test(2,)

获取context上下文目标key值示例

#!/usr/bin/env python
# -*- coding:utf-8 -*-

from odoo import models, fields,api

class EstatePropertyTag(models.Model):
    _name = 'estate.property.tag'
    _description = 'estate property tag'
    _order = 'name'

    name = fields.Char(string='tag', required=True)
    color = fields.Integer(string='Color')


    @api.model
    def create(self, vals_list):
        res = super(EstatePropertyTag, self).create(vals_list)
        # 获取上下文目标key值
        if not self.env.context.get('is_sync', True):
            # do something you need
        return res

参考连接

https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/addons/orm.html#

前言

现在有这么个需求,网上购物,需要根据不同的规则计算商品折扣,比如VIP客户增加5%的折扣,购买金额超过1000元的增加10%的折扣等,而且这些规则可能随时发生变化,甚至增加新的规则。面对这个需求,你该怎么实现呢?难道是计算规则一变,就要修改业务代码,重新测试,上线吗。

其实,我们可以通过规则引擎来实现,Drools 就是一个开源的业务规则引擎,可以很容易地与 spring boot 应用程序集成,那本文就用Drools来实现一下上面说的需求吧。

欢迎关注个人公众号【JAVA旭阳】交流沟通

引入依赖

我们创建一个spring boot应用程序,pom中添加drools相关的依赖,如下:

<dependency>
  <groupId>org.drools</groupId>
  <artifactId>drools-core</artifactId>
  <version>7.59.0.Final</version>
</dependency>
<dependency>
  <groupId>org.drools</groupId>
  <artifactId>drools-compiler</artifactId>
  <version>7.59.0.Final</version>
</dependency>
<dependency>
  <groupId>org.drools</groupId>
  <artifactId>drools-decisiontables</artifactId>
  <version>7.59.0.Final</version>
</dependency>

Drools配置类

创建一个名为
DroolsConfig
的配置 java 类。

@Configuration
public class DroolsConfig {
    // 制定规则文件的路径
    private static final String RULES_CUSTOMER_RULES_DRL = "rules/customer-discount.drl";
    private static final KieServices kieServices = KieServices.Factory.get();

    @Bean
    public KieContainer kieContainer() {
        KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
        kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_CUSTOMER_RULES_DRL));
        KieBuilder kb = kieServices.newKieBuilder(kieFileSystem);
        kb.buildAll();
        KieModule kieModule = kb.getKieModule();
        KieContainer kieContainer = kieServices.newKieContainer(kieModule.getReleaseId());
        return kieContainer;
    }
}
  • 定义了一个
    KieContainer

    Spring Bean

    KieContainer
    用于通过加载应用程序的
    /resources
    文件夹下的规则文件来构建规则引擎。
  • 创建
    KieFileSystem
    实例并配置规则引擎并从应用程序的资源目录加载规则的
    DRL
    文件。
  • 使用
    KieBuilder
    实例来构建
    drools
    模块。我们可以使用KieSerive单例实例来创建
    KieBuilder
    实例。
  • 最后,使用
    KieService
    创建一个
    KieContainer
    并将其配置为
    spring bean

添加业务Model

创建一个订单对象
OrderRequest
,这个类中的字段后续回作为输入信息发送给定义的
drools
规则中,用来计算给定客户订单的折扣金额。

@Getter
@Setter
public class OrderRequest {
    /**
     * 客户号
     */
    private String customerNumber;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 订单金额
     */
    private Integer amount;
    /**
     * 客户类型
     */
    private CustomerType customerType;
}

此外,定义一个客户类型
CustomerType
的枚举,规则引擎会根据该值计算客户订单折扣百分比,如下所示。

public enum CustomerType {
    LOYAL, NEW, DISSATISFIED;

    public String getValue() {
        return this.toString();
    }
}

最后,创建一个订单折扣类
OrderDiscount
,用来表示计算得到的最终的折扣,如下所示。

@Getter
@Setter
public class OrderDiscount {

    /**
     * 折扣
     */
    private Integer discount = 0;
}

我们将使用上述响应对象返回计算出的折扣。

定义drools 规则

前面的
DroolsConfig
类中指定
drools
规则的目录,现在我们在
/src/main/resources/rules
目录下添加
customer-discount.drl
文件,在里面定义对应的规则。

这个
drl
文件虽然不是java文件,但还是很容易看懂的。

  • 我们使用了一个名为
    orderDiscount
    的全局参数,可以在多个规则之间共享。
  • drl
    文件可以包含一个或多个规则。我们可以使用
    mvel
    语法来指定规则。此外,每个规则使用
    rule
    关键字进行描述。
  • 每个规则
    when-then
    语法来定义规则的条件。
  • 根据订单请求的输入值,我们正在为结果添加折扣。如果规则表达式匹配,每个规则都会向全局结果变量添加额外的折扣。

完整的规则源码如下:

import com.alvin.drools.model.OrderRequest;
import com.alvin.drools.model.CustomerType;
global com.alvin.drools.model.OrderDiscount orderDiscount;

dialect "mvel"

// 规则1: 根据年龄判断
rule "Age based discount"
    when
        // 当客户年龄在20岁以下或者50岁以上
        OrderRequest(age < 20 || age > 50)
    then
        // 则添加10%的折扣
        System.out.println("==========Adding 10% discount for Kids/ senior customer=============");
        orderDiscount.setDiscount(orderDiscount.getDiscount() + 10);
end

// 规则2: 根据客户类型的规则
rule "Customer type based discount - Loyal customer"
    when
        // 当客户类型是LOYAL
        OrderRequest(customerType.getValue == "LOYAL")
    then
        // 则增加5%的折扣
        System.out.println("==========Adding 5% discount for LOYAL customer=============");
        orderDiscount.setDiscount(orderDiscount.getDiscount() + 5);
end

rule "Customer type based discount - others"
    when
    OrderRequest(customerType.getValue != "LOYAL")
then
    System.out.println("==========Adding 3% discount for NEW or DISSATISFIED customer=============");
    orderDiscount.setDiscount(orderDiscount.getDiscount() + 3);
end

rule "Amount based discount"
    when
        OrderRequest(amount > 1000L)
    then
        System.out.println("==========Adding 5% discount for amount more than 1000$=============");
    orderDiscount.setDiscount(orderDiscount.getDiscount() + 5);
end

添加Service层

创建一个名为
OrderDiscountService
的服务类,如下:。

@Service
public class OrderDiscountService {

    @Autowired
    private KieContainer kieContainer;

    public OrderDiscount getDiscount(OrderRequest orderRequest) {
        OrderDiscount orderDiscount = new OrderDiscount();
        // 开启会话
        KieSession kieSession = kieContainer.newKieSession();
        // 设置折扣对象
        kieSession.setGlobal("orderDiscount", orderDiscount);
        // 设置订单对象
        kieSession.insert(orderRequest);
        // 触发规则
        kieSession.fireAllRules();
        // 中止会话
        kieSession.dispose();
        return orderDiscount;
    }
}
  • 注入
    KieContainer
    实例并创建一个
    KieSession
    实例。
  • 设置了一个
    OrderDiscount
    类型的全局参数,它将保存规则执行结果。
  • 使用
    insert()
    方法将请求对象传递给
    drl
    文件。
  • 调用
    fireAllRules()
    方法触发所有规则。
  • 最后通过调用
    KieSession

    dispose()
    方法终止会话。

添加Controller

创建一个名为
OrderDiscountController

Controller
类,具体代码如下:

@RestController
public class OrderDiscountController {

    @Autowired
    private OrderDiscountService orderDiscountService;

    @PostMapping("/get-discount")
    public ResponseEntity<OrderDiscount> getDiscount(@RequestBody OrderRequest orderRequest) {
        OrderDiscount discount = orderDiscountService.getDiscount(orderRequest);
        return new ResponseEntity<>(discount, HttpStatus.OK);
    }
}

测试一下

运行
spring boot
应用程序并通过发送客户订单请求 JSON 来访问 REST API 端点。

  • 对于年龄 < 20 且金额 > 1000 的
    LOYAL
    客户类型,我们应该根据我们定义的规则获得
    20%
    的折扣。

总结

我们通过
drools
规则引擎简单实现了这样一个折扣的业务,现在产品经理说要你加一条规则,比如地址是杭州的折扣加10%,你就直接改这个drl文件,其他时间用来摸鱼就好了,哈哈~~。更多关于
drools
的用法大家可以去官网探索。

欢迎关注个人公众号【JAVA旭阳】交流沟通