Напишіть імпортовані модулі

Важливо

Цей посібник передбачає знайомство з посібником Серверний фреймворк 101 та посібником Визначення даних модуля.

Хоча, як розробники, ми воліємо мати повну потужність Python для написання наших модулів, іноді це неможливо; зазвичай на керованих хостингових рішеннях, які не дозволяють розгортати власний код Python, як-от платформа Odoo.com.

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

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

Постановка проблеми

Як і в посібнику Серверний фреймворк 101, ми працюватимемо над концепціями нерухомості.

Наша мета - створити новий застосунок для керування нерухомістю подібним (хоча й простішим) способом до того, що описано в посібнику Серверний фреймворк 101. Ми визначимо моделі, поля та логіку у файлах даних XML, а не у файлах Python.

Після завершення цього посібника ми зможемо досягти наступного в нашому додатку:

  • Керування об’єктами нерухомості, що продаються

  • Опублікуйте ці властивості на вебсайті

  • Приймайте пропозиції онлайн на вебсайті

  • Виставити рахунок покупцю після продажу нерухомості

Структура модуля

Як і в будь-якому проєкті розробки, чітка структура полегшує керування та підтримку коду.

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

estate
├── actions
│   └── *.xml
├── models
│   └── *.xml
├── security
│   └── ir.model.access.csv
│   └── estate_security.xml
├── views
│   └── *.xml
├── __init__.py
└── __manifest__.py

Єдині файли Python, які у вас будуть, це файли __init__.py та __manifest__.py. Файл __manifest__.py буде таким самим, як і для будь-якого модуля Odoo, але також імпортуватиме його моделі у список data.

Не забудьте перерахувати файли в розділі data файлу __manifest__.py у порядку залежностей, зазвичай починаючи з файлів моделі.

Файл __init__.py порожній, але він потрібен для того, щоб Odoo розпізнав модуль, якщо ви коли-небудь захочете розгорнути свій модуль класичним способом (додавши його до шляху додатків). Він не є абсолютно необхідним для модулів, які будуть імпортовані, але його бажано зберегти.

Розгортання модуля

Щоб розгорнути модуль, вам потрібно створити zip-файл модуля та завантажити його до вашого екземпляра Odoo. Переконайтеся, що модуль base_import_module встановлено у вашому екземплярі, потім перейдіть до Додатки ‣ Імпорт модуля та завантажте zip-файл. Ви повинні бути в режим розробника, щоб побачити пункт меню Import Module.

Якщо ви зміните модуль, вам потрібно буде створити новий zip-файл і завантажити його знову, що призведе до повторного завантаження всіх даних у модулі. Однак зверніть увагу, що деякі операції неможливі, наприклад, зміна типу поля, яке ви створили раніше. Дані, створені попередніми версіями модуля (наприклад, видалені поля), не будуть автоматично видалені. Загалом, найпростіший спосіб вирішити цю проблему – почати з нової бази даних або видалити модуль перед завантаженням нової версії.

Під час завантаження модуля майстер прийматиме два варіанти:

  • Force init: якщо ваш модуль вже встановлено, і ви завантажуєте його знову; вибір цієї опції призведе до примусового оновлення всіх даних, позначених як noupdate="1" у XML-файлах.

  • Import demo data: пояснюється без потреби

Також можна розгорнути модуль за допомогою інструмента командного рядка odoo-bin за допомогою команди deploy:

$ odoo-bin deploy <path_to_your_module> https://<your_odoo_instance> --login <your_login> --password <your_password>

Ця команда також приймає опцію --force, яка еквівалентна опції Force init у майстрі.

Зверніть увагу, що користувач, якого ви використовуєте для розгортання модуля, повинен мати права доступу Administration/Settings.

Exercise

  1. Створіть такі папки та файли:

    • /home/$USER/src/tutorials/estate/__init__.py

    • /home/$USER/src/tutorials/estate/__manifest__.py

    Файл __manifest__.py має визначати лише назву та залежності наших модулів. Єдиним необхідним модулем фреймворку наразі є base (та base_import_module - хоча ваш модуль, строго кажучи, не залежить від нього, він вам потрібен для можливості імпорту вашого модуля).

  2. Створіть zip-файл вашого модуля та завантажте його до вашого екземпляра Odoo.

