第8章:計算フィールドとOnchangeメカニズム

:doc:`モデル <07_relations>間のリレーションは、Odoo モジュールの主要なコンポーネントです。どのビジネスケースでもモデル化する必要があります。 ただし、与えられたモデル内のフィールド間のリンクが必要になる場合があります。 場合によっては、あるフィールドの値は、他のフィールドの値から決定されることがあります。

このようば場合をサポートするのが、計算フィールドとonchangeメカニズムの概念です。この章は技術的には複雑ではありませんが、両概念の意味論(セマンティクス)は非常に重要です。また、この章では初めてPythonのロジックを書きます。今まではクラス定義とフィールド宣言以外は書いていませんでした。

計算フィールド

参考: このトピックに関連するドキュメントは、 計算フィールド にあります。

注釈

目標: このセクションの最後には、

  • 物件モデルでは、総面積(total area)とベストオファー(best offer)が計算されるようになります。

フィールドを計算
  • 物件オファーモデルでは、有効期限を計算し、更新されるようになります。

フィールドを逆数で計算

不動産モジュールでは、居住面積(living area)と庭面積(garden area)を定義しています。そのため、総面積を両方のフィールドの合計として定義することは、自然なことです。これには、計算フィールドの概念を使います。つまり、あるフィールドの値は、他のフィールドの値から計算されます。

これまで、フィールドはデータベースに直接格納され、データベースから直接取得されてきました。フィールドは 計算 することもできます。この場合、フィールドの値はデータベースから取得するのではなく、モデルのメソッドを呼び出してその場で計算されます。

計算されたフィールドを作成するには、フィールドを作成し、その属性 compute にメソッドの名前を設定します。計算メソッドは、 self 内の全てのレコードに対して、計算フィールドの値を設定しなければなりません。

By convention, compute methods are private, meaning that they cannot be called from the presentation tier, only from the business tier (see 第1章:アーキテクチャの概要). Private methods have a name starting with an underscore _.

依存関係

計算フィールドの値は、通常、計算されたレコードの他のフィールドの値に依存します。ORMは、開発者がデコレータ depends() を用いて計算メソッドに依存関係を指定することを期待しています。与えられた依存関係は、それが変更されるたびに、フィールドの再計算をトリガするように、ORMによって使用されます。

from odoo import api, fields, models

class TestComputed(models.Model):
    _name = "test.computed"

    total = fields.Float(compute="_compute_total")
    amount = fields.Float()

    @api.depends("amount")
    def _compute_total(self):
        for record in self:
            record.total = 2.0 * record.amount

注釈

self はコレクションです。

オブジェクト selfレコードセット 、つまりレコードの順序付きコレクションです。このオブジェクトは len(self)iter(self) といった Python の標準的なコレクション操作に加え、 recs1 | recs2 といった追加のセット操作をサポートしています。

self を反復すると、レコードが1つずつ得られ、各レコードはそれ自体がサイズ1のコレクションとなります。 record.name のようにドット表記を使うことで,単一のレコードのフィールドにアクセスしたり代入したりすることができます.

Odoo には計算フィールドの例がたくさんあります。単純な例は、 これ です。

Exercise

総面積を計算してみましょう。

  • estate.propertytotal_area フィールドを追加します。living_areagarden_area の合計値として定義します。

  • このセクションの 目標 の最初の画像に示されているようにフォームビューにフィールドを追加します。

リレーショナルフィールドでは、フィールドを経由するパスを依存関係として使用することが可能です。

description = fields.Char(compute="_compute_description")
partner_id = fields.Many2one("res.partner")

@api.depends("partner_id.name")
def _compute_description(self):
    for record in self:
        record.description = "Test for partner %s" % record.partner_id.name

この例は、 Many2one で説明していますが、 Many2manyOne2many でも有効です。こちらの 例を確認してください。

モジュールで試してみましょう!

Exercise

ベストオファーを計算してみましょう。

  • estate.propertybest_price フィールドを追加します。これは、オファーの price (価格)のうち最も高い(つまり最大値) ものとして定義されます。

  • このセクションの 目標 の最初の画像に示されているようにフォームビューにフィールドを追加します。

ヒント: mapped() メソッドを使ってみるといいでしょう。簡単な例は こちら をご覧ください。

逆関数

計算フィールドは、デフォルトでは読み取り専用であることにお気づきでしょうか。これは、ユーザが値を設定することを想定していないためです。

しかし、場合によっては、値を直接設定することができた方が便利なこともあります。この不動産の例では、オファーの有効期間を定義し、有効期限を設定することができます。期間または期限(日付)のいずれかを設定し、一方が他方に影響を与えることができるようにしたいと思います。

この機能をサポートするために、Odoo では 逆(inverse) 関数を使用することができます。

from odoo import api, fields, models

class TestComputed(models.Model):
    _name = "test.computed"

    total = fields.Float(compute="_compute_total", inverse="_inverse_total")
    amount = fields.Float()

    @api.depends("amount")
    def _compute_total(self):
        for record in self:
            record.total = 2.0 * record.amount

    def _inverse_total(self):
        for record in self:
            record.amount = record.total / 2.0

例は こちら です。

computeメソッドにはフィールドを指定し、inverseメソッドにはフィールドの依存関係を指定します。

inverse メソッドはレコードを保存するときに呼び出され、compute メソッドは依存関係の変更ごとに呼び出されることに注意してください。

Exercise

オファーの有効期限を計算してみましょう。

  • estate.property.offer モデルに次のフィールドを追加します。

フィールド

デフォルト

有効期間

整数

7

date_deading

日付

date_deadline は計算されたフィールドで、提供された create_datevalidity の 2 つのフィールドの合計として定義されます。 ユーザーが日付(create_date)または有効期限(validity)を設定できるように、逆関数を適切に定義します。

ヒント: create_date はレコードが作成された時にのみ代入されるため、作成時のクラッシュを防ぐためのフォールバックが必要です。

  • このセクションの 目標 の2番目の画像に示されているように、フォームビューとリストビューにフィールドを追加します。

補足情報

計算フィールドは、デフォルトではデータベースに 保存されません。 そのため、 search メソッドが定義されていない限り、計算フィールドで検索することは できません。 このトピックは、このトレーニングの範囲を超えているので、ここでは取り上げません。例は こちら にあります。

もう一つの解決策は、 store=True 属性でフィールドを保存することです。これは通常便利ですが、モデルに追加される計算負荷に注意してください。先ほどの例をもう一度見てみましょう。

description = fields.Char(compute="_compute_description", store=True)
partner_id = fields.Many2one("res.partner")

@api.depends("partner_id.name")
def _compute_description(self):
    for record in self:
        record.description = "Test for partner %s" % record.partner_id.name

パートナーの name (名前)が変更されるたびに、その名前を参照している 全レコードdescription (説明)が自動的に再計算されます。何百万ものレコードを再計算する必要がある場合、再計算はすぐに限界に達してしまいます。

また、計算されたフィールドは別の計算されたフィールドに依存することができることも注目に値します。ORMは賢いので、すべての依存関係を正しい順序で正しく再計算しますが、時にはパフォーマンスの低下を伴います。

一般的に、計算フィールドを定義する際には、パフォーマンスを常に念頭に置かなければなりません。計算対象となるフィールドが複雑であればあるほど(例えば、多くの依存関係がある場合や、ある計算フィールドが他の計算フィールドに依存している場合など) 、計算にかかる時間は長くなります。常に、事前に計算フィールドのコストを評価するための時間を取るようにしてください。多くの場合、あなたのコードが本番サーバーに配置されたときに初めて、プロセス全体の速度を低下させていることに気付きます。かっこ悪いですね :-(

Onchangeメカニズム

参考: このトピックに関連するドキュメントは、 onchange() にあります。

注釈

目標: このセクションの最後には、庭を有効にすると、デフォルトの面積が10、方向が北(North)に設定されるようになります。

Onchange

不動産モジュールでは、ユーザーのデータ入力を支援したいと考えています。 garden フィールドが設定されている場合、庭面積と方角に対して、デフォルト値を与えたいと思います。また、 garden フィールドが未チェック状態の場合は、庭面積がゼロにリセットされ、方角もクリアされるようにします。このように、あるフィールドの値が他のフィールドの値を変更するようにします。

onchange メカニズムは、ユーザーがフィールドの値を入力したときに、データベースに何も保存せずにクライアント・インターフェースがフォームを更新する方法を提供します。これを実現するために、 self がフォームビューのレコードを表すメソッドを定義し、 onchange() でデコレーションして、どのフィールドでトリガーされるかを指定します。 self に加えた変更は、フォームに反映されます。

from odoo import api, fields, models

class TestOnchange(models.Model):
    _name = "test.onchange"

    name = fields.Char(string="Name")
    description = fields.Char(string="Description")
    partner_id = fields.Many2one("res.partner", string="Partner")

    @api.onchange("partner_id")
    def _onchange_partner_id(self):
        self.name = "Document for %s" % (self.partner_id.name)
        self.description = "Default description for %s" % (self.partner_id.name)

この例では、パートナーを変更すると、名前(name)と説明(description)の値も変更されます。その後、名前と説明の値を変更するかどうかは、ユーザー次第です。また、 self が繰り返しではないことに注意してください。これは、このメソッドがフォームビューでのみ実行されるためで、selfは常に1つのレコードです。

Exercise

庭面積と方角の値を設定してみましょう。

garden に True が設定されている場合、庭面積 (10) と方角 (North) の値を設定するため、estate.property モデルに onchange を作成します。未チェック状態の場合は、フィールドをクリアします。

補足情報

Onchangesメソッドは、ブロックしない警告メッセージ() を返すこともできます。

どのように使うのですか?

計算フィールドとonchangeメカニズムの使い分けに厳密なルールはありません。

多くの場合、同じ結果を得るために計算フィールドとonchangeメカニズムの両方を使用することができます。計算フィールドは、フォーム・ビューのコンテキストの外でもトリガされるので、常に好ましいものです。ビジネスロジックをモデルに追加するためにonchangeを使用してはいけません。これは よくない 使い方です。なぜなら、onchangeメカニズムはプログラムでレコードを作成するときには自動的にトリガーされず、フォームビューでのみトリガーされるからです。

計算フィールドとonchangeのよくある落とし穴として、 凄くスマートに なろうとしてロジックを追加しすぎることです。これは期待していた結果とは逆の結果になることがあります。エンドユーザーが過度の自動化で混乱してしまうのです。

計算フィールドはデバッグしやすい傾向にあります。このようなフィールドは指定されたメソッドによって設定されるので、いつ値が設定されたかを簡単に追跡できます。一方、onchangeメカニズムでは混乱を招く可能性があります。onchangeメカニズムの範囲を知ることは非常に困難です。複数のonchangeメソッドが同じフィールドを設定する可能性があるため、値がどこから来たのかを追跡するのは容易ではありません。

ストアド計算フィールドを使用する際には、依存関係に注意してください。計算フィールドが他の計算フィールドに依存している場合、値を変更すると大量の再計算が行われます。これはパフォーマンスの低下につながります。

:doc:`next chapter <09_actions>`では、ボタンがクリックされたときにビジネスロジックをトリガーする方法を見ていきます。