Docurain Labo

Docurainサービス開発日記

Docurainテンプレートとデータ構造のバランス

DocurainではExcelファイルのA列を使って、変数を定義したり、ロジックを記述できます。それらを使うことで、細かな体裁や出力の制御が可能です。構造化プログラミング相当のことができるため、ある意味「何でもできてしまいます」。

しかし、あまりDocurainのロジックを記述しすぎると、メンテナンス性に問題が出たり、デザインがプログラマブルになってしまうといった問題があります。

今回は1枚目と中間、最終ページで異なる帳票を例として、テンプレートとデータのバランスを解説します。

帳票の仕様

今回は次のような請求書を出力します。これはよくある帳票の形式でしょう。

  • 1枚目だけに出力するデータあり(ヘッダー)
  • 1枚目は10明細
  • 2枚目以降(最終ページを除く)は30明細のみ
  • 最終ページは30明細 + 集計金額などを出力

すべてDocurainテンプレートで行う場合

まず最初はDocurainテンプレートで行う場合を紹介します(以下パターン1とします)。これは元データを生成するシステム側の負担が小さいのが利点です。つまり、次のような関係性にあります。

  • 出力するシステム
    生成するデータがシンプル
  • Docurainテンプレート
    テンプレートロジック記述量大

今回のデータは次のようになっています。価格部分(linesキー以下)が50明細あります。

{
  "header": {
    "companyName": "ルート42株式会社",
    "name": "ドキュメント 太郎",
    "zipcode": "110-0012",
    "address": "東京都台東区竜泉1-10-6 秋田屋ビル2F",
    "number": "1000-01",
    "tel": "000-000-0000",
    "date": "2021/10/20",
    "note": "いつもお世話になっています。\n2021年10月納品分に関する請求書になります。"
  },
  "company": {
    "companyName": "帳票株式会社",
    "name": "帳票 花子",
    "zipcode": "100-0000",
    "address": "東京都千代田区六本木1-1-1",
    "tel": "111-1111-1111"
  },
  "lines": [
    {
      "note": "商品1",
      "price": 100
    },
    :
    {
      "note": "商品50",
      "price": 1000
    }
  ]
}

そして、これを帳票化するためのDocurainテンプレートは次のようになります。

f:id:moongift:20211024202428p:plain

システム側で帳票に合わせて出力データをカスタマイズする場合

次に帳票に合わせて、システム側の出力データをカスタマイズする場合です(以下パターン2とします)。この場合のシステム側、Docurainテンプレートの関係性は次のようになります。

  • システム側
    ロジック記述が必要
  • Docurainテンプレート
    • ロジック記述少
    • メンテナンスが容易

この場合のデータは次のようになります。

{
  "totalPrice": 27500,
  "billingPrice": 30250,
  "header": {
    // この中の情報は前述のデータと同じ
  },
  "company": {
    // この中の情報は前述のデータと同じ
  },
  "pages": [
    {
      "lines": [
        {
          "note": "商品1",
          "price": 100
        },
        :
        {
          "note": "商品10",
          "price": 1000
        }
      ]
    },
    {
      "lines": [
        {
          "note": "商品11",
          "price": 100
        },
        :
        {
          "note": "商品30",
          "price": 1000
        }
      ]
    },
    {
      "lines": [
        {
          "note": "商品31",
          "price": 100
        },
        :
        {
          "note": "商品50",
          "price": 1000
        }
      ]
    }
  ]
}

そして帳票テンプレートは次のようになります。

f:id:moongift:20211019211522p:plain

生成される帳票

生成される帳票はどちらも変わりません。

f:id:moongift:20211019212444p:plain

相違について

では、ここからパターン1と2、それぞれの違いを解説します。

データについて

まずデータについてですが、パターン1と2で次の点が異なります。

集計金額の有無

パターン1にはなかった、明細の集計金額をパターン2には持たせています。これにより、Docurainテンプレート側で集計金額の計算が不要になります。また、小数点以下の計算において、浮動小数点によって予期せぬ端数を発生させることがあります。四捨五入の計算がシステムとExcelテンプレートで異なって設定してしまうと問題になるでしょう。数値計算ロジックをシステム側で行っておくことで、そうした誤差の発生をなくせます。

{
  "totalPrice": 27500,
  "billingPrice": 30250,
}

ページごとに商品数を制御する

パターン1の場合は、全明細をlinesキーの中に入れています。後述しますが、テンプレート側では明細データを区切るロジックが必要になります。

"lines": [
  {
    "note": "商品1",
    "price": 100
  },
  :
  {
    "note": "商品50",
    "price": 1000
  }
]

パターン2では、出力時に元々ページごとの明細件数にデータを分割してます。pagesキーは配列で、最初が1ページ目、次が2ページ目…となっています。

"pages": [
  {
    "lines": [
      {
        "note": "商品1",
        "price": 100
      },
      :
    ]
  },
  {
    "lines": [
      {
        "note": "商品11",
        "price": 100
      },
      :
    ]
  },
  {
    "lines": [
      {
        "note": "商品31",
        "price": 100
      },
      :
    ]
  }
]

テンプレートについて