Моделі та основні поля

Як ви можете собі уявити, визначення моделей та полів у XML-файлах не таке просте, як у Python.

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

Крім того, XML набагато детальніший, ніж Python.

Почнемо з визначення простої моделі для представлення об’єкта нерухомості в каталозі models нашого модуля.

Моделі Odoo зберігаються в базі даних як записи ir.model. Як і будь-який інший запис, їх можна визначити у XML-файлах:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="model_real_estate_property" model="ir.model">
        <field name="name">Real Estate Property</field>
        <field name="model">x_estate.property</field>
    </record>
</odoo>

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

Як і для класичних моделей, визначених у Python, Odoo автоматично додасть кілька полів до моделі:

  • id (Id) Унікальний ідентифікатор запису моделі.

  • create_date (Datetime) Дата створення запису.

  • create_uid (Many2one) Користувач, який створив запис.

  • write_date (Datetime) Дата останньої зміни запису.

  • write_uid (Many2one) Користувач, який останнім чином змінював запис.

Ми також можемо додати кілька полів до нашої нової моделі. Додамо кілька простих полів, таких як назва (рядок), ціна продажу (число з плаваючою комою), опис (як html) та поштовий індекс (як char).

Як і у випадку з моделями, поля – це просто записи моделі ir.model.fields і можуть бути визначені як такі у файлах даних:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <!-- ...model definition from before... -->
    <record id="field_real_estate_property_name" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_name</field>
        <field name="field_description">Name</field>
        <field name="ttype">char</field>
        <field name="required">True</field>
    </record>

    <record id="field_real_estate_property_selling_price" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_selling_price</field>
        <field name="field_description">Selling Price</field>
        <field name="ttype">float</field>
        <field name="required">True</field>
    </record>

    <record id="field_real_estate_property_description" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_description</field>
        <field name="field_description">Description</field>
        <field name="ttype">html</field>
    </record>

    <record id="field_real_estate_property_postcode" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_postcode</field>
        <field name="field_description">Postcode</field>
        <field name="ttype">char</field>
    </record>
</odoo>

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

  • name: технічна назва поля (має починатися з x_)

  • field_description: мітка поля

  • help: текст довідки для поля, що відображається в інтерфейсі

  • ttype: тип поля (наприклад, char, integer, float, html тощо)

  • required: чи є поле обов’язковим чи ні (за замовчуванням: False)

  • readonly: чи є поле доступним лише для читання чи ні (за замовчуванням: False)

  • index: чи індексовано поле чи ні (за замовчуванням: False)

  • copied: чи копіюється поле під час дублювання запису чи ні (за замовчуванням: True для нереляційних необчислюваних полів, False для реляційних та обчислюваних полів)

  • translate: чи є поле перекладним чи ні (за замовчуванням: False)

Атрибути також доступні для керування очищенням HTML, а також для інших, більш розширених функцій; для повного списку зверніться до моделі ir.model.fields у базі даних, доступній у меню Налаштування ‣ Технічні ‣ Структура бази даних ‣ Поля, або перегляньте визначення моделі ir.model.fields у модулі base.

Exercise

Додайте в таблицю такі основні поля:

Поле

Тип

Обов’язково

x_date_availability

Date

x_expected_price

Float

True

x_bedrooms

Integer

x_living_area

Integer

x_facades

Integer

x_garage

Boolean

x_garden

Boolean

x_garden_area

Integer

x_garden_orientation

Вибір

Поле x_garden_orientation повинно мати 4 можливі значення: „North“, „South“, „East“ та „West“. Список вибору потрібно створити, спочатку створивши запис ir.model.fields для самого поля, а потім створивши записи ir.model.fields.selection. Ці записи приймають три поля: field_id, name (назва в інтерфейсі користувача) та value (значення в базі даних). Також можна встановити поле sequence, яке контролює порядок, у якому вибрані елементи відображаються в інтерфейсі користувача (спочатку відображаються нижчі значення послідовності).

Значення по замовчуванню

