Оцінка на розробку 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 переживає міграцію Odoo на наступну версію, вирішується саме тут — у виборі, якого ззовні ніхто не бачить і який усі відчувають через два роки.
Тести і що тут насправді робить AI
Кожна нетривіальна зміна отримує тести: TransactionCase для логіки ORM, тур HttpCase, коли змінюється екран, обидва з тегом post_install, щоб вони ганялися проти тих модулів, які реально стоятимуть поруч із моїм.
Я пишу їх з AI, і цю фразу розтягнули настільки, що її варто визначити. AI швидкий у механічній половині: зібрати фікстури, покрити третю й четверту гілку умови, перетворити «перевірити, що скасоване замовлення звільняє резерв» на код, який запускається. Тести раніше були першим, що викреслювали, коли оцінка тиснула. Тепер — ні, і в цьому вся практична різниця: не в тому, що тести стали кращими, а в тому, що їх тепер пишуть.
Чого AI не може — це вирішити, що саме тестувати. Він не знає, що ця конкретна інтеграція падає, коли постачальник надсилає кому як десятковий роздільник, бо цей факт живе не в коді — він живе в останніх двох роках експортів того постачальника. Вибір випадків, які заслуговують на тест, досі моя робота, і передати її я поки не навчився.
Два правила навколо цього. Згенерований тест має спершу впасти, а вже потім пройти: якщо я не можу зламати код і побачити червоне, тест не перевіряє нічого, а згенеровані тести дуже переконливо вміють не перевіряти нічого. І фікстури — це вигадані записи, а не експорт ваших клієнтів.
Стейджинг на реальних даних, потім продакшен
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, бо повна згода перезапускає взаємодії сторінки, і обробник закриття модалки спрацював уже на свіжому екземплярі, який усе ще тримав значення за замовчуванням. На сервері перевірка, що читає цю куку, очікує словник і видаляє все інше — гілка сумісності робить рівно те, для чого її написали. Тож кожен, хто натиснув «прийняти», отримував запит знову на наступній сторінці, а аналітика за ним лишалася на нулі, маючи при цьому цілком справний вигляд.
Другий дефект ховається в екрані налаштувань. Чи можна завантажити трекер, вирішує метод, який починається з «якщо банер cookie вимкнений, згода надана» — логіка тут така, що власник, який вимкнув банер, зібрав згоду якось інакше. Зніміть цю галочку — і кожен відвідувач мовчки вважається таким, що погодився, годину поспіль, бо відрендерена сторінка закешована. Ніщо в тому екрані налаштувань не має небезпечного вигляду.
Третій: подію конверсії почепили на момент життєвого циклу сторінки, який на час завантаження скрипта вже минув, бо скрипт їде у відкладеному бандлі, що стартує після завершення завантаження сторінки. Форма працювала, лід приходив, число лишалося нулем.
Усі три знайшлися однаково — відкрити живий сайт, натиснути обидві кнопки, прочитати куку і вкладку мережі замість коду. Це п'ятнадцять хвилин, і це єдине місце, де ці баги існують. Я виставляю їх у рахунок як частину роботи.
Скільки коштує наступна версія
Стаття була б нечесною, якби радила питати про оновлення, а потім ухилилася від відповіді. Оновлювати базу — робота Odoo, і роблять вони це добре. Оновлювати ваш власний код — моя, і ціна залежить від одного: скільки модуля стоїть поверх поведінки ядра, яка зрушила.
Модуль, який додає власну модель, кілька полів і кілька подань, зазвичай переживає мажорну версію за півдня правок. Модуль, який перевизначає create, патчить звіт і успадковує чотири шаблони ядра, — це тиждень. Варто знати до того, як він стане вашим: create має декоратор @api.model_create_multi і отримує список записів, а не один, тож перевизначення, написане під один набір значень, не падає голосно — воно тихо обробляє перший рядок пакетного імпорту й не помічає решти.
І оновлення йдуть версія за версією. Odoo нічого не пропускає, тож власний код переносять на кожному кроці, а не один раз у кінці.
Права — частина функціоналу
Кожна модель, яку я додаю, приїжджає з правилами доступу і з record rule, коли дані не для всіх, — написаними одночасно з моделлю. Права, прикручені до готового модуля заднім числом, — це те, як продавець починає читати маржу чужого регіону.
Найпильніше в будь-якому коді, включно зі своїм, я шукаю 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.