MENU

問い合わせ


    【Railsで作る見積・請求管理システム 第1回】取引先と品目のマスタを作る(scaffoldと入力チェック)

    見積書を作る前に、まず「誰に」「何を」売るのかを登録しておく土台が要ります。この第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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      コメント

      コメント一覧 (2件)

      目次