У Python значення за замовчуванням можна встановити для полів за допомогою аргументу default в оголошенні поля. У модулях даних значення за замовчуванням встановлюються шляхом створення запису ir.default для кожного поля. Наприклад, можна встановити значення за замовчуванням для поля x_selling_price на 100000 для всіх властивостей, створивши наступний запис:

<odoo>
    <!-- ...model definition from before... -->
    <record id="default_real_estate_property_selling_price" model="ir.default">
        <field name="field_id" ref="estate.field_real_estate_property_selling_price" />
        <field name="json_value">100000</field>
    </record>
</odoo>

Для отримання додаткової інформації зверніться до моделі ir.default у базі даних, доступній у меню Налаштування ‣ Технічні ‣ Дії ‣ Користувацькі значення за замовчуванням, або перегляньте визначення моделі ir.default у модулі base.

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

Ці значення за замовчуванням є статичними, але можуть бути встановлені компанією та/або користувачем за допомогою полів user_id та company_id запису ir.default. Це означає, що, наприклад, неможливо встановити динамічне значення за замовчуванням «сьогодні» для поля x_date_availability.

Безпека

Безпека в модулях даних така ж сама, як і для модулів Python, і її можна знайти в Розділ 4: Безпека – короткий вступ.

Зверніться до цього посібника для отримання детальної інформації.

Exercise

  1. Створіть файл ir.model.access.csv у відповідній папці та визначте його у файлі __manifest__.py.

  2. Надайте дозволи на читання, запис, створення та від’єднання групи base.group_user.

Порада

Попереджувальне повідомлення в журналі містить більшу частину рішення ;-)

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

Представлення – це компоненти інтерфейсу користувача, які дозволяють користувачам взаємодіяти з даними. Вони визначені у XML-файлах і знаходяться в каталозі views вашого модуля.

Оскільки представлення (views) та дії (actions) вже визначені у Розділ 5: Нарешті, трохи інтерфейсу користувача для експериментів та Розділ 6: Основні представлення, ми не будемо вдаватися тут у подробиці.

Exercise

Додайте базовий інтерфейс користувача до модуля estate.

Додайте базовий інтерфейс користувача до модуля estate, щоб дозволити користувачам переглядати, створювати, редагувати та видаляти об’єкти нерухомості.

  • Створіть дію для моделі x_estate.property.

  • Створіть деревоподібне представлення для моделі x_estate.property.

  • Створіть представлення форми для моделі x_estate.property.

  • Додайте представлення до дії.

  • Додайте пункт меню до головного меню, щоб дозволити користувачам отримати доступ до дії.

Зв’язки

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

Як і в Розділ 7: Зв’язки між моделями, ми додамо деякі зв’язки до нашого модуля estate. Ми додамо посилання на:

  • клієнт, який придбав нерухомість

  • агент з нерухомості, який продав нерухомість

  • тип нерухомості: будинок, квартира, пентхаус, замок…

  • перелік тегів, що характеризують нерухомість: затишно, з ремонтом…

  • перелік отриманих пропозицій

Багато до одного

Багато-до-одного – це просте посилання на інший об’єкт. Наприклад, щоб визначити посилання на res.partner, ми можемо визначити нове поле в нашій моделі:

<odoo>
    <!-- ...model definition from before... -->
    <record id="field_real_estate_property_partner_id" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_partner_id</field>
        <field name="field_description">Customer</field>
        <field name="ttype">many2one</field>
        <field name="relation">res.partner</field>
    </record>
</odoo>

У випадку полів багато до одного можна встановити кілька атрибутів для деталізації зв’язку:

  • relation: назва моделі, з якою потрібно зв’язати (обов’язково)

  • ondelete: дія, яку потрібно виконати під час видалення запису (за замовчуванням: set null)

  • domain: фільтр домену, який застосовується до зв’язку

