Ниже — подробные ответы в логике актуальной документации Odoo 19 и текущей практики экосистемы. Для старых веток отдельные детали могут отличаться, но базовые механизмы те же. (Odoo)
- Как устроен модуль в Odoo?
Типичный модуль Odoo — это Python-пакет с __manifest__.py и __init__.py, внутри которого обычно лежат подкаталоги по зонам ответственности. Обязательного “жёсткого” набора папок нет, но де-факто структура такая: models/ — ORM-модели и бизнес-логика; views/ — XML-описания форм, списков, search view, actions, menus; security/ — права доступа (ir.model.access.csv) и record rules; data/ — справочные/служебные записи, sequence, cron, mail templates, конфигурация; demo/ — демонстрационные данные; wizards/ или wizard/ — временные модели и их представления для мастеров; report/ — отчёты; controllers/ — HTTP-контроллеры; static/ — JS/CSS/изображения; tests/ — тесты. __manifest__.py описывает метаданные модуля, зависимости и списки загружаемых файлов. (Odoo)
Важно понимать, что это не просто “папки для порядка”: именно через manifest Odoo понимает, какие XML/CSV загружать, в каком порядке, какие модули должны стоять раньше, и какие хуки вызывать. Поэтому модуль в Odoo — это не только код, а связка из Python, ORM-описаний, XML-представлений, security-правил и данных инициализации. (Odoo)
- Как реализуются зависимости между модулями? Как сделать, чтобы один модуль устанавливался вместе с другим?
Зависимости задаются в __manifest__.py через поле depends. Это означает: перед установкой текущего модуля Odoo должен установить перечисленные зависимости, а также загрузить их модели, поля, XML id и расширения. Если ваш модуль использует модель, view, security group или поле из другого модуля — этот модуль должен быть в depends. (Odoo)
Если нужно, чтобы модуль всегда тянул за собой другой, самый прямой способ — указать его в depends. Тогда установка текущего модуля автоматически приведёт к установке зависимого. Но если речь о “связующем” модуле, который должен ставиться только когда установлены оба базовых модуля, для этого обычно используют auto_install, а не просто жёсткий depends. (Odoo)
- Что делает
auto_install=Trueв manifest.py? Когда это использовать?
auto_install=True говорит Odoo: модуль нужно установить автоматически, если выполнены условия по его зависимостям. Классический сценарий — link module, который интегрирует два других модуля. Например, есть модуль продаж и модуль склада, а третий модуль лишь добавляет между ними интеграцию. Тогда этот третий модуль можно сделать auto_install=True, и он подтянется сам, когда установлены его зависимости. (Odoo)
Есть важный нюанс: auto_install может быть не только булевым, но и списком. В таком случае автоустановка произойдёт, когда уже установлены модули из этого списка, а недостающие зависимости тоже будут дотянуты. Это удобно для технических “мостов”, локализаций, интеграционных прослоек и функциональности, которая не нужна сама по себе, а только как дополнение к уже имеющемуся стеку. (Odoo)
- Какие типы данных используются в Odoo (
demo,data)? В чём разница?
В manifest обычно есть два ключевых списка файлов: data и demo. Файлы из data загружаются как обычные данные модуля: security, views, actions, menus, sequences, cron, templates, базовые настройки, справочники и так далее. Это “боевые” данные, которые нужны модулю для работы. (Odoo)
demo — это демонстрационные данные. Они загружаются только если база/установка разрешает demo-data (--with-demo), и обычно используются для примеров, обучения, тестовых сценариев, showcase-данных для продаж/складов/CRM. В production их обычно не ставят. Практически: data — обязательно для функциональности, demo — только для примеров и стендов. (Odoo)
- Что означает
noupdate="1"? Как влияет на обновление модуля? Можно ли менять такие записи?
В XML-файлах Odoo блок <data noupdate="1"> означает: записи будут загружены при установке, но при последующих обновлениях модуля не будут автоматически переписываться данными из XML. Это нужно для записей, которые после установки ожидаемо живут своей жизнью: например, права, настройки, email-шаблоны, cron-задачи, которые администратор может вручную править. (Odoo)
При этом noupdate не делает запись неизменяемой. Её можно менять через ORM, UI или SQL. Смысл только в том, что апдейт модуля не перезапишет её содержимое из XML. Если всё же нужно обновить такую запись доставкой модуля, это обычно делают через upgrade script/миграцию, а не рассчитывают на обычную повторную загрузку XML. Для служебных случаев есть и режимы реинициализации, влияющие на noupdate-записи, но это уже скорее инструмент обновления/разработки, а не штатная бизнес-практика. (Odoo)
- В QWeb используется
t-inherit-mode. Какие режимы существуют и чем отличаются?
В актуальном QWeb есть два основных режима: primary и extension. primary создаёт новый дочерний шаблон, наследующий исходный. То есть вы не портите родительский template, а строите свой поверх него. Это правильно, когда вам нужен отдельный шаблон-результат, который опирается на базовый. (Odoo)
extension работает иначе: он изменяет родительский шаблон на месте. Это режим для патчинга существующего шаблона через XPath и похожие механизмы. Его применяют, когда задача — не сделать новый template, а встроиться в уже существующий экран/отчёт/сниппет. На интервью хороший ответ такой: primary — “сделать новый шаблон-наследник”, extension — “модифицировать базовый template in place”. (Odoo)
- Разница между
_inheritи_inherits
_inherit — это классическое расширение существующей модели. Вы добавляете поля, методы, constraints, override-ите поведение, но логически продолжаете жить в той же модели. Обычно это один и тот же business object, просто расширенный. Например, вы добавили поле и метод в res.partner. Это самый частый сценарий. (Odoo)
_inherits — это делегирование. У вашей модели есть собственная таблица, но она хранит ссылку(и) на “родительскую” модель через foreign key, и поля делегированной модели прозрачно доступны как будто свои. Это ближе к композиции, чем к обычному наследованию. Используют, когда нужен новый бизнес-объект со своей жизнью и таблицей, но при этом хочется повторно использовать поля другой модели без копирования. Кратко: _inherit — расширяю существующую модель; _inherits — строю новую модель поверх другой через делегирование. (Odoo)
- Зачем после
cr.execute()вызыватьinvalidate_cache()?
Потому что ORM Odoo агрессивно кэширует поля recordset’ов. Если вы сделали UPDATE/INSERT/DELETE напрямую через SQL, ORM об этом сам не узнает, а значит в рамках того же environment/request может продолжать отдавать старые значения из кэша. В итоге вы видите “призраки”: в БД данные уже новые, а через ORM ещё старые. (Odoo)
Это критично после любых изменяющих SQL-операций (CREATE, UPDATE, DELETE). После них нужно инвалидировать соответствующий кэш модели/recordset. Если изменённые поля участвуют в зависимостях stored computed fields, одной инвалидизации мало: ORM ещё нужно сообщить, что поля были модифицированы, чтобы запустить пересчёты зависимых вычисляемых полей. Если этого не сделать, можно получить несогласованность между БД, кэшем и вычисляемыми значениями. (Odoo)
- Какие реляционные поля есть в Odoo? Когда применять
Many2one,One2many,Many2many?
Основные реляционные поля — Many2one, One2many, Many2many. Many2one — это ссылка на одну запись другой модели. Классический пример: заказ имеет одного клиента, invoice имеет одну компанию, строка заказа относится к одному order. Это “обычный foreign key”. (Odoo)
One2many — обратная сторона Many2one. Оно не существует само по себе: для One2many должен быть соответствующий Many2one на другой модели. Это удобно для “у объекта есть набор дочерних строк”: у заказа — строки заказа, у партнёра — контакты, у счёта — линии. (Odoo)
Many2many — когда с обеих сторон может быть много записей: товар принадлежит многим тегам, пользователь состоит во многих группах, группа содержит многих пользователей. Практическое правило такое: Many2one — главный рабочий FK; One2many — удобно показывать и управлять набором дочерних записей; Many2many — когда нужна симметричная или почти симметричная связь “многие ко многим”. (Odoo)
- Что такое
NewID? Когда встречается?
NewID — это внутренний ORM-псевдоидентификатор для ещё не сохранённой записи. Он нужен, чтобы Odoo мог работать с “виртуальными” объектами до фактической вставки строки в БД: в onchange, в new(), во временных x2many-линиях формы и в других сценариях, где запись уже существует в памяти как ORM-объект, но ещё не имеет реального integer id из PostgreSQL. По сути, это маркер “эта запись уже есть в ORM-контексте, но ещё не flush/create в БД”. (GitLab)
На практике NewID чаще всего всплывает в отладке, ошибках onchange, в доменах/логике на несохранённых строках One2many и при работе с record.new(vals). На интервью достаточно объяснить: это временный псевдо-id несохранённой записи. (GitHub)
- Разница между
compute,inverseиonchange
compute — это способ вычислить значение поля из других полей. Поле объявляется вычисляемым, а метод compute присваивает ему значение. Если поле stored, его значение может храниться в БД и пересчитываться при изменении зависимостей (@api.depends). (Odoo)
inverse нужен для обратной записи: computed field по умолчанию read-only, а inverse позволяет пользователю/коду записать значение в computed field, а метод уже разложит это изменение по исходным данным. Иными словами: compute считает поле “вперёд”, inverse раскладывает введённое значение “назад” в зависимости. (Odoo)
onchange — это вообще другой механизм. Он работает на уровне формы в web client: пользователь меняет поле, Odoo вызывает метод и возвращает изменения интерфейсу без сохранения в БД. Документация прямо рекомендует не строить на onchange критическую бизнес-логику, потому что он не срабатывает при обычном программном create/write, а привязан к формам. Практически: compute/inverse — часть модели и инвариантов; onchange — UI-удобство. (Odoo)
- Что делает
check_companyв полях связей?
check_company=True на реляционном поле добавляет автоматическую проверку согласованности компаний. Odoo будет следить, чтобы связанная запись была допустима с точки зрения multi-company логики текущей записи и активной компании. Это защита от ошибок вида “документ компании A ссылается на справочник компании B”. (Odoo)
Использовать его нужно там, где межмодельная связь должна уважать границы компании: бухгалтерия, склады, контрагенты/журналы/счета, документы с company_id. Особенно полезно на Many2one, где пользователь вручную выбирает связанную запись. Это не замена всей multi-company-логике, но очень полезный встроенный guardrail. (Odoo)
- Если выполнили SQL напрямую, что сделать, чтобы изменения стали видны ORM?
Правильная последовательность такая. Сначала, если ORM мог ещё не сбросить свои pending changes в БД, нужно сделать flush соответствующих полей/моделей. Затем выполнить SQL. После изменяющего SQL — инвалидировать кэш затронутой модели или recordset. Если вы меняли поля, от которых зависят stored computed fields, дополнительно сообщить ORM через modified(...), чтобы зависимые поля пересчитались. (Odoo)
То есть ответ на интервью хороший такой: “перед raw SQL — flush, после raw SQL — invalidate cache, а при изменении зависимостей вычисляемых полей — ещё и modified()/recompute”. Просто cr.execute() сам по себе недостаточен, если вы хотите сохранить консистентность ORM-состояния. (Odoo)
- Что такое lazy loading в Odoo?
В терминологии практиков Odoo lazy loading — это то, что поля записи не читаются все подряд заранее. ORM загружает значения по мере первого доступа к ним, а затем держит их в кэше environment. Это снижает ненужные запросы и не тащит из БД всё подряд “на всякий случай”. (Odoo)
Но lazy loading в Odoo почти сразу сочетается с prefetch: как только вы обратились к полю одной записи из recordset’а, ORM обычно подгружает это же поле и набор простых stored fields для более широкого recordset’а. Поэтому практический ответ: lazy loading — отложенная загрузка при первом обращении, а prefetch — её оптимизация против N+1 запросов. (Odoo)
- Как работает
prefetch_fieldsи как влияет на производительность?
Базовый механизм такой: когда вы читаете поле у записи, ORM не ограничивается только этой одной ячейкой. Он использует кэш и prefetch-эвристику: подгружает поля для более широкого recordset’а, а все простые stored fields одной таблицы часто читаются одним SQL-запросом. Это сильно уменьшает число запросов и борется с N+1-проблемой. (Odoo)
На практике также используется контекстный флаг prefetch_fields=False — его можно встретить и в коде Odoo — чтобы отключить/сократить эту эвристику в специальных сценариях, например при очень больших проходах по данным, где избыточный prefetch раздувает память и тащит слишком много лишнего. Поэтому влияние на производительность двоякое: по умолчанию prefetch обычно ускоряет, но в отдельных heavy-duty случаях его ослабляют ради контроля памяти и лишней выборки. (GitHub)
- Что такое mixins в Odoo? Как работают?
Mixins в Odoo — это переиспользуемые модели/классы, которые добавляют группе моделей общий набор возможностей через наследование. Документация прямо описывает mixins как Odoo-модели, предоставляющие полезные функции через inheritance. Самые типовые примеры — mail.thread, mail.activity.mixin, image.mixin. (Odoo)
Используют их, когда нужно подключить типовое поведение без копипаста: chatter, активности, изображения, utm, portal, rating и т.д. Это часто сочетается с _inherit: вы либо строите собственную модель и наследуете mixin, либо расширяете существующую модель и добавляете ей mixin-функциональность. (Odoo)
- Чем динамические отчёты отличаются от обычных?
Термин “динамический отчёт” в Odoo не всегда формализован одинаково, но обычно под “обычными” понимают классические QWeb-отчёты: есть ir.actions.report, шаблон QWeb/HTML и, если нужен PDF, рендер через wkhtmltopdf. Это статический документ, который вы генерируете на основе recordset’а и печатаете/скачиваете. (Odoo)
Под “динамическими” чаще имеют в виду интерактивные отчёты в стиле accounting/reporting engine: пользователь меняет фильтры, разворачивает строки, делает drill-down, пересчитывает представление без генерации фиксированного печатного документа. В Odoo это часто строится на моделях/линиях/выражениях отчётного движка и клиентской отрисовке. То есть обычный отчёт — это “сгенерируй документ”, а динамический — “дай интерактивное аналитическое представление”. (Odoo)
- Что такое abstract model?
AbstractModel — это модель без полноценной самостоятельной таблицы данных, предназначенная для общего API, shared logic и переиспользуемого поведения. Документация прямо выделяет Model, TransientModel и AbstractModel, причём для abstract-моделей _auto=False по умолчанию. Идея такая: abstract model существует, чтобы от неё наследовались другие модели, а не чтобы бизнес-пользователь создавал её записи как самостоятельный объект. (Odoo)
Использовать её стоит для базовых классов, сервисных слоёв, mixins, общих методов, общего контрактного API. Если вам не нужен самостоятельный бизнес-объект с отдельной жизнью в БД, а нужна только общая логика — abstract model подходит лучше обычной модели. (Odoo)
- Что такое transient model? Для каких задач предназначена?
TransientModel — это временная модель: записи в БД у неё есть, но они считаются временными и автоматически очищаются (vacuum) через некоторое время. Документация прямо связывает с ними wizard’ы. Это не “совсем не сохраняется”, а “сохраняется временно и не является долговременным бизнес-объектом”. (Odoo)
Типовые задачи — мастера действий, временные формы, промежуточные параметры, settings-визарды, подтверждения, импортные шаги. Очень характерный признак: такие записи живут ради UI-процесса/сценария, а не как часть долгосрочных предметных данных компании. (Odoo)
- Разница между abstract и transient моделями
AbstractModel — это, по сути, шаблон/база для наследования, обычно без собственной предметной таблицы. Её задача — дать общий код и интерфейсы. TransientModel — наоборот, вполне реальные записи в БД, но временные, с автоматической очисткой. (Odoo)
Поэтому кратко: abstract — для переиспользования логики; transient — для временных данных процесса. Если вам нужен общий метод do_something() для десятка моделей — abstract. Если нужен wizard “подтвердить операцию и выбрать параметры” — transient. (Odoo)
- Что такое миграции в Odoo? Когда нужны и как реализуются?
Миграции — это код, который приводит существующую БД и данные в соответствие новой версии модуля. Они нужны, когда меняются модели, поля, XML ID, связи, справочники, названия, логика хранения или требуется преобразование уже существующих данных. Документация про custom DB upgrade отдельно подчёркивает случаи с переименованием моделей/полей/XML ID и нюансы noupdate-данных. (Odoo)
Реализуются миграции через upgrade scripts: Python-файлы с функцией migrate(cr, version) в дереве вида $module/migrations/$version/ или $module/upgrades/$version/, с фазами pre, post, end. Внутри можно выполнять SQL и ORM-операции, а для сложных апгрейдов использовать odoo.upgrade.util. Это правильный инструмент для версионных преобразований, а не hooks. (Odoo)
- Что такое hooks в Odoo? Какие типы существуют и где объявляются?
Hooks — это специальные функции жизненного цикла модуля, которые объявляются в manifest. В актуальной документации manifest перечисляет pre_init_hook, post_init_hook и uninstall_hook. Они используются для действий до инициализации, после установки и при удалении модуля. (Odoo)
Объявляются они в __manifest__.py, а сами функции должны быть доступны модулю через __init__.py. По текущей документации hooks получают env. Типичные сценарии: разовая инициализация данных, сложная подготовка окружения, cleanup при uninstall. Но для обычного обновления версии и трансформации данных лучше использовать миграции, а не hooks. (Odoo)
- Как работает транзакция в Odoo? Когда commit и rollback?
Фреймворк Odoo обычно сам открывает SQL-курсор/транзакцию на время одного RPC/API-вызова. Если выполнение прошло успешно, транзакция коммитится. Если в процессе было исключение, происходит rollback. В JSON-2 API документация прямо пишет, что каждый вызов выполняется в собственной SQL-транзакции: успех — commit, ошибка — discard/rollback. Coding guidelines также прямо предупреждают: вручную коммитить без очень веской причины не надо. (Odoo)
Практический вывод: пишите бизнес-операции так, чтобы целостное действие укладывалось в один вызов/транзакцию. Тогда у вас меньше шансов поймать частично применённые изменения. Отдельные специальные сценарии — shell, cron, batch processing — могут коммитить иначе, но базовая модель именно такая: один запрос — одна транзакция. (Odoo)
- Как работает savepoint? Где используется и зачем?
savepoint — это внутренняя точка частичного отката внутри уже открытой транзакции. Если кусок кода внутри savepoint падает, можно откатить только этот участок, не убивая всю внешнюю транзакцию. Это удобно, когда вы хотите попробовать рискованную операцию, но не потерять всё остальное. (Odoo)
В Odoo savepoint используется и в тестовой инфраструктуре (SavepointCase), и в прикладном коде вокруг участков, где локальная ошибка допустима. Но ими нельзя злоупотреблять: coding guidelines отдельно предупреждают, что слишком много savepoint’ов в одной транзакции бьёт по производительности PostgreSQL. (Odoo)
- Что при одновременном доступе к одним и тем же записям? Как Odoo предотвращает race conditions?
Главная защита — не “магия Odoo”, а транзакционная модель PostgreSQL плюс правильная организация бизнес-методов. Разные RPC/API-вызовы идут в разных транзакциях, и если вы разнесли критическую бизнес-операцию на несколько отдельных вызовов, между ними могут вклиниться конкурентные изменения. Документация JSON-2 прямо предостерегает от таких сценариев для резервирований, платежей и подобных случаев. (Odoo)
Для критических секций Odoo использует и row locking-паттерны. В документации по cron можно увидеть try_lock_for_update() с повторной проверкой домена уже после захвата блокировки. Плюс есть API-методы, которые объединяют действия в одну транзакцию, чтобы избежать race между ними; пример — search_read, где документация отдельно говорит, что так устраняется гонка между отдельными search и read. Хороший инженерный ответ: Odoo опирается на транзакции БД, constraints и блокировки строк; а разработчик обязан держать критические операции атомарными. (Odoo)
- Как работает
ensure_one()? Когда использовать?
ensure_one() проверяет, что recordset содержит ровно одну запись. Если записей 0 или больше 1, метод выбрасывает ошибку. Это защитный механизм для методов, которые по смыслу должны работать только на singleton: открыть форму конкретного документа, построить URL одной записи, провести один платёж, напечатать один чек. (Odoo)
Важно не злоупотреблять ensure_one(). В Odoo ORM очень много методов естественно работают батчами, и насильственное сведение всего к singleton ухудшает производительность и делает код менее “odoo-way”. Используйте ensure_one() там, где семантика действительно однообъектная, а не просто “так проще было написать”. (Odoo)
- Разница между миграциями и хуками
Миграции — это версионные преобразования данных/схемы при обновлении модуля. Они живут в migrations/ или upgrades/, завязаны на версию и запускаются именно во время update. Это инструмент для эволюции уже существующих баз. (Odoo)
Hooks — это точки жизненного цикла установки/удаления модуля, объявленные в manifest. Их задача — разовые действия вокруг install/uninstall, а не сопровождение версионных изменений по цепочке релизов. Поэтому правило простое: меняете существующие данные между версиями — миграция; нужно особое действие до/после установки или при uninstall — hook. (Odoo)
- Что такое
queue_jobв Odoo? Как работает и зачем?
queue_job — это не core Odoo, а популярный модуль экосистемы OCA для асинхронной очереди задач. Его назначение — откладывать выполнение методов Odoo в фон, чтобы не держать пользователя в HTTP-запросе и не выполнять тяжёлую работу синхронно в UI-потоке. OCA описывает его как integrated job queue / asynchronous jobs. (GitHub)
Нужен он для интеграций, импорта/экспорта, синхронизаций, массовых операций, длительной генерации документов, задач с retry и фоновой обработкой. Архитектурно идея простая: бизнес-метод превращается в job, jobrunner подбирает его и исполняет отдельно, часто с каналами, повторами и мониторингом состояния. На интервью хорошо подчеркнуть: это стандартный инструмент асинхронности в Odoo-экосистеме, но не встроенное ядро фреймворка. (GitHub)
- Как задать значение по умолчанию для поля?
Способов несколько. Самый прямой — атрибут default= у поля: можно поставить литерал или callable. Второй важный механизм — default_get(), если значение зависит от контекста, пользователя, других полей или сложной логики инициализации. Документация ORM отдельно описывает default_get() как метод построения значений по умолчанию. (Odoo)
Ещё есть defaults через context: ключи вида default_my_field прокидывают значение по умолчанию при открытии форм/создании записей. Coding guidelines отдельно предупреждают, что такие ключи могут неожиданно “утечь” дальше по цепочке создания других объектов, если не контролировать контекст. И ещё один источник — ir.default, который используется для пользовательских/компанейских default-значений. (Odoo)
- Разница между
@api.constrainsи_sql_constraints? Что раньше срабатывает?
@api.constrains — это Python-уровень. Метод вызывается ORM на записях, где были изменены указанные поля, и должен бросать ValidationError, если бизнес-правило нарушено. Это удобно для сложных проверок, межполейной логики, условий, которые неудобно или невозможно выразить чистым SQL. (Odoo)
_sql_constraints — это ограничения уровня базы данных. Они быстрее и надёжнее для инвариантов типа UNIQUE, CHECK, простых неотрицательностей и вообще всего, что БД умеет выразить сама. Документация прямо говорит, что SQL constraints обычно эффективнее Python constraints и их стоит предпочитать, если это возможно. (Odoo)
Что “раньше” — на собеседовании лучше отвечать аккуратно: не надо полагаться на порядок, надо полагаться на слой ответственности. Python constraint живёт в ORM-логике, SQL constraint — в PostgreSQL и является финальной гарантией при фактическом INSERT/UPDATE. Поэтому практическое правило такое: всё, что можно жёстко и дёшево гарантировать на уровне БД, держите в _sql_constraints; всё, что является сложной бизнес-валидацией, держите в @api.constrains. (Odoo)