MY HATCH

Odoo のフォームに HTML の部品を足す — 注意の帯・グラフ・リンクをビューの継承で

Odoo Community の受注フォームに、注意の帯・リボン・CSS だけの棒グラフ・手順のカード・リンクのボタンを足しました。画面の XML を継承して XPath で差し込む作り方を、実際に動かしたコードで説明します。

6分
最終更新: 2026年10月26日
フォームビューの継承XPathアドオン販売
目次

目次

このページには目次がありません

Odoo のフォームに HTML の部品を足す — 注意の帯・グラフ・リンクをビューの継承で

受注を開いたときに「この受注は前受金を確かめる」「この得意先は最近どれくらい買っているか」が目に入れば、聞き回る手間が減ります。項目を増やすほどではない、注意書き・小さなグラフ・よく使うリンクを、フォームに置きたい場面です。

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 &lt; 300000">
        <i class="fa fa-exclamation-triangle" title="注意"/>
        <span><strong>30万円以上の受注です。</strong>前受金の要否を営業の責任者に確かめてください。</span>
      </div>
    </xpath>
    <!-- 新しいタブは次の節 -->
  </field>
</record>

受注 S00015 のフォーム。右上に「請求待ち」のリボン、得意先の欄の上に黄色い注意の帯

請求がまだで、合計が 30万円 以上の受注(画面は見本のデータです)

受注 S00003 のフォーム。リボンも帯も出ていない

見積の段階で金額も小さい受注には、どちらも出ません(画面は見本のデータです)

条件に書けるのは、そのフォームの項目です。金額の基準のような会社ごとに変わる値は、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 の拡張の作り方」にまとめています。

「作り方」の記事をもっと見る

ERP を、借りずに所有する。

Odoo Community を日本の業務に合わせ、ソース一式を人数無制限でお渡しします。

相談する(無料)