はじめに
Odoo を触っていると、値が自動で算出されるフィールドに出会うことがあります。中でも「保存される計算フィールド」は、算出結果を計算してすぐにデータベースに書き込む点で特別です。つまり表示だけではなく、テーブルの実際の列として保持されます。
この違いは見た目以上に重要です。読み取りごとに計算されるだけのフィールドは、検索や絞り込み、集計に使えず、エクスポート時にも扱いづらい。一方で保存される計算フィールドは通常のカラムと同じように検索・グルーピング・出力が可能になります。
本記事では、Odoo のデータモデル内部で保存計算フィールドがどう動くか、Odoo Studio と Python のどちらで作るか、現場で役立つユースケース、そして失敗を避けるための注意点までを解説します。
Odoo における「保存される計算フィールド」とは何か
Odoo の ORM では、モデル上の各フィールドが一つのデータを担います。通常はユーザー入力を保存するのが普通ですが、「計算フィールド」は Python の処理で値を決める点が異なります。
「保存される計算フィールド」は要するに compute を使い、さらに store=True を付けたものです。依存フィールドに変化があれば計算処理が実行され、その結果がデータベースの列に書き込まれます。そして他の通常フィールドと同じように扱えます。
実際の実装イメージ
(例)数量と単価から合計を求めて保存するフィールドの基本パターンは、compute 関数で乗算して結果をレコードに代入し、store=True を指定する形になります。
ここで重要なのは store=True の有無です。これが無いとフィールドは読み取り時に都度計算されますが、データベースには永続化されません。
ユーザーインタフェース上では、保存された計算フィールドは普通のフィールドと見分けがつきません。フォームや一覧、帳票に表示され、フィルタやグループ化、エクスポートにも使えます。値が算出されたものだという目印は特にありません。
保存計算フィールドと非保存計算フィールドの違い
この差を理解することは Odoo 開発では必須です。
- 非保存計算フィールド:読み取り時にその場で計算されます。検索・絞り込み・グルーピングには使えません。ストレージは節約できますが、SQL レベルでの問い合わせには参加しません。
- 保存計算フィールド:依存する値が変わったときに計算され、DB に保存されます。検索やフィルタ、エクスポートが可能で、通常のカラムと同様に振る舞いますが、その分ディスク領域を消費します。
どちらが良いかは用途次第です。単にフォーム表示だけで十分なら非保存で問題ありません。検索や集計で使う必要があるなら、必ず保存する設計にします。
フィールドの動き方
設計時には ORM が提供する自動再計算の仕組みを活用します。@api.depends() に依存フィールドを列挙すると、ORM が変更を検知して再計算をトリガーしてくれます。
依存フィールドのどれかが変わると、Odoo はそのレコードを再計算対象としてマークし、compute 関数を実行して結果を DB に書き戻します。
再計算の流れ(ライフサイクル)
処理の流れを順に追うとわかりやすいです。
- 1) ユーザーや自動処理が @api.depends() に挙がるフィールドを更新する。
- 2) ORM がその変更を検出し、依存しているレコード群を特定する。
- 3) 該当レコードに対して compute 関数が呼ばれる。
- 4) 計算結果がデータベースのカラムに書き込まれる。
- 5) 以降、その値は検索・フィルタ・エクスポートに利用可能になる。
通常は同一トランザクション内で即時再計算が行われますが、大量更新では一部をバックグラウンドで処理することがあります。
関連モデルをまたぐ依存関係
@api.depends() ではドット記法で関連モデルのフィールドまで追跡できます。
(例)partner_id.country_id.name のように記述すると、そのパス上の変更を監視できます。
この機能により、関連先の国名が変われば紐づくすべてのレコードが自動で再計算されます。Odoo フレームワークの強力な特徴の一つです。
データベースへの影響
保存されるため、PostgreSQL のテーブルに実際のカラムが作られます。これにより SQL クエリで直接参照でき、検索や絞り込みのパフォーマンスは通常のカラムと同じく高速です。
実務での利用例
実務での具体的な使いどころ(5つ)
1. 販売:受注行ごとの粗利率
受注行の単価と原価から粗利率を計算して保存すれば、一覧で粗利でフィルタしたり、利益率が低い行をすぐに抽出できます。ピボットで粗利帯ごとに集計するのも簡単です。
2. CRM:最終アクティビティからの非稼働日数
最終の予定活動日からの経過日数を保存しておけば、毎朝スケジュールで再計算することで非稼働のリードを簡単に抽出できます。手動で追跡する手間が減ります。
3. 在庫:実際利用可能数の事前計算
手持ち数から引当分を差し引いた正味の在庫数を保持しておくと、商品一覧で可用性で並び替えや絞り込みができ、画面上で都度複雑な在庫計算を実行する必要がなくなります。
4. 会計:顧客ごとの延滞請求書件数
取引先に対して延滞請求書の件数を保持すれば、連絡先一覧をワンクリックで延滞件数順に並べ替えられます。オンザフライ計算では実現しづらい利便性です。
5. 生産:BOM の総作業見積時間
BOM に紐づく各工程の所要時間を合算して保存しておけば、BOM の総作業時間で並び替えや絞り込みが可能になり、キャパシティ計画やスケジューリングで役立ちます。工程が追加・変更されれば自動更新されます。
作成方法とカスタマイズ手順
保存計算フィールドの作り方は主に二つ:Studio を使う簡易な方法と、Python コードで作る本格的な方法です。
Odoo Studio を使う場合
Studio ならコードを書かずに計算フィールドを追加できます。整数・浮動小数点・通貨型フィールド作成時に式を設定でき、内部で依存関係の管理も自動で行われます。
Studio は同一レコード上の簡単な算術式には向いています。開発環境不要で手軽に導入できますが、関連モデルのフィールド参照や条件分岐、子レコードの集約などが絡む複雑なロジックには向きません。その場合はカスタムモジュールで Python 実装が必要です。
この違いを踏まえ、カスタマイズ計画では Studio と Python の使い分けを明確にしておきましょう。
カスタム Python モジュールで作る場合
より複雑なロジックが必要なら、独自モジュール内で Python によるフィールド定義と compute 関数を実装します。以下は受注行に粗利率フィールドを追加する典型例のイメージです。
(例)price_unit と purchase_price を使って粗利率を算出し、store=True で保存するコードを用意します。実際のコードは compute 内でゼロ除算や未設定値を考慮して安全に実装します。
モジュールをインストールすると、DB にカラムが作成され、既存レコードに対して初回の一括再計算が実行され、指定した依存フィールドの変化を追跡し始めます。
カスタムフィールドには慣例的に x_ プレフィックスを付けてコアフィールドとの衝突を避けるのが一般的です。これは Odoo 開発での標準的な運用です。
計算結果をユーザーが上書きできるようにするには
計算フィールドは通常は読み取り専用です。ユーザーが値を手動で上書きできるようにするには、compute に対する inverse メソッドを実装します。inverse はユーザーが直接フィールドを書き込んだときに呼ばれ、元データ(ソースフィールド)を更新する役割を担います。デフォルト値を提供して時々手動修正が必要なケースで有用です。
Odoo Studio フィールドと XML-RPC 経由の管理
ir.model.fields 経由で API から標準フィールドを作ることは可能ですが、保存計算フィールドの核となる compute ロジックはサーバー側のコードで実装される必要があります。配備の自動化でフィールド定義を作る用途には API が便利ですが、実際の計算処理はモジュールとしてサーバーに置かなければなりません。
運用上のベストプラクティス
実務で経験のあるコンサルタントが守るべき運用ルール
依存関係を正確に宣言すること
@api.depends() には compute メソッド内で参照するすべてのフィールドを列挙します。漏れがあると依存先が変わっても再計算されず、結果が古いままになります。コードを一行ずつ確認して漏れを防ぎましょう。
compute 関数は高速に保つこと
依存変更時に多数のレコードで実行されるため、計算処理内での追加クエリはできるだけ避けます。関連データが必要な場合は既にロードされているフィールドを使い、不要な検索は最小限にしてください。
store=True は必要な場合だけ使う
保存することでストレージを消費し、再計算時に書き込みが発生します。表示だけで十分なら非保存の方が軽量です。何でもかんでも store=True にするのは避けましょう。
計算ロジックで例外ケースを扱うこと
ゼロ除算や関連レコードの未存在、NULL 値などを考慮してください。何らかの理由で通常計算ができない場合の安全なデフォルトを設定することが重要です。
大規模テーブルでの初回再計算を見越す
新しい保存計算フィールドを作った際、既存のすべての行に対して初回再計算が走ります。数十万行規模だと時間がかかるため、ステージングで事前検証し、運用上の対応(メンテナンス時間やバッチ処理)を計画してください。
循環依存を避けること
A が B に依存し、同時に B が A に依存するような設計はモジュール読み込み時にエラーになります。依存関係は一方向に流れるよう設計しましょう。
よくある落とし穴
よくあるミス:store=True を付け忘れる
フォーム上では値が表示されるため気付きにくいのですが、のちにフィルタやレポートに使おうとすると動かないことが発覚します。検索可能にする必要があるなら最初から store=True を付けておきましょう。
@api.depends の依存漏れ
compute 内で partner_id.country_id を参照しているのに decorator に partner_id しか書いていないと、国名変更時に再計算されません。アクセスしているフィールドの全経路を明示する必要があります。
計算中の例外が黙殺される問題
compute で例外が発生すると、そのレコードの再計算がスキップされ、以前の値が残ります。サーバーログには出ますがユーザーには見えないため、データが古いままになりやすいです。欠損や異常値での挙動を含めて十分にテストしてください。
大規模データでの性能劣化
開発環境では問題なくても、本番でテーブルが肥大化するとボトルネックになります。レコードごとに余分なクエリを発生させない設計を心掛けてください。例えばレコード 1 件あたり 1 件増える検索が 1 万件分になると、合計 1 万回の検索が走ります。
compute 内で sudo() を乱用しないこと
権限を無視して sudo() でデータを参照するとセキュリティリスクになります。計算結果が本来見せるべきでない情報を露出してしまう可能性があるため、sudo() の使用は慎重に検討してください。
あらゆる操作で即時再計算が期待できるわけではない点
対話操作では同期的に再計算されますが、インポートやバッチ処理、特定のコンテキストでは再計算が遅延することがあります。保存値が常に書き戻される前提で他の業務ロジックを組まないようにし、実際に使うコンテキストで動作を確認してください。
まとめ
結論として、保存計算フィールドは Odoo の拡張で非常に有用な武器です。計算を自動化してデータの一貫性を保ちつつ、検索や集計にそのまま利用できる利点があります。
押さえておくべき要点
- 検索やフィルター、エクスポートが必要なら必ず store=True を使う。
- @api.depends() にはクロスモデルの経路を含めてすべての依存を宣言する。
- compute 処理は高速化し、例外や欠損値を明示的に扱う。
- 簡単な算式なら Odoo Studio で十分。複雑なロジックは Python 実装にする。
- 大規模テーブルへの導入時は初回再計算の影響を計画に入れる。
新しいモジュール作成、既存モデルの拡張、あるいは Odoo のフィールド種別を学ぶ際に、保存計算フィールドの概念を深く理解しておくことは非常に価値があります。ORM、DB 層、業務ロジックが交差する領域だからです。
導入支援が必要ですか?
Dasolo は Odoo の実装、カスタマイズ、最適化を支援します。計算フィールドの追加や、計算値に基づく帳票作成、さらなる Odoo 開発のご相談まで、経験豊富なチームが対応可能です。
Odoo プロジェクトで支援が必要ならぜひご相談ください。具体的なユースケースを伺い、最適な設計と実装方針をご提案します。