Odoo 每年 10 月發布新版(v17、v18、v19),每次升級都伴隨大量 breaking changes——對顧問與企業 IT 來說,升級既是享受新功能,也是技術債的一次總清算。本文彙整三大版本重點變更、OpenUpgrade 實戰流程,以及我們在多個銀級伙伴項目中踩過的坑。
📑 目錄
- 為什麼要升級——業務與技術的雙重驅動力
- 三大版本 Breaking Changes 速查表
- 升級前的準備工作清單
- OpenUpgrade 實戰流程
- 常見程式碼遷移範例
- 升級後的驗證與回歸測試
- 踩坑經驗與最佳實踐
一、為什麼要升級——業務與技術的雙重驅動力
「現在用得好好的,幹嘛折騰升級?」——這是每個客戶第一次聽到升級建議時的反應。但 Odoo 的版本策略有兩個關鍵特點:
- 每年 10 月發布新版(v17 Oct 2023、v18 Oct 2024、v19 Oct 2025)
- 官方只維護最近 3 個版本——超出後不再發布安全補丁
這意味著停留在 v16 的客戶,理論上已在維護窗口的邊緣。升級的動力通常來自三方面:
1. 安全性合規
企業客戶(尤其是金融、醫療、製造)對 CVE 修復有硬性要求。Odoo 過期版本不會再有安全補丁,這對運行在公網的 Odoo 是真實風險。
2. 業務新功能
每個版本都會帶來核心模組的體驗升級:
- v17:全新文件管理 Knowledge、生產排程重寫、報表引擎現代化
- v18:原生 AI 欄位(自動填寫)、POS 大改、Website Builder 拖拽編輯
- v19:MCP 原生整合、更深度的自動化、財務報表 AI 助手
3. 技術債清理
每次升級都是強迫自己 review 客製代碼的機會。我們在多個項目中發現:升級過程中清理掉的 dead code、有害繼承,往往讓系統性能提升 20-40%。
⚠️ 經驗法則:不要跳級升級(如 v14 直接到 v18)。中間版本的 breaking changes 會疊加,問題排查難度指數級上升。建議每年小升一級(v16→v17→v18),每次項目化推進。
二、三大版本 Breaking Changes 速查表
下表整理了 v16→v17→v18 對開發者影響最大的變更。完整清單請參考官方 CHANGELOG.md。
| 影響層面 | v16 → v17 | v17 → v18 | 影響等級 |
|---|---|---|---|
| XML 視圖標籤 | <tree> 廢棄,改用 <list> | 完全移除 <tree> 支援 | BREAKING |
| 視圖 attrs | attrs 字典寫法廢棄,新寫法 invisible/readonly/required 直接表達式 | attrs 完全移除 | BREAKING |
| Python decorator | @api.multi 移除(從 v15 起已無用) | @api.model 行為調整 | DEPREC |
| ORM 欄位 | compute_sudo 預設行為變更 | 新增 tracking=True 自動 chatter | NEW |
| 前端 OWL | 從 legacy 到 OWL 2,static/src/ 結構調整 | OWL 元件生命周期改寫法 | BREAKING |
| QWeb 模板 | 部分模板命名空間變更 | Website 模板結構大幅調整 | DEPREC |
| POS | 後台介面重構 | POS 從 OWL 1 升級 OWL 2,大量內部 API 變動 | BREAKING |
| 報表 | QWeb engine 升級,部分內建 helper 移除 | 新增 t-out 替代 t-raw(XSS 防護) | BREAKING |
| Website Builder | — | 視圖儲存格式變更,舊網站需重建 | BREAKING |
| 安全性 | CSRF token 機制調整 | 預設關閉 debug 模式 | DEPREC |
2.1 XML 視圖:<tree> → <list>(最常踩坑)
v17 起官方宣告 <tree> 廢棄,v18 完全移除。如果你有大量客製的 list 視圖,升級前必須全文搜尋替換。
<!-- v16 寫法(v18 已失效) -->
<tree string="訂單列表" decoration-info="state == 'draft'">
<field name="name"/>
<field name="partner_id"/>
<field name="amount_total"/>
</tree>
<!-- v18 正確寫法 -->
<list string="訂單列表" decoration-info="state == 'draft'">
<field name="name"/>
<field name="partner_id"/>
<field name="amount_total"/>
</list>
OpenUpgrade 不會自動處理這個。需要手動寫 migration script(後面會示範)。
2.2 attrs 移除:字典式寫法 → 直接表達式
v16 之前的字典寫法冗長且難讀,v17 起鼓勵新寫法,v18 完全移除:
<!-- v16 舊寫法(v18 已失效) -->
<field name="delivery_date"
attrs="{'invisible': [('state', '=', 'draft')],
'required': [('is_express', '=', True)]}"/>
<!-- v18 新寫法:直接表達式,更易讀也更快 -->
<field name="delivery_date"
invisible="state == 'draft'"
required="is_express"/>
2.3 報表 t-raw → t-out(安全強化)
v18 起 t-out 自動 HTML escape,徹底解決 t-raw 帶來的 XSS 風險:
<!-- 舊:可能 XSS --> <t t-raw="doc.partner_id.comment"/> <!-- 新:自動轉義 --> <t t-out="doc.partner_id.comment"/> <!-- 確實需要 HTML:用 t-out 但仍過濸(推荐白名單方式) --> <t t-out="sanitize_html(doc.partner_id.comment)"/>
三、升級前的準備工作清單
升級失敗的項目,90% 不是升級本身出問題,而是「沒準備好就上」。下面這份清單是我們項目裏的標準 SOP:
📋 升級準備檢查表
- 完整備份:資料庫(pg_dump -Fc)+ filestore 目錄(/opt/odoo/.local/share/Odoo/filestore/)+ 自訂模組源碼(Git tag)
- 盤點所有自訂模組:用 grep -r "odoo>=16.0" 統計依賴版本,列出每個模組的維護負責人
- 第三方模組來源確認:OCA 模組通常會跟進版本,付費模組需向廠商索取升級包
- 靜態分析:跑 flake8 + pylint-odoo,先清掉所有 warning
- 識別高風險模組:POS、Website、E-commerce、報表客製——這些是大改區
- 建立 staging:1:1 複製生產環境(規格、流量、外掛),不要直接在 dev 升完就上生產
- 撰寫升級計劃文件:時程、回滾方案、責任分工、客戶溝通話術
- 預約升級窗口:避開業務高峰期(如月結、促銷季)
🚨 血淚教訓:曾有客戶因為「備份只備了資料庫,沒備 filestore」,升級後使用者頭像、產品圖、附件全失。filestore 跟 DB 是強綁定的,必須一起備份。
四、OpenUpgrade 實戰流程
OpenUpgrade 是 OCA 維護的開源升級框架,能自動處理 70-80% 的常見變更(欄位重命名、表結構調整、預設資料遷移)。剩下的 20-30% 需要手寫 migration script。
4.1 環境準備
# 1. 克隆 OpenUpgrade(注意選擇對應目標版本分支) git clone https://github.com/OCA/OpenUpgrade.git cd OpenUpgrade git checkout 18.0 # 目標是升到 v18 就切 18.0 分支 # 2. 安裝依賴 pip install -r requirements.txt # 3. 準備新的 Odoo 18 環境 # 從官網下載對應版本的 deb/rpm,或用 docker docker pull odoo:18.0
4.2 升級流程(命令列實戰)
# === 階段 1:分析與乾跑 ===
# 先做一次「乾跑」,讓 OpenUpgrade 列出所有需要執行的遷移
odoo-bin \
--database= odoo_prod_upgrade \
--upgrade-path=/path/to/OpenUpgrade/openupgrade_scripts \
--update=all \
--stop-after-init \
--no-http \
--log-level=info
# === 階段 2:執行預分析 ===
odoo-bin \
--database= odoo_prod_upgrade \
--update=all \
--load=base,openupgrade_framework \
--stop-after-init \
--no-http \
--log-file=/tmp/upgrade.log
# === 階段 3:實際升級 ===
# 注意:這一步會對每個模組執行 pre-migrate 和 post-migrate 腳本
odoo-bin \
--database= odoo_prod_upgrade \
--update=all \
--upgrade-path=/path/to/OpenUpgrade/openupgrade_scripts \
--load=base,openupgrade_framework \
--stop-after-init \
--log-file=/tmp/upgrade.log 2>&1
# === 階段 4:升級完成後啟動 ===
odoo-bin \
--database= odoo_prod_upgrade \
--addons-path=/opt/odoo/addons,/opt/odoo/custom \
--db-filter=^odoo_prod_upgrade$ \
--http-port=8069
4.3 Migration Script 結構
每個自訂模組都需要對應版本提供 migrations/ 目錄,結構如下:
# 自訂模組目錄結構 my_module/ ├── __manifest__.py ├── models/ ├── views/ ├── migrations/ # 升級腳本目錄 │ ├── 16.0.1.0.0/ │ │ ├── pre-migrate.py # 升級前執行 │ │ └── post-migrate.py # 升級後執行 │ ├── 17.0.1.0.1/ │ │ ├── pre-migrate.py │ │ └── post-migrate.py │ └── 18.0.1.0.2/ │ ├── pre-migrate.py │ └── post-migrate.py
💡 版本號命名規則:{原版}.{目標版}.{次版本}.{修訂號}。例如 16.0.1.0.0 是 v16 的 1.0.0 版,升到 v18 後變 18.0.1.0.2。
五、常見程式碼遷移範例
以下是 v16→v18 升級中最高頻的遷移場景。
5.1 欄位重命名 + 資料遷移
場景:原本有 delivery_days(整數),現在要改成 delivery_deadline(日期)。
pre-migrate.py(v16→v17)
# 升級前先把資料搬到新欄位(舊欄位此時還在)
def migrate(cr, version):
if version == '16.0.1.0.0':
# 把舊欄位資料複製到新欄位(搭配 ORM 改動後的新欄位)
cr.execute("""
ALTER TABLE sale_order
ADD COLUMN IF NOT EXISTS delivery_deadline date;
UPDATE sale_order
SET delivery_deadline = CURRENT_DATE + (delivery_days || ' days')::interval
WHERE delivery_days IS NOT NULL;
""")
新版 models 定義
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
delivery_deadline = fields.Date(
string="交期",
tracking=True, # v18 新功能:自動寫入 chatter
)
5.2 XML 視圖批量替換(<tree> → <list>)
OpenUpgrade 不會幫你改視圖,需要手寫:
# pre-migrate.py:批量替換
import os, re
def migrate_tree_to_list(module_path):
pattern = re.compile(r'<tree(\s|>)')
for root, _, files in os.walk(module_path):
for f in files:
if f.endswith('.xml'):
p = os.path.join(root, f)
with open(p, 'r', encoding='utf-8') as fp:
content = fp.read()
new_content = pattern.sub(r'<list\1', content)
if new_content != content:
print(f"Patched: {p}")
with open(p, 'w', encoding='utf-8') as fp:
fp.write(new_content)
def migrate(cr, version):
# 對所有自訂模組執行
for module in ['my_module_a', 'my_module_b']:
migrate_tree_to_list(f'/opt/odoo/custom/{module}')
5.3 Python decorator 清理
v17 起 @api.multi 已無作用(所有方法預設 multi),v18 會直接報錯:
# v16 寫法(v18 已失效)
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.multi # ← 刪掉這個
def action_confirm(self):
super().action_confirm()
self._create_warranty()
return True
# v18 寫法
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
super().action_confirm()
self._create_warranty()
return True
5.4 QWeb 報表 t-raw 替換
# pre-migrate.py:批次替換 t-raw 為 t-out
def migrate(cr, version):
import os, re, glob
pattern = re.compile(r't-raw=')
for xml in glob.iglob('/opt/odoo/custom/*/report/*.xml', recursive=True):
content = open(xml, encoding='utf-8').read()
new = pattern.sub('t-out=', content)
if new != content:
print(f"Fixed: {xml}")
open(xml, 'w', encoding='utf-8').write(new)
六、升級後的驗證與回歸測試
升級完成 ≠ 升級成功。沒有驗證的升級是定時炸彈。
6.1 自動化回歸測試清單
🧪 升級後必測場景
- 模組載入:所有自訂模組能正常 --update=all 無報錯
- 資料完整性:抽樣核對升級前後的 record 數量、總金額、關鍵欄位值
- 報表產出:銷售單、發票、財務報表能正常生成(格式、數字)
- POS 收銀流程:建單、付款、開收據、列印(最容易因 OWL 升級壞掉)
- 網店下單:結帳 → 付款 → 訂單生成 → 庫存扣減
- 使用者權限:不同群組用同一組資料測試 CRUD
- 效能對比:同樣操作在 v16 和 v18 的回應時間(v18 通常更快但要驗證)
- 排程任務:確認所有 cron 還在執行(升級有時會重置 cron 狀態)
6.2 快速健康檢查 SQL
-- 1. 確認升級後無明顯資料流失
SELECT
relname AS table_name,
n_live_tup AS row_count,
pg_size_pretty(pg_relation_size(relid)) AS size
FROM pg_stat_user_tables
WHERE schemaname = 'public'
ORDER BY n_live_tup DESC
LIMIT 20;
-- 2. 檢查是否有遺留的舊欄位(OpenUpgrade 應已清理)
SELECT table_name, column_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND column_name LIKE '%legacy%'
OR column_name LIKE '%old%';
-- 3. 確認所有外鍵完整
SELECT conrelid::regclass, conname
FROM pg_constraint
WHERE contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_class WHERE oid = conrelid
);
七、踩坑經驗與最佳實踐
坑 1:客製模組依賴官方 deprecated API
症狀:升級後某模組啟動失敗,日誌顯示 AttributeError: 'X' object has no attribute 'Y'。
原因:客製代碼調用了官方內部方法,官方重構後方法被刪。
解法:升級前先用 grep -r "_" 找出所有調用底層方法的位置,逐一替換為官方公開 API。
坑 2:Website Builder 客製區塊整片失效
症狀:v18 升級後,網站自訂頁面排版全部變形或空白。
原因:v18 改了 Website view 的內部 schema(snippet 結構、key 命名)。
解法:在 staging 先升級並截圖對比,準備「重建 vs 修補」的兩手方案。我們遇到過一個項目,重建比重修快 3 倍。
坑 3:POS IoT 硬體驅動不相容
症狀:升級後 POS 收銀機、印表機、錢箱不工作。
原因:POS IoT 是獨立元件(基於 Raspberry Pi),有自己的版本對應關係。
解法:檢查 POS IoT 對 Odoo 版本的支援矩陣,必要時同步升級 IoT image。
最佳實踐彙整
| 階段 | 最佳實踐 |
|---|---|
| 規劃 | 每版本停 6 個月穩定期再升;不要為了新功能立刻跳 |
| 準備 | 完整備份(含 filestore)+ Git tag + staging 1:1 復刻 |
| 執行 | OpenUpgrade 先乾跑 + 寫 pre/post 腳本 + 保留 v16 環境 1 個月可隨時回滾 |
| 驗證 | SQL 健康檢查 + 自動化測試 + 業務方 UAT 雙重確認 |
| 上線 | 避開業務高峰 + 監控 24 小時 + 緊急回滾 SOP 準備好 |
| 沉澱 | 記錄每個 migration script,未來可重用;建立客戶的版本路線圖 |
💼 給客戶的真實建議:升級不是一次性事件,而是持續的過程。把每年的升級計畫寫進 IT roadmap(與 ERP 廠商簽訂維護合約時就鎖定升級窗口),比「撐到不能撐才升級」的成本低得多。
需要專業團隊協助你的 Odoo 升級?
ASTA Systems Ltd 是香港 Odoo 銀級伙伴(Odoo Silver Partner),專注為企業客戶提供 Odoo 升級遷移、客製開發與系統整合服務。從升級評估、OpenUpgrade 流程到上線後維護,我們有完整的項目方法論。
📧 聯絡我們取得免費升級評估報價