Exercise

  1. Створіть нову модель x_estate.property.type з такими полями:

    Поле

    Тип

    Обов’язково

    name

    Char

    True

  2. Додайте дію, список та пункт меню для моделі x_estate.property.type.

  3. Додайте права доступу до моделі x_estate.property.type, щоб надати доступ користувачам.

  4. Створіть такі поля в моделі x_estate.property:

    Поле

    Тип

    Обов’язково

    x_property_type_id

    Many2one (x_estate.property.type)

    True

    x_partner_id (buyer)

    Many2one (res.partner)

    x_user_id (salesperson)

    Many2one (res.users)

  5. Включіть нові поля у представлення форми моделі x_estate.property.

Many-to-many

Зв’язок many-to-many – це зв’язок до списку об’єктів. У нашому прикладі ми визначимо відношення many-to-many до нової моделі x_estate.property.tag. Цей тег представляє характеристику нерухомості, наприклад: відремонтований, затишний тощо.

Властивість може мати багато тегів, а тег може бути призначений багатьом властивостям – це типовий зв’язок many-to-many.

Поля many-to-many визначаються так само, як і поля many-to-one, але з параметром ttype, встановленим на many2many. Атрибут relation також встановлюється на назву моделі, з якою потрібно зв’язати. Для керування відношенням можна встановити інші атрибути:

  • relation_table: назва таблиці, яку слід використовувати для зв’язку

  • column1 та column2: назви стовпців, які слід використовувати для зв’язку

Ці атрибути є необов’язковими та зазвичай їх слід вказувати лише тоді, коли між двома моделями є кілька полів типу many-to-many, щоб уникнути конфліктів; у більшості випадків Odoo ORM зможе визначити правильну таблицю зв’язків та стовпці для використання.

Exercise

  1. Створіть нову модель x_estate.property.tag з такими полями:

    Поле

    Тип

    Обов’язково

    name

    Char

    True

  2. Додайте дію, список та пункт меню для моделі x_estate.property.tag.

  3. Додайте права доступу до моделі x_estate.property.tag, щоб надати доступ користувачам.

  4. Створіть такі поля в моделі x_estate.property:

    Поле

    Тип

    x_property_tag_ids

    Many2many (x_estate.property.tag)

  5. Включіть нове поле у представленні форми моделі x_estate.property.

One-to-many

Зв’язок one-to-many – це зв’язок до списку об’єктів. У нашому прикладі ми визначимо відношення one-to-many до нової моделі x_estate.property.offer. Ця пропозиція представляє собою пропозицію, зроблену клієнтом, щодо купівлі нерухомості.

Поля one-to-many визначаються так само, як і поля many-to-one, але з параметром ttype, встановленим на one2many. Атрибут relation також встановлюється на назву моделі, з якою потрібно зв’язати. Для керування відношенням необхідно встановити інший атрибут:

  • relation_field: назва поля у пов’язаній моделі, яке містить посилання на батьківську модель (поле many-to-one). Використовується для зв’язування двох моделей разом.

Exercise

  1. Створіть нову модель x_estate.property.offer з такими полями:

    Поле

    Тип

    Обов’язково

    Значення

    x_price

    Float

    True

    x_status

    Вибір

    Accepted, Refused

    x_partner_id

    Many2one (res.partner)

    True

    x_property_id

    Many2one (x_estate.property)

    True

  2. Додайте права доступу до моделі x_estate.property.offer, щоб надати доступ користувачам.

  3. Створіть деревоподібне представлення та представлення форми з полями price, partner_id та status.
    Не потрібно створювати дію чи меню.
  4. Додайте поле x_offer_ids до вашої моделі x_estate.property та у її форматі.

Обчислювані поля

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

Посилання: документацію, пов’язану з цією темою, можна знайти в Обчислювані поля.

Модулі даних можуть визначати обчислювані поля будь-якого типу, але їхня кількість досить обмежена порівняно з модулями Python. Дійсно, оскільки модулі даних призначені для розгортання на системах, які не дозволяють виконання довільного коду, дозволений код Python дуже обмежений.

Примітка

Весь код Python, написаний для модулів даних, виконується в ізольованому середовищі, яке обмежує операції, які можна виконувати. Наприклад, ви не можете імпортувати бібліотеки, не можете отримати доступ до жодних файлів ОС і навіть не можете друкувати в консоль. Деякі утиліти надаються, але це залежить від типу ізольованого середовища, яке використовується.

