Odoo のフォームに HTML の部品を足す — 注意の帯・グラフ・リンクをビューの継承で
Odoo Community の受注フォームに、注意の帯・リボン・CSS だけの棒グラフ・手順のカード・リンクのボタンを足しました。画面の XML を継承して XPath で差し込む作り方を、実際に動かしたコードで説明します。
目次
目次
このページには目次がありません

受注を開いたときに「この受注は前受金を確かめる」「この得意先は最近どれくらい買っているか」が目に入れば、聞き回る手間が減ります。項目を増やすほどではない、注意書き・小さなグラフ・よく使うリンクを、フォームに置きたい場面です。
Odoo Community には、画面の上で部品を並べる道具(Studio)がありません。Studio は Enterprise 側の機能です。Community では、画面の定義(XML)を継承するアドオンを作って足します。この記事では、受注フォームに5つの部品を足し、捨てる用の DB で動かしたコードをそのまま載せます。
Odoo の画面は、HTML を混ぜて書ける XML
Odoo のフォームは、ir.ui.view に入っている XML で決まります。この XML には、項目(field)やボタン(button)と並べて、div・h3・ul・a といった HTML の要素をそのまま書けます。Odoo の画面は Bootstrap の上で動いているので、alert や btn のようなクラスも効きます。
足すときは、元の画面を書き換えずに継承します。inherit_id で元の画面を指し、xpath で差し込む場所を探し、position で前・後ろ・中を選びます。継承の基本は「本格拡張はアドオンで — Odoo の拡張開発の基本」の XPath の節にまとめています。
今回足した部品と置き場所は次のとおりです。
| 部品 | 置いた場所 | 書き方 |
|---|---|---|
| リボン「請求待ち」 | フォームの右上 | 標準の web_ribbon を条件つきで |
| 注意の帯 | 得意先の欄の上 | div class="alert" を条件つきで |
| 月別の受注の棒グラフ | 新しいタブ | 計算項目が作る HTML と CSS |
| 出荷前の確認のカード(畳める決まりつき) | 新しいタブ | ul と details |
| ボタンとリンク | 新しいタブ | button type="action" と a target="_blank" |
1. 条件で出る、注意の帯とリボン
1つめは、受注の状態や金額によって出たり消えたりする部品です。どちらも invisible に条件を書くだけで、Python のコードは要りません。
<record id="view_order_form_customhtml" model="ir.ui.view">
<field name="name">sale.order.form.jp_demo_customhtml</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="after">
<widget name="web_ribbon" title="請求待ち" bg_color="text-bg-warning"
invisible="invoice_status != 'to invoice'"/>
</xpath>
<xpath expr="//group[@name='sale_header']" position="before">
<div class="alert alert-warning d-flex gap-2 align-items-center" role="status"
invisible="amount_total < 300000">
<i class="fa fa-exclamation-triangle" title="注意"/>
<span><strong>30万円以上の受注です。</strong>前受金の要否を営業の責任者に確かめてください。</span>
</div>
</xpath>
<!-- 新しいタブは次の節 -->
</field>
</record>
請求がまだで、合計が 30万円 以上の受注(画面は見本のデータです)
見積の段階で金額も小さい受注には、どちらも出ません(画面は見本のデータです)
条件に書けるのは、そのフォームの項目です。金額の基準のような会社ごとに変わる値は、XML に直接書かずに設定から読む形にしておくと、基準を変えるたびにアドオンを入れ直さずに済みます(ここでは見本なので直接書いています)。
2. タブを足して、カード・グラフ・リンクを並べる
2つめは、受注の明細の隣にタブ「得意先の状況」を足し、その中に HTML の部品をまとめて置く形です。タブ(page)の中は、ほぼ普通の HTML として書けます。
<xpath expr="//page[@name='order_lines']" position="after">
<page string="得意先の状況" name="jp_customer">
<div class="jp-grid">
<div class="jp-card">
<h3 class="jp-card-title">月別の受注(税抜・確定分)</h3>
<field name="x_sales_chart" nolabel="1" readonly="1"/>
</div>
<div class="jp-card">
<h3 class="jp-card-title">出荷の前に</h3>
<ul class="jp-list">
<li><strong>納期</strong>:得意先の希望日と在庫を見比べる</li>
<li><strong>送り先</strong>:届け先の住所が請求先と違わないか</li>
<li><strong>請求</strong>:締め日と支払い条件が合っているか</li>
</ul>
<details class="jp-details">
<summary>締め日の決まり</summary>
<p class="mb-0">月末締めの得意先は、25日以降の出荷を翌月の請求に回さない。</p>
</details>
</div>
</div>
<div class="jp-links">
<button name="%(sale.action_orders)d" type="action" class="btn btn-primary"
context="{'search_default_partner_id': partner_id}" string="この得意先の受注"/>
<button name="action_view_invoice" type="object" class="btn btn-secondary"
invisible="invoice_count == 0" string="請求書を開く"/>
<a class="btn btn-link" role="button" href="https://www.invoice-kohyo.nta.go.jp/"
target="_blank" rel="noopener">インボイスの登録番号を調べる(国税庁)
<i class="fa fa-external-link" title="別のタブ"/></a>
</div>
</page>
</xpath>
足したタブ。「締め日の決まり」は押すと開きます(画面は見本のデータです)
ボタンとリンクは、行き先で書き分けます。
- Odoo の中の一覧へ:
type="action"のボタンに、開く画面と絞り込み(context)を渡します。押すと、得意先と「販売オーダ」で絞った受注の一覧が 5 件で開きました - このレコードの処理へ:
type="object"のボタンで、モデルのメソッドを呼びます。請求書がまだ無い受注ではinvisibleで隠しています - 外のサイトへ:
aにtarget="_blank"を付け、別のタブで開きます。btnのクラスを付けたaにrole="button"が無いと、インストールのときに Odoo が警告を出しました
3. CSS だけで描く棒グラフ
グラフは、計算項目が作る HTML と、棒の高さを決める CSS だけで描いています。JavaScript もグラフのライブラリも使いません。
from dateutil.relativedelta import relativedelta
from markupsafe import Markup, escape
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = "sale.order"
x_sales_chart = fields.Html(
string="得意先の月別の受注", compute="_compute_x_sales_chart", sanitize=False
)
@api.depends("partner_id")
def _compute_x_sales_chart(self):
today = fields.Date.context_today(self)
start = today.replace(day=1) - relativedelta(months=5)
months = [start + relativedelta(months=i) for i in range(6)]
for order in self:
if not order.partner_id:
order.x_sales_chart = False
continue
rows = self.env["sale.order"]._read_group(
[("partner_id", "child_of", order.partner_id.commercial_partner_id.id),
("state", "=", "sale"),
("date_order", ">=", fields.Datetime.to_datetime(start))],
["date_order:month"], ["amount_untaxed:sum"],
)
total = {d.date().replace(day=1): s for d, s in rows}
top = max(total.values(), default=0) or 1
bars = [
Markup(
"<div class='jp-bar'><span class='jp-bar-val'>{v}</span>"
"<span class='jp-bar-fill' style='height:{h}%'></span>"
"<span class='jp-bar-lbl'>{m}月</span></div>"
).format(v=escape(f"{total.get(m, 0) / 10000:,.0f}万" if total.get(m) else "–"),
h=round(total.get(m, 0) / top * 100), m=m.month)
for m in months
]
order.x_sales_chart = Markup("<div class='jp-bars'>{}</div>").format(Markup("").join(bars))
見た目は SCSS のファイルにまとめ、マニフェストの assets で Odoo の画面に読み込ませます。色や枠線は Odoo の変数($o-action・$border-color)を使うと、標準の画面と色がそろいます。
# __manifest__.py
{
"name": "受注フォームの HTML 部品(記事の見本)",
"version": "19.0.1.0.0",
"depends": ["sale_management"],
"data": ["views/sale_order_views.xml"],
"assets": {
"web.assets_backend": ["jp_demo_customhtml/static/src/scss/customhtml.scss"],
},
"license": "LGPL-3",
}
// customhtml.scss(グラフの部分だけ抜粋)
.o_form_view {
.jp-bars { display: flex; align-items: flex-end; gap: 14px; height: 170px;
border-bottom: 1px solid $border-color; }
.jp-bar { flex: 1; display: flex; flex-direction: column; justify-content: flex-end;
align-items: center; height: 100%; position: relative; padding-bottom: 20px; }
.jp-bar-fill { width: 70%; max-width: 36px; min-height: 2px; background: $o-action;
border-radius: 6px 6px 0 0; }
.jp-bar-lbl { position: absolute; bottom: 0; font-size: .8rem; color: $text-muted; }
}
グラフは直近の 6 か月を並べます。見本の得意先には、捨てる用の DB で 5月〜8月 の受注を足してから撮りました。
作るときに決めておくこと
- 元の画面を
replaceで消さない。before・after・insideで差し込むだけにします。2つのアドオンが同じ場所を消したり足したりすると、入れる順番でインストールが止まることを確かめています - グラフの HTML は保存しない計算項目で作る。DB に列ができないので、アドオンを外しても消えるデータがありません
sanitize=Falseは、自分のコードで組んだ HTML にだけ使う。得意先の名前のように利用者が入れた文字を混ぜるときは、escapeを通してから入れます- 画面の定義を開発者モードで直接書き換えない。どこを変えたかがコードに残らず、次に触る人が追えません。足す物はアドオンにまとめます
- 動きのある部品は、JavaScript の部品として別に作る。開く・閉じる程度なら
detailsで足ります。数字を取りに行く、入力を受けるといった動きは、Odoo の画面の部品(OWL)をassetsで読み込ませて作ります
どこまでを HTML の部品で済ませるか
- 注意書き、手順、よく使うリンク、軽いグラフ → この記事のように、フォームの XML に HTML を足すだけで足ります
- 集計の切り口を利用者が変えたい → 標準のレポート(ピボット・グラフの画面)に任せます
- 新しい入力の画面や、複数の伝票をまたぐ作業の画面 → 当社が画面を丸ごと足します
足した物を版上げで壊さないための決まりは「アップデートしても壊れない — Odoo の拡張の作り方」にまとめています。


