Toggle navigation

Building Interface Extensions

本指南是关于为Odoo的Web客户端创建模块的。

要使用Odoo创建网站,请参见建立网站;要添加业务功能或扩展Odoo的现有业务系统,请参见构建模块

一个简单的模块

让我们从一个简单的Odoo模块开始,该模块包含基本的Web组件配置,并让我们测试Web框架。

该示例模块在线可用,可以使用以下命令下载:

$ git clone http://github.com/odoo/petstore

无论您在何处执行命令,都会创建一个petstore文件夹。 然后,您需要将该文件夹添加到Odoo的addons路径,创建一个新数据库并安装oepetstore模块。

如果浏览petstore文件夹,则应看到以下内容:

oepetstore
|-- images
|   |-- alligator.jpg
|   |-- ball.jpg
|   |-- crazy_circle.jpg
|   |-- fish.jpg
|   `-- mice.jpg
|-- __init__.py
|-- oepetstore.message_of_the_day.csv
|-- __manifest__.py
|-- petstore_data.xml
|-- petstore.py
|-- petstore.xml
`-- static
    `-- src
        |-- css
        |   `-- petstore.css
        |-- js
        |   `-- petstore.js
        `-- xml
            `-- petstore.xml

该模块已经拥有各种服务器定制。 稍后我们将再次介绍这些内容,现在让我们集中讨论static文件夹中与Web相关的内容。

Odoo模块的“ Web”端使用的文件必须放置在static文件夹中,以便Web浏览器可以使用它们,浏览器无法提取该文件夹之外的文件。 src / csssrc / jssrc / xml子文件夹是常规的,并非绝对必要。

oepetstore/static/css/petstore.css
当前为空,将保留CSS用于宠物商店的内容
oepetstore/static/xml/petstore.xml
通常为空,将保存QWeb模板
oepetstore/static/js/petstore.js

最重要(也是最有趣)的部分包含应用程序的逻辑(至少是它的Web浏览器端),如javascript。 当前应如下所示:

odoo.oepetstore = function(instance, local) {
    var _t = instance.web._t,
        _lt = instance.web._lt;
    var QWeb = instance.web.qweb;

    local.HomePage = instance.Widget.extend({
        start: function() {
            console.log("pet store home page loaded");
        },
    });

    instance.web.client_actions.add(
        'petstore.homepage', 'instance.oepetstore.HomePage');
}

仅在浏览器的控制台中显示一条小消息。

必须在模块内定义static文件夹中的文件,以便正确加载它们。 src / xml中的所有内容都在__ manifest __。py中定义,而src / csssrc / js的内容在petstore.xml或类似文件中定义。

Odoo JavaScript模块

Javascript没有内置模块。 结果,将不同文件中定义的变量全部混在一起,并且可能会发生冲突。 这引起了用于构建干净名称空间的各种模块模式,并限制了命名冲突的风险。

Odoo框架使用一种这样的模式来定义Web插件中的模块,以便命名空间代码和正确地排序其加载。

oepetstore / static / js / petstore.js包含模块声明:

odoo.oepetstore = function(instance, local) {
    local.xxx = ...;
}

在Odoo网站中,模块被声明为在全局odoo变量​​上设置的函数。 该函数的名称必须与插件相同(在本例中为oepetstore),以便框架可以找到它并自动对其进行初始化。

Web客户端加载模块时,它将调用root函数并提供两个参数:

  • 第一个参数是Odoo Web客户端的当前实例,它可以访问Odoo定义的各种功能(翻译,网络服务)以及核心或其他模块定义的对象。
  • 第二个参数是Web客户端自动创建的您自己的本地名称空间。 应该从模块外部访问对象和变量(因为Odoo Web客户端需要调用它们,或者因为其他人可能要自定义它们)应该在该命名空间中设置。

Classes

Much as modules, and contrary to most object-oriented languages, javascript does not build in classes1 although it provides roughly equivalent (if lower-level and more verbose) mechanisms.

For simplicity and developer-friendliness Odoo web provides a class system based on John Resig's Simple JavaScript Inheritance.

New classes are defined by calling the extend() method of odoo.web.Class():

var MyClass = instance.web.Class.extend({
    say_hello: function() {
        console.log("hello");
    },
});

The extend() method takes a dictionary describing the new class's content (methods and static attributes). In this case, it will only have a say_hello method which takes no parameters.

Classes are instantiated using the new operator:

var my_object = new MyClass();
my_object.say_hello();
// print "hello" in the console

And attributes of the instance can be accessed via this:

var MyClass = instance.web.Class.extend({
    say_hello: function() {
        console.log("hello", this.name);
    },
});

var my_object = new MyClass();
my_object.name = "Bob";
my_object.say_hello();
// print "hello Bob" in the console

Classes can provide an initializer to perform the initial setup of the instance, by defining an init() method. The initializer receives the parameters passed when using the new operator:

var MyClass = instance.web.Class.extend({
    init: function(name) {
        this.name = name;
    },
    say_hello: function() {
        console.log("hello", this.name);
    },
});

var my_object = new MyClass("Bob");
my_object.say_hello();
// print "hello Bob" in the console

It is also possible to create subclasses from existing (used-defined) classes by calling extend() on the parent class, as is done to subclass Class():

var MySpanishClass = MyClass.extend({
    say_hello: function() {
        console.log("hola", this.name);
    },
});

var my_object = new MySpanishClass("Bob");
my_object.say_hello();
// print "hola Bob" in the console

When overriding a method using inheritance, you can use this._super() to call the original method:

var MySpanishClass = MyClass.extend({
    say_hello: function() {
        this._super();
        console.log("translation in Spanish: hola", this.name);
    },
});

var my_object = new MySpanishClass("Bob");
my_object.say_hello();
// print "hello Bob \n translation in Spanish: hola Bob" in the console

Widgets Basics

The Odoo web client bundles jQuery for easy DOM manipulation. It is useful and provides a better API than standard W3C DOM2, but insufficient to structure complex applications leading to difficult maintenance.

Much like object-oriented desktop UI toolkits (e.g. Qt, Cocoa or GTK), Odoo Web makes specific components responsible for sections of a page. In Odoo web, the base for such components is the Widget() class, a component specialized in handling a page section and displaying information for the user.

Your First Widget

The initial demonstration module already provides a basic widget:

local.HomePage = instance.Widget.extend({
    start: function() {
        console.log("pet store home page loaded");
    },
});

It extends Widget() and overrides the standard method start(), which — much like the previous MyClass — does little for now.

This line at the end of the file:

instance.web.client_actions.add(
    'petstore.homepage', 'instance.oepetstore.HomePage');

registers our basic widget as a client action. Client actions will be explained later, for now this is just what allows our widget to be called and displayed when we select the Pet Store ‣ Pet Store ‣ Home Page menu.

Display Content

Widgets have a number of methods and features, but the basics are simple:

  • set up a widget
  • format the widget's data
  • display the widget

The HomePage widget already has a start() method. That method is part of the normal widget lifecycle and automatically called once the widget is inserted in the page. We can use it to display some content.

All widgets have a $el which represents the section of page they're in charge of (as a jQuery object). Widget content should be inserted there. By default, $el is an empty <div> element.

A <div> element is usually invisible to the user if it has no content (or without specific styles giving it a size) which is why nothing is displayed on the page when HomePage is launched.

Let's add some content to the widget's root element, using jQuery:

local.HomePage = instance.Widget.extend({
    start: function() {
        this.$el.append("<div>Hello dear Odoo user!</div>");
    },
});

That message will now appear when you open Pet Store ‣ Pet Store ‣ Home Page

The HomePage widget is used by Odoo Web and managed automatically. To learn how to use a widget "from scratch" let's create a new one:

local.GreetingsWidget = instance.Widget.extend({
    start: function() {
        this.$el.append("<div>We are so happy to see you again in this menu!</div>");
    },
});

We can now add our GreetingsWidget to the HomePage by using the GreetingsWidget's appendTo() method:

local.HomePage = instance.Widget.extend({
    start: function() {
        this.$el.append("<div>Hello dear Odoo user!</div>");
        var greeting = new local.GreetingsWidget(this);
        return greeting.appendTo(this.$el);
    },
});
  • HomePage first adds its own content to its DOM root
  • HomePage then instantiates GreetingsWidget
  • Finally it tells GreetingsWidget where to insert itself, delegating part of its $el to the GreetingsWidget.

When the appendTo() method is called, it asks the widget to insert itself at the specified position and to display its content. The start() method will be called during the call to appendTo().

To see what happens under the displayed interface, we will use the browser's DOM Explorer. But first let's alter our widgets slightly so we can more easily find where they are, by adding a class to their root elements:

local.HomePage = instance.Widget.extend({
    className: 'oe_petstore_homepage',
    ...
});
local.GreetingsWidget = instance.Widget.extend({
    className: 'oe_petstore_greetings',
    ...
});

If you can find the relevant section of the DOM (right-click on the text then Inspect Element), it should look like this:

<div class="oe_petstore_homepage">
    <div>Hello dear Odoo user!</div>
    <div class="oe_petstore_greetings">
        <div>We are so happy to see you again in this menu!</div>
    </div>
</div>

Which clearly shows the two <div> elements automatically created by Widget(), because we added some classes on them.

We can also see the two message-holding divs we added ourselves

Finally, note the <div class="oe_petstore_greetings"> element which represents the GreetingsWidget instance is inside the <div class="oe_petstore_homepage"> which represents the HomePage instance, since we appended

Widget Parents and Children

In the previous part, we instantiated a widget using this syntax:

new local.GreetingsWidget(this);

The first argument is this, which in that case was a HomePage instance. This tells the widget being created which other widget is its parent.

As we've seen, widgets are usually inserted in the DOM by another widget and inside that other widget's root element. This means most widgets are "part" of another widget, and exist on behalf of it. We call the container the parent, and the contained widget the child.

Due to multiple technical and conceptual reasons, it is necessary for a widget to know who is its parent and who are its children.

getParent()

can be used to get the parent of a widget:

local.GreetingsWidget = instance.Widget.extend({
    start: function() {
        console.log(this.getParent().$el );
        // will print "div.oe_petstore_homepage" in the console
    },
});
getChildren()

can be used to get a list of its children:

local.HomePage = instance.Widget.extend({
    start: function() {
        var greeting = new local.GreetingsWidget(this);
        greeting.appendTo(this.$el);
        console.log(this.getChildren()[0].$el);
        // will print "div.oe_petstore_greetings" in the console
    },
});

When overriding the init() method of a widget it is of the utmost importance to pass the parent to the this._super() call, otherwise the relation will not be set up correctly:

local.GreetingsWidget = instance.Widget.extend({
    init: function(parent, name) {
        this._super(parent);
        this.name = name;
    },
});

Finally, if a widget does not have a parent (e.g. because it's the root widget of the application), null can be provided as parent:

new local.GreetingsWidget(null);

Destroying Widgets

If you can display content to your users, you should also be able to erase it. This is done via the destroy() method:

greeting.destroy();

When a widget is destroyed it will first call destroy() on all its children. Then it erases itself from the DOM. If you have set up permanent structures in init() or start() which must be explicitly cleaned up (because the garbage collector will not handle them), you can override destroy().

The QWeb Template Engine

In the previous section we added content to our widgets by directly manipulating (and adding to) their DOM:

this.$el.append("<div>Hello dear Odoo user!</div>");

This allows generating and displaying any type of content, but gets unwieldy when generating significant amounts of DOM (lots of duplication, quoting issues, ...)

As many other environments, Odoo's solution is to use a template engine. Odoo's template engine is called QWeb.

QWeb is an XML-based templating language, similar to Genshi, Thymeleaf or Facelets. It has the following characteristics:

  • It's implemented fully in JavaScript and rendered in the browser
  • Each template file (XML files) contains multiple templates
  • It has special support in Odoo Web's Widget(), though it can be used outside of Odoo's web client (and it's possible to use Widget() without relying on QWeb)

Using QWeb

First let's define a simple QWeb template in the almost-empty oepetstore/static/src/xml/petstore.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <t t-name="HomePageTemplate">
        <div style="background-color: red;">This is some simple HTML</div>
    </t>
</templates>

Now we can use this template inside of the HomePage widget. Using the QWeb loader variable defined at the top of the page, we can call to the template defined in the XML file:

local.HomePage = instance.Widget.extend({
    start: function() {
        this.$el.append(QWeb.render("HomePageTemplate"));
    },
});

QWeb.render() looks for the specified template, renders it to a string and returns the result.

However, because Widget() has special integration for QWeb the template can be set directly on the widget via its template attribute:

local.HomePage = instance.Widget.extend({
    template: "HomePageTemplate",
    start: function() {
        ...
    },
});

Although the result looks similar, there are two differences between these usages:

  • with the second version, the template is rendered right before start() is called
  • in the first version the template's content is added to the widget's root element, whereas in the second version the template's root element is directly set as the widget's root element. Which is why the "greetings" sub-widget also gets a red background

QWeb Context

QWeb templates can be given data and can contain basic display logic.

For explicit calls to QWeb.render(), the template data is passed as second parameter:

QWeb.render("HomePageTemplate", {name: "Klaus"});

with the template modified to:

<t t-name="HomePageTemplate">
    <div>Hello <t t-esc="name"/></div>
</t>

will result in:

<div>Hello Klaus</div>

When using Widget()'s integration it is not possible to provide additional data to the template. The template will be given a single widget context variable, referencing the widget being rendered right before start() is called (the widget's state will essentially be that set up by init()):

<t t-name="HomePageTemplate">
    <div>Hello <t t-esc="widget.name"/></div>
</t>
local.HomePage = instance.Widget.extend({
    template: "HomePageTemplate",
    init: function(parent) {
        this._super(parent);
        this.name = "Mordecai";
    },
    start: function() {
    },
});

Result:

<div>Hello Mordecai</div>

Template Declaration

We've seen how to render QWeb templates, let's now see the syntax of the templates themselves.

A QWeb template is composed of regular XML mixed with QWeb directives. A QWeb directive is declared with XML attributes starting with t-.

The most basic directive is t-name, used to declare new templates in a template file:

<templates>
    <t t-name="HomePageTemplate">
        <div>This is some simple HTML</div>
    </t>
</templates>

t-name takes the name of the template being defined, and declares that it can be called using QWeb.render(). It can only be used at the top-level of a template file.

Escaping

The t-esc directive can be used to output text:

<div>Hello <t t-esc="name"/></div>

It takes a Javascript expression which is evaluated, the result of the expression is then HTML-escaped and inserted in the document. Since it's an expression it's possible to provide just a variable name as above, or a more complex expression like a computation:

<div><t t-esc="3+5"/></div>

or method calls:

<div><t t-esc="name.toUpperCase()"/></div>

Outputting HTML

To inject HTML in the page being rendered, use t-raw. Like t-esc it takes an arbitrary Javascript expression as parameter, but it does not perform an HTML-escape step.

<div><t t-raw="name.link(user_account)"/></div>

Conditionals

QWeb can have conditional blocks using t-if. The directive takes an arbitrary expression, if the expression is falsy (false, null, 0 or an empty string) the whole block is suppressed, otherwise it is displayed.

<div>
    <t t-if="true == true">
        true is true
    </t>
    <t t-if="true == false">
        true is not true
    </t>
</div>

Iteration

To iterate on a list, use t-foreach and t-as. t-foreach takes an expression returning a list to iterate on t-as takes a variable name to bind to each item during iteration.

<div>
    <t t-foreach="names" t-as="name">
        <div>
            Hello <t t-esc="name"/>
        </div>
    </t>
</div>

Defining attributes

QWeb provides two related directives to define computed attributes: t-att-name and t-attf-name. In either case, name is the name of the attribute to create (e.g. t-att-id defines the attribute id after rendering).

t-att- takes a javascript expression whose result is set as the attribute's value, it is most useful if all of the attribute's value is computed:

<div>
    Input your name:
    <input type="text" t-att-value="defaultName"/>
</div>

t-attf- takes a format string. A format string is literal text with interpolation blocks inside, an interpolation block is a javascript expression between {{ and }}, which will be replaced by the result of the expression. It is most useful for attributes which are partially literal and partially computed such as a class:

<div t-attf-class="container {{ left ? 'text-left' : '' }} {{ extra_class }}">
    insert content here
</div>

Calling other templates

Templates can be split into sub-templates (for simplicity, maintainability, reusability or to avoid excessive markup nesting).

This is done using the t-call directive, which takes the name of the template to render:

<t t-name="A">
    <div class="i-am-a">
        <t t-call="B"/>
    </div>
</t>
<t t-name="B">
    <div class="i-am-b"/>
</t>

rendering the A template will result in:

<div class="i-am-a">
    <div class="i-am-b"/>
</div>

Sub-templates inherit the rendering context of their caller.

To Learn More About QWeb

For a QWeb reference, see QWeb.

Exercise

Widget Helpers

Widget's jQuery Selector

Selecting DOM elements within a widget can be performed by calling the find() method on the widget's DOM root:

this.$el.find("input.my_input")...

But because it's a common operation, Widget() provides an equivalent shortcut through the $() method:

local.MyWidget = instance.Widget.extend({
    start: function() {
        this.$("input.my_input")...
    },
});

Easier DOM Events Binding

We have previously bound DOM events using normal jQuery event handlers (e.g. .click() or .change()) on widget elements:

local.MyWidget = instance.Widget.extend({
    start: function() {
        var self = this;
        this.$(".my_button").click(function() {
            self.button_clicked();
        });
    },
    button_clicked: function() {
        ..
    },
});

While this works it has a few issues:

  1. it is rather verbose
  2. it does not support replacing the widget's root element at runtime as the binding is only performed when start() is run (during widget initialization)
  3. it requires dealing with this-binding issues

Widgets thus provide a shortcut to DOM event binding via events:

local.MyWidget = instance.Widget.extend({
    events: {
        "click .my_button": "button_clicked",
    },
    button_clicked: function() {
        ..
    }
});

events is an object (mapping) of an event to the function or method to call when the event is triggered:

  • the key is an event name, possibly refined with a CSS selector in which case only if the event happens on a selected sub-element will the function or method run: click will handle all clicks within the widget, but click .my_button will only handle clicks in elements bearing the my_button class
  • the value is the action to perform when the event is triggered

    It can be either a function:

    events: {
        'click': function (e) { /* code here */ }
    }
    

    or the name of a method on the object (see example above).

    In either case, the this is the widget instance and the handler is given a single parameter, the jQuery event object for the event.

Widget Events and Properties

Events

Widgets provide an event system (separate from the DOM/jQuery event system described above): a widget can fire events on itself, and other widgets (or itself) can bind themselves and listen for these events:

local.ConfirmWidget = instance.Widget.extend({
    events: {
        'click button.ok_button': function () {
            this.trigger('user_chose', true);
        },
        'click button.cancel_button': function () {
            this.trigger('user_chose', false);
        }
    },
    start: function() {
        this.$el.append("<div>Are you sure you want to perform this action?</div>" +
            "<button class='ok_button'>Ok</button>" +
            "<button class='cancel_button'>Cancel</button>");
    },
});

This widget acts as a facade, transforming user input (through DOM events) into a documentable internal event to which parent widgets can bind themselves.

trigger() takes the name of the event to trigger as its first (mandatory) argument, any further arguments are treated as event data and passed directly to listeners.

We can then set up a parent event instantiating our generic widget and listening to the user_chose event using on():

local.HomePage = instance.Widget.extend({
    start: function() {
        var widget = new local.ConfirmWidget(this);
        widget.on("user_chose", this, this.user_chose);
        widget.appendTo(this.$el);
    },
    user_chose: function(confirm) {
        if (confirm) {
            console.log("The user agreed to continue");
        } else {
            console.log("The user refused to continue");
        }
    },
});

on() binds a function to be called when the event identified by event_name is. The func argument is the function to call and object is the object to which that function is related if it is a method. The bound function will be called with the additional arguments of trigger() if it has any. Example:

start: function() {
    var widget = ...
    widget.on("my_event", this, this.my_event_triggered);
    widget.trigger("my_event", 1, 2, 3);
},
my_event_triggered: function(a, b, c) {
    console.log(a, b, c);
    // will print "1 2 3"
}

Properties

Properties are very similar to normal object attributes in that they allow storing data on a widget instance, however they have the additional feature that they trigger events when set:

start: function() {
    this.widget = ...
    this.widget.on("change:name", this, this.name_changed);
    this.widget.set("name", "Nicolas");
},
name_changed: function() {
    console.log("The new value of the property 'name' is", this.widget.get("name"));
}
  • set() sets the value of a property and triggers change:propname (where propname is the property name passed as first parameter to set()) and change
  • get() retrieves the value of a property.

Exercise

Modify existing widgets and classes

The class system of the Odoo web framework allows direct modification of existing classes using the include() method:

var TestClass = instance.web.Class.extend({
    testMethod: function() {
        return "hello";
    },
});

TestClass.include({
    testMethod: function() {
        return this._super() + " world";
    },
});

console.log(new TestClass().testMethod());
// will print "hello world"

This system is similar to the inheritance mechanism, except it will alter the target class in-place instead of creating a new class.

In that case, this._super() will call the original implementation of a method being replaced/redefined. If the class already had sub-classes, all calls to this._super() in sub-classes will call the new implementations defined in the call to include(). This will also work if some instances of the class (or of any of its sub-classes) were created prior to the call to include().

Translations

The process to translate text in Python and JavaScript code is very similar. You could have noticed these lines at the beginning of the petstore.js file:

var _t = instance.web._t,
    _lt = instance.web._lt;

These lines are simply used to import the translation functions in the current JavaScript module. They are used thus:

this.$el.text(_t("Hello user!"));

In Odoo, translations files are automatically generated by scanning the source code. All piece of code that calls a certain function are detected and their content is added to a translation file that will then be sent to the translators. In Python, the function is _(). In JavaScript the function is _t() (and also _lt()).

_t() will return the translation defined for the text it is given. If no translation is defined for that text, it will return the original text as-is.

_lt() ("lazy translate") is similar but somewhat more complex: instead of translating its parameter immediately, it returns an object which, when converted to a string, will perform the translation.

It is used to define translatable terms before the translations system is initialized, for class attributes for instance (as modules are loaded before the user's language is configured and translations are downloaded).

Communication with the Odoo Server

通讯模型

使用Odoo进行的大多数操作都涉及与实现业务问题的模型进行通信,然后这些模型(可能)与某些存储引擎(通常为PostgreSQL)进行交互。

尽管jQuery提供了用于网络交互的$。ajax函数,但是与Odoo进行通信需要其他元数据,这些元数据的设置在每次调用之前都是冗长且容易出错的。 结果,Odoo Web提供了更高级别的通信原语。

为了证明这一点,文件petstore.py已经包含一个带有示例方法的小型模型:

class message_of_the_day(models.Model):
    _name = "oepetstore.message_of_the_day"

    @api.model
    def my_method(self):
        return {"hello": "world"}

    message = fields.Text(),
    color = fields.Char(size=20),

这将声明一个具有两个字段的模型,以及一个返回文字字典的方法my_method()

这是一个示例小部件,它调用my_method()并显示结果:

local.HomePage = instance.Widget.extend({
    start: function() {
        var self = this;
        var model = new instance.web.Model("oepetstore.message_of_the_day");
        model.call("my_method", {context: new instance.web.CompoundContext()}).then(function(result) {
            self.$el.append("<div>Hello " + result["hello"] + "</div>");
            // will show "Hello world" to the user
        });
    },
});

用于调用Odoo模型的类是odoo.Model() 以Odoo模型的名称作为第一个参数(在此为oepetstore.message_of_the_day)进行实例化。

call()可用于调用Odoo模型的任何(公共)方法。 它采用以下位置参数:

name
调用方法的名称,my_method此处
args

位置参数的数组以提供给该方法。 由于该示例没有提供位置参数,因此未提供args参数。

这是另一个带有位置参数的示例:

@api.model
def my_method2(self, a, b, c): ...
model.call("my_method", [1, 2, 3], ...
// with this a=1, b=2 and c=3
kwargs

要传递的关键字参数的映射。 该示例提供了单个命名参数context

@api.model
def my_method2(self, a, b, c): ...
model.call("my_method", [], {a: 1, b: 2, c: 3, ...
// with this a=1, b=2 and c=3

call()返回以模型方法返回的值作为第一个参数的延迟解析。

复合上下文

上一节使用了context参数,该参数未在方法调用中说明:

model.call("my_method", {context: new instance.web.CompoundContext()})

上下文就像Web客户端在调用方法时始终将其赋予服务器的“魔术”参数。 上下文是包含多个键的字典。 最重要的键之一是用户的语言,服务器使用它来翻译应用程序的所有消息。 另一个是用户的时区,如果不同国家的人使用Odoo,则该时间区可正确计算日期和时间。

在所有方法中,argument都是必需的,否则可能发生不好的事情(例如应用程序未正确翻译)。 因此,当调用模型的方法时,应始终提供该参数。 解决该问题的方法是使用odoo.web.CompoundContext()

CompoundContext()是用于将用户上下文(带有语言,时区等)传递给服务器以及向上下文添加新键的类(某些模型的方法使用任意键添加到上下文中)。 通过为其构造函数提供任意数量的词典或其他CompoundContext()实例来创建它。 它将合并所有这些上下文,然后再将它们发送到服务器。

model.call("my_method", {context: new instance.web.CompoundContext({'new_key': 'key_value'})})
@api.model
def my_method(self):
    print self.env.context
    // will print: {'lang': 'en_US', 'new_key': 'key_value', 'tz': 'Europe/Brussels', 'uid': 1}

您可以在参数context中看到该词典,其中包含一些与Odoo中当前用户的配置有关的键,以及在实例化时添加的new_key键。 CompoundContext()

查询

虽然call()足以与Odoo模型进行任何交互,但Odoo Web提供了一个帮助您更简单,更清晰地查询模型(根据各种条件获取记录):query()search()和:read()的常见组合的快捷方式。 它提供了更清晰的语法来搜索和读取模型:

model.query(['name', 'login', 'user_email', 'signature'])
     .filter([['active', '=', true], ['company_id', '=', main_company]])
     .limit(15)
     .all().then(function (users) {
    //处理用户记录
});

与:

model.call('search', [['active', '=', true], ['company_id', '=', main_company]], {limit: 15})
    .then(function (ids) {
        return model.call('read', [ids, ['name', 'login', 'user_email', 'signature']]);
    })
    .then(function (users) {
        // do work with users records
    });
  • query()将字段的可选列表作为参数(如果未提供命令行,则将提取模型的所有字段)。 它返回odoo.web.Query(),可以在执行之前进一步自定义
  • Query()表示正在构建的查询。 这是不可变的,用于自定义查询的方法实际上会返回修改后的副本,因此可以同时使用原始版本和新版本。 有关自定义选项,请参见Query()

根据需要设置查询后,只需调用all()即可执行查询并返回延期查询的结果。 结果与read()相同,后者是字典数组,其中每个字典都是请求的记录,每个请求的字典都是键。

练习

现有的Web组件

动作管理程序

在Odoo中,许多操作都是从action开始的:打开菜单项(进入视图),打印报告,...

动作是描述客户端应如何响应内容激活的数据。 动作可以存储(并通过模型读取),也可以动态生成(通过javascript代码在客户端本地生成,或通过模型的方法远程生成)。

在Odoo Web中,负责处理和响应这些动作的组件是动作管理器

使用动作管理器

通过创建描述正确类型的动作的字典,并使用它调用动作管理器实例,可以从javascript代码中显式调用动作管理器。

do_action()Widget()的快捷方式,它查找“当前”动作管理器并执行该动作:

instance.web.TestWidget = instance.Widget.extend({
    dispatch_to_new_action: function() {
        this.do_action({
            type: 'ir.actions.act_window',
            res_model: "product.product",
            res_id: 1,
            views: [[false, 'form']],
            target: 'current',
            context: {},
        });
    },
});

最常见的操作类型ir.actions.act_window,它为模型提供视图(以各种方式显示模型),其最常见的属性是:

res_model
在视图中显示的模型
res_id (optional)
对于表单视图,是res_model中的预选记录
views
列出通过操作可用的视图。 [view_id,view_type]view_id的列表可以是正确类型的视图的数据库标识符,也可以是false来使用默认情况下查看指定类型。 视图类型不能多次出现。 默认情况下,该操作将打开列表的第一个视图。
target
current(默认值)(用操作替换Web客户端的“内容”部分),或new在对话框中打开操作。
context
要在操作中使用的其他上下文数据。

客户动作

在本指南中,我们使用了一个简单的Home小部件,当选择正确的菜单项时,Web客户端会自动启动。 但是Odoo网站如何知道如何启动此小部件? 因为该小部件已注册为客户端操作

客户端动作(顾名思义)是一种在Odoo Web的javascript中几乎完全在客户端中定义的动作类型。 服务器仅发送一个动作标签(任意名称),并可选地添加一些参数,但除此之外,所有均由自定义客户端代码处理。

我们的小部件通过以下方式注册为客户端操作的处理程序:

instance.web.client_actions.add('petstore.homepage', 'instance.oepetstore.HomePage');

instance.web.client_actionsRegistry(),其中动作管理器在需要执行客户端动作处理程序时会查找它。 add()的第一个参数是客户端操作的名称(标签),第二个参数是从Odoo Web客户端根目录到小部件的路径。

当必须执行客户端操作时,操作管理器会在注册表中查找其标记,遍历指定路径并显示最终找到的小部件。

在服务器端,我们仅定义了ir.actions.client操作:

<record id="action_home_page" model="ir.actions.client">
    <field name="tag">petstore.homepage</field>
</record>

和一个打开动作的菜单:

<menuitem id="home_page_petstore_menu" parent="petstore_menu"
          name="Home Page" action="action_home_page"/>

视图架构

Odoo Web的许多有用性(和复杂性)都存在于视图中。 每种视图类型都是在客户端中显示模型的一种方式。

视图管理器

ActionManager实例收到ir.actions.act_window类型的动作时,它将视图本身的同步和处理委托给view manager ,然后将根据原始操作的要求设置一个或多个视图:

Views视图

大多数Odoo视图是通过odoo.web.View()的子类实现的,该子类提供了一些用于处理事件和显示模型信息的通用基本结构。

搜索视图在主要的Odoo框架中被视为视图类型,但是由Web客户端单独处理(因为它是永久性的固定装置,可以与其他视图交互,而常规视图则无法做到) 。

视图负责加载自己的描述XML(使用fields_view_get)和所需的任何其他数据源。 为此,为视图提供了一个可选的视图标识符,该标识符设置为view_id属性。

视图还提供了一个DataSet()实例,该实例包含最必要的模型信息(模型名称以及可能的各种记录ID)。

视图可能还想通过覆盖do_search()并根据需要更新其DataSet()来处理搜索查询。

表单视图字段

通常需要扩展Web窗体视图以添加新的字段显示方式。

All built-in fields have a default display implementation, a new form widget may be necessary to correctly interact with a new field type (e.g. a GIS field) or to provide new representations and ways to interact with existing field types (e.g. validate Char fields which should contain email addresses and display them as email links).

To explicitly specify which form widget should be used to display a field, simply use the widget attribute in the view's XML description:

<field name="contact_mail" widget="email"/>

Fields are instantiated by the form view after it has read its XML description and constructed the corresponding HTML representing that description. After that, the form view will communicate with the field objects using some methods. These methods are defined by the FieldInterface interface. Almost all fields inherit the AbstractField abstract class. That class defines some default mechanisms that need to be implemented by most fields.

Here are some of the responsibilities of a field class:

  • The field class must display and allow the user to edit the value of the field.
  • It must correctly implement the 3 field attributes available in all fields of Odoo. The AbstractField class already implements an algorithm that dynamically calculates the value of these attributes (they can change at any moment because their value change according to the value of other fields). Their values are stored in Widget Properties (the widget properties were explained earlier in this guide). It is the responsibility of each field class to check these widget properties and dynamically adapt depending of their values. Here is a description of each of these attributes:

    • required: The field must have a value before saving. If required is true and the field doesn't have a value, the method is_valid() of the field must return false.
    • invisible: When this is true, the field must be invisible. The AbstractField class already has a basic implementation of this behavior that fits most fields.
    • readonly: When true, the field must not be editable by the user. Most fields in Odoo have a completely different behavior depending on the value of readonly. As example, the FieldChar displays an HTML <input> when it is editable and simply displays the text when it is read-only. This also means it has much more code it would need to implement only one behavior, but this is necessary to ensure a good user experience.
  • Fields have two methods, set_value() and get_value(), which are called by the form view to give it the value to display and get back the new value entered by the user. These methods must be able to handle the value as given by the Odoo server when a read() is performed on a model and give back a valid value for a write(). Remember that the JavaScript/Python data types used to represent the values given by read() and given to write() is not necessarily the same in Odoo. As example, when you read a many2one, it is always a tuple whose first value is the id of the pointed record and the second one is the name get (ie: (15, "Agrolait")). But when you write a many2one it must be a single integer, not a tuple anymore. AbstractField has a default implementation of these methods that works well for simple data type and set a widget property named value.

Please note that, to better understand how to implement fields, you are strongly encouraged to look at the definition of the FieldInterface interface and the AbstractField class directly in the code of the Odoo web client.

Creating a New Type of Field

In this part we will explain how to create a new type of field. The example here will be to re-implement the FieldChar class and progressively explain each part.

Simple Read-Only Field

Here is a first implementation that will only display text. The user will not be able to modify the content of the field.

local.FieldChar2 = instance.web.form.AbstractField.extend({
    init: function() {
        this._super.apply(this, arguments);
        this.set("value", "");
    },
    render_value: function() {
        this.$el.text(this.get("value"));
    },
});

instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');

In this example, we declare a class named FieldChar2 inheriting from AbstractField. We also register this class in the registry instance.web.form.widgets under the key char2. That will allow us to use this new field in any form view by specifying widget="char2" in the <field/> tag in the XML declaration of the view.

In this example, we define a single method: render_value(). All it does is display the widget property value. Those are two tools defined by the AbstractField class. As explained before, the form view will call the method set_value() of the field to set the value to display. This method already has a default implementation in AbstractField which simply sets the widget property value. AbstractField also watch the change:value event on itself and calls the render_value() when it occurs. So, render_value() is a convenience method to implement in child classes to perform some operation each time the value of the field changes.

In the init() method, we also define the default value of the field if none is specified by the form view (here we assume the default value of a char field should be an empty string).

Read-Write Field

Read-only fields, which only display content and don't allow the user to modify it can be useful, but most fields in Odoo also allow editing. This makes the field classes more complicated, mostly because fields are supposed to handle both editable and non-editable mode, those modes are often completely different (for design and usability purpose) and the fields must be able to switch between modes at any moment.

To know in which mode the current field should be, the AbstractField class sets a widget property named effective_readonly. The field should watch for changes in that widget property and display the correct mode accordingly. Example:

local.FieldChar2 = instance.web.form.AbstractField.extend({
    init: function() {
        this._super.apply(this, arguments);
        this.set("value", "");
    },
    start: function() {
        this.on("change:effective_readonly", this, function() {
            this.display_field();
            this.render_value();
        });
        this.display_field();
        return this._super();
    },
    display_field: function() {
        var self = this;
        this.$el.html(QWeb.render("FieldChar2", {widget: this}));
        if (! this.get("effective_readonly")) {
            this.$("input").change(function() {
                self.internal_set_value(self.$("input").val());
            });
        }
    },
    render_value: function() {
        if (this.get("effective_readonly")) {
            this.$el.text(this.get("value"));
        } else {
            this.$("input").val(this.get("value"));
        }
    },
});

instance.web.form.widgets.add('char2', 'instance.oepetstore.FieldChar2');
<t t-name="FieldChar2">
    <div class="oe_field_char2">
        <t t-if="! widget.get('effective_readonly')">
            <input type="text"></input>
        </t>
    </div>
</t>

In the start() method (which is called immediately after a widget has been appended to the DOM), we bind on the event change:effective_readonly. That allows us to redisplay the field each time the widget property effective_readonly changes. This event handler will call display_field(), which is also called directly in start(). This display_field() was created specifically for this field, it's not a method defined in AbstractField or any other class. We can use this method to display the content of the field depending on the current mode.

From now on the conception of this field is typical, except there is a lot of verifications to know the state of the effective_readonly property:

  • In the QWeb template used to display the content of the widget, it displays an <input type="text" /> if we are in read-write mode and nothing in particular in read-only mode.
  • In the display_field() method, we have to bind on the change event of the <input type="text" /> to know when the user has changed the value. When it happens, we call the internal_set_value() method with the new value of the field. This is a convenience method provided by the AbstractField class. That method will set a new value in the value property but will not trigger a call to render_value() (which is not necessary since the <input type="text" /> already contains the correct value).
  • In render_value(), we use a completely different code to display the value of the field depending if we are in read-only or in read-write mode.

The Form View Custom Widgets

Form fields are used to edit a single field, and are intrinsically linked to a field. Because this may be limiting, it is also possible to create form widgets which are not so restricted and have less ties to a specific lifecycle.

Custom form widgets can be added to a form view through the widget tag:

<widget type="xxx" />

This type of widget will simply be created by the form view during the creation of the HTML according to the XML definition. They have properties in common with the fields (like the effective_readonly property) but they are not assigned a precise field. And so they don't have methods like get_value() and set_value(). They must inherit from the FormWidget abstract class.

Form widgets can interact with form fields by listening for their changes and fetching or altering their values. They can access form fields through their field_manager attribute:

local.WidgetMultiplication = instance.web.form.FormWidget.extend({
    start: function() {
        this._super();
        this.field_manager.on("field_changed:integer_a", this, this.display_result);
        this.field_manager.on("field_changed:integer_b", this, this.display_result);
        this.display_result();
    },
    display_result: function() {
        var result = this.field_manager.get_field_value("integer_a") *
                     this.field_manager.get_field_value("integer_b");
        this.$el.text("a*b = " + result);
    }
});

instance.web.form.custom_widgets.add('multiplication', 'instance.oepetstore.WidgetMultiplication');

FormWidget is generally the FormView() itself, but features used from it should be limited to those defined by FieldManagerMixin(), the most useful being:

  • get_field_value(field_name)() which returns the value of a field.
  • set_values(values)() sets multiple field values, takes a mapping of {field_name: value_to_set}
  • An event field_changed:field_name is triggered any time the value of the field called field_name is changed
[1] as a separate concept from instances. In many languages classes are full-fledged objects and themselves instance (of metaclasses) but there remains two fairly separate hierarchies between classes and instances
[2] as well as papering over cross-browser differences, although this has become less necessary over time