У випадку методів обчислення, пісочниця дуже обмежена та надає лише мінімальний мінімум утиліт для виконання коду. Окрім вбудованих модулів Python, ви також маєте доступ до модулів datetime, dateutil та time (наприклад, для допомоги з обчисленням дат).

Також зауважте, що «призначення крапкою» вимкнено в пісочниці, тому ви не можете записати property.x_total_area = 1 у методі compute. Вам потрібно використовувати доступ до словника: property['x_total_area'] = 1. Крапкова нотація для поля access працює нормально: property.x_garden_area поверне значення поля x_garden_area.

Раніше ми визначили два поля «площа» в нашій моделі x_estate.property: living_area та garden_area. Щоб визначити обчислюване поле в моделі, яке повертає суму двох площ, ми можемо додати наступний код до нашого модуля даних:

<odoo>
    <!-- ...model definition from before... -->
    <record id="field_real_estate_property_total_area" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_total_area</field>
        <field name="field_description">Total Area</field>
        <field name="ttype">float</field>
        <field name="depends">x_living_area,x_garden_area</field>
        <field name="compute"><![CDATA[
for property in self:
    property['x_total_area'] = property.x_living_area + property.x_garden_area
        ]]>
        </field>
    </record>
</odoo>

Примітка

У той час як у діях сервера ви виконуєте ітерацію по змінній records, у випадку обчислюваного поля ви виконуєте ітерацію по змінній self, яка містить набір записів, для якого обчислюється поле.

Атрибут depends використовується для визначення полів, від яких залежить обчислюване поле, а атрибут compute використовується для визначення коду, який виконується для обчислення поля (за допомогою коду Python).

На відміну від модулів Python, обчислювані поля зберігаються за замовчуванням. Якщо ви не хочете, щоб обчислюване поле зберігалося (наприклад, з міркувань продуктивності або щоб уникнути роздування бази даних), ви можете встановити атрибут store у значення False. Те саме стосується readonly: якщо ви хочете, щоб ваше обчислюване поле не можна було редагувати, вам потрібно встановити атрибут readonly у значення True.

Розділ CDATA використовується для вказівки XML-парсерам, що вміст є рядком, а не XML; це запобігає спробам парсера інтерпретувати код Python як XML або додавати зайві пробіли тощо, коли код вставляється в базу даних під час встановлення модуля.

Exercise

  1. Додайте обчислюване поле до моделі x_estate.property, яке повертає суму полів x_living_area та x_garden_area, як показано вище.

  2. Включити поле у представленні форми моделі x_estate.property.

Примітка

На відміну від модулів Python, неможливо визначити інверсний метод або метод пошуку для обчислюваних полів.

Код та бізнес-логіка

Дії сервера

У модулі Python ви можете вільно визначати будь-який метод у вашій моделі. Одним із поширених шаблонів використання є додавання так званих методів «дії» до вашої моделі, а потім прив’язування цих методів до кнопок в інтерфейсі користувача (наприклад, для підтвердження цінової пропозиції, публікації рахунку-фактури тощо).

У модулі даних ви можете досягти того ж ефекту, визначивши Дії сервера, пов’язаний з вашою моделлю. Дії сервера представляють собою фрагменти логіки, які динамічно виконуються на сервері. Ці дії можна налаштувати вручну в базі даних безпосередньо через меню Налаштування ‣ Технічні ‣ Дії ‣ Дії сервера і вони можуть бути різних типів; у нашому випадку ми використовуватимемо тип code, який дозволяє нам запускати будь-який код Python в ізольованому середовищі.

Це середовище містить кілька утиліт, які допоможуть вам взаємодіяти з базою даних Odoo:

  • env: середовище запису

  • model: модель запису

  • user та uid: поточний користувач та його id

  • datetime, dateutil, timezone та time: бібліотеки для допомоги в обчисленні дати/часу

  • float_compare: корисна функція для порівняння двох значень типу «float» із заданою точністю

  • b64encode та b64decode: допоміжні функції для кодування та декодування значень у base64

  • Command: допоміжний клас для створення складних виразів та команд (див. клас Command у довіднику ORM)

