Посилання Javascript

У цьому документі представлено фреймворк Javascript Odoo. Цей фреймворк не є великою програмою з точки зору рядків коду, але він є досить загальним, тому що в основному це машина для перетворення декларативного опису інтерфейсу в живу програму, здатну взаємодіяти з кожною моделлю та записами в базі даних. Можна навіть використовувати веб-клієнт для зміни інтерфейсу веб-клієнта.

Огляд

Фреймворк Javascript розроблено для роботи з трьома основними варіантами використання:

  • веб-клієнт: це приватний веб-додаток, де можна переглядати та редагувати бізнес-дані. Це односторінковий додаток (сторінка ніколи не перезавантажується, лише нові дані завантажуються із сервера, коли це необхідно)

  • веб-сайт: це публічна частина Odoo. Це дозволяє неідентифікованому користувачеві переглядати певний вміст, робити покупки або виконувати багато дій як клієнт. Це класичний веб-сайт: різні маршрути з контролерами та деякий JavaScript, щоб він працював.

  • точка продажу: це інтерфейс для точки продажу. Це спеціалізований односторінковий додаток.

Деякий код javascript є спільним для цих трьох випадків використання та об’єднується разом (див. нижче в розділі активів). Цей документ буде зосереджений переважно на дизайні веб-клієнта.

Веб-клієнт

Односторінковий додаток

Коротше кажучи, webClient, екземпляр WebClient є кореневим компонентом усього інтерфейсу користувача. Його відповідальність полягає в тому, щоб оркеструвати всі різні підкомпоненти та надавати послуги, такі як rpcs, локальне сховище тощо.

Під час виконання веб-клієнт є односторінковим додатком. Йому не потрібно запитувати повну сторінку від сервера кожного разу, коли користувач виконує дію. Натомість він запитує лише те, що йому потрібно, а потім замінює/оновлює представлення. Крім того, він керує URL-адресою: вона синхронізується зі станом веб-клієнта.

Це означає, що поки користувач працює над Odoo, клас веб-клієнта (і менеджер дій) фактично створює та знищує багато підкомпонентів. Стан дуже динамічний, і кожен віджет може бути знищений у будь-який час.

Огляд JS-коду веб-клієнта

Тут ми надаємо дуже короткий огляд коду веб-клієнта в додатку web/static/src/js. Зауважте, що він навмисно не є вичерпним. Ми розглядаємо лише найважливіші файли/папки.

  • boot.js: це файл, який визначає модульну систему. Спочатку його потрібно завантажити.

  • core/: це набір будівельних блоків нижчого рівня. Зокрема, він містить систему класів, систему віджетів, утиліти паралелізму та багато інших класів/функцій.

  • chrome/: у цій папці ми маємо більшість великих віджетів, які складають більшу частину інтерфейсу користувача.

  • chrome/abstract_web_client.js і chrome/web_client.js: разом ці файли визначають віджет WebClient, який є кореневим віджетом для веб-клієнта.

  • chrome/action_manager.js: це код, який перетворює дію на віджет (наприклад, канбан або вигляд форми)

  • chrome/search_X.js усі ці файли визначають представлення пошуку (це не перегляд з точки зору веб-клієнта, лише з точки зору сервера)

  • fields: тут визначено всі віджети полів головного представлення

  • views: тут розташовані представлення

Управління активами

Керувати активами в Odoo не так просто, як у деяких інших додатках. Однією з причин є те, що ми маємо різноманітні ситуації, коли потрібні деякі, але не всі активи. Наприклад, потреби веб-клієнта, точки продажу, веб-сайту чи навіть мобільного додатку різні. Крім того, деякі активи можуть бути великими, але рідко потрібні. У такому випадку ми іноді хочемо, щоб вони завантажувалися ліниво.

Основна ідея полягає в тому, що ми визначаємо набір бандлів у xml. Бандл тут визначається як набір файлів (javascript, css, scss). В Odoo найважливіші комплекти визначено у файлі addons/web/views/webclient_templates.xml. Це виглядає так:

<template id="web.assets_common" name="Common Assets (used in backend interface and website)">
    <link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ui/jquery-ui.css"/>
    ...
    <script type="text/javascript" src="/web/static/src/js/boot.js"></script>
    ...
</template>

Файли з пакунка можна вставити у шаблон за допомогою директиви t-call-assets:

<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_common" t-css="false"/>

Ось що відбувається, коли шаблон рендериться сервером з цими директивами:

  • всі файли scss, описані в бандлу, компілюються у файли css. Файл з назвою file.scss буде скомпільовано у файл з назвою file.scss.css.

  • якщо ми перебуваємо в режимі debug=assets

    • директива t-call-assets з атрибутом t-js, встановленим у false, буде замінена списком тегів таблиці стилів, що вказують на css-файли

    • директива t-call-assets з атрибутом t-css, встановленим у false, буде замінена списком тегів скриптів, що вказують на js-файли

  • якщо ми не перебуваємо в режимі *debug=assets

    • css-файли будуть об’єднані та мінімізовані, після чого буде згенеровано тег таблиці стилів

    • js-файли об’єднуються та мінімізуються, після чого генерується тег скрипту

Зверніть увагу, що файли ресурсів кешуються, тому теоретично браузер повинен завантажити їх лише один раз.

Основні бандли

Коли сервер Odoo запускається, він перевіряє мітку часу кожного файлу в пакунку і, якщо потрібно, створює/перестворює відповідні бандли.

Ось кілька важливих бандлів, про які потрібно знати більшості розробників:

  • web.assets_common: цей пакет містить більшість активів, які є спільними для веб-клієнта, веб-сайту, а також точки продажу. Передбачається, що він містить будівельні блоки нижчого рівня для фреймворку odoo. Зверніть увагу, що він містить файл boot.js, який визначає систему модулів odoo.

  • web.assets_backend: цей пакет містить код, специфічний для веб-клієнта (зокрема, веб-клієнт/менеджер дій/представлення)

  • web.assets_frontend: цей бандл стосується всього, що є специфічним для публічного веб-сайту: ел. комерція, форум, блог, управління подіями, …

Додавання файлів до бандлу активів

Правильний спосіб додати файл, розташований в addons/web, до пакунка простий: достатньо додати тег script або stylesheet до пакунка у файлі webclient_templates.xml. Але коли ми працюємо в іншому додатку, нам потрібно додати файл з цього додатку. В такому випадку це потрібно зробити в три кроки:

  1. додайте файл assets.xml в папку views/

  2. додайте рядок „views/assets.xml“ до ключа „data“ у файлі маніфесту

  3. створіть успадковане представленн потрібного бандла і додайте файл(и) за допомогою виразу xpath. Наприклад,

<template id="assets_backend" name="helpdesk assets" inherit_id="web.assets_backend">
    <xpath expr="//script[last()]" position="after">
        <link rel="stylesheet" type="text/scss" href="/helpdesk/static/src/scss/helpdesk.scss"/>
        <script type="text/javascript" src="/helpdesk/static/src/js/helpdesk_dashboard.js"></script>
    </xpath>
</template>

Примітка

Зверніть увагу, що всі файли в бандлі завантажуються одразу, коли користувач завантажує веб-клієнт odoo. Це означає, що файли передаються через мережу кожного разу (за винятком випадків, коли активний кеш браузера). У деяких випадках може бути краще відкладено завантажувати деякі ресурси. Наприклад, якщо віджет вимагає великої бібліотеки, і цей віджет не є основною частиною інтерфейсу, то може бути гарною ідеєю завантажити бібліотеку лише тоді, коли віджет фактично створено. Клас віджету має вбудовану підтримку саме для цього випадку використання. (див. розділ Механізм шаблонів QWeb)

Що робити, якщо файл не завантажується/оновлюється

Є багато різних причин, чому файл може не завантажуватися належним чином. Ось кілька речей, які ви можете спробувати вирішити проблему:

  • після запуску сервера він не знає, чи було змінено файл активів. Отже, ви можете просто перезапустити сервер, щоб відновити активи.

  • перевірте консоль (в інструментах розробника, зазвичай відкривається за допомогою F12), щоб переконатися, що немає очевидних помилок

  • спробуйте додати console.log на початку вашого файлу (перед будь-яким визначенням модуля), щоб ви могли бачити, чи був завантажений файл чи ні

  • в інтерфейсі користувача, в режимі налагодження (INSERT LINK HERE TO DEBUG MODE), є можливість змусити сервер оновити свої файли активів.

  • використовувати режим debug=assets. Це фактично обійде пакети активів (зверніть увагу, що це фактично не вирішить проблему. Сервер все ще використовує застарілі пакети)

  • нарешті, найзручнішим способом зробити це для розробника є запуск сервера з опцією –dev=all. Це активує параметри перегляду файлів, які за потреби автоматично анулюють ресурси. Зауважте, що це не дуже добре працює, якщо ОС Windows.

  • не забудьте оновити свою сторінку!

  • або, можливо, щоб зберегти ваш файл коду…

Примітка

Після того, як файл ресурсу було створено повторно, вам потрібно оновити сторінку, щоб перезавантажити відповідні файли (якщо це не спрацює, файли можуть бути кешованими).

Система модулів Javascript

Після того, як ми зможемо завантажити наші javascript-файли в браузер, нам потрібно переконатися, що вони завантажуються в правильному порядку. Для цього в Odoo визначена невелика система модулів (розташована у файлі addons/web/static/src/js/boot.js, який потрібно завантажити в першу чергу).

