Одна из величайших сил Odoo , помимо его Активное сообщество , это расширяемость, обеспечиваемая через модули.
В основе этого расширяемости лежит модель наследования, с концепцией повторно открытых моделей.
Модели расширения
Упрощенное определение партнер модель:
from odoo import models class ResPartner(models.Model) _name = "res.partner" name = fields.Char(required=True) # ... other fields def _address_fields(self): """Returns the list of address fields that are synced from the parent.""" return list(ADDRESS_FIELDS)
Любой модуль может расширить модель, добавить поля или методы. Здесь, модуль partner_address_street3 который добавляет поля третьей улицы:
class ResPartner(models.Model): _inherit = "res.partner" street3 = fields.Char('Street 3') def _address_fields(self): fields = super()._address_fields()[:] fields.append('street3') return fields
Если вы знакомы с Odoo Development, вы, вероятно, уже знаете это. Если это новое для вас и заинтересовано в более подробной информации, вы можете начать с начало Анкет
За кулисами Магия этого наследства состоит в том, что, когда начинается Odoo, оно находит все подклассы модели. Модель ( фактически модели. Базодель ) и создает график после зависимости модулей – определенных в __manifest__.py В каждом модуле – и используя имя модели в _ наследовать атрибуты. Оттуда это динамически создает совершенно новый агрегированный класс с MRO генерируется из своего графика классов моделей. Этот последний класс – это то, что используется для реальных случаев моделей.
Возможности расширений являются мощными, но у них также есть слабость, поощряемая рамками, чтобы выращивать то, что некоторые называют жирными моделями. Каждый модуль добавляет свои собственные методы и логику в модели, что может привести к таким проблемам, как столкновения в пространстве имен, плохая читабельность из -за смешивания отзывов или дублированной логики.
Мне нравится принцип единственной ответственности и идея композиции над наследством, когда это имеет смысл. Хорошо развязанные классы также легче тестировать.
При этом мы можем сказать, что я ищу способ определить «классы обслуживания», классы, которые могут делать одну вещь и делать это хорошо. Использование класса Python не будет решением, так как он не был бы расширим другими модулями Odoo. Но… У odoo это уже есть.
Это не широко используется для этой цели, но класс модели. AbstractModel
может быть описан как модель, которая не поддерживается таблицей баз данных, поэтому у нее нет полей, только чистого питона. Его название может сбивать с толку, потому что оно не «абстрактно» в чистом ООП -определении класса, который не может быть создан. Это действительно касается хранилища базы данных.
Две основные цели, которые я видел для абстрактных моделей:
- «Реальные» абстрактные модели, которые затем включаются в обычные модели, чтобы обмениваться и предоставлять дополнительные поля или логику (Mixin/Multy Wonditance’s Way)
- Услуги, которые связаны с темой этой статьи
Вот как можно определить сервис, основанную на абстрактной модели:
class SettlePaymentService(models.AbstractModel): _name = "settle.payment.service" def settle(self, payment): client = self._psp_client() client.settle(payment.id) # ...
И модель может затем использовать эту услугу с:
class PaymentTransaction(models.Model): _inherit = "payment.transaction" def button_settle(self): self.env["settle.payment.service"].settle(self)
Если я говорю об услугах, это потому, что это то, о чем представлены компоненты. Достаточно ли абстрактные модели для этого варианта использования? Да. Нет, это не так. “Это зависит.” Часто это довольно хороший ответ на технический вопрос. Я опишу, что они могут сделать, и позволю вам судить, нуждаетесь ли вам или нет.
Начнем с компонентов
Немного контекста
Компоненты предоставляются Одуационная ассоциация сообщества модуль. Вы можете найти его на GitHub
OCA/Connector
Odoo Generic Connector Framework (очередь заданий, асинхронные задачи, каналы)
🔥 Это Не только Для разработки разъемов он размещен в этом проекте, потому что он родился из этого варианта использования и является зависимостью для модуля соединителя.
Он имеет долгую историю, так как я начал работать над первой версией Connector в 2013 году, но к тому времени у него была совсем другая форма. Я написал модуль компонента и переосмыслил модуль разъема на него в 2017 году, что означает, что он был доступен с Odoo 10.0.
Некоторые из модулей, реализованных на компонентах на моей головке, являются различными разъемами для различных услуг (Jira, Magento, …) Odoo Rest Framework , Эди Проект для Odoo, набор хранения , применение сканера штрих -кода OCA WMS проект… Дайте мне знать в комментариях о случаях использования, которые вы знаете!
Как определить компонент?
Определение компонента очень похоже на модель:
from odoo.addons.component.core import Component class SettlePayment(Component) _name = "settle.payment.service" def settle(self, payment): client = self._psp_client() client.settle(payment.id)
И наследство работает так же, как и по модулям:
class SettlePayment(Component) _inherit = "settle.payment.service" def settle(self, payment): _logger.info("added a logger when we settle") super().settle(payment)
Вы отметили, что до тех пор, пока нет, это выглядит довольно похоже на примеры с моделями выше, с разумом. Классы и механизм наследования одинаковы, но:
-
Компонент
Занятия никогда не имеют таблицы баз данных. - Нет
_inherits
так как это имеет смысл только для моделей с таблицами._inherit
работает так же. - Нет эквивалента
TransientModel
, это не имеет никакого смысла без стола. - Есть
AbstractComponent
Класс, который не может быть инстаанчин, он используется только в качестве «базовой интерфейс/базовой реализации» для других компонентов.
Вы можете получить компонент по его названию от модели, что -то вроде того, когда вы получите модель от self.env
:
class PaymentTransaction(models.Model): _inherit = "payment.transaction" def button_settle(self): with self.env["payment.collection"].work_on(self._name) as work: settle_service = work.component_by_name("settle.payment.service") settle_service.settle(self)
Но это не совсем то, как это предназначено для использования! Кроме того, я обещаю, что вернусь позже к тому, что это work_on ()
около.
Сопоставление компонентов с помощью использования
Здесь мы действительно начинаем, держись!
Используя модели. AbstractModel
Для услуг – это хорошо, пока вам не понадобятся более динамичные вещи.
Например: если тип платежа является «банком», используйте сервис Bankexportservice
, если тип «CreditCard», используйте SettlePaymentservice
. Или экспорт «Res.Partner» должен быть выполнен с использованием CSV в службу SFTP, а также экспорт «продукта. Продукт» с использованием API REST.
Это может быть полностью хорошо с некоторыми условиями в модели, которые бы назвали одну из услуг. Или вы можете использовать динамические свойства компонентов. В первом случае звонящий несет ответственность за выбор службы с компонентами, служба выбирается во время выполнения в зависимости от контекста без вмешательства на стороне вызывающего абонента.
Каждый компонент назначается _usage
Анкет Реальная жизнь Использование «record.exporter», «record.importer», «search», «record.validator», «webservice.request», … Это Это то, что будет использоваться для поиска необходимого вам компонента для действия, которое вы хотите запустить.
С этим новым обучением давайте снова напишем компонент:
class RecordExporter(Component): _name = "generic.record.exporter" _usage = "record.exporter" def export(self, record): _logger.info("the generic exporter only logs data: %s", record.read())
Вызывающий компонент:
class ResUsers(models.Model): _inherit = "res.users" def button_export(self): self.ensure_one() with self.env["my.collection"].work_on(self._name) as work: work.component(usage="record.exporter").export(self)
Почему лучше найти мой компонент с помощью использования, а не по имени? Потому что имя фиксированное и это то, что используется для определения зависимостей. Если модуль, который я не контролирует, определяет базовые компоненты, и они получают по имени в середине куски кода, я застрял. Если они получат использование, я могу Используйте наследство ( _inherit
), чтобы изменить использование начального компонента и создать совершенно новый или вариант компонента с ожидаемым использованием ( _usage
). Это также то, что позволяет динамическому отправке компонентов, как мы увидим в следующем разделе.
По меньшим словам: имя определяет наследование, использование определяет, какой компонент используется во время выполнения.
Хочу больше? Здесь мы получаем компонент с использованием с work.component () . Когда вы находитесь внутри компонента, вы можете использовать self.component () . Это метод, который вы будете использовать больше всего. Предполагается, что для контекста существует только один компонент для того же использования, в противном случае он не знал бы, какой из них использовать. В некоторых случаях вы можете получить все компоненты для использования и можете использовать || work.many_components ()
Применить на … модели?
Когда я говорил о динамической отправке (возможно, на языковом злоупотреблении, но у меня нет лучших слов), вот пример.
На компонентах _apply_on
Атрибут определяет, какие модели могут использоваться компонент. Значение по умолчанию – Нет
, что означает все модели.
Я продолжаю использовать универсальный экспортер, определенный выше, но добавлю другой экспортер для продуктов:
class CSVRecordExporter(Component): _name = "csv.record.exporter" _inherit = "generic.record.exporter" _usage = "record.exporter" _apply_on = ["res.partner"] def export(self, record): csv_data = self._generate_csv(record) self._export_to_sftp(csv_data)
Метод экспорта здесь строит CSV и подталкивает его к SFTP. _inherit
не является строго обязательным, но часто имеет смысл. Реальное дополнение для примера – _apply_on
Анкет
И вызывающий компонент … не изменился:
class ResPartner(models.Model): _inherit = "res.partner" def button_export(self): self.ensure_one() with self.env["my.collection"].work_on(self._name) as work: work.component(usage="record.exporter").export(self)
Код на 100% такой же, как и код для пользователей, верно? И все же, когда мы называем Button_export
Данные будут регистрироваться только для пользователей и будут экспортироваться как CSV для партнеров.
_name
Из двух экспортеров отличается, но нам не пришлось менять вызывающий абонент услуги. Надеемся, что вы должны начать видеть потенциал для обмена кодами и расширяемости (в реальном примере, Button_export
Затем можно добавить в абстрактную модель и включить как в пользователях, так и в модели партнеров, тогда один и тот же метод возвращает соответствующий компонент для каждой модели).
Хочу больше? В компоненте вы можете объявить _ component_match Метод, чтобы очень точно определить, какой компонент выбрать. Следующий компонент можно использовать только в том случае, если для текущего контекста (|| workcontext ||, предоставленное || work_on () ||) совпадает с условием. Введите полноэкранный режим Выйдите из полноэкранного режима Мы можем дать дополнительный контекст “konycontext”: Введите полноэкранный режим Выйдите из полноэкранного режима
Коллекция и work_on ()
Я обещал, что вернусь к этому work_on
Мы видели в более ранних фрагментах кода.
Чтобы объяснить это, давайте начнем с причины, по которой он должен существовать. Поскольку компоненты очень поливалентные, используемые для многих различных вариантов использования, различными классами модулей, проблема, которой я должен был избежать, были столкновения с имен. Что если я установите модуль A, который определяет record.exporter
и модуль B делает то же самое? Мы могли бы добавить префиксы вручную в использовании, но это кажется неуклюжим.
Дополнительный атрибут существует в компонентах … Коллекция.
Каждый компонент назначен коллекции. Как правило, базовый модуль для реализации разъема, общей структуры, такой как отдых или эди … Коллекция может быть “magento.backend” или “edi.backend”.
С другой стороны, каждая коллекция имеет свой собственный набор компонентов. Коллекция – это реестр компонентов. Компоненты _name
S являются глобальными, но для данного использования компоненты в конечном итоге будут отфильтрованы на текущей коллекции.
Компонент с набором сбора:
class RecordExporter(Component): _name = "record.exporter" _collection = "foo.collection"
🌈 Вы довольно часто увидите коллекцию слов и бэкэнд, используемые взаимозаменяемо, потому что исторически модуль разъема использовал исключительно термин «бэкэнд». Последний не имел смысла для более общей компонентной системы, поэтому она стала сбором.
Что тогда из этой коллекции? Это относится к модели, которая станет ссылкой для этой коллекции. Это может быть Модель
или AbstractModel
в зависимости от необходимости. Преимущество Модель
это то, что он может хранить поля, такие как URL -адрес службы, учетные данные, …
class FooCollection(models.Model): _name = 'foo.collection' _inherit = 'collection.base'
_name
модели должен соответствовать _collection
своих компонентов, и модель должна _inherit
от Collection.base
.
Теперь к work_on
Контекст -менеджер. Это сделано доступным Collection.base
. Он инициализирует и возвращает WorkContext
, который является контейнером для текущего контекста (сбор, модель, ENV и произвольные значения), который создает клей между текущим «контекстом» (не контекстом ODOO, но сбором, моделью и значениями, которые мы хотим использовать в компонентах).
✨ WorkContext
немного похоже на env
Odoo, но для компонентов.
work_on ()
Морозирует разрыв между моделями ODOO и компонентами. Из модели это необходимо для достижения компонентов.
class FooCollection(models.Model): _name = 'foo.collection' _inherit = 'collection.base' def export_all(self, records): with self.work_on(records._name) as work: work.component(usage="export.batch").export(records)
Или из другой модели:
class ResPartner(models.Model): _inherit = 'res.partner' def sync_from_foo(self): with self.env["foo.collection"].work_on("res.partner") as work: work.component(usage="sync").run_sync(self)
Однажды в компонент, work_on ()
больше не требуется, так как каждый компонент удерживает WorkContext
и способен достичь других компонентов.
Вот упрощенный пример, который использовал бы другие компоненты для экспорта записи:
class FooSync(Component): _name = "foo.sync" _collection = "foo.collection" _usage = "sync" def run_sync(self, record): api_client = self.component(usage="webservice") data = api_client.read(record.id) odoo_data = self.component(usage="mapper").map(data) record.write(odoo_data)
Этот минимальный компонент показывает, что мы можем разделить работу в небольших услугах с их собственными обязанностями. Здесь у нас есть одно данные о чтении услуг из веб -сервиса и вторая служба, которая отображает внешние данные с словарем, который мы можем подавать в write ()
Анкет
Хочу больше? Стоит отметить, что _ имя а также _ наследовать Атрибуты работают так же, как и модели Odoo, поэтому для них требуется префиксы. Это меньше проблема, так как имя должно использоваться только для определения наследования. Работая над реализацией с использованием компонентов, я советую определить || MyCollectionBaseComponent (AbstractComponent) || это устанавливает || _collection || , затем заставить все компоненты унаследовать от него: вам не нужно будет установить || _collection || Везде, и получите хорошее преимущество в том, что смогут настроить все компоненты вашей реализации сразу, если это необходимо (очень похоже на || basemodel ||). Когда вы звоните || work_on () || , например, вы можете передать произвольные аргументы, если у вас есть несколько компонентов, которые необходимо использовать клиентскую трансверверскую API, вы можете открыть соединение один раз, передать его в || WorkContext || и используйте его из компонентов. Введите полноэкранный режим Выйдите из полноэкранного режима Из компонента: Введите полноэкранный режим Выйдите из полноэкранного режима Вы на самом деле можете создать компонент без сбора, который затем может быть извлечен из любого контекста. Но это действительно можно использовать с осторожностью, поскольку оно загрязняет глобальное пространство имен. Хорошей практикой, если вы хотите поделиться компонентами в нескольких реализациях, является создание || AbstractComponent || без сбора и реализовать компонент в каждой коллекции. Таким образом, вы можете поделиться кодом, но сохранить больше контроля над тем, когда/как его можно использовать. Компоненты обнаруживаются по умолчанию в соответствии с моделью, переданной || work_on () || , но || work.component () || Метод также принимает || model_name || Параметр, который найдет компонент для другой модели!
Резюме
Компоненты используются для создания сервисных классов. Они зарегистрированы в коллекциях (реестры компонентов). У них есть система наследования, похожая на классы модели.
Чтобы иметь возможность работать с компонентами, мы используем диспетчер контекста work_on ()
в коллекции. Когда мы спрашиваем компонент для данного использования, реестр коллекции будет соответствовать компонентам в этом порядке:
- Найдите совпадение
Компонент
Для данного использования (_usage
) для текущей коллекции (_collection
) и модель (_apply_on
) - Найдите совпадение
Компонент
Для данного использования (_usage
) для текущей коллекции (_collection
) и любая модель (no_apply_on
на компоненте) - Найдите совпадение
Компонент
Для данного использования (_usage
) для любой коллекции (no_collection
на компоненте) и любая модель (нет_apply_on
на компоненте)
В любом из приведенных выше шагов, в случае нескольких кандидатов метод _component_match
призывается к каждому кандидату, чтобы ограничить дальнейшее матч.
AbstractComponent
никогда не возвращаются механизмом диспетчеры, их можно использовать для обмена кодом в конкретных компонентах.
Следующий?
Может быть, вам интересно, что такое «разъемы»? Может быть, я должен написать об этом однажды, но в нескольких словах: Разъем Модуль-это набор предварительно определенных компонентов, специализирующихся для реализации разъемов с внешними службами.
Спасибо, что прочитали это далеко!
Оригинал: “https://dev.to/guewen/introduction-to-odoo-components-bn0”