入力画面の動きを変える — Odoo の画面の拡張(onchange・計算項目・OWL)
数量を入れたら注意を出す、納期までの日数を出す、全角の注文番号を半角にそろえる。Odoo 19 Community の受注の画面で、小さなアドオンを入れて実際に動かした例で説明します。
目次
目次
このページには目次がありません

受注の画面で数量を打ったら、その場で「多すぎませんか」と出てほしい。全角で打たれた注文番号を、保存の前に半角へそろえたい。こうした「入力の途中で画面が反応する」動きは、Odoo ではアドオンで作ります。Community には、ブラウザで動く JavaScript を画面の上で登録する設定がありません(画面を作る道具 Studio も Enterprise 側です)。
この記事では、Odoo 19 Community の受注の画面に小さなアドオンを入れ、4 つの道具を実際に動かした結果を見せます。
道具は「サーバーで決める」と「ブラウザで変える」の2系統
| 道具 | どこで動くか | 向いていること |
|---|---|---|
@api.onchange | サーバー(Python) | 値が変わった瞬間に、別の項目を埋める・注意を出す |
計算項目(compute) | サーバー(Python) | 他の項目から決まる値を、常に画面に出す |
ビューの条件(invisible・readonly・required) | ブラウザ(XML の式) | 状態に応じて項目を隠す・触れなくする・必須にする |
OWL の部品と patch | ブラウザ(JavaScript) | 入力のしかたや、画面の部品そのものを変える |
業務の決まり(いくつ以上なら注意、何日前なら警告)は Python 側に置くのが基本です。一覧の取り込みや API から入った値にも同じ計算が効き、ブラウザの JavaScript は打ち手の手元の見た目に絞れます。
なお Community に入っている自動化ルール(base_automation)にも「UI変更時」という起動条件があり、Python のコードを画面から登録できます。ソースを読むと、この条件は「Python コードを実行」の動作でしか使えない決まりでした(画面からの登録は今回は試していません)。
例1:数量を入れたら注意を出す(onchange)
明細の数量が 100 以上になったら、ケースと個の取り違えを疑う注意を出します。
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
@api.onchange("product_uom_qty")
def _onchange_jp_large_qty(self):
if self.product_uom_qty >= 100:
return {"warning": {
"title": "数量が多めです",
"message": "%s を %s%s で入れています。ケースと個の取り違えがないか確かめてください。"
% (self.product_id.display_name, int(self.product_uom_qty), self.product_uom_id.name),
}}
見積の明細で数量を 120 にして次の欄へ移ると、保存の前にこの注意が出ました。下の合計も、同じ瞬間に新しい数量で計算し直されています。
数量を 120 にして欄を移った直後(画面は見本のデータです)
warning を返す onchange は、注意を出すだけで保存は止めません。入れてはいけない値を止めたいときは、保存のときに検査する仕組み(@api.constrains)と組み合わせます。
例2:納期までの日数を出す(計算項目+onchange)
納品予定日から「あと何日か」を計算する項目を足し、3 日を切ったら注意も出します。
class SaleOrder(models.Model):
_inherit = "sale.order"
x_jp_days_left = fields.Integer("納期まで(日)", compute="_compute_x_jp_days_left")
@api.depends("commitment_date")
def _compute_x_jp_days_left(self):
today = fields.Date.context_today(self)
for order in self:
if order.commitment_date:
due = fields.Datetime.context_timestamp(order, order.commitment_date).date()
order.x_jp_days_left = (due - today).days
else:
order.x_jp_days_left = 0
@api.onchange("commitment_date")
def _onchange_jp_commitment_date(self):
if self.commitment_date and self.x_jp_days_left < 3:
return {"warning": {"title": "納期が近い受注です",
"message": "納期まで %s 日です。出荷の手配を先に確かめてください。" % self.x_jp_days_left}}
画面には、ビューの継承で項目を1行足します。invisible の式で、納品予定日が空のときは隠します。
<xpath expr="//group[@name='sale_shipping']" position="inside">
<field name="x_jp_days_left" invisible="not commitment_date"/>
</xpath>
「その他情報」のタブで納品予定日を 2 日後にすると、注意が出ました。
納品予定日を 2 日後にして欄を移った直後(画面は見本のデータです)
注意を閉じると、配送の欄の下に「納期まで(日)」が 2 と出ています。保存しなくても、入力した時点で計算されます。
足した計算項目。納品予定日が空なら行ごと隠れます(画面は見本のデータです)
つまずいた所:他のアドオンが置き換えた場所は指せない
最初は、納品予定日を囲む div[@name='commitment_date_div'] の後ろに足そうとして、インストールが止まりました。依存に書いたのは販売(sale_management)だけですが、在庫のアドオン(sale_stock)が入っていると、その div を position="replace" で置き換えています。元の XML にある名前でも、継承が重なった後の画面には残っていないことがあります。欄の枠(group の名前)のように、置き換えられにくい場所を指すと通りました。
例3:全角の注文番号を半角にそろえる(入力の部品を足す)
先方の注文番号(画面の名前は「顧客参照」)は、全角と半角が混ざると検索で見つからなくなります。打った文字を半角にそろえる入力の部品を、標準の文字の部品から派生させて足します。
import { registry } from "@web/core/registry";
import { CharField, charField } from "@web/views/fields/char/char_field";
export function toHalfWidth(value) {
return value
.replace(/[!-~]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0))
.replace(/ /g, " ");
}
export class JpHalfwidthField extends CharField {
parse(value) {
return super.parse(toHalfWidth(value));
}
}
registry.category("fields").add("jp_halfwidth", { ...charField, component: JpHalfwidthField });
使う側は、ビューの継承でその項目の widget を差し替えるだけです。
<xpath expr="//field[@name='client_order_ref']" position="attributes">
<attribute name="widget">jp_halfwidth</attribute>
</xpath>
DB に入った値も半角でした。JavaScript のファイルはマニフェストの assets に書くと、Odoo の画面の読み込みに組み込まれます。
"assets": {"web.assets_backend": ["jp_demo_clientscript/static/src/**/*"]},
例4:保存の後に一言出す(patch)
最後は、既にある部品の動きを patch で変える例です。受注を保存したとき、顧客参照が空なら黄色の通知を出します。
import { patch } from "@web/core/utils/patch";
import { useService } from "@web/core/utils/hooks";
import { FormController } from "@web/views/form/form_controller";
patch(FormController.prototype, {
setup() {
super.setup(...arguments);
this.jpNotification = useService("notification");
},
async onRecordSaved(record, changes) {
await super.onRecordSaved(...arguments);
if (record.resModel === "sale.order" && !record.data.client_order_ref) {
this.jpNotification.add("顧客参照(先方の注文番号)が空のままです。",
{ title: "保存しました", type: "warning" });
}
},
});
保存した直後の右上の通知(画面は見本のデータです)
FormController は、受注に限らず全部のフォームの画面で使われている部品です。patch はそのすべてに効くので、上のように対象のモデルで必ず絞ります。また super を呼ばない書き方をすると、元の処理や他のアドオンの patch が黙って消えます。Python の super() と同じ決まりです(「本格拡張はアドオンで」)。
試して分かった細かいこと
- 入れるのに Odoo の再起動は要りませんでした。動いている Odoo の横で、コマンドで同じ DB にアドオンを入れたところ、画面を読み直すだけで Python の処理も JavaScript も効きました
- 値を確定する操作で、注意が2回出ることがありました。日付の欄で Enter を押すと同じ注意が2つ重なり、Tab で欄を移ると1つでした。注意の文は、2回出ても困らない書き方にしておきます
- 画面を離れると、編集中の値が保存されます。試している途中でブラウザを閉じたら、入れた数量と日付が保存されていました。試すときは捨ててよい DB で行います
どれを使うか
| やりたいこと | 道具 |
|---|---|
| 入れた値に応じて、別の項目を埋める・注意を出す | @api.onchange |
| 他の項目から決まる値を画面に出す | 計算項目 |
| 状態で項目を隠す・必須にする | ビューの invisible・required の式 |
| 打ち方そのものを変える(半角・桁区切り・候補) | 入力の部品を派生させて足す |
| 既存の画面の部品の動きを変える | patch(対象で絞り、super を呼ぶ) |
画面の動きは、Python で決まりを持ち、JavaScript は手元の打ちやすさに絞ると、取り込みや API から入った値とも食い違いません。当社が日本の業務向けに足すアドオンでも、この分け方で作ります。アドオンを外したときに何が残るか、更新で壊さない書き方は「アップデートしても壊れない — Odoo の拡張の作り方」にまとめています。