Система модулів Odoo, натхненна AMD, працює шляхом визначення функції define на глобальному об’єкті odoo. Потім ми визначаємо кожен javascript-модуль, викликаючи цю функцію. У фреймворку Odoo модуль - це фрагмент коду, який буде виконано якнайшвидше. Він має назву і, можливо, деякі залежності. Коли завантажуються його залежності, модуль також завантажується. Значення модуля - це значення, що повертається функцією, яка визначає модуль.

Як приклад, це може виглядати так:

// in file a.js
odoo.define('module.A', function (require) {
    "use strict";

    var A = ...;

    return A;
});

// in file b.js
odoo.define('module.B', function (require) {
    "use strict";

    var A = require('module.A');

    var B = ...; // something that involves A

    return B;
});

Альтернативний спосіб визначення модуля полягає в тому, щоб явно вказати список залежностей у другому аргументі.

odoo.define('module.Something', ['module.A', 'module.B'], function (require) {
    "use strict";

    var A = require('module.A');
    var B = require('module.B');

    // some code
});

Якщо деякі залежності відсутні або не готові, то модуль просто не завантажиться. Через кілька секунд на консолі з’явиться попередження.

Зауважте, що циклічні залежності не підтримуються. Це має сенс, але це означає, що потрібно бути обережним.

Визначення модуля

Методу odoo.define передається три аргументи:

  • moduleName: назва модуля javascript. Це має бути унікальний рядок. Угода полягає в тому, що назва додатку odoo має супроводжуватися конкретним описом. Наприклад, «web.Widget» описує модуль, визначений в web додатку, який експортує клас Widget (тому що перша буква пишеться з великої літери)

    Якщо назва не є унікальною, буде створено виняток і відображено на консолі.

  • dependencies: другий аргумент є необов’язковим. Якщо він заданий, це має бути список рядків, кожен з яких відповідає javascript-модулю. Тут описуються залежності, які необхідно завантажити перед виконанням модуля. Якщо залежності не вказані тут явно, то система модуля витягне їх з функції, викликавши toString, а потім за допомогою regexp знайде всі оператори require.

  odoo.define('module.Something', ['web.ajax'], function (require) {
    "use strict";

    var ajax = require('web.ajax');

    // some code here
    return something;
});
  • нарешті, останній аргумент є функцією, яка визначає модуль. Його значення, що повертається, є значенням модуля, яке може бути передано іншим модулям, яким це потрібно. Зауважте, що є невеликий виняток для асинхронних модулів, див. наступний розділ.

Якщо станеться помилка, вона буде зареєстрована (у режимі налагодження) у консолі:

  • Відсутні залежності: Ці модулі не відображаються на сторінці. Можливо, на сторінці відсутній файл JavaScript або неправильно вказано назву модуля

  • Помилка модулів: виявлено помилку JavaScript

  • Відхилені модулі: модуль повертає відхилену обіцянку. Він (та його залежні модулі) не завантажується.

  • Відхилені пов’язані модулі: модулі, які залежать від відхиленого модуля

  • Non loaded modules: Modules who depend on a missing or a failed module

Асинхронні модулі

It can happen that a module needs to perform some work before it is ready. For example, it could do a rpc to load some data. In that case, the module can simply return a promise. In that case, the module system will simply wait for the promise to complete before registering the module.

odoo.define('module.Something', function (require) {
    "use strict";

    var ajax = require('web.ajax');

    return ajax.rpc(...).then(function (result) {
        // some code here
        return something;
    });
});

Кращі практики

  • remember the convention for a module name: addon name suffixed with module name.

  • declare all your dependencies at the top of the module. Also, they should be sorted alphabetically by module name. This makes it easier to understand your module.

  • declare all exported values at the end

  • try to avoid exporting too many things from one module. It is usually better to simply export one thing in one (small/smallish) module.

  • asynchronous modules can be used to simplify some use cases. For example, the web.dom_ready module returns a promise which will be resolved when the dom is actually ready. So, another module that needs the DOM could simply have a require('web.dom_ready') statement somewhere, and the code will only be executed when the DOM is ready.

  • try to avoid defining more than one module in one file. It may be convenient in the short term, but this is actually harder to maintain.

Система класів

Odoo було розроблено до появи класів ECMAScript 6. В Ecmascript 5 стандартним способом визначення класу є визначення функції та додавання методів до її об’єкта-прототипу. Це добре, але це трохи складно, коли ми хочемо використовувати успадкування, міксини.

З цих причин Odoo вирішив використати власну систему класів, натхненну John Resig. Базовий клас знаходиться у web.Class, у файлі class.js.

Створення підкласу

Давайте обговоримо, як створюються класи. Основним механізмом є використання методу extend (це більш-менш еквівалент extend у класах ES6).

var Class = require('web.Class');

var Animal = Class.extend({
    init: function () {
        this.x = 0;
        this.hunger = 0;
    },
    move: function () {
        this.x = this.x + 1;
        this.hunger = this.hunger + 1;
    },
    eat: function () {
        this.hunger = 0;
    },
});

У цьому прикладі функція init є конструктором. Він буде викликаний, коли буде створено екземпляр. Створення екземпляра здійснюється за допомогою ключового слова new.

Спадкування

Зручно мати можливість успадкувати існуючий клас. Це просто робиться за допомогою методу extend для суперкласу. Під час виклику методу фреймворк таємно повторно прив’язує спеціальний метод: _super до поточного викликаного методу. Це дозволяє нам використовувати this._super щоразу, коли нам потрібно викликати батьківський метод.

var Animal = require('web.Animal');

var Dog = Animal.extend({
    move: function () {
        this.bark();
        this._super.apply(this, arguments);
    },
    bark: function () {
        console.log('woof');
    },
});

var dog = new Dog();
dog.move()

Міксини

Система класів odoo не підтримує множинне успадкування, але для тих випадків, коли нам потрібно поділитися деякою поведінкою, у нас є система mixin: метод extend може фактично приймати довільну кількість аргументів і об’єднуватиме їх усі в новий клас.

var Animal = require('web.Animal');
var DanceMixin = {
    dance: function () {
        console.log('dancing...');
    },
};

var Hamster = Animal.extend(DanceMixin, {
    sleep: function () {
        console.log('sleeping');
    },
});

У цьому прикладі клас Hamster є підкласом Animal, але він також містить DanceMixin.

Виправлення існуючого класу

Це нечасто, але іноді нам потрібно змінити інший клас на місці. Мета полягає в тому, щоб мати механізм для зміни класу та всіх майбутніх/теперішніх екземплярів. Це робиться за допомогою методу include:

var Hamster = require('web.Hamster');

Hamster.include({
    sleep: function () {
        this._super.apply(this, arguments);
        console.log('zzzz');
    },
});

Очевидно, що це небезпечна операція, і її слід виконувати обережно. Але з огляду на те, як Odoo структурований, іноді необхідно в одному аддоні змінити поведінку віджета/класу, визначеного в іншому аддоні. Зверніть увагу, що він змінить усі екземпляри класу, навіть якщо вони вже були створені.

Віджети

Клас Widget дійсно є важливим будівельним блоком інтерфейсу користувача. Практично все в інтерфейсі користувача знаходиться під контролем віджета. Клас Widget визначено в модулі web.Widget, у widget.js.

Коротше кажучи, функції, які надає клас Widget, включають:

  • батьківські/дочірні відносини між віджетами (PropertiesMixin)

  • широке керування життєвим циклом із функціями безпеки (наприклад, автоматичне знищення дочірніх віджетів під час знищення батьківського)

  • автоматичне відтворення за допомогою qweb

  • різноманітні корисні функції для взаємодії із зовнішнім середовищем.

Ось приклад базового віджета лічильника:

var Widget = require('web.Widget');

var Counter = Widget.extend({
    template: 'some.template',
    events: {
        'click button': '_onClick',
    },
    init: function (parent, value) {
        this._super(parent);
        this.count = value;
    },
    _onClick: function () {
        this.count++;
        this.$('.val').text(this.count);
    },
});

For this example, assume that the template some.template (and is properly loaded: the template is in a file, which is properly defined in the qweb key in the module manifest) is given by:

<div t-name="some.template">
    <span class="val"><t t-esc="widget.count"/></span>
    <button>Increment</button>
</div>

Цей приклад віджета можна використовувати таким чином:

// Create the instance
var counter = new Counter(this, 4);
// Render and insert into DOM
counter.appendTo(".some-div");

Цей приклад ілюструє деякі особливості класу Widget, включаючи систему подій, систему шаблонів, конструктор з початковим аргументом parent.

Життєвий цикл віджета

Як і багато компонентних систем, клас віджетів має чітко визначений життєвий цикл. Звичайний життєвий цикл такий: викликається init, потім willStart, потім виконується рендеринг, потім start і, нарешті, destroy.

Widget.init(parent)

це конструктор. Передбачається, що метод init ініціалізує базовий стан віджета. Він є синхронним і може бути перевизначений, щоб отримати більше параметрів від творця/батька віджета

Аргументи
  • parent (Widget()) – батьківський елемент нового віджета, який використовується для обробки автоматичного знищення та поширення подій. Може бути null, щоб віджет не мав батьківського елемента.

Widget.willStart()

цей метод буде викликаний фреймворком один раз під час створення віджета та в процесі його додавання до DOM. Метод willStart - це хук, який має повертати обіцянку. Фреймворк JS чекатиме, поки ця обіцянка завершиться, перш ніж перейти до етапу візуалізації. Зауважте, що на даний момент віджет не має кореневого елемента DOM. Хук willStart здебільшого корисний для виконання деякої асинхронної роботи, наприклад отримання даних із сервера