Крім того, ви маєте доступ до набору записів, над яким виконується дія (зазвичай це один запис, коли дія виконується з представлення форми, та кілька записів, коли дія виконується з подапредставленняння списку), через змінні record та records.

Примітка

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

Якщо модуль website встановлено, об’єкт request буде доступний у «пісочниці» коду, і ви зможете призначити об’єкт response змінній response, щоб повернути відповідь клієнту аналогічним чином. Більш детально це розглядається в розділі Контролери веб-сайту.

Наприклад, ми могли б визначити дію в моделі x_estate.property, яка встановлює поле x_status усіх її пропозицій на Refused:

<record id="action_x_estate_property_refuse_all_offers" model="ir.actions.server">
    <field name="name">Refuse all offers</field>
    <field name="model_id" ref="estate.model_real_estate_property"/>
    <field name="state">code</field>
    <field name="code"><![CDATA[
for property in records:
    property.x_offer_ids.write({'x_status': 'refused'})
    ]]></field>
</record>

Щоб включити цю дію як кнопку у вікні форми моделі x_estate.property, ми можемо додати наступний вузол button у заголовок нашого вікна форми:

<!-- form view definition from your code... -->
<header>
    <button name="estate.action_x_estate_property_refuse_all_offers" type="action" string="Refuse all offers"/>
</header>

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

<record id="action_x_estate_property_refuse_all_offers" model="ir.actions.server">
    <field name="name">Refuse all offers</field>
    <field name="model_id" ref="estate.model_real_estate_property"/>
    <field name="state">code</field>
    <field name="binding_model_id" ref="estate.model_real_estate_property"/>
    <field name="binding_view_types">tree,form</field>
    <field name="code"><![CDATA[
for property in records:
    property.x_offer_ids.write({'x_status': 'refused'})
    ]]></field>
</record>

Це зробить дію доступною у значку шестерні () моделі x_estate.property, у списку (коли один або декілька записів вибрано за допомогою прапорця) та у представленні форми.

Exercise

  1. Додайте дію сервера до моделі x_estate.property.offer, яка встановлює поле x_status пропозиції на Accepted та відповідно оновлює ціну продажу та покупця нерухомості, до якої додано пропозицію. Ця дія також повинна позначати всі інші пропозиції щодо тієї ж нерухомості як Refused.

  2. Додайте кнопку у вбудований список пропозицій, яка дозволяє виконати цю дію

../../_images/offer_accept_button.png

Перевизначення моделей Python

Через елементи інтерфейсу користувача

На відміну від модулів Python, неможливо повністю перевизначити метод моделі Python.

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

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

Щоб досягти цього, вам потрібно буде:

  • створити дію сервера, яка викликає оригінальний метод кнопки, та додати власну логіку до або після виклику цього методу

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

<record id="view_sale_order_form" model="ir.ui.view">
    <field name="name">sale.order.form.inherit.estate</field>
    <field name="model">sale.order</field>
    <field name="inherit_id" ref="sale.view_order_form" />
    <field name="arch" type="xml">
        <xpath expr="//button[@name='action_confirm'][@type='object']" position="attributes">
            <attribute name="type">action</attribute>
            <attribute name="name">estate.action_x_estate_property_create_from_sale_order</attribute>
        </xpath>
        <!-- since the button is present twice in the original view, we need to replace it twice -->
        <xpath expr="//button[@name='action_confirm'][@type='object']" position="attributes">
            <attribute name="type">action</attribute>
            <attribute name="name">estate.action_x_estate_property_create_from_sale_order</attribute>
        </xpath>
    </field>
</record>

<record id="action_x_estate_property_create_from_sale_order" model="ir.actions.server">
    <field name="name">Confirm and create property from sale order</field>
    <field name="model_id" ref="sale.model_sale_order"/>
    <field name="state">code</field>
    <field name="code"><![CDATA[
for order in records:
    order.action_confirm()
    property_type = env['x_estate.property.type'].sudo().search([('x_name', '=', 'Other')], limit=1)
    property = env['x_estate.property'].sudo().create({
        'x_name': order.name,
        'x_expected_price': 0,
        'x_selling_price': 0,
        'x_sale_order_id': order.id,
        'x_property_type_id': property_type.id,
    })
    ]]></field>
