Предупреждение
Это руководство требует установленную Odoo
Запуск и остановка сервера Odoo
Odoo использует клиент-серверную архитектуру, где клиентами являются web браузеры, которые работают с Odoo сервером через RPC протокол.
Бизнес-логика и расширение обычно выполняются на стороне сервера, хотя к клиенту могут быть добавлены поддерживающие клиентские функции (например, новые представления данных, такие как интерактивные карты).
Чтобы запустить сервер, просто вызовите команду odoo.py в командной строке, добавив при необходимости полный путь к файлу:
odoo.py
Сервер можно остановить двойным нажатием Ctrl-C
в консоли, или убив соответствующий процесс в операционной системе.
Создание модуля в Odoo
Расширения как сервера, так и клиента представляют собой модули, которые могут быть загружены в базу данных.
Odoo модули могут, как добавлять абсолютно новую бизнес-логику, так и расширять существующую: модуль может быть создан для того, чтобы добавить план счетов соответствующей страны таким образом, чтобы Odoo мог поддерживать именно ваш тип бухгалтерского учета. В тоже время, другой модуль добавляет поддержку отображения состояния вашего автобусного парка в режиме реального времени.
Таким образом, все в Odoo начинается и заканчивается модулями.
Состав модуля
Модуль Odoo может содержать следующие элементы:
- Бизнес-объекты:
объявленные как Python классы, эти ресурсы автоматически сохраняются в Odoo в соответствии с ее конфигурацией
- Данные
XML или CSV файлы объявляют метаданные (представления или бизнес-процессы), данные конфигурации (параметризация модулей), демонстрацию данных и многое другое
- Веб-контроллеры
Обрабатывать запросы от веб-браузеров
- Статические ресурсы
Изображения, CSS или javascript-файлы, используемые веб-интерфейсом или веб-сайтом
Структура модуля
Каждый модуль представляет собой каталог с файлами и находится в Каталоге для модулей. Путь к Каталогам для модулей назначается при помощи команды --addons-path
.
Совет
большинство параметров командной строки так же могут быть указаны в файле конфигурации
Содержимое Odoo модуля описывается в его манифесте. Подробное описание манифеста находится здесь.
Модуль так же является Python пакетом с файлом __init__.py
, содержащим инструкции импорта для различных Python файлов в модуле.
Например, если в модуле есть только mymodule.py
, тогда файл __init__.py
должен содержать:
from . import mymodule
Odoo предоставляет механизм, который помогает установить новый модуль, odoo.py имеет команду scaffold, которая может создать пустой модуль:
$ odoo.py scaffold <module name> <where to put it>
Команда создаст подкаталог для вашего модуля, и автоматически создаст группу стандартных файлов для модуля. Большинство из них содержит закомментированный Python код или XML. Об использование большинства этих файлов будет рассказано в данном руководстве.
Exercise
Создание модуля
Используйте командную строку, чтобы создать пустой модуль Open Academy и установить его в Odoo.
Вызовите команду
odoo.py scaffold openacademy addons
.Адаптируйте файл манифеста к вашему модулю.
Не беспокойтесь о содержании других файлов.
# -*- coding: utf-8 -*-
{
'name': "Open Academy",
'summary': """Manage trainings""",
'description': """
Open Academy module for managing trainings:
- training courses
- training sessions
- attendees registration
""",
'author': "Your Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/master/openerp/addons/base/module/module_data.xml
# for the full list
'category': 'Test',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base'],
# always loaded
'data': [
# 'security/ir.model.access.csv',
'templates.xml',
],
# only loaded in demonstration mode
'demo': [
'demo.xml',
],
}
# -*- coding: utf-8 -*-
from . import controllers
from . import models
# -*- coding: utf-8 -*-
from openerp import http
# class Openacademy(http.Controller):
# @http.route('/openacademy/openacademy/', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('/openacademy/openacademy/objects/', auth='public')
# def list(self, **kw):
# return http.request.render('openacademy.listing', {
# 'root': '/openacademy/openacademy',
# 'objects': http.request.env['openacademy.openacademy'].search([]),
# })
# @http.route('/openacademy/openacademy/objects/<model("openacademy.openacademy"):obj>/', auth='public')
# def object(self, obj, **kw):
# return http.request.render('openacademy.object', {
# 'object': obj
# })
<openerp>
<data>
<!-- -->
<!-- <record id="object0" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 0</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object1" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 1</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object2" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 2</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object3" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 3</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object4" model="openacademy.openacademy"> -->
<!-- <field name="name">Object 4</field> -->
<!-- </record> -->
<!-- -->
</data>
</openerp>
# -*- coding: utf-8 -*-
from openerp import models, fields, api
# class openacademy(models.Model):
# _name = 'openacademy.openacademy'
# name = fields.Char()
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_openacademy_openacademy,openacademy.openacademy,model_openacademy_openacademy,,1,0,0,0
<openerp>
<data>
<!-- <template id="listing"> -->
<!-- <ul> -->
<!-- <li t-foreach="objects" t-as="object"> -->
<!-- <a t-attf-href="{{ root }}/objects/{{ object.id }}"> -->
<!-- <t t-esc="object.display_name"/> -->
<!-- </a> -->
<!-- </li> -->
<!-- </ul> -->
<!-- </template> -->
<!-- <template id="object"> -->
<!-- <h1><t t-esc="object.display_name"/></h1> -->
<!-- <dl> -->
<!-- <t t-foreach="object._fields" t-as="field"> -->
<!-- <dt><t t-esc="field"/></dt> -->
<!-- <dd><t t-esc="object[field]"/></dd> -->
<!-- </t> -->
<!-- </dl> -->
<!-- </template> -->
</data>
</openerp>
Объектно-реляционное отображение
Ключевой компонент Odoo это ORM. Этот слой избавляет от необходимости писать самому SQL запросы руками и предоставляет гибкие и безопасные сервисы 2.
Бизнес-объекты объявляются как расширяемые классы Python Model
, которая автоматически интегрирует их в систему хранения данных.
Модели данных могут быть сконфигурированы настройкой некоторого количества атрибутов в их описании. Наиболее важный атрибут это _name
. Он необходим и определяет название модели данных в системе Odoo. Вот пример минимального описания модели данных:
from openerp import models
class MinimalModel(models.Model):
_name = 'test.model'
Поля модели данных
Поля используются для определения того, что модель данных может хранить и где. Поля определяются как атрибуты в классе модели:
from openerp import models, fields
class LessMinimalModel(models.Model):
_name = 'test.model2'
name = fields.Char()
Базовые атрибуты
Подобно самой модели данных, ее поля могут быть настроены путем передачи значений атрибутов в качестве параметров:
name = field.Char(required=True)
Некоторые атрибуты доступны во всех полях, вот самые распространенные из них:
string
(unicode
, по умолчанию: field's name)Надпись для поля в UI (пользовательском интерфейсе).
required
(bool
, по умолчанию:False
)Если
True
, поле не может быть пустым оно должно иметь значение по умолчанию или всегда быть заполнено при создании записи.help
(unicode
, по умолчанию:''
)Портянка, показывает всплывающее поле с описанием объекта для пользователей в UI.
index
(bool
, по умолчанию:False
)Просит у Odoo создать database index для колонки
Простые поля
Есть два наиболее распространенных вида полей: «простые» поля, которые представляют собой атомарные значения, хранящиеся непосредственно в таблице модели, и «реляционные» поля, связывающие записи (одной модели данных или разных моделей данных).
Зарезервированные поля
Odoo создает несколько полей, которые есть во всех моделях данных1. Эти поля управляются системой и их не нужно прописывать, но их можно прочитать если существует такая необходимость:
Специальные поля
По умолчанию Odoo так же требует поле name
для всех моделей данных для различных типов отображения и поиска. Поле, используемое для этих целей может быть отменено _rec_name
.
Exercise
Определить модель данных
Определите новую модель данных в модуле Course в модуле openacademy. Курс имеет небольшое описание. Курсы должны иметь названия.
Отредактируйте файл openacademy/models.py
для того, чтобы включить класс Course.
from openerp import models, fields, api
class Course(models.Model):
_name = 'openacademy.course'
name = fields.Char(string="Title", required=True)
description = fields.Text()
Данные
Odoo - система со сверхточным механизмом управления данными. Несмотря на то что ее поведение управляется при помощи кода Python, часть значений модуля находится в данных, которые он устанавливает при загрузке.
Совет
Существуют модули, которые существуют только для добавления данных в Odoo.
Данные модуля описываются через data files, XML файлы с элементами <record>
. Каждый элемент <record>
создает или модифицирует записи в базе данных.
<openerp>
<data>
<record model="{model name}" id="{record identifier}">
<field name="{a field name}">{a value}</field>
</record>
</data>
<openerp>
model
это имя модели данных Odoo записиid
это external identifier, который позволяет видеть к какой записи эти данные относятся (без необходимости знать их идентификаторы в базе данных)<field>
элементы имеют параметрname
который является именем поля в модели данных (напримерdescription
). Тело элемента и есть значение поля.
Файлы данных должны быть описаны в файле манифеста, для того, чтобы они были загружены их нужно записать в 'data'
список (загрузка данных происходит всегда) или в 'demo'
список (загрузка данных происходит только в режиме "Загрузка демо-данных").
Exercise
Описать демонстрационные данные
Создать демонстрационные данные для заполнения модели данных Courses некоторым количеством Курсов.
Отредактируйте файл openacademy/demo.xml
и вставьте необходимые данные.
<openerp>
<data>
<record model="openacademy.course" id="course0">
<field name="name">Course 0</field>
<field name="description">Course 0's description
Can have multiple lines
</field>
</record>
<record model="openacademy.course" id="course1">
<field name="name">Course 1</field>
<!-- no description for this one -->
</record>
<record model="openacademy.course" id="course2">
<field name="name">Course 2</field>
<field name="description">Course 2's description</field>
</record>
</data>
</openerp>
Базовые представления данных
Базовые представления данных (далее представления). Представления определяют каким образом записи или модель данных будут отображены. Каждый тип представлений являет собой некий заранее оформленный внешний вид для данных, которые находятся в базе данных Odoo (например в виде списка записей или календаря, и т.д. …). Представления могут быть запрошены через вызов их типа (например список контрагентов) или напрямую через их идентификатор. Для общих запросов будет использовано представление с правильным типом и низким приоритетом (таким образом, представление с наименьшим приоритетом каждого типа, будет являться им по умолчанию
Наследование представлений позволяет изменять представления, объявленные в другом месте (добавлять или удалять содержимое).
Стандартное использование представления
Представление создается как запись модели данных `` ir.ui.view``. Тип преставления назначается корневым элементом поля arch
:
<record model="ir.ui.view" id="view_id">
<field name="name">view.name</field>
<field name="model">object_name</field>
<field name="priority" eval="16"/>
<field name="arch" type="xml">
<!-- view content: <form>, <tree>, <graph>, ... -->
</field>
</record>
Опасно
Содержимое представления это XML.
Таким образом, поле `` arch`` должно быть объявлено как type = "xml "
,для того, чтобы его правильно распознал парсер.
Преставления в виде Tree
Преставления в виде Tree, так же называются преставлениями в виде списков, отражают записи в табличной форме
Их корневой элемент <tree>
. Самая простая форма преставления в виде tree это список всех полей таблицы, которые мы хотим показать на экране (каждое поле это колонка):
<tree string="Idea list">
<field name="name"/>
<field name="inventor_id"/>
</tree>
Преставления в виде Form
Преставления в виде Form используются для создания и модификации единичных записей.
Корневым элементом является <form>
, который состоит из высокоуровневых элементов (группы, поля для записей) и интерактивных элементов (кнопки и поля):
<form string="Idea form">
<group colspan="4">
<group colspan="2" col="2">
<separator string="General stuff" colspan="2"/>
<field name="name"/>
<field name="inventor_id"/>
</group>
<group colspan="2" col="2">
<separator string="Dates" colspan="2"/>
<field name="active"/>
<field name="invent_date" readonly="1"/>
</group>
<notebook colspan="4">
<page string="Description">
<field name="description" nolabel="1"/>
</page>
</notebook>
<field name="state"/>
</group>
</form>
Exercise
Настройте преставление в виде Form используя XML.
Создайте ваше собственное преставление в виде Form для объекта Course. Что нужно показать: имя и описание курса.
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="course_form_view">
<field name="name">course.form</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<form string="Course Form">
<sheet>
<group>
<field name="name"/>
<field name="description"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
Exercise
Notebooks
В преставлении в виде Form для объекта Курс расположите поле для описания под закладкой, таким образом, что в дальнейшем можно будет добавить еще закладки с дополнительной информацией.
Внесите следующие изменения в преставление в виде Form для объекта Курс:
<sheet>
<group>
<field name="name"/>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="About">
This is an example of notebooks
</page>
</notebook>
</sheet>
</form>
</field>
Представления формы могут также использовать простой HTML для создания своих макетов:
<form string="Idea Form">
<header>
<button string="Confirm" type="object" name="action_confirm"
states="draft" class="oe_highlight" />
<button string="Mark as done" type="object" name="action_done"
states="confirmed" class="oe_highlight"/>
<button string="Reset to draft" type="object" name="action_draft"
states="confirmed,done" />
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only" string="Idea Name" />
<h1><field name="name" /></h1>
</div>
<separator string="General" colspan="2" />
<group colspan="2" col="2">
<field name="description" placeholder="Idea description..." />
</group>
</sheet>
</form>
Преставления в виде Search
Преставления в виде Search настраивают поля search для связи с преставлением в виде списка (а так же другими совокупными представлениями). Их корневой элемент <search>
и он состоит из полей определяющих какие поля могут участвовать в поиске:
<search>
<field name="name"/>
<field name="inventor_id"/>
</search>
Если для модели данных не существует поискового представления, Odoo генерирует одно, которое позволяет только поиск в поле `` name``.
Exercise
Поиск курсов
Позволить поиск по курсам, основанный на их названии или их описании.
</field>
</record>
<record model="ir.ui.view" id="course_search_view">
<field name="name">course.search</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="description"/>
</search>
</field>
</record>
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
Отношения между моделями данных
Запись из одной модели данных может быть связана с записью в другой модели данных. Например запись коммерческого предложения связана с записью клиента, которая содержит данные о клиента; и она (запись клиента) так же связана с записями коммерческого предложения.
Exercise
Создайте модель данных для занятий
Для модуля Open Academy, мы рассматриваем модель данных sessions: Занятия это возникновение преподаваемого Отношения между моделями данных курса в заданное время для заданной аудитории.
Создайте модель данных для sessions. Занятия имеют имя, дату начала, продолжительность и количество мест. Добавьте действие и пункт меню, чтобы отобразить их. Сделайте новую модель видимой через пункт меню.
Создайте Python класс Session в файле
openacademy/models.py
.Добавьте доступ к объекту в файле
openacademy/view/openacademy.xml
.
name = fields.Char(string="Title", required=True)
description = fields.Text()
class Session(models.Model):
_name = 'openacademy.session'
name = fields.Char(required=True)
start_date = fields.Date()
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
<!-- Full id location:
action="openacademy.course_list_action"
It is not required when it is the same module -->
<!-- session form view -->
<record model="ir.ui.view" id="session_form_view">
<field name="name">session.form</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<form string="Session Form">
<sheet>
<group>
<field name="name"/>
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="session_menu" name="Sessions"
parent="openacademy_menu"
action="session_list_action"/>
</data>
</openerp>
Примечание
digits=(6, 2)
определяет точность числа с плавающей запятой: 6 - это общее количество цифр, а 2 - количество цифр после запятой. Обратите внимание, что в результате число цифр перед запятой максимально
Реляционные поля
Реляционные поля связывают записи, как внутри самой модели данных (иерархические), так и между записями в разных моделях данных.
Типы реляционных полей:
Many2one(other_model, ondelete='set null')
Простая ссылка на другой объект:
print foo.other_id.name
См.также
One2many(other_model, related_field)
Виртуальная связь обратная
Many2one
. ЭтоOne2many
ведет себя как контейнер записей и предоставляет доступ (возможно пустой) к набору записей:for other in foo.other_ids: print other.name
Many2many(other_model)
Двунаправленная множественная связь, любая запись на одной стороне может быть связана с любым количеством записей на другой стороне. Ведет себя как контейнер записей, предоставляя доступ, возможно пустой, к набору записей:
for other in foo.other_ids: print other.name
Exercise
Many2one связи
Используя many2one, измените модели данных Course и Session, для того чтобы отразить их связи с остальными моделями данных:
Курс имеет ответственного пользователя; Значение этого поля является записью встроенной модели
res.users
.У занятия есть инструктор; значение этого поля является записью во встроенной модели данных
res.partner
.Занятие относится к course; значение этого поля является записью в
openacademy.course
и является необходимым.Адаптируйте представления.
Добавьте к моделям данных соответствующие поля `` Many2one``
Добавьте их в представления.
name = fields.Char(string="Title", required=True)
description = fields.Text()
responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)
class Session(models.Model):
_name = 'openacademy.session'
start_date = fields.Date()
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor")
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
<sheet>
<group>
<field name="name"/>
<field name="responsible_id"/>
</group>
<notebook>
<page string="Description">
</field>
</record>
<!-- override the automatically generated list view for courses -->
<record model="ir.ui.view" id="course_tree_view">
<field name="name">course.tree</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<tree string="Course Tree">
<field name="name"/>
<field name="responsible_id"/>
</tree>
</field>
</record>
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
<form string="Session Form">
<sheet>
<group>
<group string="General">
<field name="course_id"/>
<field name="name"/>
<field name="instructor_id"/>
</group>
<group string="Schedule">
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- session tree/list view -->
<record model="ir.ui.view" id="session_tree_view">
<field name="name">session.tree</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<tree string="Session Tree">
<field name="name"/>
<field name="course_id"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
Exercise
Обратные связи one2many
Используя одно обратное реляционное поле one2many, модифицируйте модели, чтобы отразить связь между курсами и занятиями.
Измените класс
Course
Добавьте поле для представления в виде Form для курса.
responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
class Session(models.Model):
<page string="Description">
<field name="description"/>
</page>
<page string="Sessions">
<field name="session_ids">
<tree string="Registered sessions">
<field name="name"/>
<field name="instructor_id"/>
</tree>
</field>
</page>
</notebook>
</sheet>
Exercise
Множественные many2many связи
Используя поле many2many, модифицируйте Session модель данных, чтобы связать каждое занятие с присутствующими. Присутствующие будут представлены в виде записей контрагентов, поэтому мы свяжем их со встроенной моделью данных res.partner
. Внесите соответствующие изменения в представления.
Модифицируйте класс
Session
Добавьте поле в представления в виде Form.
instructor_id = fields.Many2one('res.partner', string="Instructor")
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
<field name="seats"/>
</group>
</group>
<label for="attendee_ids"/>
<field name="attendee_ids"/>
</sheet>
</form>
</field>
Наследование
Наследование моделей данных
Odoo предлагает механизм наследования в двух вариантах для расширения существующей модели данных путем создания дополнительного модуля
Первый механизм наследования позволяет модулю изменять поведение модели данных, определенной в другом модуле:
добавлять поля к модели данных
Переопределить определения полей в модели данных
Добавить ограничения в модель данных
Добавить методы в модель данных
Переопределить существующие методы в модели данных.
Второй механизм наследования (делегирование) позволяет связать каждую запись модели с записью в родительской модели и обеспечивает прозрачный доступ к полям родительской записи.

Наследование представления
Помимо модификации существующих представлений (путем перезаписи), Odoo позволяет наследовать представления где дочерние "расширения" представлений могут быть быть закреплены над корневыми представлениями и могут добавлять или удалять содержимое своего родителя.
Расширение представления соотносится со своим родителем, используя поле inherit_id
, и во отличии от одиночного представления его поле arch
состоит из любого количества элементов xpath
, предоставляя возможность выбора альтернативного наполнения родительского представления:
<!-- improved idea categories list -->
<record id="idea_category_list2" model="ir.ui.view">
<field name="name">id.category.list2</field>
<field name="model">idea.category</field>
<field name="inherit_id" ref="id_category_list"/>
<field name="arch" type="xml">
<!-- find field description and add the field
idea_ids after it -->
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" string="Number of ideas"/>
</xpath>
</field>
</record>
expr
Выражение XPath, выбирает определенный элемент родительского представления. Вызывает ошибку, если он не соответствует ни одному элементу или более одного.
position
Команда для применения к конкретному элементу:
inside
присоединяет тело внутри контейнера
xpath
после выбранного элементаreplace
заменяет выбранный элемент
xpath
контейнеромbefore
вставляет контейнер
xpath
, как неотъемлемую часть в начале выбранного элементаafter
вставляет контейнер
xpaths
, как неотъемлемую часть в конце выбранного элементаattributes
предоставляет альтенативные атрибуты выбранного элемента используя специальную команду
attribute
внутри контейнераxpath
Совет
При выборе одиночного элемента атрибут position
может быть напрямую назначен искомому элементу. В примере указаны два наследования с одинаковым результатом.
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" />
</xpath>
<field name="description" position="after">
<field name="idea_ids" />
</field>
Exercise
Измените существующий проект
Используя модель наследования, измените существующую модель Partner, добавив туда поле boolean
instructor
, и поле many2many, которое соответствует отношению «занятие-партнер»Используя наследование представлений, отобразите эти поля в представлении вида Form партнера
Примечание
Этот способ дает возможность разработчику просмотреть представление, найти его внешний ID и место для нового поля.
Создайте файл
openacademy/partner.py
импортируйте его в__init__.py
Создайте файл
openacademy/views/partner.xml
и добавьте его в__openerp__.py
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import partner
# 'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
],
# only loaded in demonstration mode
'demo': [
# -*- coding: utf-8 -*-
from openerp import fields, models
class Partner(models.Model):
_inherit = 'res.partner'
# Add a new column to the res.partner model, by default partners are not
# instructors
instructor = fields.Boolean("Instructor", default=False)
session_ids = fields.Many2many('openacademy.session',
string="Attended Sessions", readonly=True)
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- Add instructor field to existing view -->
<record model="ir.ui.view" id="partner_instructor_form_view">
<field name="name">partner.instructor</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="Sessions">
<group>
<field name="instructor"/>
<field name="session_ids"/>
</group>
</page>
</notebook>
</field>
</record>
<record model="ir.actions.act_window" id="contact_list_action">
<field name="name">Contacts</field>
<field name="res_model">res.partner</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="configuration_menu" name="Configuration"
parent="main_openacademy_menu"/>
<menuitem id="contact_menu" name="Contacts"
parent="configuration_menu"
action="contact_list_action"/>
</data>
</openerp>
Домены
В Odoo, Домены это значения в которых закодировано состояние записей. Домен это список критериев используемых для выбора из подмножества записей в модели данных. Каждый критерий представляет собой треугольник где вершинами являются имя поля, оператор и значение.
К примеру, при использовании в модели данных Product домен выбирает все services цена которых выше 1000 за одну позицию:
[('product_type', '=', 'service'), ('unit_price', '>', 1000)]
По умолчанию, критерии тесно взаимосвязаны с безусловным AND. Логические операторы &
(AND), |
(OR) и !
(NOT) могут использоваться для явного объединения критериев. Они используются в позиции префикса (оператор вставляется перед своими аргументами, а не между ними). Например, чтобы выбрать продукты "которые являются услугами ИЛИ имеют цену за единицу, которая НЕ попадает между 1000 и 2000":
['|',
('product_type', '=', 'service'),
'!', '&',
('unit_price', '>=', 1000),
('unit_price', '<', 2000)]
Параметр domain
может быть добавлен к зависимым полям, с целью отделения действительных записей для работы, в процессе выбора записей в базе для работы с клиентским интерфейсом.
Exercise
Домены в зависимых полях
При выборе instructor для Session должны быть видны только инструкторы (партнеры с параметром instructor
, установленными на True
).
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=[('instructor', '=', True)])
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
Примечание
Домен, объявленный как список, исполняется на стороне сервера, и не может обращаться к динамическим значениям справа э а домен, объявленный, как строка, исполняется со стороны клиента, разрешая имена полей справа
Exercise
Более сложные домены
Создайте новые категории партнеров Teacher / Level 1 и Teacher / Level 2. Инструктором для занятия может быть как инструктор, так и преподаватель (любого уровня).
Измените домен модели Session
Измените
openacademy/view/partner.xml
таким образом, чтобы иметь доступ к Partner categories:
seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=['|', ('instructor', '=', True),
('category_id.name', 'ilike', "Teacher")])
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
<menuitem id="contact_menu" name="Contacts"
parent="configuration_menu"
action="contact_list_action"/>
<record model="ir.actions.act_window" id="contact_cat_list_action">
<field name="name">Contact Tags</field>
<field name="res_model">res.partner.category</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="contact_cat_menu" name="Contact Tags"
parent="configuration_menu"
action="contact_cat_list_action"/>
<record model="res.partner.category" id="teacher1">
<field name="name">Teacher / Level 1</field>
</record>
<record model="res.partner.category" id="teacher2">
<field name="name">Teacher / Level 2</field>
</record>
</data>
</openerp>
Вычисляемые поля и значения по умолчанию
Помимо того, что поля могут сохраняться и быть в прямом доступе непосредственно из базы данных, они также могут вычислены. В этом случае, значения полей не извлекаются непосредственно из базы, а вычисляются 'на лету' путем вызова метода модели данных.
Для создания вычисляемого поля вам нужно создать поле, установив атрибут compute
в его метод. Метод вычисления должен просто устанавливать значение поля в вычисляемое в любой записи self
.
Опасно
self
это собирательный операнд
Объект self
это recordset, то есть упорядоченный набор записей. Он поддерживает стандартные операции с Python над коллекциями, такие как len(self)
и iter(self)
, плюс дополнительные операции с наборами, такие как recs1 + recs2
.
Во взаимодействии с self
можно собрать записи одна за одной, где каждая представляет собой комплект размера 1. Вы можете объединять, разделять записи при помощи точки record.name
.
import random
from openerp import models, fields
class ComputedModel(models.Model):
_name = 'test.computed'
name = fields.Char(compute='_compute_name')
@api.multi
def _compute_name(self):
for record in self:
record.name = str(random.randint(1, 1e6))
Зависимости
Значение вычисленного поля обычно зависит от значений других полей в вычисляемой записи. ORM подразумевает, что разработчик укажет эти зависимости для метода compute с помощью декоратора depends()
. Данные зависимости используются ORM для инициирования перерасчета поля при изменении некоторых его зависимостей:
from openerp import models, fields, api
class ComputedModel(models.Model):
_name = 'test.computed'
name = fields.Char(compute='_compute_name')
value = fields.Integer()
@api.depends('value')
def _compute_name(self):
for record in self:
self.name = "Record with value %s" % self.value
Exercise
Вычисляемые поля
Добавьте процент занятых мест к модели данных Session
Отобразите поле в виде Tree и в виде Form
Отобразите поле как индикатор выполнения
Добавьте вычисляемое поле к Session
Покажите значение поля в представлении Session:
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
if not r.seats:
r.taken_seats = 0.0
else:
r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
<field name="taken_seats" widget="progressbar"/>
</group>
</group>
<label for="attendee_ids"/>
<tree string="Session Tree">
<field name="name"/>
<field name="course_id"/>
<field name="taken_seats" widget="progressbar"/>
</tree>
</field>
</record>
Значения по умолчанию
Любому полю может быть присвоено значение по умолчанию. В определении поля добавьте опцию default=X
, где X
является либо литеральным значением Python (логическое, целочисленное, float, string), либо функцией, берущей набор записей и возвращающей значение:
name = fields.Char(default="Unknown")
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
Примечание
Объект self.env
предоставляет доступ к параметрам запроса и другим полезным вещам:
self.env.cr
илиself._cr
это объект Курсор базы данных, который используется для формирования запросов к базе.self.env.uid
илиself._uid
является идентификатором текущего пользователя базы данныхself.env.user
запись текущего пользователяself.env.context
илиself._context
это контекстная директорияself.env.ref(xml_id)
возвращает запись, соответствующую XML idself.env[model_name]
возвращает окружение данной модели данных
Exercise
Активные объекты - Значения по умолчанию
Установите start_date по умолчанию на текущую дату (см.
Date
).Добавьте поле
active
в классе Session установив значение sessions активным по умолчанию.
_name = 'openacademy.session'
name = fields.Char(required=True)
start_date = fields.Date(default=fields.Date.today)
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
active = fields.Boolean(default=True)
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=['|', ('instructor', '=', True),
<field name="course_id"/>
<field name="name"/>
<field name="instructor_id"/>
<field name="active"/>
</group>
<group string="Schedule">
<field name="start_date"/>
Примечание
В Odoo есть встроенные правила, в которых поля с полем active
со значением False
скрыты.
Onchange
Механизм «onchange» предоставляет возможность для клиентского интерфейса обновлять форму, когда пользователь заполняет значение в поле, не сохраняя ничего в базе данных.
Например, предположим, что модель имеет три поля amount
, unit_price
и price
, и вы хотите обновить цену в форме, когда какие-либо другие поля изменены. Для этого определите метод, в котором self
представляет запись в виде формы, и обозначьте его при помощи функции onchange()
, чтобы указать, в каком поле она должна быть запущена. Любые изменения, внесенные вами в self
, будут отражены в форме.
<!-- content of form view -->
<field name="amount"/>
<field name="unit_price"/>
<field name="price" readonly="1"/>
# onchange handler
@api.onchange('amount', 'unit_price')
def _onchange_price(self):
# set auto-changing field
self.price = self.amount * self.unit_price
# Can optionally return a warning and domains
return {
'warning': {
'title': "Something bad happened",
'message': "It was very bad indeed",
}
}
Поведение вычисляемых полей, обозначенных тегом onchange
ет быть предугадано путем отслеживания изменений в форме Session измените количество мест или присутствующих, и taken_seats
индикатор прогресса обновиться автоматически.
Exercise
Предупреждение
Добавьте явный onchange
, чтобы предупредить о недопустимых значениях, таких как отрицательное количество мест или большее количество участников, чем места.
r.taken_seats = 0.0
else:
r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats
@api.onchange('seats', 'attendee_ids')
def _verify_valid_seats(self):
if self.seats < 0:
return {
'warning': {
'title': "Incorrect 'seats' value",
'message': "The number of available seats may not be negative",
},
}
if self.seats < len(self.attendee_ids):
return {
'warning': {
'title': "Too many attendees",
'message': "Increase seats or remove excess attendees",
},
}
Ограничения модели данных
Odoo предоставляет два способа настройки автоматической проверки инвариантов: Python constraints
и SQL constraints
.
Ограничение Python определяется как метод с декоратором constrains()
, и вызывается в наборе записей. Декоратор определяет, какие поля участвуют в ограничении, так что ограничение автоматически оценивается, когда одно из этих полей изменяется. Подразумевается, что метод вернет ошибку, если его инвариант не выполняется:
from openerp.exceptions import ValidationError
@api.constrains('age')
def _check_something(self):
for record in self:
if record.age > 20:
raise ValidationError("Your record is too old: %s" % record.age)
# all records passed the test, don't return anything
Exercise
Добавьте ограничения Python
Добавьте ограничение, которое проверяет, что преподаватель не присутствует в списке посетителей его собственного занятия.
# -*- coding: utf-8 -*-
from openerp import models, fields, api, exceptions
class Course(models.Model):
_name = 'openacademy.course'
'message': "Increase seats or remove excess attendees",
},
}
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
if r.instructor_id and r.instructor_id in r.attendee_ids:
raise exceptions.ValidationError("A session's instructor can't be an attendee")
Ограничения SQL определяются с помощью атрибута модели данных _sql_constraints
. Последнему присваивается список троек из строк (name, sql_definition, message)
, где name
является допустимым именем ограничения SQL, sql_definition
является выражением table_constraint и message
- сообщением об ошибке.
Exercise
Добавить ограничения SQL
С помощью PostgreSQL's documentation добавьте следующие ограничения:
Проверьте, что описание курса и название курса различны
Укажите уникальное название курса
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
_sql_constraints = [
('name_description_check',
'CHECK(name != description)',
"The title of the course should not be the description"),
('name_unique',
'UNIQUE(name)',
"The course title must be unique"),
]
class Session(models.Model):
_name = 'openacademy.session'
Exercise
Упражнение 6 - Добавьте механизм дублирования записи
Поскольку мы добавили ограничение уникальности названия курса, невозможно использовать функцию дублирования (
).Переопределите метод copy
, который позволяет дублировать объект Course, изменяя исходное имя на "Copy of [original name]".
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
@api.multi
def copy(self, default=None):
default = dict(default or {})
copied_count = self.search_count(
[('name', '=like', u"Copy of {}%".format(self.name))])
if not copied_count:
new_name = u"Copy of {}".format(self.name)
else:
new_name = u"Copy of {} ({})".format(self.name, copied_count)
default['name'] = new_name
return super(Course, self).copy(default)
_sql_constraints = [
('name_description_check',
'CHECK(name != description)',
Расширенные представления
Преставления в виде Tree
Представления вида Tree могут принимать дополнительные атрибуты для дальнейшей настройки их поведения:
colors
Сопоставление цветов с условиями. Если условие оценивается как `` True``, соответствующий цвет применяется к строке:
<tree string="Idea Categories" colors="blue:state=='draft';red:state=='trashed'"> <field name="name"/> <field name="state"/> </tree>
Разделы разделяются символом
;
, цвет и условие разделяются символом:
.editable
"top"
или"bottom"
. Делает представления вида Tree доступным для редактирования в самих строках (вместо того, чтобы просматривать форму), значение - это позиция, где появляются новые строки.
Exercise
Использование цветов
Измените представление вида Tree модели данных Session таким образом, чтобы курсы продолжительностью менее 5 дней были окрашены в синий цвет, а длящиеся более 15 дней, окрашены в красный цвет.
Измените представление вида Tree модели данных Session:
<field name="name">session.tree</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<tree string="Session Tree" colors="#0000ff:duration<5;red:duration>15">
<field name="name"/>
<field name="course_id"/>
<field name="duration" invisible="1"/>
<field name="taken_seats" widget="progressbar"/>
</tree>
</field>
Представления вида Calendar
Отображает записи, как события календаря. Их корневым элементом является <calendar>
, а их наиболее распространенными атрибутами являются:
color
Имя поля, используемое для сегментации цветом. Цвета автоматически распределяются по событиям. События в одном цветовом сегменте (записи, имеющие одинаковое значение для их поля
@color
) будут иметь одинаковый цвет.date_start
Поле записи, содержащее дату/время начала события
date_stop
(опционально)Поле записи, содержащее конечную дату/время события
Поле (для определения метки для каждого события в календаре)
<calendar string="Ideas" date_start="invent_date" color="inventor_id">
<field name="name"/>
</calendar>
Exercise
Представление вида Calendar
Добавьте представление вида Calendar к модели данных Session, чтобы пользователь мог просматривать события, связанные с Open Academy.
Добавить поле
end_date
, вычисленное изstart_date
иduration
Совет
Обратная функция делает поле доступным для записи и позволяет перемещать курсы (с помощью перетаскивания) в представлении вида Calendar
Добавить представление вида Calendar в модель данных Session
Добавьте представление вида Calendar к действиям модели данных Session
# -*- coding: utf-8 -*-
from datetime import timedelta
from openerp import models, fields, api, exceptions
class Course(models.Model):
attendee_ids = fields.Many2many('res.partner', string="Attendees")
taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')
end_date = fields.Date(string="End Date", store=True,
compute='_get_end_date', inverse='_set_end_date')
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
},
}
@api.depends('start_date', 'duration')
def _get_end_date(self):
for r in self:
if not (r.start_date and r.duration):
r.end_date = r.start_date
continue
# Add duration to start_date, but: Monday + 5 days = Saturday, so
# subtract one second to get on Friday instead
start = fields.Datetime.from_string(r.start_date)
duration = timedelta(days=r.duration, seconds=-1)
r.end_date = start + duration
def _set_end_date(self):
for r in self:
if not (r.start_date and r.end_date):
continue
# Compute the difference between dates, but: Friday - Monday = 4 days,
# so add one day to get 5 days instead
start_date = fields.Datetime.from_string(r.start_date)
end_date = fields.Datetime.from_string(r.end_date)
r.duration = (end_date - start_date).days + 1
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
</field>
</record>
<!-- calendar view -->
<record model="ir.ui.view" id="session_calendar_view">
<field name="name">session.calendar</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<calendar string="Session Calendar" date_start="start_date"
date_stop="end_date"
color="instructor_id">
<field name="name"/>
</calendar>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar</field>
</record>
<menuitem id="session_menu" name="Sessions"
Преставления в виде Search
Элементы <field>
представления вида Search могут иметь filter_domain
, который переопределяет домен, сгенерированный для поиска по данному полю. В данной области self
представляет собой значение, введенное пользователем. В приведенном ниже примере он используется для поиска по обоим полям name
и``description``.
Представление вида Search также могут содержать элементы <filter>
, которые действуют как кнопки предопределенных поисков. Фильтры должны иметь один из следующих атрибутов:
domain
Добавит данный домен в текущий поиск
context
Добавит некоторый контекст в текущий поиск; Используйте ключ
group_by
для группировки результатов по данному имени поля
<search string="Ideas">
<field name="name"/>
<field name="description" string="Name and description"
filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
<field name="inventor_id"/>
<field name="country_id" widget="selection"/>
<filter name="my_ideas" string="My Ideas"
domain="[('inventor_id', '=', uid)]"/>
<group string="Group By">
<filter name="group_by_inventor" string="Inventor"
context="{'group_by': 'inventor_id'}"/>
</group>
</search>
Чтобы использовать в действии нестандартное представление вида Search, нужно указать связь, используя поле search_view_id
в записи действия.
Действие также может устанавливать значения по умолчанию для полей поиска через поле context
: контекстные ключи формы search_default_field_name
будут инициализировать field_name с предоставленным значением. Чтобы иметь значение по умолчанию и вести себя как логические значения (их можно включить только по умолчанию), в фильтре поиска должно быть определено необязательное @name
.
Exercise
Преставления в виде Search
Добавьте кнопку для фильтрации курсов, для которых текущий пользователь является ответственным в представлении вида Search курса. Сделайте его выбранным по умолчанию.
Добавьте кнопку для группировки Курсов по ответственному пользователю.
<search>
<field name="name"/>
<field name="description"/>
<filter name="my_courses" string="My Courses"
domain="[('responsible_id', '=', uid)]"/>
<group string="Group By">
<filter name="by_responsible" string="Responsible"
context="{'group_by': 'responsible_id'}"/>
</group>
</search>
</field>
</record>
<field name="res_model">openacademy.course</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context" eval="{'search_default_my_courses': 1}"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">Create the first course
</p>
Представление вида Gantt
Горизонтальные гистограммы, которые обычно используются для отображения планирования и продвижения проекта, их корневым элементом является <gantt>
.
<gantt string="Ideas" date_start="invent_date" color="inventor_id">
<level object="idea.idea" link="id" domain="[]">
<field name="inventor_id"/>
</level>
</gantt>
Exercise
Диаграммы Ганта
Добавьте диаграмму Ганта, чтобы пользователь мог просматривать планирование сеансов, связанное с модулем Open Academy. Курсы должны быть сгруппированы по инструкторам.
Создайте вычисляемое поля, выражающее длительность курса в часах
Определите представления вида Gantt. Добавьте в действие вызов модели данных Session в виде данного представления.
end_date = fields.Date(string="End Date", store=True,
compute='_get_end_date', inverse='_set_end_date')
hours = fields.Float(string="Duration in hours",
compute='_get_hours', inverse='_set_hours')
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
end_date = fields.Datetime.from_string(r.end_date)
r.duration = (end_date - start_date).days + 1
@api.depends('duration')
def _get_hours(self):
for r in self:
r.hours = r.duration * 24
def _set_hours(self):
for r in self:
r.duration = r.hours / 24
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
</field>
</record>
<record model="ir.ui.view" id="session_gantt_view">
<field name="name">session.gantt</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<gantt string="Session Gantt" color="course_id"
date_start="start_date" date_delay="hours"
default_group_by='instructor_id'>
<field name="name"/>
</gantt>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar,gantt</field>
</record>
<menuitem id="session_menu" name="Sessions"
Представление вида Graph
Представления вида Graph позволяют делать сборный обзор и анализ моделей данных, их корневым элементом является <graph>
.
Представления вида Graph имеют 4 режима отображения, режим по умолчанию выбирается с помощью атрибута @type
.
- Pivot
Многомерная таблица, позволяет выбирать фильтры и измерения, чтобы получить правильный агрегированный набор данных, прежде чем перейти к более графическому обзору
- Bar (по умолчанию)
Гистограмма, первое измерение используется для определения групп по горизонтальной оси, другие измерения определяют агрегированные бары внутри каждой группы.
По умолчанию столбцы расположены бок о бок, они могут быть сложены с помощью
@stacked="True"
на<graph>
- Line
2-мерная линейная диаграмма
- Pie
2-мерный круговая диаграмма, разделенная на сектора
Представления вида Graph содержат <field>
с обязательным атрибутом @type
, принимающим значения:
row
(по умолчанию)Поле по умолчанию, в отношении которого будет совершен сбор данных
measure
объединяет значения величин всех дочерних объектов, игнорируя их группы
<graph string="Total idea score by Inventor">
<field name="inventor_id"/>
<field name="score" type="measure"/>
</graph>
Предупреждение
Представления вида Graph выполняют сбор значений из базы данных, они не работают с несохраненными вычисленными полями.
Exercise
Представления вида Graph
Добавьте представление вида Graph в объект «Session», отображающий для каждого курса количество участников в виде гистограммы.
Добавте количество посетителей в виде сохраненного вычисляемого поля
Затем добавьте соответствующее представление
hours = fields.Float(string="Duration in hours",
compute='_get_hours', inverse='_set_hours')
attendees_count = fields.Integer(
string="Attendees count", compute='_get_attendees_count', store=True)
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
for r in self:
r.duration = r.hours / 24
@api.depends('attendee_ids')
def _get_attendees_count(self):
for r in self:
r.attendees_count = len(r.attendee_ids)
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
</field>
</record>
<record model="ir.ui.view" id="openacademy_session_graph_view">
<field name="name">openacademy.session.graph</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<graph string="Participations by Courses">
<field name="course_id"/>
<field name="attendees_count" type="measure"/>
</graph>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar,gantt,graph</field>
</record>
<menuitem id="session_menu" name="Sessions"
Представление вида Kanban
Используется для организации задач, производственных процессов и т.д ... их корневым элементом является <kanban>
.
Представление вида Kanban показывает набор карточек, которые могут быть сгруппированы в столбцы. Каждая карточка представляет собой запись, а в каждом столбце - значения поля состояния.
Например, задачи проекта могут быть организованы поэтапно (каждый столбец - это этап) или ответственным (каждый столбец является пользователем) и т.д.
Представления вида Kanban определяют структуру каждой карточки, как сочетание элементов формы (включая базовый HTML) и Qweb.
Exercise
Представление вида Kanban
Добавьте представление вида Kanban, в котором отображаются занятия, сгруппированные по курсу (столбцы, таким образом, являются курсами).
Добавьте поле integer
color
к модели данных SessionДобавить представление вида Kanban и обновить действие.
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
active = fields.Boolean(default=True)
color = fields.Integer()
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=['|', ('instructor', '=', True),
</record>
<record model="ir.ui.view" id="view_openacad_session_kanban">
<field name="name">openacad.session.kanban</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<kanban default_group_by="course_id">
<field name="color"/>
<templates>
<t t-name="kanban-box">
<div
t-attf-class="oe_kanban_color_{{kanban_getcolor(record.color.raw_value)}}
oe_kanban_global_click_edit oe_semantic_html_override
oe_kanban_card {{record.group_fancy==1 ? 'oe_kanban_card_fancy' : ''}}">
<div class="oe_dropdown_kanban">
<!-- dropdown menu -->
<div class="oe_dropdown_toggle">
<span class="oe_e">#</span>
<ul class="oe_dropdown_menu">
<li>
<a type="delete">Delete</a>
</li>
<li>
<ul class="oe_kanban_colorpicker"
data-field="color"/>
</li>
</ul>
</div>
<div class="oe_clear"></div>
</div>
<div t-attf-class="oe_kanban_content">
<!-- title -->
Session name:
<field name="name"/>
<br/>
Start date:
<field name="start_date"/>
<br/>
duration:
<field name="duration"/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,calendar,gantt,graph,kanban</field>
</record>
<menuitem id="session_menu" name="Sessions"
parent="openacademy_menu"
Бизнес-процессы
Бизнес-процессы - это модели данных, связанные с бизнес-объектами, описывающими их динамику. Бизнес-процессы также используются для отслеживания состояния процессов, которые изменяются с течением времени.
Exercise
Бизнес-процесс на практике
Добавьте поле state
к модели данных Session. Оно будет использоваться для определения текущего состояния бизнес-процесса.
Курс может иметь три возможных состояния: Draft (по умолчанию), Confirmed и Done.
В форму редактирования курса добавьте поле (только для чтения) для визуализации состояния и кнопки для его изменения. Допустимые переходы:
- Draft -> Confirmed
- Confirmed -> Draft
- Confirmed -> Done
- Done -> Draft
Добавте новое поле
state
Добавьте методы перехода состояния, которые можно вызвать с помощью кнопок в представлении. Задача этих кнопок изменять состояние записи.
Добавьте соответствующие кнопки к представлению вида Form модели данных Session
attendees_count = fields.Integer(
string="Attendees count", compute='_get_attendees_count', store=True)
state = fields.Selection([
('draft', "Draft"),
('confirmed', "Confirmed"),
('done', "Done"),
], default='draft')
@api.multi
def action_draft(self):
self.state = 'draft'
@api.multi
def action_confirm(self):
self.state = 'confirmed'
@api.multi
def action_done(self):
self.state = 'done'
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<form string="Session Form">
<header>
<button name="action_draft" type="object"
string="Reset to draft"
states="confirmed,done"/>
<button name="action_confirm" type="object"
string="Confirm" states="draft"
class="oe_highlight"/>
<button name="action_done" type="object"
string="Mark as done" states="confirmed"
class="oe_highlight"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<group>
<group string="General">
Бизнес-процессы могут быть связаны с любым объектом в Odoo и настроены по вашему желанию. Бизнес-процессы используются для структурирования и управления жизненными циклами бизнес-объектов и документов, а также определения переходов, триггеров и т.д. с помощью графических инструментов. Бизнес-процессы, активности (узлы или действия) и транзакции (условия) объявляются как XML-записи. Токены, которые перемещаются в бизнес-процессах, называются workitems.
Предупреждение
Бизнес-процесс, связанный с моделью данных, создается только при создании записей модели данных. Таким образом, нет экземпляра бизнес-процесса, связанного с экземплярами сеанса, созданными до определения бизнес-процесса
Exercise
Бизнес-процесс
Замените бизнес-процесс модели данных Session на бизнес-процесс, соответствующий действительности. Преобразуйте представление вида Form модели данных Session, чтобы кнопки вызывали бизнес-процесс, а не методы модели данных.
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
],
# only loaded in demonstration mode
'demo': [
('draft', "Draft"),
('confirmed', "Confirmed"),
('done', "Done"),
])
@api.multi
def action_draft(self):
<field name="arch" type="xml">
<form string="Session Form">
<header>
<button name="draft" type="workflow"
string="Reset to draft"
states="confirmed,done"/>
<button name="confirm" type="workflow"
string="Confirm" states="draft"
class="oe_highlight"/>
<button name="done" type="workflow"
string="Mark as done" states="confirmed"
class="oe_highlight"/>
<field name="state" widget="statusbar"/>
<openerp>
<data>
<record model="workflow" id="wkf_session">
<field name="name">OpenAcademy sessions workflow</field>
<field name="osv">openacademy.session</field>
<field name="on_create">True</field>
</record>
<record model="workflow.activity" id="draft">
<field name="name">Draft</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="flow_start" eval="True"/>
<field name="kind">function</field>
<field name="action">action_draft()</field>
</record>
<record model="workflow.activity" id="confirmed">
<field name="name">Confirmed</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">function</field>
<field name="action">action_confirm()</field>
</record>
<record model="workflow.activity" id="done">
<field name="name">Done</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">function</field>
<field name="action">action_done()</field>
</record>
<record model="workflow.transition" id="session_draft_to_confirmed">
<field name="act_from" ref="draft"/>
<field name="act_to" ref="confirmed"/>
<field name="signal">confirm</field>
</record>
<record model="workflow.transition" id="session_confirmed_to_draft">
<field name="act_from" ref="confirmed"/>
<field name="act_to" ref="draft"/>
<field name="signal">draft</field>
</record>
<record model="workflow.transition" id="session_done_to_draft">
<field name="act_from" ref="done"/>
<field name="act_to" ref="draft"/>
<field name="signal">draft</field>
</record>
<record model="workflow.transition" id="session_confirmed_to_done">
<field name="act_from" ref="confirmed"/>
<field name="act_to" ref="done"/>
<field name="signal">done</field>
</record>
</data>
</openerp>
Совет
Чтобы проверить, правильно ли создаются экземпляры бизнес-процесса вместе с сеансами, перейдите по адресу
Exercise
Автоматические транзакции
Автоматически переводить курсы из состояния Draft в состояние Confirmed, если зарезервировано более половины мест на курсе.
<field name="act_to" ref="done"/>
<field name="signal">done</field>
</record>
<record model="workflow.transition" id="session_auto_confirm_half_filled">
<field name="act_from" ref="draft"/>
<field name="act_to" ref="confirmed"/>
<field name="condition">taken_seats > 50</field>
</record>
</data>
</openerp>
Exercise
Действия сервера
Замените методы Python для синхронизации состояния курса с помощью действий сервера.
Как бизнес-процесс, так и действия сервера могут быть полностью созданы из пользовательского интерфейса.
<field name="on_create">True</field>
</record>
<record model="ir.actions.server" id="set_session_to_draft">
<field name="name">Set session to Draft</field>
<field name="model_id" ref="model_openacademy_session"/>
<field name="code">
model.search([('id', 'in', context['active_ids'])]).action_draft()
</field>
</record>
<record model="workflow.activity" id="draft">
<field name="name">Draft</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="flow_start" eval="True"/>
<field name="kind">dummy</field>
<field name="action"></field>
<field name="action_id" ref="set_session_to_draft"/>
</record>
<record model="ir.actions.server" id="set_session_to_confirmed">
<field name="name">Set session to Confirmed</field>
<field name="model_id" ref="model_openacademy_session"/>
<field name="code">
model.search([('id', 'in', context['active_ids'])]).action_confirm()
</field>
</record>
<record model="workflow.activity" id="confirmed">
<field name="name">Confirmed</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">dummy</field>
<field name="action"></field>
<field name="action_id" ref="set_session_to_confirmed"/>
</record>
<record model="ir.actions.server" id="set_session_to_done">
<field name="name">Set session to Done</field>
<field name="model_id" ref="model_openacademy_session"/>
<field name="code">
model.search([('id', 'in', context['active_ids'])]).action_done()
</field>
</record>
<record model="workflow.activity" id="done">
<field name="name">Done</field>
<field name="wkf_id" ref="wkf_session"/>
<field name="kind">dummy</field>
<field name="action"></field>
<field name="action_id" ref="set_session_to_done"/>
</record>
<record model="workflow.transition" id="session_draft_to_confirmed">
Безопасность
Чтобы политика безопасности была согласована (без противоречий), необходимо правильно настроить контроль доступа.
Контроль доступа, основанный на группах пользователей
Группы создаются как записи модели данных res.groups
, и предоставляют доступ к меню через определение меню (в соответствующем XML файле). Однако даже без меню объекты могут все еще быть доступны косвенно, поэтому для групп должны быть определены разрешения на уровне объекта (чтение, запись, создание, отключение). Они обычно вставляются через CSV-файлы внутри модулей. Также можно ограничить доступ к определенным полям для представления или объекта, используя атрибут групп полей.
Права доступа
Права доступа определяются, как записи в `` ir.model.access``. каждое правило связано с определенной моделью данных, группой (или же no group для глобальных ресурсов), а также набором разрешений: чтение, запись, создание, отключение. Обычно, такие правила заданы в файле CSV, названном по имени модели данных: ir.model.access.csv
.
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_idea_idea,idea.idea,model_idea_idea,base.group_user,1,1,1,0
access_idea_vote,idea.vote,model_idea_vote,base.group_user,1,1,1,0
Exercise
Добавить управление доступом через интерфейс Odoo
Создайте нового пользователя "John Smith". Затем создайте группу "OpenAcademy/Session Read" с доступом на чтение к модели Session.
Создайте нового пользователя John Smith через
Создайте новую группу
session_read
через , она должен иметь доступ на чтение модели данных SessionОтредактируйте John Smith и сделайте его членом
session_read
.Войдите в систему как John Smith, чтобы проверить правильность настройки прав доступа.
Exercise
Добавление контроля доступа с помощью файлов данных в вашем модуле
Используя файлы данных,
Создайте группу OpenAcademy/Manager с полным доступом ко всем моделям данных OpenAcademy
Сделайте Session и Course доступными для чтения для всех пользователей
Создайте файл
openacademy/security/security.xml
, с принадлежностью к группе OpenAcademy/ManagerОтредактируйте файл
openacademy/security/ir.model.access.csv
с правами доступа к моделям данныхИ наконец, отредактируйте файл
openacademy/__ openerp __.py
, добавив в него информацию о новых файлах с данными.
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
course_manager,course manager,model_openacademy_course,group_manager,1,1,1,1
session_manager,session manager,model_openacademy_session,group_manager,1,1,1,1
course_read_all,course all,model_openacademy_course,,1,0,0,0
session_read_all,session all,model_openacademy_session,,1,0,0,0
<openerp>
<data>
<record id="group_manager" model="res.groups">
<field name="name">OpenAcademy / Manager</field>
</record>
</data>
</openerp>
Правила записи
Права записи определяют ограничения доступа к списку записей конкретной модели данных. Правило представляет собой запись модели данных ir.rule
и связано с моделью данных, группами (поле many2many), разрешениями, к которым применяется ограничение, а также с domain. Domain указывает, к каким записям права доступа ограничены.
Ниже приведен пример правила, которое предотвращает удаление записей, в котором не указан атрибут cancel
. Обратите внимание, что значение поля groups
должно соответствовать тому же соглашению, что и метод write()
в ORM.
<record id="delete_cancelled_only" model="ir.rule">
<field name="name">Only cancelled leads may be deleted</field>
<field name="model_id" ref="crm.model_crm_lead"/>
<field name="groups" eval="[(4, ref('base.group_sale_manager'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1" />
<field name="domain_force">[('state','=','cancel')]</field>
</record>
Exercise
Правило записи
Добавьте правило записи для модели данных «Course» и группу «OpenAcademy/Manager», которая ограничивает write
и unlink
доступ к ответственным за курс. Если у курса нет ответственного, все пользователи группы должны иметь возможность изменить его.
Создайте новое правило в openacademy/security/security.xml
:
<record id="group_manager" model="res.groups">
<field name="name">OpenAcademy / Manager</field>
</record>
<record id="only_responsible_can_modify" model="ir.rule">
<field name="name">Only Responsible can modify Course</field>
<field name="model_id" ref="model_openacademy_course"/>
<field name="groups" eval="[(4, ref('openacademy.group_manager'))]"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="0"/>
<field name="perm_unlink" eval="1"/>
<field name="domain_force">
['|', ('responsible_id','=',False),
('responsible_id','=',user.id)]
</field>
</record>
</data>
</openerp>
Помощники
Помощники описывают интерактивную работу с пользователями через динамические формы. Мастер - это модель данных, которая расширяет класс: TransientModel
вместо Model
. Класс TransientModel
расширяет Model
и повторно использует все его существующие механизмы со следующими особенностями:
Записи мастера не являются постоянными; Они автоматически удаляются из базы данных через определенное время. Вот почему они называются transient (транзитные).
Модели помощников не нуждаются в явных объявлениях прав доступа: пользователи имеют все права доступа к записям помощников.
Записи мастера могут относиться как к обычным записям, так и к записям полей many2one, а регулярные записи не могут ссылаться на записи мастера через поле many2one.
Мы хотим создать помощника, который позволит пользователям становиться участниками определенного Курса или списка Курсов одновременно.
Exercise
Определение помощника
Создайте модель помощника с отношением many2one с моделью Session и отношением many2many с моделью Partner.
Добавьте новый файл openacademy/wizard.py
:
from . import controllers
from . import models
from . import partner
from . import wizard
# -*- coding: utf-8 -*-
from openerp import models, fields, api
class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
session_id = fields.Many2one('openacademy.session',
string="Session", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
Запуск помощников
Запустить помощника можно через ir.actions.act_window
, указав в поле target
значение new
. Последний открывает окно помощника в новом всплывающем окне. Действие может быть вызвано пунктом меню.
Также, существует и другой вариант запуска: Используйте код ir.actions.act_window
, как указано выше, но, добавив туда еще одну инструкцию src_model
, определяющую, в контексте какой модели действие будет доступно. Помощник появится строго при определенных условиях, расположившись поверх основного окна. В данном случае, через ORM, все это прописывается в файле XML с тэгом act_window
.
<act_window id="launch_the_wizard"
name="Launch the Wizard"
src_model="context_model_name"
res_model="wizard_model_name"
view_mode="form"
target="new"
key2="client_action_multi"/>
Помощник использует стандартное представление, а его кнопки могут снабжаться атрибутом special = "cancel "
, чтобы закрыть окно помощника без сохранения.
Exercise
Запуск помощника
Определите представление вида Form.
Добавьте действие, позволяющее запуск в контексте модели данных Session.
В помощнике определите значение по умолчанию для поля сессии; используйте контекстный параметр
self._context
для получения текущей сессии.
class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
def _default_session(self):
return self.env['openacademy.session'].browse(self._context.get('active_id'))
session_id = fields.Many2one('openacademy.session',
string="Session", required=True, default=_default_session)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
parent="openacademy_menu"
action="session_list_action"/>
<record model="ir.ui.view" id="wizard_form_view">
<field name="name">wizard.form</field>
<field name="model">openacademy.wizard</field>
<field name="arch" type="xml">
<form string="Add Attendees">
<group>
<field name="session_id"/>
<field name="attendee_ids"/>
</group>
</form>
</field>
</record>
<act_window id="launch_session_wizard"
name="Add Attendees"
src_model="openacademy.session"
res_model="openacademy.wizard"
view_mode="form"
target="new"
key2="client_action_multi"/>
</data>
</openerp>
Exercise
Регистрация слушателей курсов
В помощнике нужно добавить кнопки и ввести соответствующий метод для добавления слушателей в данный Курс.
<field name="attendee_ids"/>
</group>
<footer>
<button name="subscribe" type="object"
string="Subscribe" class="oe_highlight"/>
or
<button special="cancel" string="Cancel"/>
</footer>
</form>
</field>
</record>
session_id = fields.Many2one('openacademy.session',
string="Session", required=True, default=_default_session)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
@api.multi
def subscribe(self):
self.session_id.attendee_ids |= self.attendee_ids
return {}
Exercise
Регистрация слушателей на несколько курсов
Измените модель данных помощника, чтобы слушатели могли быть зарегистрированы на несколько Курсов.
<form string="Add Attendees">
<group>
<field name="session_ids"/>
<field name="attendee_ids"/>
</group>
<footer>
<button name="subscribe" type="object"
class Wizard(models.TransientModel):
_name = 'openacademy.wizard'
def _default_sessions(self):
return self.env['openacademy.session'].browse(self._context.get('active_ids'))
session_ids = fields.Many2many('openacademy.session',
string="Sessions", required=True, default=_default_sessions)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
@api.multi
def subscribe(self):
for session in self.session_ids:
session.attendee_ids |= self.attendee_ids
return {}
Переводы
Каждый модуль может иметь несколько вариантов перевода. Все они находятся в папке i18n в виде LANG.po файлов, где LANG - код локали для языка, или же, комбинация Язык_Страна, в случае, если они отличаются (пример: pt.po или pt_BR.po). Перевод будет автоматически втянут в Odoo для всех отмеченных языков. Разработчики используют английский для создания модулей, затем экспортируют термины модуля в Odoo's через gettext POT export (
без указания языка), создавая пустой шаблон POT, добавляя затем еще и файлы переводов PO. Множество IDE's снабжены специальными инструментами для работы с файлами PO/POT.Совет
Формат GNU gettext (Portable Object), используемый Odoo, встроен в LaunchPad, делая его кроссплатформенным приложением для перевода.
|- idea/ # The module directory
|- i18n/ # Translation files
| - idea.pot # Translation Template (exported from Odoo)
| - fr.po # French translation
| - pt_BR.po # Brazilian Portuguese translation
| (...)
Совет
По умолчанию, Odoo's файлы POT представляют собой файлы типа XML с определениями в полях на языке Python, где любая строка Python может быть переведена путем окружения ее функцией openerp._()
(например _("Label")
)
Exercise
Переведите модуль
Выберите второй язык, устанавливаемый в Odoo. Переведите модуль при помощи инструментов, предоставляемых Odoo.
Создайте каталог
openacademy/i18n/
Установите любой подходящий язык (
)Синхронизируйте переводимиые определения (
)Создайте шаблон перевода, экспортировав (
) без определения языка, сохранив его в папкеopenacademy/i18n/
Создайте файл перевода, экспортируя (
) и указав язык. Сохраните его вopenacademy/i18n/
Откройте экспортированный файл перевода (с помощью основного текстового редактора или специального редактора PO-файлов, например POEdit, и переведите отсутствующие термины
В
models.py
, добавив функцию импорта кopenerp._
и отметьте недостающие строки как переводимыеПовторите шаги 3-6
# -*- coding: utf-8 -*-
from datetime import timedelta
from openerp import models, fields, api, exceptions, _
class Course(models.Model):
_name = 'openacademy.course'
default = dict(default or {})
copied_count = self.search_count(
[('name', '=like', _(u"Copy of {}%").format(self.name))])
if not copied_count:
new_name = _(u"Copy of {}").format(self.name)
else:
new_name = _(u"Copy of {} ({})").format(self.name, copied_count)
default['name'] = new_name
return super(Course, self).copy(default)
if self.seats < 0:
return {
'warning': {
'title': _("Incorrect 'seats' value"),
'message': _("The number of available seats may not be negative"),
},
}
if self.seats < len(self.attendee_ids):
return {
'warning': {
'title': _("Too many attendees"),
'message': _("Increase seats or remove excess attendees"),
},
}
def _check_instructor_not_in_attendees(self):
for r in self:
if r.instructor_id and r.instructor_id in r.attendee_ids:
raise exceptions.ValidationError(_("A session's instructor can't be an attendee"))
Отчеты
Отчеты для печати
В Odoo 8.0 встроен новый движок для ведения документации, базирующийся на Qweb, Twitter Bootstrap и Wkhtmltopdf.
Отчет представляет собой комбинацию двух элементов:
an
ir.actions.report.xml
, где<report>
shortcut element is provided, it sets up various basic parameters for the report (defaulявляется ярлыком, устанавливающим различные параметры для отчета (тип по умолчанию, необходимость сохранения отчета в базе после его генерации,…)<report id="account_invoices" model="account.invoice" string="Invoices" report_type="qweb-pdf" name="account.report_invoice" file="account.report_invoice" attachment_use="True" attachment="(object.state in ('open','paid')) and ('INV'+(object.number or '').replace('/','')+'.pdf')" />
Стандартный вид QWeb view для отчета:
<t t-call="report.html_container"> <t t-foreach="docs" t-as="o"> <t t-call="report.external_layout"> <div class="page"> <h2>Report title</h2> </div> </t> </t> </t> the standard rendering context provides a number of elements, the most important being: ``docs`` the records for which the report is printed ``user`` the user printing the report
Поскольку отчеты являются стандартными веб-страницами, они доступны через URL, а выходные параметры могут обрабатываться по этому URL-адресу, например, HTML-версия отчета Invoice доступна через http://localhost:8069/report/html/account.report_invoice/1 (если установлен модуль ``account``) и версия PDF через http://localhost:8069/report/pdf/account.report_invoice/1.
Опасно
Если кажется, что в вашем отчете PDF отсутствуют стили (т.е. Текст появляется, но стиль/макет отличается от версии html), возможно, ваш процесс wkhtmltopdf не может 'достучаться' до вашего веб-сервера, чтобы загрузить их.
Проверьте логи сервера,в них можно найти проблемы с загрузкой CSS при создании отчета в формате PDF.
Процесс wkhtmltopdf использует системный параметр web.base.url
качестве корневого каталога для доступа ко всем файлам отчетов, однако, их параметр обновляется каждый раз при входе администратора. Если ваш сервер находится за прокси, он может быть недоступен. Это исправляется путем редактирования системных параметров:
report.url
, указывающих на URL доступа к серверу (возможноhttp://localhost:8069
или нечто подобное). Этот адрес будет использоваться только для данной операции.web.base.url.freeze
, установленный вTrue
, остановит автоматическое обновление черезweb.base.url
.
Exercise
Создайте отчет для модели данных Session
Для каждого курса в отчете должно быть отображено ее имя, время ее начала и окончания, а также список участников слушателей Курса.
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
'reports.xml',
],
# only loaded in demonstration mode
'demo': [
<openerp>
<data>
<report
id="report_session"
model="openacademy.session"
string="Session Report"
name="openacademy.report_session_view"
file="openacademy.report_session"
report_type="qweb-pdf" />
<template id="report_session_view">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="doc">
<t t-call="report.external_layout">
<div class="page">
<h2 t-field="doc.name"/>
<p>From <span t-field="doc.start_date"/> to <span t-field="doc.end_date"/></p>
<h3>Attendees:</h3>
<ul>
<t t-foreach="doc.attendee_ids" t-as="attendee">
<li><span t-field="attendee.name"/></li>
</t>
</ul>
</div>
</t>
</t>
</t>
</template>
</data>
</openerp>
Сводные отчеты
Exercise
Определите сводный отчет
Определите сводный отчет, содержащий созданный вами график, вид календаря сеансов и список курсов (переключаемый на вид). Эта панель должна быть доступна через меню и автоматически отображаться в веб-клиенте, когда выбрано главное меню OpenAcademy.
Создайте файл
openacademy/views/session_board.xml
.В нем должно быть прописано представление сводного отчета, действия, соотносящиеся с этим представлением, действия для открытия сводного отчета, а также переопределение пунктов главного меню, скоординированное в соответствии с добавляемыми действиями для панели инструментовПримечание
Доступные стили для сводного отчета:
1
,1-1
,1-2
,2-1
и1-1-1
Добавьте информацию о новом файле в
openacademy/__openerp__.py
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'board'],
# always loaded
'data': [
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
'views/session_board.xml',
'reports.xml',
],
# only loaded in demonstration mode
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.actions.act_window" id="act_session_graph">
<field name="name">Attendees by course</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">graph</field>
<field name="view_id"
ref="openacademy.openacademy_session_graph_view"/>
</record>
<record model="ir.actions.act_window" id="act_session_calendar">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">calendar</field>
<field name="view_id" ref="openacademy.session_calendar_view"/>
</record>
<record model="ir.actions.act_window" id="act_course_list">
<field name="name">Courses</field>
<field name="res_model">openacademy.course</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<record model="ir.ui.view" id="board_session_form">
<field name="name">Session Dashboard Form</field>
<field name="model">board.board</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Session Dashboard">
<board style="2-1">
<column>
<action
string="Attendees by course"
name="%(act_session_graph)d"
height="150"
width="510"/>
<action
string="Sessions"
name="%(act_session_calendar)d"/>
</column>
<column>
<action
string="Courses"
name="%(act_course_list)d"/>
</column>
</board>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="open_board_session">
<field name="name">Session Dashboard</field>
<field name="res_model">board.board</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="usage">menu</field>
<field name="view_id" ref="board_session_form"/>
</record>
<menuitem
name="Session Dashboard" parent="base.menu_reporting_dashboard"
action="open_board_session"
sequence="1"
id="menu_board_session" icon="terp-graph"/>
</data>
</openerp>
Веб сервисы
Модуль веб-сервиса предлагает общий интерфейс для всех веб-сервисов
- XML-RPC
- JSON-RPC
Бизнес-объекты также могут быть доступны через механизм распределенных объектов. Все они могут быть изменены с помощью клиентского интерфейса в контекстных представлениях.
Odoo доступна через интерфейсы XML-RPC/JSON-RPC, для которых библиотеки существуют на многих языках.
Библиотека XML-RPC
В данном примере отражено взаимодействие программы на Python с библиотекой xmlrpclib
:
import xmlrpclib
root = 'http://%s:%d/xmlrpc/' % (HOST, PORT)
uid = xmlrpclib.ServerProxy(root + 'common').login(DB, USER, PASS)
print "Logged in as %s (uid: %d)" % (USER, uid)
# Create a new note
sock = xmlrpclib.ServerProxy(root + 'object')
args = {
'color' : 8,
'memo' : 'This is a note',
'create_uid': uid,
}
note_id = sock.execute(DB, uid, PASS, 'note.note', 'create', args)
Exercise
Добавим новую службу к клиенту
Напишите программу на Python, которая может отправлять запросы XML-RPC на компьютер, на котором запущен Odoo (ваш компьютер или вашего инструктора). Эта программа должна отображать все Курсы и их соответствующее количество мест. Она также должен уметь создать новое занятие для одного из курсов.
import functools
import xmlrpclib
HOST = 'localhost'
PORT = 8069
DB = 'openacademy'
USER = 'admin'
PASS = 'admin'
ROOT = 'http://%s:%d/xmlrpc/' % (HOST,PORT)
# 1. Login
uid = xmlrpclib.ServerProxy(ROOT + 'common').login(DB,USER,PASS)
print "Logged in as %s (uid:%d)" % (USER,uid)
call = functools.partial(
xmlrpclib.ServerProxy(ROOT + 'object').execute,
DB, uid, PASS)
# 2. Read the sessions
sessions = call('openacademy.session','search_read', [], ['name','seats'])
for session in sessions:
print "Session %s (%s seats)" % (session['name'], session['seats'])
# 3.create a new session
session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : 2,
})
Вместо использования жестко запрограммированного ID курса код может искать курс по имени
# 3.create a new session for the "Functional" course
course_id = call('openacademy.course', 'search', [('name','ilike','Functional')])[0]
session_id = call('openacademy.session', 'create', {
'name' : 'My session',
'course_id' : course_id,
})
Библиотека JSON-RPC
В следующем примера программа на Python взаимодействует с сервером Odoo при помощи стандартных библиотек Python urllib2
и json
:
import json
import random
import urllib2
def json_rpc(url, method, params):
data = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": random.randint(0, 1000000000),
}
req = urllib2.Request(url=url, data=json.dumps(data), headers={
"Content-Type":"application/json",
})
reply = json.load(urllib2.urlopen(req))
if reply.get("error"):
raise Exception(reply["error"])
return reply["result"]
def call(url, service, method, *args):
return json_rpc(url, "call", {"service": service, "method": method, "args": args})
# log in the given database
url = "http://%s:%s/jsonrpc" % (HOST, PORT)
uid = call(url, "common", "login", DB, USER, PASS)
# create a new note
args = {
'color' : 8,
'memo' : 'This is another note',
'create_uid': uid,
}
note_id = call(url, "object", "execute", DB, uid, PASS, 'note.note', 'create', args)
А вот та же программа, использующая jsonrpclib:
import jsonrpclib
# server proxy object
url = "http://%s:%s/jsonrpc" % (HOST, PORT)
server = jsonrpclib.Server(url)
# log in the given database
uid = server.call(service="common", method="login", args=[DB, USER, PASS])
# helper function for invoking model methods
def invoke(model, method, *args):
args = [DB, uid, PASS, model, method] + list(args)
return server.call(service="object", method="execute", args=args)
# create a new note
args = {
'color' : 8,
'memo' : 'This is another note',
'create_uid': uid,
}
note_id = invoke('note.note', 'create', args)
Примеры могут быть легко адаптированы из XML-RPC в JSON-RPC.
Примечание
Существует ряд высокоуровневых API на разных языках для доступа к системам Odoo без явного обращения к XML-RPC или JSON-RPC, таких как:
Запись прямых SQL-запросов возможна, но требует осторожности, поскольку обходит все механизмы аутентификации и безопасности Odoo.