[Rendering]()

Цей крок автоматично виконується фреймворком. Відбувається те, що фреймворк перевіряє, чи ключ шаблону визначено у віджеті. Якщо це так, тоді він візуалізує цей шаблон із ключем widget, прив’язаним до віджета в контексті рендерингу (див. приклад вище: ми використовуємо widget.count у шаблоні QWeb для читання значення з віджета ). Якщо шаблон не визначено, ми читаємо ключ tagName і створюємо відповідний елемент DOM. Коли візуалізація завершена, ми встановлюємо результат як властивість $el віджета. Після цього ми автоматично прив’язуємо всі події в ключі events і custom_events.

Widget.start()

коли візуалізація завершиться, фреймворк автоматично викличе метод start. Це корисно для виконання деяких спеціалізованих робіт після візуалізації. Наприклад, створення бібліотеки.

Має повернути обіцянку, щоб вказати, коли його роботу виконано.

Повертає

promise

Widget.destroy()

Це завжди останній крок у житті віджета. Коли віджет знищено, ми в основному виконуємо всі необхідні операції очищення: видалення віджета з дерева компонентів, розв’язування всіх подій, …

Викликається автоматично, коли батьківський елемент віджета знищено, має бути викликаний явно, якщо віджет не має батьківського елемента або якщо його видалено, але його батьківський елемент залишається.

Зауважте, що willStart і start метод не обов’язково викликаються. Віджет можна створити (буде викликано метод init), а потім знищити (метод destroy), не додаючи його до DOM. Якщо це так, willStart і start навіть не будуть викликані.

API віджета

Widget.tagName

Використовується, якщо для віджета не визначено шаблон. За замовчуванням div буде використано як назву тегу для створення елемента DOM, який буде встановлено як кореневий DOM віджета. Можна додатково налаштувати цей згенерований корінь DOM за допомогою таких атрибутів:

Widget.id

Використовується для створення атрибута id у створеному корені DOM. Зауважте, що це рідко потрібно, і, ймовірно, не є гарною ідеєю, якщо віджет можна використовувати кілька разів.

Widget.className

Використовується для створення атрибута class у створеному корені DOM. Зверніть увагу, що насправді він може містити більше одного класу css: „some-class other-class“

Widget.attributes

Відображення (літерал об’єкта) назв атрибутів на значення атрибутів. Кожна з цих пар k:v буде встановлена як атрибут DOM у згенерованому корені DOM.

Widget.el

необроблений елемент DOM, встановлений як кореневий для віджета (доступний лише після запуску методу життєвого циклу)

Widget.$el

обгортка jQuery навколо el. (доступно лише після запуску методу життєвого циклу)

Widget.template

Має бути встановлено назву QWeb шаблону. Якщо встановлено, шаблон буде відображено після ініціалізації віджета, але до його запуску. Кореневий елемент, згенерований шаблоном, буде встановлено як корінь DOM віджета.

Widget.xmlDependencies

Список шляхів до xml-файлів, які потрібно завантажити перед відтворенням віджета. Це не призведе до завантаження того, що вже було завантажено. Це корисно, коли ви хочете ліниво завантажувати свої шаблони або якщо ви хочете надати спільний доступ до віджета між веб-сайтом та інтерфейсом веб-клієнта.

