見積書を作る前に、まず「誰に」「何を」売るのかを登録しておく土台が要ります。この第1回では、Ruby on Rails 8の scaffold を使って、取引先マスタと品目マスタの登録・一覧画面を作り、入力チェックと日本語化まで仕上げます。前回の第0回(Rails 8の雛形を動かして、Excel見積からの脱却を始める)で作った雛形の続きです。
「Excelの取引先一覧と品目一覧を、そのままシステムに移したい。でも入力ミスや重複が心配…」
結論から言うと、マスタ管理は「scaffoldで骨組みを作り、入力チェックを足して、日本語にする」の3手順で、1日かからず形になります。 本当に大事なのは画面の数ではなく、「品目コードの重複禁止」「消費税率は8%と10%だけ」といったルールをデータの入口で固定しておくことです。この回では、そのルールをテストコードで守る方法まで書きます。
取引先・品目マスタで作るもの
作るのは次の2つです。どちらも「一覧・検索・登録・編集・削除」ができます。
| マスタ | 主な項目 | 入力チェック(この回で入れるもの) |
|---|---|---|
| 取引先 | 取引先名・カナ・郵便番号・住所・メール・電話・備考 | 取引先名は必須/郵便番号は7桁/メールの形式 |
| 品目 | 品目コード・品目名・単位・単価(税抜)・税率・使用中 | コードは重複禁止/単価は0以上の整数/税率は8か10 |
マスタは後の回で見積書の明細から参照されます。そのため、最初に項目の型とルールを決めておくと、後から作り直す手間が減ります。
scaffoldで骨組みを一気に作る
scaffold(足場)は、データの保存先(モデル・テーブル)・画面・処理の入口(コントローラー)・URLの設定を、1コマンドでまとめて生成するRailsの機能です。入力フォームから一覧までの基本形が数秒でできます。
📰 出典:Ruby on Rails Guides「Getting Started with Rails」
bin/rails generate scaffold Customer name:string name_kana:string postal_code:string \
address:string email:string phone:string note:text --no-test-framework
bin/rails generate scaffold Item code:string name:string unit:string \
unit_price:integer tax_rate:integer active:boolean --no-test-framework
--no-test-framework は自動生成のテストを作らない指定です。この連載では、ルールに合わせたテストを自分で書きます。生成された画面は英語表記で、入力チェックも付いていません。ここからが「磨き込み」です。
テーブル定義でも「守り」を入れる
生成されたマイグレーション(テーブル定義の変更履歴)を、実務向けに直します。ポイントは、アプリ側の入力チェックだけに頼らず、データベース自体にも制約を入れることです。
# db/migrate/20261002064437_create_items.rb(抜粋)
create_table :items do |t|
t.string :code, null: false
t.string :name, null: false
t.string :unit
t.integer :unit_price, null: false, default: 0
t.integer :tax_rate, null: false, default: 10
t.boolean :active, null: false, default: true
t.timestamps
end
add_index :items, :code, unique: true
null: false:空のまま保存できない(必須項目)default:入力しなかったときの初期値(税率は標準の10%)unique: true:同じ品目コードを2つ登録できない
単価を integer(整数)にしているのは、円単位の金額を小数で持つと端数の誤差が出るためです。金額計算の詳細は第4回で扱います。
モデルに入力チェック(バリデーション)を書く
Railsでは、入力チェックのことをバリデーションと呼び、モデルに宣言的に書きます。
📰 出典:Ruby on Rails Guides「Active Record Validations」
# app/models/item.rb
class Item < ApplicationRecord
# 消費税率(%)。軽減税率の8%と標準の10%だけを許す
TAX_RATES = [ 8, 10 ].freeze
validates :code, presence: true, uniqueness: true, length: { maximum: 30 }
validates :name, presence: true, length: { maximum: 100 }
validates :unit_price, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :tax_rate, inclusion: { in: TAX_RATES }
scope :active, -> { where(active: true) }
scope :search, ->(keyword) {
next all if keyword.blank?
pattern = "%#{sanitize_sql_like(keyword.strip)}%"
where("code ILIKE :p OR name ILIKE :p", p: pattern)
}
end
消費税率を8と10に限定した根拠は、国税庁が示す現行の税率(標準10%・軽減8%)です。
📰 出典:国税庁「消費税の軽減税率制度について」
税率は法改正で変わりうるため、本番運用では「税率を設定として管理する」設計も検討します。この連載では、わかりやすさを優先して定数にしています(執筆時点:2026年10月)。
取引先のモデルも同様です。
# app/models/customer.rb
class Customer < ApplicationRecord
validates :name, presence: true, length: { maximum: 100 }
validates :postal_code, format: { with: /\A\d{3}-?\d{4}\z/ }, allow_blank: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true
# search スコープは Item と同じ作り(名前・カナを部分一致で検索)
end
検索の sanitize_sql_like は、検索語に含まれる % や _ をただの文字として扱うための処理です。これがないと「%」と入力しただけで全件が出てしまいます。PostgreSQLの ILIKE は、大文字小文字を区別しない部分一致です。
エラーメッセージと項目名を日本語にする
Railsの初期設定は英語です。config/application.rb の config.i18n.default_locale = :ja を指定済みなので、日本語の辞書ファイルを置きます。
📰 出典:Ruby on Rails Guides「Rails Internationalization (I18n) API」
# config/locales/ja.yml(抜粋)
ja:
activerecord:
models:
customer: 取引先
item: 品目
attributes:
item:
code: 品目コード
unit_price: 単価(税抜)
tax_rate: 消費税率
errors:
format: "%{attribute}%{message}"
messages:
blank: を入力してください
taken: はすでに使われています
これで「品目コードはすでに使われています」のような、現場の人に伝わるエラーが画面に出ます。画面側は、エラーを共通部品(パーシャル)にまとめて、取引先・品目の両方で使い回しています。
<%# app/views/shared/_errors.html.erb %>
<% if record.errors.any? %>
<div class="errors" role="alert">
<p>入力内容を確認してください(<%= record.errors.count %>件)</p>
<ul>
<% record.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
コントローラーを整える
scaffoldが作るコントローラーはJSON出力にも対応した長いものです。この連載は画面だけを使うため、HTMLのみに絞って短くしています。
# app/controllers/items_controller.rb(抜粋)
def index
@items = Item.search(params[:q]).order(:code, :id)
end
def create
@item = Item.new(item_params)
if @item.save
redirect_to @item, notice: "#{Item.model_name.human}を登録しました。"
else
render :new, status: :unprocessable_content
end
end
private
def item_params
params.expect(item: [ :code, :name, :unit, :unit_price, :tax_rate, :active ])
end
params.expect は、受け取る項目を明示的に絞る仕組みです。ここに書いていない項目は無視されるため、画面に無い項目をこっそり書き換えられる攻撃(マスアサインメント)を防げます。
ルールをテストで固定する
入力チェックは、書いただけでは安心できません。あとで誰かが直したときに壊れていないことを、テストで確認します。
# test/models/item_test.rb(抜粋)
test "品目コードは重複できない" do
item = Item.new(code: "DSG-001", name: "別の品目", unit_price: 100, tax_rate: 10)
assert_not item.valid?
assert_includes item.errors.full_messages, "品目コードはすでに使われています"
end
test "税率は8%と10%だけ" do
assert Item.new(code: "X", name: "X", unit_price: 0, tax_rate: 8).valid?
assert_not Item.new(code: "X", name: "X", unit_price: 0, tax_rate: 5).valid?
end
画面から登録するまでの流れも、コントローラーのテストで確認しています(test/controllers/items_controller_test.rb)。
動作確認の方法
cd blogs/it_hacchu/series/rails-mitsumori/code
bin/rails db:prepare # テーブル作成
bin/rails db:seed # サンプルの取引先・品目を登録
bin/rails test # テスト
bundle exec rubocop # コードの書き方チェック
bin/rails server # http://localhost:3000/items
開発環境では、テスト19件が通り、RuboCop(コードの書き方チェックツール)も指摘なしでした。サンプルデータを入れて表示した品目一覧が次の画面です。