</record>

За допомогою правил автоматизації

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

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

Документація: повнішу документацію з цієї теми можна знайти у Правила автоматизації.

Примітка

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

Після встановлення, правила автоматизації керуються через меню Налаштування ‣ Технічні ‣ Автоматизація ‣ Правила автоматизації.

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

Якби ми переписали наш приклад з попереднього розділу, використовуючи правила автоматизації, знадобилося б кілька змін:

  • дія сервера більше не повинна викликати оригінальний метод кнопки (натомість оригінальний метод ініціюватиме зміну, яка запустить правило автоматизації)

  • розширення представлення не потрібне

  • нам потрібно визначити правило автоматизації, щоб ініціювати дію сервера у разі відповідної події

<record id="action_x_estate_property_create_from_sale_order" model="ir.actions.server">
    <field name="name">Create property from sale order</field>
    <field name="model_id" ref="sale.model_sale_order"/>
    <field name="state">code</field>
    <field name="code"><![CDATA[
for order in records:
    property_type = env['x_estate.property.type'].sudo().search([('x_name', '=', 'Other')], limit=1)
    property = env['x_estate.property'].sudo().create({
        'x_name': order.name,
        'x_expected_price': 0,
        'x_selling_price': 0,
        'x_sale_order_id': order.id,
        'x_property_type_id': property_type.id,
    })
    ]]></field>
</record>

<record id="automation_rule_x_estate_property_create_from_sale_order" model="base.automation">
    <field name="name">Create property from sale order</field>
    <field name="model_id" ref="sale.model_sale_order"/>
    <field name="trigger">on_state_set</field>
    <field name="trg_selection_field_id" ref="sale.selection__sale_order__state__sale"/>
    <field name="trigger_field_ids" eval="[(4, ref('sale.field_sale_order__state'))]"/>
    <field name="action_server_ids" eval="[(4, ref('estate.action_x_estate_property_create_from_sale_order'))]"/>
</record>

Зверніть увагу, що XML ID для стандартних моделей, полів, значень вибору тощо Odoo можна знайти в самому екземплярі Odoo, перейшовши до відповідного запису в технічному меню та скориставшись пунктом меню View Metadata меню налагодження. XML ID для моделей – це просто назва моделі з крапками, заміненими підкресленнями, та префіксом model_ (наприклад, sale.model_sale_order – це sale.order, як визначено в модулі sale); XML ID для полів – це назва моделі з крапками, заміненими підкресленнями, та префіксом field_, назва моделі та назва поля (наприклад, sale.field_sale_order__name – це XML ID для поля name моделі sale.order, яка визначена в модулі sale).

Контролери веб-сайту

HTTP-контролери в Odoo зазвичай визначаються в каталозі controllers модуля. У модулях даних можна визначити дії сервера, які поводяться як контролери, якщо встановлено модуль веб-сайту.

Після встановлення модуля веб-сайту дії сервера можна позначити як Available on the website та вказати шлях (повний шлях завжди починається з префікса /actions, щоб уникнути колізій URL-адрес); глобальний об’єкт request стає доступним у локальній області дії сервера коду.

Об’єкт request надає кілька методів для доступу до тіла запиту:

  • request.get_http_params(): витягує пари ключ-значення з рядка запиту та форм, присутніх у тілі запиту (як application/x-www-form-urlencoded, так і multipart/form-data).

  • request.get_json_data(): витягує дані JSON з тіла запиту.

Оскільки неможливо повернути значення з дії сервера, щоб визначити відповідь, яку потрібно повернути, можна призначити змінній response об’єкт, подібний до відповіді, який буде автоматично повертатися на веб-сайт.

Ось приклад простого контролера веб-сайту, який повертатиме список властивостей при виклику URL-адреси /actions/estate:

