はじめに
odooには、External APIが備わっており、Excel等のVBAを使って、odooモデルのデータにアクセスすることができます。
External API は、External JSON-2 API に移行されます(2028年秋のodoo22.0)
現:External API
https://www.odoo.com/documentation/19.0/developer/reference/external_api.html
新:External JSON-2 API
https://www.odoo.com/documentation/19.0/developer/reference/external_api.html
External APIを使うには、XMLやJSONの知識が必要ですが、odoo-json-rpc-vba を使えば、直接的にADODB.Recordsetにデータを抽出できます。
これは、VBA開発者にとって朗報です。
odoo-json-rpc-vba 付属のサンプルコード
odoo-json-rpc-vba には、サンプルコードが付属していますので、odoo開発環境を用意すれば、すぐに試すことができます。
尚、サンプルコードの詳細については、AIに聞いてみてください。
付属のサンプルコード
Public Sub DoExampleOdx()
Dim oModelView As OdxModelView
Dim oDomain As OdFilterDomain
Dim oNames As OdFilterDomain
Dim oClient As OdClient
Dim oCtx As OdxContext
Dim wb As Workbook
Dim sht As Worksheet
Dim rng As Range
Dim colNameList As New Collection
Dim colTagList As New Collection
Dim v As Variant
Dim s As String
Set oClient = GetAuthConn()
Set oCtx = NewContext(oClient)
Set wb = Workbooks.Add
Set sht = wb.Sheets(1)
SetWorksheetName sht, "OdxModelView Example"
Set rng = sht.Range("A1")
' target name
s = "Mitchell"
colTagList.Add s
colNameList.Add "Mitchell Admin", s
s = "Marc"
colTagList.Add s
colNameList.Add "Marc Demo", s
s = "Gemini"
colTagList.Add s
colNameList.Add "Gemini Furniture", s
DebugRange rng, "--------------"
DebugRange rng, " DoExampleOdx"
DebugRange rng, "--------------"
' ==================
' New OdxModelView
' ==================
Set oModelView = oCtx.NewModelView("res.partner")
With oModelView
' -------------
' res.partner
' -------------
DebugRange rng, .ModelName
DebugRangeSchema rng, .ModelName, .GetModelSchema()
.AddField "name"
.AddField "is_company"
.AddField "is_public"
With .AddField("company_id") ' many2one
' --------------------------
' res.company
' --------------------------
DebugRange rng, "company_id: many2one --> " & .ModelName
Debug.Assert .IsMany2One
DebugRangeSchema rng, .ModelName, .GetModelSchema()
.AddField "name"
.AddField "city"
With .AddField("currency_id") ' many2one
' --------------------------
' res.currency
' --------------------------
DebugRange rng, "company_id: many2one --> currency_id: many2one --> " & .ModelName
Debug.Assert .IsMany2One
DebugRangeSchema rng, .ModelName, .GetModelSchema()
.AddField "name"
.AddField "symbol"
End With
.AddField "layout_background"
End With
With .AddField("child_ids") ' one2many
' --------------------------
' res.partner
' --------------------------
DebugRange rng, "child_ids: one2many --> " & .ModelName
Debug.Assert .IsOne2Many
DebugRangeSchema rng, .ModelName, .GetModelSchema()
.AddField "name"
End With
With .AddField("category_id") ' many2many
' --------------------------
' res.partner.category
' --------------------------
DebugRange rng, "category_id: many2many --> " & .ModelName
Debug.Assert .IsMany2Many
DebugRangeSchema rng, .ModelName, .GetModelSchema()
.AddField "name"
End With
End With
' ==================
' Fetch data
' ==================
Set oNames = NewDomain
For Each v In colNameList
oNames.AddArity v
Next v
Set oDomain = NewDomain
oDomain.AddArity NewField("name").IsIn(oNames)
oModelView.ExecuteSearchRead oDomain
' ==================
' All
' ==================
oModelView.ClearFilter ' Unfiltered
With oModelView
DebugPrintModelView wb, .RefMe
With .GetRelatedModelView("company_id", True)
DebugPrintModelView wb, .RefMe
With .GetRelatedModelView("currency_id", True)
DebugPrintModelView wb, .RefMe
End With
End With
With .GetRelatedModelView("child_ids", True)
DebugPrintModelView wb, .RefMe
End With
With .GetRelatedModelView("category_id", True)
DebugPrintModelView wb, .RefMe
End With
End With
' ==================
' Filtered
' ==================
For Each v In colTagList
s = colNameList(CStr(v))
oModelView.SetFilter "name = '" & s & "'" ' Filtered
With oModelView
DebugPrintModelView wb, .RefMe, CStr(v)
With .GetRelatedModelView("company_id")
DebugPrintModelView wb, .RefMe, v & "(company_id)"
With .GetRelatedModelView("currency_id")
DebugPrintModelView wb, .RefMe, v & "(currency_id)"
End With
End With
With .GetRelatedModelView("child_ids")
DebugPrintModelView wb, .RefMe, v & "(child_ids)"
End With
With .GetRelatedModelView("category_id")
DebugPrintModelView wb, .RefMe, v & "(category_id)"
End With
End With
Next v
sht.Select
wb.Saved = True
wb.Activate
End Sub
AIの要約
OdxModelView は「モデル → フィールド → 関連モデル → domain → search_read → Excel 出力」を一括で扱うための VBA ORM。
DoExampleOdx はその全機能を網羅したサンプル。
実用サンプル(販売アプリ)
実用的なサンプルとして、販売アプリ(sale.order)を中心として、関連モデル(partner / order_line / product / taxes)をたどり、Excelシートに抽出・表示してみます。
モデル:sale.order
- partner_id(顧客)
- order_line(明細行-複数)
- product_id(商品)
- product_template_attribute_value_ids(商品バリアントの属性値セット)
Public Sub DoExampleOdx_SaleOrder()
Dim oModelView As OdxModelView
Dim oDomain As OdFilterDomain
Dim oClient As OdClient
Dim oCtx As OdxContext
Dim wb As Workbook
Dim sht As Worksheet
Dim rng As Range
' ==================
' Setup
' ==================
Set oClient = GetAuthConn()
Set oCtx = NewContext(oClient)
Set wb = Workbooks.Add
Set sht = wb.Sheets(1)
SetWorksheetName sht, "SaleOrder ModelView"
Set rng = sht.Range("A1")
DebugRange rng, "--------------"
DebugRange rng, " DoExampleOdx_SaleOrder"
DebugRange rng, "--------------"
' ==================
' sale.order ModelView
' ==================
Set oModelView = oCtx.NewModelView("sale.order")
With oModelView
DebugRange rng, .ModelName
DebugRangeSchema rng, .ModelName, .GetModelSchema()
' ---- Basic fields ----
.AddField "name" ' SO number
.AddField "date_order"
.AddField "state"
.AddField "amount_total"
.AddField "currency_id"
' ---- Many2one: partner_id (customer) ----
With .AddField("partner_id")
DebugRange rng, "partner_id → " & .ModelName
Debug.Assert .IsMany2One
DebugRangeSchema rng, .ModelName, .GetModelSchema()
.AddField "name"
.AddField "email"
.AddField "phone"
End With
' ---- One2many: order_line ----
With .AddField("order_line")
DebugRange rng, "order_line → " & .ModelName
Debug.Assert .IsOne2Many
DebugRangeSchema rng, .ModelName, .GetModelSchema()
.AddField "name"
.AddField "product_uom_qty"
.AddField "price_unit"
.AddField "price_subtotal"
.AddField "currency_id"
' ---- Many2one: product_id ----
With .AddField("product_id")
DebugRange rng, "order_line.product_id → " & .ModelName
Debug.Assert .IsMany2One
DebugRangeSchema rng, .ModelName, .GetModelSchema()
.AddField "name"
.AddField "default_code"
.AddField "list_price"
End With
' ---- Many2many: product_template_attribute_value_ids ----
With .AddField("product_template_attribute_value_ids")
DebugRange rng, "order_line.product_template_attribute_value_ids → " & .ModelName
Debug.Assert .IsMany2Many
DebugRangeSchema rng, .ModelName, .GetModelSchema()
.AddField "name"
.AddField "product_attribute_value_id"
.AddField "attribute_line_id"
.AddField "price_extra"
.AddField "currency_id"
End With
End With
End With
' ==================
' Domain: Confirmed orders only
' ==================
Set oDomain = NewDomain
oDomain.AddArity NewField("state").Eq("sale")
oModelView.ExecuteSearchRead oDomain
' ==================
' Output: main + related models
' ==================
With oModelView
DebugPrintModelView wb, .RefMe, "sale.order"
With .GetRelatedModelView("partner_id", True)
DebugPrintModelView wb, .RefMe, "partner_id"
End With
With .GetRelatedModelView("order_line", True)
DebugPrintModelView wb, .RefMe, "order_line"
With .GetRelatedModelView("product_id", True)
DebugPrintModelView wb, .RefMe, "product_id"
End With
With .GetRelatedModelView("product_template_attribute_value_ids", True)
DebugPrintModelView wb, .RefMe, "value_ids"
End With
End With
End With
' ==================
' Output: filtered
' ==================
oModelView.SetFilter "name = 'S00007'" ' filter by SO number
With oModelView
DebugPrintModelView wb, .RefMe, "sale.order"
With .GetRelatedModelView("partner_id")
DebugPrintModelView wb, .RefMe, "partner_id"
End With
With .GetRelatedModelView("order_line")
DebugPrintModelView wb, .RefMe, "order_line"
' "order_line" records
Do Until .Recordset.EOF
With .GetRelatedModelView("product_id")
DebugPrintModelView wb, .RefMe, "product_id"
End With
With .GetRelatedModelView("product_template_attribute_value_ids")
DebugPrintModelView wb, .RefMe, "value_ids"
End With
.Recordset.MoveNext
Loop
End With
End With
sht.Select
wb.Saved = True
wb.Activate
End Sub
明細行のアクセスについて
1件の sale.order に対して、複数の order_line がありますので、 order_lineの Recordset のカーソルを移動させながら、 product_id と product_template_attribute_value_ids とをたどり、抽出・表示しています。
そのため、このサンプルコードでは、明細行の1件毎にシートを追加しています。
カーソル移動
SetFilerや GetRelatedModelView により、内包されている ADODB.Recordset の Filter がセットされ、カーソルは、1件目のレコードセットに移動されます。
EOF が真になるまで、 MoveNext でカーソルを移動します。
つまり
odooのモデルでは、一般的な外部キー(FK:Foreign Key)である、many2one項目に加えて、その逆のone2manyやmany2manyを備えているため、 sale.order から order_line をたどることができます。
そして、このツールでは、親レコードの値(現在行)を使って、関連するRecordsetを絞り込んでいます。
-
SetFilter
→ ModelView が持つ Recordset 自体を絞り込む -
GetRelatedModelView
→ 親レコードの値を使って、関連 ModelView の Recordset を
Filter で絞り込んで生成する -
親の SetFilter は子には継承されない。
子は 親の現在行に基づく Filter だけで絞られる
おわりに
-
odoo-json-rpc-vba を使うと、VBA から Odoo のモデルにアクセスし、
データを ADODB.Recordset として直接扱うことができます -
OdxModelView は、
モデル定義 → フィールド追加 → 関連モデルの展開 → domain 指定 → search_read → Excel 出力 を一括で扱える仕組みになっています -
SetFilter は、ModelView が保持している Recordset 全体を絞り込むために使われます。
-
GetRelatedModelView は、親レコード(現在行)の値をもとに、
関連する ModelView の Recordset を Filter を使って絞り込んで生成します -
親の SetFilter は子には継承されません
子の ModelView は、親の現在行に基づく 独自の Filter によって絞り込まれます -
販売アプリの例では、
sale.order
→ partner
→ order_line
→ → product
→ → attributes
と関連をたどりながら、Excel に 階層的に 出力しています