var EditorMenuBar = Widget.extend({
    xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
    ...
Widget.events

Події - це відображення селектора подій (назви події та додаткового селектора CSS, розділених пробілом) на зворотний виклик. Зворотний виклик може бути назвою методу віджета або об’єкта функції. У будь-якому випадку this буде встановлено для віджета:

events: {
    'click p.oe_some_class a': 'some_method',
    'change input': function (e) {
        e.stopPropagation();
    }
},

Селектор використовується для делегування подій jQuery, зворотний виклик запускатиметься лише для нащадків кореня DOM, що відповідає селектеру. Якщо селектор опущено (вказано лише назва події), подію буде встановлено безпосередньо в корені DOM віджета.

Примітка: використання вбудованих функцій не рекомендується, і, ймовірно, іноді їх буде видалено в майбутньому.

Widget.custom_events

це майже те саме, що атрибут events, але ключі є довільними рядками. Вони представляють бізнес-події, викликані деякими підвіджетами. Коли спрацьовує подія, вона „вибухає“ в дереві віджетів (додаткову інформацію див. у розділі про зв’язок компонентів).

Widget.isDestroyed()
Повертає

true, якщо віджет знищується або був знищений, false інакше

Widget.$(selector)

Застосовує селектор CSS, указаний як параметр, до кореня DOM віджета:

this.$(selector);

функціонально ідентичний:

this.$el.find(selector);
Аргументи
  • selector (String()) – Селектор CSS

Повертає

Об’єкт jQuery

Примітка

цей допоміжний метод схожий на Backbone.View.$

Widget.setElement(element)

Перевстановлює корінь DOM віджета на наданий елемент, а також керує переналаштуванням різних аліасів кореня DOM, а також скасовує та повторно встановлює делеговані події.

Аргументи
  • element (Element()) – елемент DOM або об’єкт jQuery, щоб встановити як кореневу DOM віджета

Вставлення віджета в DOM

Widget.appendTo(element)

Відтворює віджет і вставляє його як останнього дочірнього елемента цілі, використовує .appendTo()

Widget.prependTo(element)

Відтворює віджет і вставляє його як останнього дочірнього елемента цілі, використовує .appendTo()

Widget.insertAfter(element)

Відтворює віджет і вставляє його як попереднього брата цільового елемента, використовує .insertAfter()

Widget.insertBefore(element)

Відтворює віджет і вставляє його як наступного брата цілі, використовує .insertBefore()

Усі ці методи приймають усе, що приймає відповідний метод jQuery (селектори CSS, вузли DOM або об’єкти jQuery). Усі вони повертають обіцянку та виконують три завдання:

  • рендеринг кореневого елемента віджета через renderElement()

  • вставляючи кореневий елемент віджета в DOM за допомогою відповідного методу jQuery

  • запуск віджета та повернення результату його запуску

Інструкції щодо віджетів

  • Слід уникати ідентифікаторів (атрибут id). У загальних додатках і модулях id обмежує можливість повторного використання компонентів і робить код більш крихким. У більшості випадків їх можна замінити нічим, класами або збереженням посилання на вузол DOM або елемент jQuery.

    Якщо id абсолютно необхідний (оскільки його вимагає стороння бібліотека), ідентифікатор має бути частково згенерований за допомогою _.uniqueId(), наприклад:

    this.id = _.uniqueId('my-widget-');
    
  • Уникайте передбачуваних/загальних назв класів CSS. Назви класів, такі як «контент» або «навігація», можуть відповідати бажаному значенню/семантиці, але ймовірно, інший розробник матиме таку саму потребу, створюючи конфлікт імен та ненавмисну поведінку. Назви загальних класів повинні мати префікс, наприклад, назву компонента, до якого вони належать (створюючи «неформальні» простори імен, подібно до C або Objective-C).

  • Слід уникати глобальних селекторів. Оскільки компонент може використовуватися кілька разів на одній сторінці (прикладом в Odoo є інформаційні панелі), запити мають бути обмежені сферою дії певного компонента. Нефільтрований вибір, як-от $(selector) або document.querySelectorAll(selector), як правило, призведе до ненавмисної або неправильної поведінки. Widget() Odoo Web має атрибут, що надає корінь DOM ($el), і ярлик для прямого вибору вузлів ($()).

  • Загалом, ніколи не припускайте, що ваші компоненти володіють або контролюють щось окрім свого особистого $el (тому уникайте використання посилання на батьківський віджет)

  • Шаблони Html/відтворення мають використовувати QWeb, якщо це не зовсім тривіально.

  • Усі інтерактивні компоненти (компоненти, що відображають інформацію на екрані або перехоплюють події DOM) мають успадковувати Widget() і правильно впроваджувати та використовувати його API та життєвий цикл.

  • Переконайтеся, що дочекалися завершення початку перед використанням $el, наприклад:

    var Widget = require('web.Widget');
    
    var AlmostCorrectWidget = Widget.extend({
        start: function () {
            this.$el.hasClass(....) // in theory, $el is already set, but you don't know what the parent will do with it, better call super first
            return this._super.apply(arguments);
        },
    });
    
    var IncorrectWidget = Widget.extend({
        start: function () {
            this._super.apply(arguments); // the parent promise is lost, nobody will wait for the start of this widget
            this.$el.hasClass(....)
        },
    });
    
    var CorrectWidget = Widget.extend({
        start: function () {
            var self = this;
            return this._super.apply(arguments).then(function() {
                self.$el.hasClass(....) // this works, no promise is lost and the code executes in a controlled order: first super, then our code.
            });
        },
    });
    

Механізм шаблонів QWeb

Веб-клієнт використовує механізм шаблонів Шаблони QWeb для візуалізації віджетів (якщо вони не перевизначають метод renderElement, щоб зробити щось інше). Механізм шаблонів Qweb JS заснований на XML і здебільшого сумісний із реалізацією python.

Now, let us explain how the templates are loaded. Whenever the web client starts, a rpc is made to the /web/webclient/qweb route. The server will then return a list of all templates defined in data files for each installed modules. The correct files are listed in the qweb entry in each module manifest.

Веб-клієнт чекатиме, доки цей список шаблонів буде завантажено, перш ніж запустити свій перший віджет.

Цей механізм працює досить добре для наших потреб, але іноді нам хочеться відкладати завантаження шаблону. Наприклад, уявіть, що у нас є віджет, який рідко використовується. У такому випадку, можливо, ми вважаємо за краще не завантажувати його шаблон у головний файл, щоб зробити веб-клієнт трохи легшим. У цьому випадку ми можемо використати ключ xmlDependencies віджета:

var Widget = require('web.Widget');

var Counter = Widget.extend({
    template: 'some.template',
    xmlDependencies: ['/myaddon/path/to/my/file.xml'],

    ...

});

При цьому віджет Counter завантажить файли xmlDependencies у своєму методі willStart, тож шаблон буде готовий, коли буде виконано рендеринг.

Система подій

Наразі Odoo підтримує дві системи подій: просту систему, яка дозволяє додавати слухачів і ініціювати події, і більш повну систему, яка також робить події „вибухаєючими“.

Обидві ці системи подій реалізовано в EventDispatcherMixin у файлі mixins.js. Цей міксин включено до класу Widget.

Система базових подій

Ця система подій була історично першою. Він реалізує простий шаблон шини. У нас є 4 основні методи:

  • on: використовується для реєстрації слухача події.

  • off: корисно видалити слухач подій.

  • once: це використовується для реєстрації слухача, який буде викликаний лише один раз.

  • trigger: ініціює подію. Це призведе до виклику кожного слухача.

Ось приклад того, як можна використовувати цю систему подій:

var Widget = require('web.Widget');
var Counter = require('myModule.Counter');

var MyWidget = Widget.extend({
    start: function () {
        this.counter = new Counter(this);
        this.counter.on('valuechange', this, this._onValueChange);
        var def = this.counter.appendTo(this.$el);
        return Promise.all([def, this._super.apply(this, arguments)]);
    },
    _onValueChange: function (val) {
        // do something with val
    },
});

// in Counter widget, we need to call the trigger method:

... this.trigger('valuechange', someValue);

Попередження

використання цієї системи подій не рекомендується, ми плануємо замінити кожен метод trigger на метод trigger_up із розширеної системи подій

Розширена система подій

Спеціальні віджети подій - це вдосконалена система, яка імітує API подій DOM. Кожного разу, коли запускається подія, вона „вибухає“ в дереві компонентів, доки не досягне кореневого віджета або не буде зупинено.

  • trigger_up: це метод, який створить невеликий OdooEvent і відправить його в дерево компонентів. Зверніть увагу, що він розпочнеться з компонента, який ініціював подію

  • custom_events: це еквівалент словника event, але для подій odoo.

Клас OdooEvent дуже простий. Він має три публічні атрибути: target (віджет, який ініціював подію), name (назва події) і data (корисне навантаження). Він також має 2 методи: stopPropagation і is_stopped.

Попередній приклад можна оновити, щоб використовувати спеціальну систему подій:

var Widget = require('web.Widget');
var Counter = require('myModule.Counter');

var MyWidget = Widget.extend({
    custom_events: {
        valuechange: '_onValueChange'
    },
    start: function () {
        this.counter = new Counter(this);
        var def = this.counter.appendTo(this.$el);
        return Promise.all([def, this._super.apply(this, arguments)]);
    },
    _onValueChange: function(event) {
        // do something with event.data.val
    },
});

// in Counter widget, we need to call the trigger_up method:

... this.trigger_up('valuechange', {value: someValue});

Реєстри

Загальною потребою в екосистемі Odoo є розширення/зміна поведінки базової системи ззовні (шляхом встановлення додатку, тобто іншого модуля). Наприклад, може знадобитися додати новий тип віджета в деякі види. У цьому випадку та багатьох інших звичайним процесом є створення потрібного компонента, а потім додавання його до реєстру (етап реєстрації), щоб решта веб-клієнта дізналася про його існування.

У системі є кілька реєстрів:

реєстр полів (експортовано web.field_registry)

Реєстр полів містить усі віджети полів, відомі веб-клієнту. Щоразу, коли представленню (як правило, формі чи списку/канбану) потрібен віджет поля, він буде виглядати саме тут. Типовий варіант використання виглядає так:

var fieldRegistry = require('web.field_registry');

var FieldPad = ...;

fieldRegistry.add('pad', FieldPad);

Зауважте, що кожне значення має бути підкласом AbstractField

переглянути реєстр

Цей реєстр містить усі представлення JS, відомі веб-клієнту (зокрема, менеджеру представлень). Кожне значення цього реєстру має бути підкласом AbstractView.

дія реєстру

Ми відстежуємо всі дії клієнта в цьому реєстрі. Це місце, де менеджер дій шукає щоразу, коли йому потрібно створити дію клієнта. У версії 11 кожне значення має бути просто підкласом Widget. Однак у версії 12 значення мають бути AbstractAction.

Зв’язок між віджетами

Існує багато способів обміну даними між компонентами.

Від батька до дитини

Це простий випадок. Батьківський віджет може просто викликати метод свого дочірнього:

this.someWidget.update(someInfo);
Від віджета до його батька/предка

У цьому випадку робота віджета полягає просто в сповіщенні свого середовища про те, що щось сталося. Оскільки ми не хочемо, щоб віджет мав посилання на свого батьківського елемента (це поєднає віджет із реалізацією його батьківського елемента), найкращий спосіб продовжити - це зазвичай ініціювати подію, яка з’явиться в дереві компонентів, за допомогою методу trigger_up:

this.trigger_up('open_record', { record: record, id: id});

Ця подія буде ініційована на віджеті, а потім виникне вгору та зрештою буде перехоплена деяким вищестоячим віджетом:

var SomeAncestor = Widget.extend({
    custom_events: {
        'open_record': '_onOpenRecord',
    },
    _onOpenRecord: function (event) {
        var record = event.data.record;
        var id = event.data.id;
        // do something with the event.
    },
});
Перехресний компонент

Міжкомпонентний зв’язок може бути досягнутий за допомогою шини. Це не найкраща форма зв’язку, оскільки вона має той недолік, що ускладнює підтримку коду. Однак він має перевагу роз’єднання компонентів. У такому випадку це робиться просто шляхом запуску та прослуховування подій на автобусі. Наприклад:

  // in WidgetA
  var core = require('web.core');

  var WidgetA = Widget.extend({
      ...
      start: function () {
          core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
      },
  });

  // in WidgetB
  var WidgetB = Widget.extend({
      ...
      someFunction: function (barcode) {
          core.bus.trigger('barcode_scanned', barcode);
      },
  });

In this example, we use the bus exported by *web.core*, but this is not
required. A bus could be created for a specific purpose.

Послуги

У версії 11.0 ми представили поняття служби. Основна ідея полягає в тому, щоб надати підкомпонентам контрольований спосіб доступу до їхнього середовища, у спосіб, який надає структурі достатній контроль і який можна тестувати.

Система послуг організована навколо трьох ідей: послуги, постачальники послуг і віджети. Це працює так, що віджети запускають (з trigger_up) події, ці події надходять до постачальника послуг, який попросить службу виконати завдання, а потім, можливо, поверне відповідь.

Послуга

Служба є екземпляром класу AbstractService. В основному він має лише назву та кілька методів. Його робота полягає в тому, щоб виконувати певну роботу, як правило, щось залежне від середовища.

Наприклад, у нас є служба ajax (робота полягає у виконанні rpc), localStorage (взаємодія з локальним сховищем браузера) та багато інших.

Ось спрощений приклад того, як реалізована служба ajax:

var AbstractService = require('web.AbstractService');

var AjaxService = AbstractService.extend({
    name: 'ajax',
    rpc: function (...) {
        return ...;
    },
});

Ця служба називається „ajax“ і визначає один метод rpc.

Постачальник послуг

Щоб служби працювали, необхідно, щоб у нас був постачальник послуг, готовий відправляти спеціальні події. У backend (веб-клієнті) це робить головний екземпляр веб-клієнта. Зауважте, що код для постачальника послуг надходить із ServiceProviderMixin.

Віджет

Віджет – це частина, яка запитує послугу. Щоб зробити це, він просто запускає подію call_service (зазвичай за допомогою допоміжної функції call). Ця подія виникне й повідомить намір решті системи.

На практиці деякі функції викликаються настільки часто, що ми маємо деякі допоміжні функції, щоб полегшити їх використання. Наприклад, метод _rpc є помічником, який допомагає створити rpc.

var SomeWidget = Widget.extend({
    _getActivityModelViewID: function (model) {
        return this._rpc({
            model: model,
            method: 'get_activity_view_id'
        });
    },
});

Попередження

Якщо віджет буде знищено, він буде від’єднаний від основного дерева компонентів і не матиме батьківського елемента. У такому разі події не розгортатимуться, а значить, робота не буде зроблена. Зазвичай це саме те, чого ми хочемо від знищеного віджета.

RPCs

Функціональність rpc надається службою ajax. Але більшість людей, ймовірно, взаємодіятимуть лише з помічниками _rpc.

Під час роботи на Odoo зазвичай існує два варіанти використання: може знадобитися викликати метод на моделі (python) (це відбувається через контролер call_kw), або може знадобитися напряму викликати контролер (доступний на певному маршруті).

  • Виклик методу на моделі python:

return this._rpc({
    model: 'some.model',
    method: 'some_method',
    args: [some, args],
});
  • Безпосередній виклик контролера:

return this._rpc({
    route: '/some/route/',
    params: { some: kwargs},
});

Повідомлення

Фреймворк Odoo має стандартний спосіб передачі різної інформації користувачеві: сповіщення, які відображаються у верхньому правому куті інтерфейсу користувача.

Є два типи сповіщень:

  • сповіщення: корисно для відображення деяких відгуків. Наприклад, коли користувач скасовує підписку на канал.

  • попередження: корисно для відображення важливої/термінової інформації. Як правило, більшість (виправних) помилок у системі.

Крім того, сповіщення можна використовувати, щоб поставити запитання користувачеві, не порушуючи його робочий процес. Уявіть собі телефонний дзвінок, отриманий через VOIP: липке сповіщення може відображатися з двома кнопками Прийняти та Відхилити.

Система оповіщення

Система сповіщень в Odoo складається з таких компонентів:

  • віджет Повідомлення: це простий віджет, призначений для створення та відображення з потрібною інформацією

  • a NotificationService: служба, яка відповідає за створення та видалення сповіщень щоразу, коли виконується запит (з custom_event). Зауважте, що веб-клієнт є постачальником послуг.

  • дія клієнта display_notification: це дозволяє ініціювати відображення сповіщення від python (наприклад, у методі, який викликається, коли користувач натискає кнопку типу object).

  • two helper functions in ServiceMixin: do_notify and do_warn

Відображення сповіщення

The most common way to display a notification is by using two methods that come from the ServiceMixin:

  • do_notify(title, message, sticky, className):

    Відобразити сповіщення типу повідомлення.

    • title: рядок. Це буде відображено вгорі як заголовок

    • message: string, the content of the notification

    • sticky: boolean, optional. If true, the notification will stay until the user dismisses it. Otherwise, the notification will be automatically closed after a short delay.

    • className: рядок, необов’язково. Це ім’я класу CSS, яке буде автоматично додано до сповіщення. Це може бути корисним для стилізації, хоча його використання не рекомендується.

  • do_warn(title, message, sticky, className):

    Відобразити сповіщення типу попередження.

    • title: рядок. Це буде відображено вгорі як заголовок

    • message: string, the content of the notification

    • sticky: boolean, optional. If true, the notification will stay until the user dismisses it. Otherwise, the notification will be automatically closed after a short delay.

    • className: рядок, необов’язково. Це ім’я класу CSS, яке буде автоматично додано до сповіщення. Це може бути корисним для стилізації, хоча його використання не рекомендується.

Ось два приклади використання цих методів:

// note that we call _t on the text to make sure it is properly translated.
this.do_notify(_t("Success"), _t("Your signature request has been sent."));

this.do_warn(_t("Error"), _t("Filter name is required."));

Ось приклад на python:

# note that we call _(string) on the text to make sure it is properly translated.
def show_notification(self):
    return {
        'type': 'ir.actions.client',
        'tag': 'display_notification',
        'params': {
            'title': _('Success'),
            'message': _('Your signature request has been sent.'),
            'sticky': False,
        }
    }

Системний трей

Systray - це права частина панелі меню в інтерфейсі, де веб-клієнт відображає кілька віджетів, наприклад меню обміну повідомленнями.

Коли меню створює SystrayMenu, воно шукатиме всі зареєстровані віджети та додаватиме їх як допоміжні віджети у належне місце.

Наразі не існує спеціального API для віджетів системної панелі. Передбачається, що вони є простими віджетами та можуть спілкуватися зі своїм середовищем так само, як інші віджети, за допомогою методу trigger_up.

Додавання нового елемента Systray

Немає системного реєстру. Правильний спосіб додати віджет – це додати його до змінної класу SystrayMenu.items.

var SystrayMenu = require('web.SystrayMenu');

var MySystrayWidget = Widget.extend({
    ...
});

SystrayMenu.Items.push(MySystrayWidget);

Виставлення замовлення

Перш ніж додати віджет до себе, системне меню відсортує елементи за властивістю послідовності. Якщо ця властивість відсутня в прототипі, замість неї буде використано 50. Отже, щоб розташувати елемент системної панелі праворуч, можна встановити дуже високий порядковий номер (і навпаки, низький номер, щоб розмістити його ліворуч).

MySystrayWidget.prototype.sequence = 100;

Управління перекладами

Деякі переклади виконуються на стороні сервера (загалом усі текстові рядки відображаються або обробляються сервером), але в статичних файлах є рядки, які потрібно перекласти. Зараз це працює так:

  • кожен перекладний рядок позначається спеціальною функцією _t (доступною в модулі JS web.core

  • ці рядки використовуються сервером для створення відповідних файлів PO

  • кожного разу, коли веб-клієнт завантажується, він викликає маршрут /web/webclient/translations, який повертає список усіх термінів, які можна перекладати

  • під час виконання кожного разу, коли викликається функція _t, вона шукатиме в цьому списку, щоб знайти переклад, і повертатиме його або оригінальний рядок, якщо його не знайдено.

Зауважте, що переклади пояснюються більш детально, з точки зору сервера, у документі Переклад модулів.

Є дві важливі функції для перекладу в javascript: _t і _lt. Різниця полягає в тому, що _lt оцінюється ліниво.

var core = require('web.core');

var _t = core._t;
var _lt = core._lt;

var SomeWidget = Widget.extend({
    exampleString: _lt('this should be translated'),
    ...
    someMethod: function () {
        var str = _t('some text');
        ...
    },
});

У цьому прикладі _lt необхідний, оскільки переклади не готові, коли модуль завантажено.

Зауважте, що функції перекладу потребують певної уваги. Рядок, заданий в аргументі, не має бути динамічним.

Сесія

Існує спеціальний модуль, наданий веб-клієнтом, який містить певну інформацію про поточний сеанс користувача. Деякі помітні клавіші є

  • uid: поточний ID користувача (його ID як res.users)

  • user_name: ім’я користувача у вигляді рядка

  • контекст користувача (ID користувача, мова та часовий пояс)

  • partner_id: ID партнера, пов’язаного з поточним користувачем

  • db: назва бази даних, яка зараз використовується

Додавання інформації до сесії

Коли маршрут /web завантажується, сервер вставляє деяку інформацію про сеанс у тег шаблону сценарію. Інформація буде читатися з методу session_info моделі ir.http. Отже, якщо потрібно додати певну інформацію, це можна зробити, перевизначивши метод session_info та додавши його до словника.

from odoo import models
from odoo.http import request


class IrHttp(models.AbstractModel):
    _inherit = 'ir.http'

    def session_info(self):
        result = super(IrHttp, self).session_info()
        result['some_key'] = get_some_value_from_db()
        return result

Тепер значення можна отримати в javascript, прочитавши його під час сеансу:

var session = require('web.session');
var myValue = session.some_key;
...

Зауважте, що цей механізм призначений для зменшення обсягу зв’язку, необхідного для готовності веб-клієнта. Це більше підходить для даних, обчислення яких є дешевим (повільний виклик session_info призведе до затримки завантаження для веб-клієнта для всіх), а також для даних, які потрібні на початку процесу ініціалізації.

Представлення

Слово „представлення“ має більше ніж одне значення. У цьому розділі йдеться про дизайн коду javascript представлень, а не про структуру arch або щось інше.

У 2017 році Odoo замінив попередній код перегляду новою архітектурою. Головною потребою було відокремити логіку рендерингу від логіки моделі.

Представлення (у загальному сенсі) тепер описуються 4 частинами: представленням, контролером, рендерером і моделлю. API цих 4 частин описано в класах AbstractView, AbstractController, AbstractRenderer і AbstractModel.

View Controller Renderer Model
  • представлення заводське. Його робота полягає в отриманні набору полів, арки, контексту та деяких інших параметрів, а потім у створенні триплету Контролер/Рендерер/Модель.

    Роль представлення полягає в правильному налаштуванні кожної частини шаблону MVC з правильною інформацією. Зазвичай він має обробляти рядок arch та витягувати дані, необхідні для кожної частини представлення.

    Зауважте, що представлення - це клас, а не віджет. Після виконання своєї роботи її можна викинути.

  • Renderer виконує одну роботу: представляє дані, що переглядаються, в елементі DOM. Кожне представлення може відтворювати дані іншим способом. Крім того, він повинен слухати відповідні дії користувача та сповіщати свого батька (Контролер), якщо це необхідно.

    Рендерер - це V у шаблоні MVC.

  • Модель: її робота полягає в отриманні та утриманні стану представлення. Зазвичай він певним чином представляє набір записів у базі даних. Модель є власником „бізнес-даних“. Це M у шаблоні MVC.

  • Контролер: його робота полягає в координації рендерера та моделі. Крім того, це головна точка входу для решти веб-клієнта. Наприклад, коли користувач змінює щось у вікні пошуку, буде викликано метод оновлення контролера з відповідною інформацією.

    Це C у шаблоні MVC.

Примітка

Код JS для представлень розроблено таким чином, щоб його можна було використовувати поза контекстом менеджера представлень/менеджера дій. Їх можна використовувати в дії клієнта або відображати на загальнодоступному веб-сайті (з певною роботою над активами).

Віджети поля

Значна частина досвіду веб-клієнта стосується редагування та створення даних. Більшість цієї роботи виконується за допомогою віджетів полів, які знають тип поля та конкретні деталі того, як значення має відображатися та редагуватися.

AbstractField

Клас AbstractField є базовим класом для всіх віджетів у представленні, для всіх представлень, які їх підтримують (наразі: Форма, Список, Канбан).

Між віджетами поля v11 і попередніми версіями багато відмінностей. Згадаємо найважливіші з них:

  • віджети є спільними для всіх представлень (ну, Форма/Список/Канбан). Більше не потрібно дублювати впровадження. Зауважте, що можна мати спеціалізовану версію віджета для представлення, додавши до нього назву представлення в реєстрі представлення: list.many2one буде вибрано з пріоритетом над many2one.

  • віджети більше не є власниками значення поля. Вони лише представляють дані та взаємодіють з рештою представлення.

  • віджетам більше не потрібно мати можливість перемикатися між режимом редагування та режимом лише для читання. Тепер, коли така зміна буде необхідною, віджет буде знищено та повторно відтворено. Це не проблема, оскільки вони все одно не володіють своєю цінністю

  • віджети полів можна використовувати за межами представлення. Їхній API трохи незручний, але вони створені як автономні.

Декоратори

Як і представлення списку, віджети полів мають просту підтримку декорацій. Метою декорацій є простий спосіб визначення кольору тексту залежно від поточного стану запису. Наприклад,

<field name="state" decoration-danger="amount &lt; 10000"/>

Допустимі назви декоратора:

  • decoration-bf

  • decoration-it

  • decoration-danger

  • decoration-info

  • decoration-muted

  • decoration-primary

  • decoration-success

  • decoration-warning

Кожен декоратор decoration-X буде зіставлен з класом css text-X, який є стандартним початковим класом css (за винятком text-it і text-bf, які обробляються odoo і відповідають курсив і жирний відповідно). Зауважте, що значенням атрибута decoration має бути дійсний вираз Python, який буде оцінюватися разом із записом як контекстом оцінки.

Нереляційні поля

Тут ми документуємо всі нереляційні поля, доступні за замовчуванням, без певного порядку.

integer (FieldInteger)

Це типовий тип поля для полів типу integer.

  • Підтримувані типи полів: integer

    Варіанти:

    • тип: встановлення типу введення (текст за замовчуванням, можна встановити на число)

    У режимі редагування поле відображається як вхід із типом атрибута HTML, встановленим на число (таким чином користувач може скористатися вбудованою підтримкою, особливо на мобільних пристроях). У цьому випадку стандартне форматування вимкнено, щоб уникнути несумісності.

    <field name="int_value" options='{"type": "number"}'/>
    
    • крок: встановіть значення кроку вгору та вниз, коли користувач натискає кнопки (лише для введення номера типу, 1 за замовчуванням)

    <field name="int_value" options='{"type": "number", "step": 100}'/>
    
    • формат: число має бути відформатовано. (істина за замовчуванням)

    За замовчуванням числа форматуються відповідно до параметрів мови.

    Цей параметр запобігає форматуванню значення поля.

    <field name="int_value" options='{"format": false}'/>
    
float (FieldFloat)

Це типовий тип поля для полів типу float.

  • Підтримувані типи полів: float

Атрибути:

  • цифри: відображена точність

<field name="factor" digits="[42,5]"/>

Варіанти:

  • тип: встановлення типу введення (текст за замовчуванням, можна встановити на число)

У режимі редагування поле відображається як вхід із типом атрибута HTML, встановленим на число (таким чином користувач може скористатися вбудованою підтримкою, особливо на мобільних пристроях). У цьому випадку стандартне форматування вимкнено, щоб уникнути несумісності.

<field name="int_value" options='{"type": "number"}'/>
  • крок: встановіть значення кроку вгору та вниз, коли користувач натискає кнопки (лише для введення номера типу, 1 за замовчуванням)

  <field name="int_value" options='{"type": "number", "step": 0.1}'/>

- format: should the number be formatted. (true by default)

By default, numbers are formatted according to locale parameters.
    This option will prevent the field's value from being formatted.

.. code-block:: xml

    <field name="int_value" options='{"format": false}'/>
float_time (FieldFloatTime)

Ціль цього віджета є належне відображення значення float, яке представляє проміжок часу (у годинах). Так, наприклад, 0.5 має бути відформатовано як 0:30, або 4.75 відповідає 4:45.

  • Підтримувані типи полів: float

float_factor (FieldFloatFactor)

Цей віджет має на меті правильно відобразити значення з плаваючою точкою, яке було перетворено з використанням коефіцієнта, указаного в параметрах. Так, наприклад, значення, збережене в базі даних, дорівнює 0,5, а коефіцієнт дорівнює 3, значення віджета має бути відформатовано як 1,5.

  • Підтримувані типи полів: float

float_toggle (FieldFloatToggle)

Метою цього віджета є заміна поля введення кнопкою, яка містить діапазон можливих значень (зазначених у параметрах). Кожен клацання дозволяє користувачеві циклічно переглядати діапазон. Мета тут полягає в тому, щоб обмежити значення поля попередньо визначеним вибором. Крім того, віджет підтримує перетворення факторів як віджет float_factor (значення діапазону мають бути результатом перетворення).

  • Підтримувані типи полів: float

<field name="days_to_close" widget="float_toggle" options='{"factor": 2, "range": [0, 4, 8]}'/>
boolean (FieldBoolean)

Це типовий тип поля для полів типу boolean.

  • Підтримувані типи полів: boolean

char (FieldChar)

Це типовий тип поля для полів типу boolean.

  • Підтримувані типи полів: char

date (FieldDate)

Це тип поля за замовчуванням для полів типу date. Зауважте, що він також працює з полями дати й часу. Він використовує часовий пояс сеансу під час форматування дат.

  • Підтримувані типи полів: date, datetime

Варіанти:

  • datepicker: додаткові налаштування для віджета datepicker.

<field name="datefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
datetime (FieldDateTime)

Це тип поля за замовчуванням для полів типу datetime.

  • Підтримувані типи полів: date, datetime

Варіанти:

  • datepicker: додаткові налаштування для віджета datepicker.

<field name="datetimefield" options='{"datepicker": {"daysOfWeekDisabled": [0, 6]}}'/>
daterange (FieldDateRange)

Цей віджет дозволяє користувачеві вибирати початкову та кінцеву дати в одному інструменті вибору.

  • Підтримувані типи полів: date, datetime

Варіанти:

  • related_start_date: застосувати до поля кінцевої дати, щоб отримати значення дати початку, яке використовується для відображення діапазону в засобі вибору.

  • related_end_date: застосувати до поля початкової дати, щоб отримати значення кінцевої дати, яке використовується для відображення діапазону в засобі вибору.

  • picker_options: додаткові налаштування для вибору.

<field name="start_date" widget="daterange" options='{"related_end_date": "end_date"}'/>
remaining_days (RemainingDays)

Цей віджет можна використовувати для полів дати та часу. У режимі лише для читання відображається різниця (у днях) між значенням поля та сьогоднішнім днем. Віджет призначений для використання з інформаційною метою: тому значення не можна змінити в режимі редагування.

  • Підтримувані типи полів: date, datetime

monetary (FieldMonetary)

Це типовий тип поля для полів типу «monetary». Він використовується для відображення валюти. Якщо в параметрі є поля валюти, він використовуватиме це, інакше повернеться до валюти за замовчуванням (у сеансі)

  • Підтримувані типи полів: monetary, float

Варіанти:

  • currency_field: іншf назва поля, яке має бути many2one у валюті.

<field name="value" widget="monetary" options="{'currency_field': 'currency_id'}"/>
text (FieldText)

Це типовий тип поля для полів типу text.

  • Підтримувані типи полів: text

handle (HandleWidget)

Завдання цього поля має відображатися як handle і дозволяє змінювати порядок різних записів шляхом їх перетягування.

Попередження

Його необхідно вказати в полі, за яким сортуються записи.

Попередження

Наявність кількох полів із handle віджетом в одному списку не підтримується.

  • Підтримувані типи полів: integer

email (FieldEmail)

У цьому полі відображається адреса ел. пошти. Основною причиною його використання є те, що він відображається як тег прив’язки з правильним href у режимі лише для читання.

  • Підтримувані типи полів: char

phone (FieldPhone)

У цьому полі відображається номер телефону. Основною причиною його використання є те, що він відображається як тег прив’язки з правильним href у режимі лише для читання, але лише в деяких випадках: ми хочемо зробити його доступним для натискання, лише якщо пристрій може викликати цей конкретний номер.

  • Підтримувані типи полів: char

url (UrlWidget)

У цьому полі відображається URL-адреса (у режимі лише для читання). Основною причиною його використання є те, що він відображається як тег прив’язки з відповідними класами css і href.

Крім того, текст тегу прив’язки можна налаштувати за допомогою атрибута text (це не змінить значення href).

  <field name="foo" widget="url" text="Some URL"/>

Options:

- website_path: (default:false) by default, the widget forces (if not already
  the case) the href value to begin with http:// except if this option is set
  to true, thus allowing redirections to the database's own website.

- Supported field types: *char*
domain (FieldDomain)

Поле «Домен» дозволяє користувачеві створити домен із технічним префіксом завдяки деревоподібному інтерфейсу та переглядати вибрані записи в реальному часі. У режимі налагодження також є вхід, щоб мати можливість безпосередньо вводити домен префікса char (або створювати розширені домени, які деревоподібний інтерфейс не дозволяє).

Зауважте, що це обмежено „статичним“ доменом (без динамічного виразу чи доступу до контекстної змінної).

  • Підтримувані типи полів: char

link_button (LinkButton)

Віджет LinkButton фактично просто відображає проміжок із піктограмою та текстовим значенням як вміст. Посилання можна натиснути, і воно відкриє нове вікно браузера зі значенням url.

  • Підтримувані типи полів: char

image (FieldBinaryImage)

Цей віджет використовується для представлення двійкового значення у вигляді зображення. У деяких випадках сервер повертає „bin_size“ замість реального зображення (bin_size - це рядок, який представляє розмір файлу, наприклад 6.5 Кб). У цьому випадку віджет створить зображення з атрибутом джерела, що відповідає зображенню на сервері.

  • Підтримувані типи полів: двійковий

Варіанти:

  • preview_image: якщо зображення завантажується лише як „bin_size“, тоді цей параметр корисний, щоб повідомити веб-клієнту, що назва поля за замовчуванням - це не назва поточного поля, а назва іншого поля.

  • accepted_file_extensions: розширення файлу, яке користувач може вибрати з діалогового вікна введення файлу (значення за замовчуванням – image/*) (cf: атрибут accept на <input type=»file»/>)

<field name="image" widget='image' options='{"preview_image":"image_128"}'/>
binary (FieldBinaryFile)

Загальний віджет, який дозволяє зберігати/завантажувати двійковий файл.

  • Підтримувані типи полів: двійковий

Варіанти:

  • accepted_file_extensions: розширення файлу, яке користувач може вибрати з діалогового вікна введення файлу (пор.: атрибут accept на <input type=»file»/>)

Атрибут:

  • назва файлу: збереження двійкового файлу призведе до втрати назви файлу, оскільки зберігається лише двійкове значення. Назву файлу можна зберегти в іншому полі. Для цього назва файлу атрибута має бути встановлено для поля, наявного у представленні.

<field name="datas" filename="datas_fname"/>
priority (PriorityWidget)

Цей віджет відображається як набір зірочок, що дозволяє користувачеві натиснути на нього, щоб вибрати значення чи ні. Це корисно, наприклад, щоб позначити завдання як високопріоритетне.

Зауважте, що цей віджет також працює в режимі „readonly“, що є незвичним.

  • Підтримувані типи полів: selection

attachment_image (AttachmentImage)

Віджет зображення для полів many2one. Якщо це поле встановлено, цей віджет відображатиметься як зображення з правильною URL-адресою src. Цей віджет не має іншої поведінки в режимі редагування чи лише для читання, він корисний лише для перегляду зображення.

  • Підтримувані типи полів: many2one

<field name="displayed_image_id" widget="attachment_image"/>
image_selection (ImageSelection)

Дозвольте користувачеві вибрати значення, натиснувши на зображення.

  • Підтримувані типи полів: selection

Параметри: словник із відображенням значення вибору на об’єкт із URL-адресою зображення (image_link) і зображення попереднього перегляду (preview_link).

Зауважте, що ця опція необов’язкова!

<field name="external_report_layout" widget="image_selection" options="{
    'background': {
        'image_link': '/base/static/img/preview_background.png',
        'preview_link': '/base/static/pdf/preview_background.pdf'
    },
    'standard': {
        'image_link': '/base/static/img/preview_standard.png',
        'preview_link': '/base/static/pdf/preview_standard.pdf'
    }
}"/>
label_selection (LabelSelection)

Цей віджет відображає просту мітку, яку не можна редагувати. Це корисно лише для відображення деякої інформації, а не для її редагування.

  • Підтримувані типи полів: selection

Варіанти:

  • класи: зіставлення значення вибору з класом css

<field name="state" widget="label_selection" options="{
    'classes': {'draft': 'default', 'cancel': 'default', 'none': 'danger'}
}"/>
state_selection (StateSelectionWidget)

Це спеціалізований віджет вибору. Припускається, що запис має деякі жорстко закодовані поля, присутні в представленні: stage_id, legend_normal, legend_blocked, legend_done. Це здебільшого використовується для відображення та зміни стану завдання в проекті з додатковою інформацією, що відображається у спадному списку.

  • Підтримувані типи полів: selection

<field name="kanban_state" widget="state_selection"/>
kanban_state_selection (StateSelectionWidget)

Це точно такий же віджет, як і state_selection

  • Підтримувані типи полів: selection

boolean_favorite (FavoriteWidget)

Цей віджет відображається як порожня (чи ні) зірочка, залежно від логічного значення. Зверніть увагу, що його також можна редагувати в режимі лише для читання.

  • Підтримувані типи полів: boolean

boolean_button (FieldBooleanButton)

Віджет Boolean Button призначений для використання в кнопці статистики у представленні форми. Мета полягає в тому, щоб відобразити гарну кнопку з поточним станом логічного поля (наприклад, „Активний“) і дозволити користувачеві змінити це поле, натиснувши на нього.

Зверніть увагу, що його також можна редагувати в режимі лише для читання.

  • Підтримувані типи полів: boolean

Варіанти:

  • термінологія: це може бути „активний“, „архівний“, „закрити“ або спеціальне зіставлення з ключами string_true, string_false, hover_true, hover_false

<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
boolean_toggle (BooleanToggle)

Відображає перемикач для представлення логічного значення. Це підполе FieldBoolean, яке зазвичай використовується для іншого вигляду.

statinfo (StatInfo)

Цей віджет призначений для представлення статистичної інформації в кнопці статистики. По суті, це просто етикетка з номером.

  • Підтримувані типи полів: integer, float

Варіанти:

  • label_field: якщо задано, віджет використовуватиме значення label_field як текст.

<button name="%(act_payslip_lines)d"
    icon="fa-money"
    type="action">
    <field name="payslip_count" widget="statinfo"
        string="Payslip"
        options="{'label_field': 'label_tasks'}"/>
</button>
percentpie (FieldPercentPie)

Цей віджет призначений для представлення статистичної інформації в кнопці статистики. Це схоже на віджет statinfo, але інформація представлена у pie діаграмі (від порожнього до повного). Зауважте, що значення інтерпретується як відсоток (число від 0 до 100).

  • Підтримувані типи полів: integer, float

<field name="replied_ratio" string="Replied" widget="percentpie"/>
progressbar (FieldProgressBar)

Представлення значення у вигляді індикатора виконання (від 0 до деякого значення)

  • Підтримувані типи полів: integer, float

Варіанти:

  • редагований: логічне значення, якщо значення можна редагувати

  • current_value: отримати pieз поля, яке повинно бути присутнім у представленні

  • max_value: отримати max_value з поля, яке має бути присутнім у представленні

  • edit_max_value: логічне значення, якщо max_value можна редагувати

  • title: заголовок панелі, відображається поверх панелі –> не перекладається, замість цього використовуйте параметр (не опцію) «title»

<field name="absence_of_today" widget="progressbar"
    options="{'current_value': 'absence_of_today', 'max_value': 'total_employee', 'editable': false}"/>
toggle_button (FieldToggleBoolean)

Цей віджет призначений для використання в логічних полях. Він перемикає кнопку між зеленим і сірим маркерами. Він також налаштував спливаючу підказку залежно від значення та деяких опцій.

  • Підтримувані типи полів: boolean

Варіанти:

  • active: рядок для спливаючої підказки, яка має бути встановлена, коли логічне значення має значення true

  • неактивний: спливаюча підказка, яку слід встановити, коли логічний параметр має значення false

<field name="payslip_status" widget="toggle_button"
    options='{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}'
/>
dashboard_graph (JournalDashboardGraph)

Це більш спеціалізований віджет, корисний для відображення графіка, що представляє набір даних. Наприклад, він використовується в режимі Канбан панелі бухгалтерського обліку.

Припускається, що поле є serialization JSON набору даних.

  • Підтримувані типи полів: char

Атрибут

  • graph_type: рядок, може бути „line“ або „bar“

<field name="dashboard_graph_data"
    widget="dashboard_graph"
    graph_type="line"/>
ace (AceEditor)

Цей віджет призначений для використання в текстових полях. Він надає Ace Editor для редагування XML і Python.

  • Підтримувані типи полів: char, text

  • badge (FieldBadge)

    Відображає значення всередині bootstrap badge pill.

    • Підтримувані типи полів: char, selection, many2one

    За замовчуванням значок має світло-сірий фон, але його можна налаштувати за допомогою механізму decoration-X. Наприклад, щоб відобразити червоний значок за певної умови:

    <field name="foo" widget="badge" decoration-danger="state == 'cancel'"/>
    

Реляційні поля

class web.relational_fields.FieldSelection()

Підтримувані типи полів: selection

web.relational_fields.FieldSelection.placeholder

рядок, який використовується для відображення певної інформації, коли значення не вибрано

<field name="tax_id" widget="selection" placeholder="Select a tax"/>
radio (FieldRadio)

Це підполе FielSelection, але спеціалізоване для відображення всіх дійсних варіантів у вигляді перемикачів.

Зауважте, що якщо використовується для записів many2one, для отримання name_gets пов’язаних записів буде виконано більше rpc.

  • Підтримувані типи полів: selection, many2one

Варіанти:

  • horizontal: якщо встановлено true, перемикачі відображатимуться горизонтально.

<field name="recommended_activity_type_id" widget="radio"
    options="{'horizontal':true}"/>
selection_badge (FieldSelectionBadge)

Це підполе FieldSelection, але спеціалізоване для відображення всіх дійсних варіантів у вигляді прямокутних значків.

  • Підтримувані типи полів: selection, many2one

<field name="recommended_activity_type_id" widget="selection_badge"/>
many2one (FieldMany2One)

Віджет за замовчуванням для полів many2one.

  • Підтримувані типи полів: many2one

Атрибути:

  • can_create: дозволити створення пов’язаних записів (має пріоритет над параметром no_create)

  • can_write: дозволити редагування пов’язаних записів (за замовчуванням: true)

Варіанти:

  • no_create: prevent the creation of related records

  • quick_create: дозволити швидке створення пов’язаних записів (за замовчуванням: true)

  • no_quick_create: prevent the quick creation of related records (don’t ask me)

  • no_create_edit: same as no_create, maybe…

  • create_name_field: під час створення пов’язаного запису, якщо цей параметр установлено, значення create_name_field буде заповнено значенням введення (за замовчуванням: name)

  • always_reload: логічне значення, за замовчуванням значення false. Якщо значення true, віджет завжди виконуватиме додатковий name_get, щоб отримати значення свого імені. Це використовується для ситуацій, коли метод name_get перевизначено (будь ласка, не робіть цього)

  • no_open: логічне значення, за замовчуванням значення false. Якщо встановлено значення true, many2one не переспрямовуватиме запис під час натискання на нього (у режимі лише для читання)

<field name="currency_id" options="{'no_create': True, 'no_open': True}"/>
list.many2one (ListFieldMany2One)

Віджет за замовчуванням для полів many2one (у представленні списку).

Спеціалізація many2one поля для представлення списку. Основна причина полягає в тому, що нам потрібно відобразити поля many2one (у режимі лише для читання) як текст, який не дозволяє відкривати відповідні записи.

  • Підтримувані типи полів: many2one

many2one_barcode (FieldMany2OneBarcode)

Віджет для полів many2one дозволяє відкрити камеру з мобільного пристрою (Android/iOS) для сканування штрих-коду.

Спеціалізація поля many2one, де користувачеві дозволено використовувати рідну камеру для сканування штрих-коду. Потім він використовує name_search для пошуку цього значення.

Якщо цей віджет встановлено, а користувач не використовує мобільний додаток, він повернеться до звичайного many2one (FieldMany2One)

  • Підтримувані типи полів: many2one

many2one_avatar (Many2OneAvatar)

Цей віджет підтримується лише для багатьох полів, які вказують на модель, яка успадковує „image.mixin“. У режимі „лише для читання“ він відображає зображення пов’язаного запису поруч із його display_name. Зауважте, що display_name не є посиланням, яке можна натиснути в цьому випадку. У редагуванні він поводиться так само, як звичайний many2one.

  • Підтримувані типи полів: many2one

many2one_avatar_user (Many2OneAvatarUser)

Цей віджет є спеціалізацією Many2OneAvatar. При натисканні на аватар ми відкриваємо вікно чату з відповідним користувачем. Цей віджет можна встановити лише для полів many2one, які вказують на модель „res.users“.

  • Підтримувані типи полів: many2one (вказує на „res.users“)

many2one_avatar_employee (Many2OneAvatarEmployee)

Те саме, що Many2OneAvatarUser, але для many2one поля вказують на „hr.employee“.

  • Підтримувані типи полів: many2one (вказує на „hr.employee“)

kanban.many2one (KanbanFieldMany2One)

Віджет за умовчанням для полів many2one (у представленні канбан). Нам потрібно вимкнути будь-яке редагування у представленнях канбан.

  • Підтримувані типи полів: many2one

many2many (FieldMany2Many)

Віджет за замовчуванням для полів many2one.

  • Підтримувані типи полів: many2one

Атрибути:

  • режим: рядок, представлення за замовчуванням для відображення

  • домен: обмежити дані певним доменом

Варіанти:

  • create_text: дозволяє налаштувати текст, який відображається під час додавання нового запису

  • посилання: домен, який визначає, чи можна додавати записи до відношення (за замовчуванням: True).

  • unlink: домен, який визначає, чи можна видаляти записи з відношення (за замовчуванням: True).

many2many_binary (FieldMany2ManyBinaryMultiFiles)

Цей віджет допомагає користувачеві завантажувати або видаляти один або кілька файлів одночасно.

Зауважте, що цей віджет є специфічним для моделі „ir.attachment“.

  • Підтримувані типи полів: many2one

Варіанти:

  • accepted_file_extensions: розширення файлу, яке користувач може вибрати з діалогового вікна введення файлу (пор.: атрибут accept на <input type=»file»/>)

many2many_tags (FieldMany2ManyTags)

Відображати many2many як список тегів.

  • Підтримувані типи полів: many2one

Варіанти:

  • create: домен, який визначає, чи можна створювати нові теги (за замовчуванням: True).

<field name="category_id" widget="many2many_tags" options="{'create': [['some_other_field', '>', 24]]}"/>
  • color_field: назва числового поля, яке повинно бути присутнім у представленні. Колір вибирається залежно від його значення.

<field name="category_id" widget="many2many_tags" options="{'color_field': 'color'}"/>
  • no_edit_color: встановіть значення True, щоб усунути можливість змінювати колір тегів (за замовчуванням: False).

<field name="category_id" widget="many2many_tags" options="{'color_field': 'color', 'no_edit_color': True}"/>
form.many2many_tags (FormFieldMany2ManyTags)

Спеціалізація віджета many2many_tags для представлень форм. У ньому є додатковий код, який дозволяє редагувати колір тегу.

  • Підтримувані типи полів: many2one

kanban.many2many_tags (KanbanFieldMany2ManyTags)

Спеціалізація віджета many2many_tags для представлень канбану.

  • Підтримувані типи полів: many2one

many2many_checkboxes (FieldMany2ManyCheckBoxes)

Це поле відображає список прапорців і дозволяє користувачеві вибрати підмножину варіантів. Зауважте, що кількість відображених значень обмежена 100. Це обмеження не можна налаштувати. Це просто дозволяє обробляти надзвичайні випадки, коли цей віджет неправильно встановлено на полі з величезним комоделем. У таких випадках перегляд списку є більш доцільним, оскільки він дозволяє розбивати на сторінки та фільтрувати.

  • Підтримувані типи полів: many2one

one2many (FieldOne2Many)

Віджет за замовчуванням для полів many2one.

Зазвичай він відображає дані у вигляді підсписку або у представленні підканбану.

  • Підтримувані типи полів: one2many

Варіанти:

  • create: домен, який визначає, чи можна створювати пов’язані записи (за замовчуванням: True).

  • create: домен, який визначає, чи можна створювати пов’язані записи (за замовчуванням: True).

<field name="turtles" options="{'create': [['some_other_field', '>', 24]]}"/>
  • create_text: рядок, який використовується для налаштування мітки/тексту „Додати“.

<field name="turtles" options="{\'create_text\': \'Add turtle\'}">
statusbar (FieldStatus)

Це справді спеціалізований віджет для представлень форм. Це панель на вершині багатьох форм, які представляють потік і дозволяють вибрати певний стан.

  • Підтримувані типи полів: selection, many2one

reference (FieldReference)

FieldReference - це комбінація select (для моделі) і FieldMany2One (для її значення). Дозволяє вибрати запис на довільній моделі.

  • Підтримувані типи полів: char, reference

Дії клієнта

The idea of a client action is a customized widget that is integrated in the web client interface, just like a act_window_action. This is useful when you need a component that is not closely linked to an existing view or a specific model. For example, the Discuss application is actually a client action.

Клієнтська дія – це термін, який має різні значення залежно від контексту:

  • з точки зору сервера, це запис моделі ir_action з полем tag типу char

  • from the perspective of the web client, it is a widget, which inherit from the class AbstractAction, and is supposed to be registered in the action registry under the corresponding key (from the field char)

Whenever a menu item is associated to a client action, opening it will simply fetch the action definition from the server, then lookup into its action registry to get the Widget definition at the appropriate key, and finally, it will instantiate and append the widget to the proper place in the DOM.

Додавання дії клієнта

A client action is a widget which will control the part of the screen below the menu bar. It can have a control panel, if necessary. Defining a client action can be done in two steps: implementing a new widget, and registering the widget in the action registry.

Реалізація нової клієнтської дії.

Це робиться шляхом створення віджета:

var AbstractAction = require('web.AbstractAction');

var ClientAction = AbstractAction.extend({
    hasControlPanel: true,
    ...
});
Реєстрація дії клієнта:

Як завжди, нам потрібно повідомити веб-клієнту про відображення між діями клієнта та фактичним класом:

var core = require('web.core');

core.action_registry.add('my-custom-action', ClientAction);

Потім, щоб використовувати дію клієнта у веб-клієнті, нам потрібно створити запис дії клієнта (запис моделі ir.actions.client) з відповідним атрибутом tag:

<record id="my_client_action" model="ir.actions.client">
    <field name="name">Some Name</field>
    <field name="tag">my-custom-action</field>
</record>

За допомогою панелі керування

By default, the client action does not display a control panel. In order to do that, several steps should be done.

  • Встановіть hasControlPanel на true. У коді віджета:

    var MyClientAction = AbstractAction.extend({
        hasControlPanel: true,
        loadControlPanel: true, // default: false
        ...
    });
    

    Попередження

    when the loadControlPanel is set to true, the client action will automatically get the content of a search view or a control panel view. In this case, a model name should be specified like this:

    init: function (parent, action, options) {
        ...
        this.controlPanelParams.modelName = 'model.name';
        ...
    }
    
  • Викликайте метод updateControlPanel щоразу, коли нам потрібно оновити панель керування. Наприклад:

    var SomeClientAction = Widget.extend({
        hasControlPanel: true,
        ...
        start: function () {
            this._renderButtons();
            this._update_control_panel();
            ...
        },
        do_show: function () {
             ...
             this._update_control_panel();
        },
        _renderButtons: function () {
            this.$buttons = $(QWeb.render('SomeTemplate.Buttons'));
            this.$buttons.on('click', ...);
        },
        _update_control_panel: function () {
            this.updateControlPanel({
                cp_content: {
                   $buttons: this.$buttons,
                },
            });
        }
    

The updateControlPanel is the main method to customize the content in controlpanel. For more information, look into the control_panel_renderer.js file.