Оценка разработки Odoo — это обычно одно число: часы. О том, что происходит внутри этих часов, оно не говорит ничего. А решается там ровно одно: будет ли модуль Odoo работать после следующего подъёма версии.
Поэтому — порядок работы на моих проектах. Почти всё в нём наросло рубцами с версии 15 по 19.
До модуля Odoo появляются два документа
Первый — замысел: что система должна делать иначе в тот день, когда работа закончена, и написано это языком бизнеса. «Кладовщик сканирует код, и строка закрывается» — замысел. «Добавить поле в stock.move.line» — нет, это уже ответ, и обычно неверный. В девяти случаях из десяти сканированию нужно правило штрихкодовой номенклатуры и переопределение на существующей приёмке, а не новое поле где бы то ни было. Замысел должен держаться на языке кладовщика достаточно долго, чтобы это было ещё видно.
Второй — архитектура, и вот здесь разработка Odoo перестаёт быть похожей на разработку вообще. Модуль Odoo не живёт один. Он сидит внутри чужого кода: один большой релиз в год, патч почти каждую неделю, от компании, которая не знает о вашем существовании. Поэтому архитектура отвечает на более узкий набор вопросов.
Какие существующие модели затронуты и хватит ли наследования. Что добавляется: поле, метод, представление, маршрут. И что остаётся нетронутым. Какое штатное поведение переопределяется — и та ли это вообще форма переопределения: _inherit расширяет модель на месте, а _inherits тихо добавляет внешний ключ и делегированную запись, и вот так в системе заводится лишняя строка на каждый товар, а почему — уже никто не помнит.
Which edition the system runs on, because a plan built around an Enterprise model is not a plan on Community. How many companies, websites and languages one instance has to serve: forty websites in a single Odoo instance is a different architecture from one.
И вопрос, который пропускают надёжнее всего: откуда берутся данные и что делает интеграция Odoo в три часа ночи, когда вторая сторона перестаёт отвечать. «Повторяет два раза, потом откладывает заказ и пишет мне на почту» — это архитектура. Тишина — тоже архитектура, просто её никто не выбирал.
Архитектуру читают до того, как её построили
Когда эта страница есть, вы её видите. Решения, не код: вот эти три модели трогаем, эту — нет, интеграция Odoo идёт через очередь заданий, поэтому неудачный вызов к поставщику превращается в запись, которую можно повторить и на которую можно посмотреть, а всё по расписанию работает как ir.cron с блокировкой — потому что задание, которое считает, что никогда не пересечётся само с собой, однажды ошибётся.
Разбор занимает около часа, и это последний дешёвый момент передумать. Передумали после реализации — платите за неделю. Передумали после запуска — платите ещё и за две недели, которые склад вводил всё по два раза.
Здесь же я говорю то, что стоит мне денег: иногда честный ответ — Studio, или модуль из магазина приложений, который поддерживает кто-то другой, или вообще ничего. Odoo это уже умеет — плохо, но сносно, — а свой модуль добавит год поддержки ради десяти сэкономленных минут в месяц. Свой модуль Odoo — самый дорогой из этих вариантов, и выигрывать он должен по существу.
Реализация: наследовать модель Odoo, а не копировать
Правило скучное: расширять, никогда не копировать. Скопированный штатный файл прекрасно работает до обновления, а после — он ваш навсегда.
Честное исключение одно. У некоторых штатных методов нет шва: нечего обернуть в super(), не за что зацепиться. Когда такой метод приходится заменить целиком, в копии стоит комментарий с именем исходного файла, веткой и версией, откуда она взята, — чтобы на следующем обновлении было что с чем сравнить, а не загадка.
На практике: наследование для моделей, xpath для представлений, QWeb для отчётов, переопределения, которые всё-таки вызывают оригинал. Поля, добавленные к штатной модели, носят префикс модуля — чтобы штатное поле с тем же именем, приехавшее через две версии, не легло поверх моего, и чтобы по имени поля было видно, какой модуль его поставил. Префикс x_ я не трогаю: он принадлежит Studio.
Две ловушки стоит назвать, потому что обе находятся ценой выкладки. Наследование представления не примет произвольный атрибут как селектор: xpath по aria-label отвергается сразу. А hasclass(...) не находит ничего, когда родительский шаблон собирает этот класс через t-attf-class, — потому что атрибута class в исходнике просто нет. Обе ошибки вылезают только при установке модуля, то есть после того, как статические проверки были зелёными. Я читаю сырой XML того шаблона, который расширяю, и никогда — отрисованный HTML, где классы уже склеены и сломанный селектор выглядит правильным.
Ещё одно, про что заказчики разумно думают наоборот: удаление модуля сносит добавленные им колонки и данные в них. Поэтому удаление проверяется на копии, и если в поле лежит то, что бизнесу ещё понадобится, это решают до того, как поле написано, а не в день, когда кто-то ставит галочку.
Почти всё, из-за чего модуль Odoo переживает миграцию на следующую версию, решается здесь — в выборе, которого снаружи не видно и который все чувствуют через два года.
Тесты и что здесь на самом деле делает ИИ
Каждое нетривиальное изменение получает тесты: TransactionCase на логику ORM, тур HttpCase, когда меняется экран, и оба с меткой post_install, чтобы они шли против тех модулей, которые реально будут стоять рядом с моим.
Пишу я их с ИИ, и эту фразу растянули достаточно, чтобы её стоило определить. Он быстр в механической половине: собрать фикстуры, закрыть третью и четвёртую ветку условия, превратить «проверить, что отменённый заказ снимает резерв» в работающий код. Раньше тесты первыми шли под нож, когда оценка поджимала. Теперь — нет, и в этом вся практическая разница: не в том, что тесты стали лучше, а в том, что их теперь пишут.
Чего он не может — решить, что тестировать. Он не знает, что вот эта интеграция ломается, когда поставщик присылает запятую как десятичный разделитель, потому что этот факт живёт не в коде — он живёт в двух последних годах выгрузок этого поставщика. Выбирать, какие случаи заслуживают теста, по-прежнему моя работа, и передать её я пока не научился.
Два правила вокруг этого. Сгенерированный тест должен сначала упасть и только потом пройти: если я не могу сломать код и увидеть красное, тест не проверяет ничего, а сгенерированные тесты очень убедительно не проверяют ничего. И фикстуры — это выдуманные записи, а не выгрузка ваших клиентов.
Стенд на реальных данных, потом боевой сервер
A change goes to staging first, on a copy of the real database. Demo data is polite. It has no customers with two spaces in the name, no products imported years ago under rules nobody wrote down, no invoices in a currency that was discontinued. It also has no volume: a search that answers instantly on demo data took eight seconds on a hundred and ten thousand products.
Копия вашей базы — всё ещё ваша база, с вашими клиентами внутри, поэтому перед работой её обезвреживают: исходящая почта выключена, задания по расписанию выключены, платёжные провайдеры — на тестовых ключах. Иначе первый же прогон процесса на стенде напишет тремстам живым людям с сервера, которого вообще-то не существует.
На стенде работу проверяет тот, кто её просил, на своих собственных записях. Потом боевой сервер, по написанному порядку: какой модуль обновляем, какой скрипт миграции идёт до обновления, а какой после, и что происходит с данными, которые уже лежат в базе. Опубликованные материалы — обычный сюрприз: запись, загруженная с noupdate, не перезаливается, что с ней ни делай, поэтому текст, который уже живёт на сайте, меняют записью в саму запись из миграции — или никак. И откат, который я хотя бы раз действительно прогонял. Предполагаемый откат — не откат.
Дефекты, которые вылезают только на живом сайте
Here is the part of Odoo development that estimates never mention. Some defects need a real cache and a real visitor before they exist at all, and a test database has neither. I have written separately about what holds and what breaks in a live Odoo system; these three are from one week on this very site.
Баннер согласия записывал ответ посетителя в неверном формате. «Только необходимые» клали корректный JSON, «Принять» клало строку true — потому что полное согласие перезапускает взаимодействия страницы, и обработчик закрытия окна отработал на свежем экземпляре, всё ещё державшем значение по умолчанию. На сервере проверка, читающая эту куку, ждёт словарь и удаляет всё остальное: ветка совместимости делает ровно то, для чего её написали. В итоге каждого, кто нажал «принять», спрашивали снова на следующей странице, а аналитика за этим стояла на нуле, выглядя при этом прекрасно установленной.
Второй прячется в экране настроек. Можно ли грузить счётчик, решает метод, который начинается со слов «если плашка согласия выключена, согласие дано»: логика в том, что владелец, выключивший плашку, собрал согласие как-то иначе. Снимите эту галочку — и каждый посетитель молча считается согласившимся, и так час подряд, потому что отрисованная страница закеширована. Ничто в этом экране настроек не выглядит опасным.
Третий: событие конверсии повесили на момент жизненного цикла страницы, который к загрузке скрипта уже прошёл, — потому что скрипт едет в отложенном наборе, стартующем после того, как страница догрузилась. Форма работала, заявка приходила, число стояло на нуле.
Все три нашлись одинаково: открыть живой сайт, нажать обе кнопки, прочитать куку и вкладку сети вместо кода. Это пятнадцать минут, и это единственное место, где такие ошибки существуют. Я считаю их частью работы и выставляю в счёт.
Во что обходится следующая версия
Статья была бы нечестной, если бы советовала спрашивать про обновления, а сама от вопроса ушла. Обновление базы — работа Odoo, и делают они её хорошо. Обновление вашего кода — моя, и цена зависит от одного: насколько модуль стоит на штатном поведении, которое сдвинулось.
Модуль, который добавляет свою модель, несколько полей и пару представлений, обычно переживает большую версию за полдня правок. Модуль, который переопределяет create, правит отчёт и наследует четыре штатных шаблона, — это неделя. Стоит знать заранее: create идёт как @api.model_create_multi и принимает список записей, а не одну, поэтому переопределение, написанное под один набор значений, не падает громко — оно тихо обрабатывает первую строку пакетного импорта и не замечает остальных.
И миграция Odoo идёт версия за версией. Odoo ничего не перепрыгивает, поэтому свой код переносят на каждом шаге, а не один раз в конце.
Права — часть задачи
Каждая добавленная мной модель приезжает со своими правами доступа, а когда данные не для всех — ещё и с правилом записи, написанным одновременно с моделью. Права, прикрученные к готовому модулю, — это ровно то, как продавец начинает читать маржу чужого региона.
Сильнее всего в чужом коде — и в своём — я ищу sudo(). Он выключает все правила разом, это самый быстрый способ заставить ошибку прав исчезнуть, а в публичном контроллере это утечка данных с удобным адресом.
Доводка после запуска
Модуль заканчивается примерно через две недели после запуска, когда возвращается список мелких неудобств: этому полю нужно значение по умолчанию, на этом экране на клик меньше, этот отчёт читают каждое утро в восемь — значит, он должен быть пунктом меню, а не фильтром, который кто-то заново расставляет руками и по понедельникам ошибается.
Проход короткий. И он же — разница между модулем, который проходит тесты, и модулем, который перестают обходить стороной. Пропустите его — получите технически верную функцию и таблицу рядом с ней, делающую настоящую работу.
Один вопрос, который стоит задать
Ничего из этого вы не вспомните, сидя напротив разработчика. Поэтому вот вопрос, который я задал бы на вашем месте:
Что станет с этим модулем, когда Odoo выпустит следующую версию?
Если в ответе назван конкретный риск — вот это переопределение стоит на методе, который Odoo трогает часто, поэтому его перепроверяют на каждом обновлении, — человек уже делал миграцию Odoo. «Всё будет нормально» тоже бывает правдой: для модуля, который добавляет два поля и список. Слушать надо, могут ли вам сказать, какой из двух — ваш.
That question works on anyone you are considering, and the answer tells you more than a badge does — which is most of the difference between an Odoo partner and an independent developer.
If you have a system that runs and a change you are not sure how to make, write to me. The first conversation is about what should be different when the work is done, not about hours.