跳至內容

Odoo 升級遷移實戰:v16→v17→v18 Breaking Changes、OpenUpgrade 與踩坑指南

2026年9月4日 作者
Odoo 升級遷移實戰:v16→v17→v18 Breaking Changes、OpenUpgrade 與踩坑指南
Asta Systems Limited
Odoo 每年 10 月發布新版(v17、v18、v19),每次升級都伴隨大量 breaking changes——對顧問與企業 IT 來說,升級既是享受新功能,也是技術債的一次總清算。本文彙整三大版本重點變更、OpenUpgrade 實戰流程,以及我們在多個銀級伙伴項目中踩過的坑。

📑 目錄

  1. 為什麼要升級——業務與技術的雙重驅動力
  2. 三大版本 Breaking Changes 速查表
  3. 升級前的準備工作清單
  4. OpenUpgrade 實戰流程
  5. 常見程式碼遷移範例
  6. 升級後的驗證與回歸測試
  7. 踩坑經驗與最佳實踐

一、為什麼要升級——業務與技術的雙重驅動力

「現在用得好好的,幹嘛折騰升級?」——這是每個客戶第一次聽到升級建議時的反應。但 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 → v17v17 → v18影響等級
XML 視圖標籤<tree> 廢棄,改用 <list>完全移除 <tree> 支援BREAKING
視圖 attrsattrs 字典寫法廢棄,新寫法 invisible/readonly/required 直接表達式attrs 完全移除BREAKING
Python decorator@api.multi 移除(從 v15 起已無用)@api.model 行為調整DEPREC
ORM 欄位compute_sudo 預設行為變更新增 tracking=True 自動 chatterNEW
前端 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:

📋 升級準備檢查表

  1. 完整備份:資料庫(pg_dump -Fc)+ filestore 目錄(/opt/odoo/.local/share/Odoo/filestore/)+ 自訂模組源碼(Git tag)
  2. 盤點所有自訂模組:用 grep -r "odoo>=16.0" 統計依賴版本,列出每個模組的維護負責人
  3. 第三方模組來源確認:OCA 模組通常會跟進版本,付費模組需向廠商索取升級包
  4. 靜態分析:跑 flake8 + pylint-odoo,先清掉所有 warning
  5. 識別高風險模組:POS、Website、E-commerce、報表客製——這些是大改區
  6. 建立 staging:1:1 複製生產環境(規格、流量、外掛),不要直接在 dev 升完就上生產
  7. 撰寫升級計劃文件:時程、回滾方案、責任分工、客戶溝通話術
  8. 預約升級窗口:避開業務高峰期(如月結、促銷季)
🚨 血淚教訓:曾有客戶因為「備份只備了資料庫,沒備 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 自動化回歸測試清單

🧪 升級後必測場景

  1. 模組載入:所有自訂模組能正常 --update=all 無報錯
  2. 資料完整性:抽樣核對升級前後的 record 數量、總金額、關鍵欄位值
  3. 報表產出:銷售單、發票、財務報表能正常生成(格式、數字)
  4. POS 收銀流程:建單、付款、開收據、列印(最容易因 OWL 升級壞掉)
  5. 網店下單:結帳 → 付款 → 訂單生成 → 庫存扣減
  6. 使用者權限:不同群組用同一組資料測試 CRUD
  7. 效能對比:同樣操作在 v16 和 v18 的回應時間(v18 通常更快但要驗證)
  8. 排程任務:確認所有 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 流程到上線後維護,我們有完整的項目方法論。

📧 聯絡我們取得免費升級評估報價

網誌: 科技文章
Odoo 客製化模組開發實戰 從 ORM 模型到部署升級的完整攻略
不改核心程式碼,也能讓 ERP 完美貼合業務。以一個「產品保修登記」模組為例,走完 Odoo 開發的完整生命週期。