変数定義

次にテンプレートについてです。まず、最初の変数定義が異なります。パターン1の場合、1ページ目とそれ以降のデータを作成しています。

## 全体用の変数定義
#set($e=$ROOT)
## 帳票のヘッダー用
#set($h=$e.header)
## 帳票の送付先用
#set($c=$e.company)
## 1ページ目の明細
#set($firstPageEntries = $e.lines.take(10))
## 2ページ目以降の明細(2次元リスト)
#set($pageEntryGroups = $e.lines.drop(10).chunk(20))
## 2ページ目以降の明細の最初に1ページ目の明細を追加
#let($pageEntryGroups.unshift($firstPageEntries))

パターン2では、明細データに関する変数は定義しません。

#set($e=$ROOT)
#set($h=$e.header)
#set($c=$e.company)

スコープの定義

フッターで小計を計算する関係上、パターン1の場合は明細での金額を合計する必要があります。しかし、明細行は動的に数が変わりますので、 D12 のようにセル指定を行ってもずれてしまいます。そこで、Docurainのscope機能を使います。

#scope("全て")
  : 省略
#end

このscopeで範囲を定義し、さらに DR.SHIFT_FORMULA を使うことで範囲を考慮した計算式の自動シフトが行われます。 DR.SHIFT_FORMULA はフッター出力にて利用します(後述)。

1ページ目のヘッダー部

1ページ目のヘッダー部については、パターン1と2で変わりません。強いて言えばパターン1の場合は pageEntryGroups を、パターン2は $e.pagesforeach でループ処理していることでしょう。

パターン1の繰り返し処理です。

f:id:moongift:20211024202553p:plain

パターン2です。

f:id:moongift:20211024202734p:plain

#foreach の中では $foreach という変数が利用できます。これを使って、最初のページと最終ページとを判定できます。判定は次のように行います。

ロジック 内容
#if($foreach.first) true ならば最初のループ(今回は最初のページ)
#if($foreach.last) true ならば最終ループ(今回は最後のページ)

明細

パターン1では1ページ目の明細 $firstPageEntries を 2ページ目以降の明細 $pageEntryGroups の中に、 unshift を使って配列の先頭に入れています。そのため、明細の出力はパターン1、パターン2ともに変わりません。

A B C
#foreach($line in $page.lines)
$!{line.note} $!{line.price}
#end

f:id:moongift:20211019211820p:plain

フッター

フッターでは集計行を出力します。パターン1の場合、先に定義したscopeを使って計算式をシフトします。

C D
小計 =DR.SHIFT_FORMULA(SUM(D19),"scope=全て, target=all")
税率 10
集計 =D22*(1 + D23/100)

パターン2ではシステム側であらかじめ金額を計算しているので、それを出力するだけです。

C D
小計 $!{e.totalPrice}
税率 10
集計 $!{e.billingPrice}

全体像

f:id:moongift:20211024202616p:plain

パターン1と2のテンプレートを同じ倍率で並べています。ロジックは小さくて見えないと思いますが、右側のパターン2の方が少ないロジック量で書かれているのは分かるかと思います。Docurainのテンプレートにはロジックを記述できますが、それでも一般的なプログラミング言語に比べると複雑な記述には向いていません。ロジックの記述量が増えてくると、数ヶ月経った後のメンテナンスや、調整で思わぬ工数がとられることでしょう。

システムによるデータ生成とテンプレートロジックの関係性

すべてシステム側で調整することや、すべてテンプレートのロジックで処理すること、どちらか一方に偏るのはバランスが悪いと言えます。大事なのはメンテナンス性や、柔軟性になります。システム側のデータですべてを制御してしまうと、帳票のちょっとした修正でもシステム側の修正が必要になります。これではせっかくExcelを使った柔軟なテンプレート作成という利点がなくなってしまいます。かといって、ロジック量が増えると、システムの改修コストは下がりますが、テンプレートが複雑で、メンテナンスできないものになってしまうでしょう。大事なのはバランスです。

理想的な切り分け方はMVVMパターン(Model-View-ViewModel)に近い形だと考えられます。MVVMパターンではデザイナーとプログラマーの分担開発を目的に実施されます。データベースなどにあるデータ(モデル)を素のままに出力するのではなく、ViewModelを介してView(帳票)で使いやすい形にすることで、帳票担当者の作業効率を向上させられます。ViewModelはViewを束縛するものではないので、View側でもある程度のロジック(繰り返し処理など)を使って帳票を設計できます。

まとめ

Docurainを使っていると、システム側はなるべく何もせず、帳票生成に関するロジックをすべてテンプレート側に持たせたくなります。しかし、それではテンプレートが複雑になりすぎて、メンテナンス性が大幅に低下します。これでは柔軟な帳票設計を可能にするDocurainを活用しきれないでしょう。

通常、Web画面開発などではMVVMの考え方を取り入れているはずですが、それは帳票でも同じである、ということです。

今回の例はシンプルな帳票ですが、世の中にはもっと複雑な帳票が数多くあります。システム側でのデータ整形を効率的に行い、効率的で継続的な更新ができる帳票設計を目指してください。