<record id="server_action_estate_list" model="ir.actions.server">
    <field name="name">Estate List Controller</field>
    <field name="model_id" ref="estate.model_real_estate_property" />
    <field name="website_published">True</field>
    <field name="website_path">estate</field>
    <field name="state">code</field>
    <field name="code"><![CDATA[
html = '<html><body><h1>Properties</h1><ul>'
for property in request.env['x_estate.property'].search([]):
    html += f'<li>{property.x_name}</li>'
html += '</ul></body></html>'
response = request.make_response(html)
    ]]></field>
</record>

В об’єкті request доступно кілька корисних методів для полегшення генерації об’єкта відповіді:

  • request.render(template, qcontext=None, lazy=True, **kw) для рендерингу шаблону QWeb з використанням його xmlid; додаткові ключові аргументи пересилаються до об’єкта werkzeug.Response (наприклад, для встановлення файлів cookie, заголовків тощо)

  • request.redirect(location, code=303, local=True) для перенаправлення на іншу URL-адресу; аргумент local використовується для визначення того, чи має перенаправлення бути відносно веб-сайту чи ні (за замовчуванням: True).

  • request.not_found() для повернення винятку werkzeug.HTTPException, щоб сигналізувати веб-сайту про помилку 404.

  • request.make_response(data, headers=None, cookies=None, status=200) для ручного створення об’єкта werkzeug.Response; аргумент status – це код стану HTTP, який потрібно повернути (за замовчуванням: 200).

  • request.make_json_response(data, headers=None, cookies=None, status=200) для ручного створення JSON-відповіді; дані будуть серіалізовані за допомогою JSON за допомогою утиліти json.dumps; це може бути корисним для налаштування зв’язку між серверами через виклики API.

Щоб дізнатися деталі реалізації або інші (менш поширені) методи, зверніться до реалізації об’єкта Request у модулі odoo.http.

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

Примітка

Модель, що використовується в полі model_id дії сервера, має бути доступною для публічного користувача для виконання операції запису; інакше дія сервера поверне помилку 403. Спосіб уникнути надання доступу – це пов’язати дію сервера з моделлю, яка вже доступна публічному користувачеві. Типовим (хоч і дивним) прикладом є пов’язування дії сервера з моделлю ir.filters.

Exercise

Додайте JSON API до вашого модуля, щоб зовнішні сервіси могли отримувати список об’єктів нерухомості на продаж.

  1. додати до моделі нове поле x_api_published, щоб контролювати, чи публікуються властивості в API чи ні

  2. додати запис прав доступу, щоб дозволити загальнодоступним користувачам читати та записувати модель

  3. заборонити будь-який запис від публічного користувача, додавши правило запису для операції запису з неможливим доменом (наприклад, [('id', '=', False)])

  4. додати правило запису, щоб властивості, позначені як x_api_published, могли бути прочитані публічним користувачем

  5. додати дію сервера для повернення списку властивостей у форматі JSON, коли викликається URL-адреса /actions/api/estate

Трохи JavaScript

Хоча імпортовані модулі не можуть містити файли Python, для файлів JavaScript такого обмеження не існує. Додавання файлів JavaScript до вашого імпортованого модуля відбувається точно так само, як і додавання їх до стандартного модуля Odoo.

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

Як приклад, додамо простий „tour“ до модуля Estate. Тури – це стандартний механізм в Odoo, який використовується для залучення користувачів, проводячи їх через ваш застосунок.

Дуже мінімалістичний тур з одним кроком можна додати, додавши цей файл до static/src/js/tour.js:

import { registry } from "@web/core/registry";


registry.category("web_tour.tours").add('estate_tour', {
    url: "/web",
    sequence: 170,
    steps: () => [{
    trigger: '.o_app[data-menu-xmlid="estate.menu_root"]',
    content: 'Start selling your properties from this app!',
    position: 'bottom',
    }],
});

Потім вам потрібно включити файл до відповідного пакету у файлі маніфесту:

{
    "name": "Real Estate",
    # [...]
    "assets": {
        "web.assets_backend": [
            "estate/static/src/js/tour.js",
        ],
    },
}

Примітка

На відміну від звичайних модулів Python, розширення glob не підтримується в імпортованих модулях; тому вам потрібно перерахувати кожен файл, який ви хочете включити до модуля, окремо.