※Linuxの開発環境で撮影したため、お使いのOSやブラウザとは見た目が異なる場合があります。
つまずきやすい点
bin/rails db:seedを何度も実行して重複する:サンプルではfind_or_create_by!を使い、何度実行しても増えないようにしています- 郵便番号をハイフンあり・なしで混在させる:この回は両方を許可しています。帳票に印字する回で表記を統一する方針を決めます
- マスタを削除すると、過去の見積が困る:今は削除できますが、見積書が品目を参照するようになる第3回以降は、削除ではなく「使用中」を外して停止する運用に切り替えます
- ログインがない:この回では誰でも画面を開けます。第2回でログインを付けるまでは、公開サーバーに置かないでください
発注者向けメモ:マスタ管理を頼むときの確認点
マスタ画面は「地味だが後から効く」部分です。開発会社に依頼するときは、次の点を確認しておくと安心です。
- ☐ 品目コード・取引先コードの採番ルール(今のExcelの番号を引き継ぐか)を伝えたか
- ☐ 重複や入力ミスを防ぐルール(必須項目・桁数・形式)を、開発会社と一覧にして合意したか
- ☐ 既存のExcelからのデータ移行(件数・表記ゆれの整理を誰が担当するか)が見積に入っているか
- ☐ マスタを「削除」ではなく「停止」にする運用の要否を決めたか
- ☐ 税率が改正されたときの変更方法(設定画面か、開発会社への依頼か)を確認したか
- ☐ 操作した人・日時の記録(誰が品目の単価を変えたか)が必要か
工数が増えやすいのは、既存データの移行と、取引先ごとの特別な単価・締め日などの例外ルールです。例外が多い場合は、最初のリリースでは標準ルールだけで始めて、後から追加する進め方も検討できます。
「Excelの名簿をそのまま取り込むと、全角半角の違いや表記ゆれで重複が出ます。移行前の整理に、思った以上の時間がかかることが多いです」
まとめと次回予告
第1回では、scaffoldで取引先・品目のマスタを作り、バリデーション・日本語化・テストで「入口のルール」を固めました。Railsの標準機能だけで、外部ライブラリなしにここまで作れます。なお、この環境ではブラウザ上での登録操作(フォーム送信)の画面確認は一覧画面のみで、登録・エラー表示は自動テストで確認しました。
次回の第2回は「ログインを付ける」です。Rails 8標準の認証ジェネレータを使い、権限の種類まで整理します。
この連載の記事一覧
この記事は連載「Railsで作る見積・請求管理システム」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【Railsで作る見積・請求管理システム 第0回】Rails 8の雛形を動かして、Excel見積からの脱却を始める
- 【Railsで作る見積・請求管理システム 第1回】取引先と品目のマスタを作る(scaffoldと入力チェック)(この記事)
- 【Railsで作る見積・請求管理システム 第2回】ログインを付ける(Rails 8標準の認証と権限の分け方)
- 【Railsで作る見積・請求管理システム 第3回】見積書と明細を作る(Hotwireで明細行をその場で追加)
- 【Railsで作る見積・請求管理システム 第4回】金額・消費税・端数処理を正しく計算する(税率ごとに1回だけ丸める)
- 【Railsで作る見積・請求管理システム 第5回】見積書をPDFで出力する(Prawnで日本語フォントを扱う)
- 【Railsで作る見積・請求管理システム 第6回】見積書をメールで送る(Solid Queueで送信を非同期にする)
- 【Railsで作る見積・請求管理システム 第7回】受注から請求書へ変換する(状態遷移と請求番号、インボイスの記載事項)
- 【Railsで作る見積・請求管理システム 第8回】入金管理と督促の一覧を作る(一部入金・期限超過の抽出)










コメント
コメント一覧 (2件)
[…] 【Railsで作る見積・請求管理システム 第1回】取引先と品目のマスタを作る… […]
[…] 【Railsで作る見積・請求管理システム 第1回】取引先と品目のマスタを作る… […]