帳票を出力する
用紙、グループごとの小計、総合計、改ページ。
帳票は type: report。明細に出す列は一覧と同じ table.columns に書き、印刷のための指定(用紙・小計・改ページ)を report に書く。
page:
type: report
id: sales_report
title: 売上明細表
repository: orderRepository
search:
filters:
- { field: orderDate, label: 受注日, type: date, operator: between }
table:
columns:
- { field: orderNo, label: 受注番号 }
- { field: customer, label: 顧客 }
- { field: amount, label: 金額, type: number, format: currency }
report:
paper: { size: A4, orientation: portrait }
rowsPerPage: 30
sort: { field: customer }
groupBy: [ { field: customer, label: 顧客, pageBreak: true } ]
totals: [ { field: amount, aggregate: sum } ]
actions:
- { id: csv, type: export, label: CSV出力, config: { filename: 売上明細, bom: true } }groupBy は「並んでいる前提」で切る
groupBy はコントロールブレイク、つまり上から見ていって値が変わったところで区切る方式。並んでいないデータに対して使うと、同じ顧客のグループが何度も現れる。
だから sort が要る。groupBy に指定した項目と同じ順で並べておく(下の「よくある間違い」参照)。並べ替えを実行するのは Repository なので、sort はそこへ渡る指定でしかない。
複数階層で切るときは外側から書く。
sort: { field: customer }
groupBy:
- { field: area, label: 地区 }
- { field: customer, label: 顧客 }小計と総合計は totals
totals に書いた項目が、グループの小計行と最後の総合計行に出る。同じ項目を2回書けば、合計と件数を並べられる。
totals:
- { field: amount, aggregate: sum }
- { field: amount, aggregate: count }aggregate の既定は sum(ダッシュボードは count が既定なので逆)。
改ページ
pageBreak: true を付けたグループは、値が変わるたびに新しい用紙から始まる。顧客ごとに1枚ずつ配る帳票はこれ。
rowsPerPage(既定 40)は1枚に載せる行数。グループの見出し行と小計行も1行として数えるので、明細だけの数ではない。A4 縦で 30〜35 行あたりが目安。
limit があるので全件は出ない
1回の実行で読む行数は limit(既定 1000)。帳票はページングしないので、これが実質の上限になる。月次の全明細を出すような帳票では足りているか必ず確認する。
一覧との違い
一覧(search / crud) | 帳票(report) | |
|---|---|---|
| 目的 | 画面で探す | 紙・PDF にする |
| ページング | pagination で切る | しない(limit まで一度に読む) |
| 小計 | 出せない | totals |
| 用紙 | 関係ない | paper |
同じデータを「画面で探す」と「印刷する」の両方したいなら、search の画面と report の画面を2枚作るのが素直。列定義は似るが、目的が違うので分けたほうが後で揉めない。
CSV も出せる
type: export のアクションを置けば、帳票の列と行から CSV が出る。用紙の指定は CSV には関係しないので、小計行や改ページは CSV には現れない(明細だけが出る)。
印刷ボタンも定義に書く
type: print のアクションを置けば、帳票に印刷ボタンが出る。帳票専用(report の無い画面に置くと validate が警告する。紙の形を決めているのは report なので、無い画面には刷るものが無い)。
actions:
- { id: printPdf, type: print, label: 印刷, config: { filename: 売上明細 } }刷るのはいま画面に出ている行。読み直さないので、画面で 3 枚に見えた帳票は 3 枚で刷られる。ファイル名は config.filename(拡張子が無ければ .pdf)で、それ以外の config は読まずにそのまま出力先に渡る(トレイや書体は印刷所の語彙なので、DSL のキーを増やさない)。
PDF にする・プリンタに送る
バイト列を作るのは Framework の外。 CSV は文字列まで Framework が作れるが、PDF はフォント・符号化・ページツリーを持つ別の世界で、刷らないアプリに背負わせる意味が無い。だから type: print が渡すのは紙の中身まで(帳票の定義・いま出ている行・役割・見せ方)で、PDF にするのは opt-in の hatake_print(純 Dart)、それをプリンタやファイルに送るのはアプリ。
HatakeScope(
printSink: (request) async {
final bytes = reportPdf(
request.page,
request.rows,
formatters: request.formatters, // 画面と同じ見え方で
roles: request.roles, // 見えない列は紙にも出さない
);
await Printing.layoutPdf(onLayout: (_) => bytes); // プリンタに送るなら printing
},
...
)出力先(printSink)を登録していなければ、押したときにそう言う。黙って何も起きないのが一番困るので。定義を1文字も変えずにバッチから刷りたいなら、reportPdf(page, rows) を直接呼んでもよい(UI を通らない=夜間バッチでも同じ1行)。
書式(format)・列幅(column.width)・見えない列(roles)・枚数は画面の帳票と同じ規則で組まれる。画面で 3 枚に見えた帳票は 3 枚で刷られる。
column.width は紙の上ではポイント(1pt = 1/72 inch)として使われ、指定の無い列が残りを分ける。全部足して紙幅を超えたら全体を同じ率で縮めるので、紙から溢れることはない(rowsPerPage が多いときは行の高さと文字も縮む)。
ただし「縮めて収める」は、刷ってから読めないと分かるということでもある。npx hatake validate が刷る前に言う。
警告 page.table.columns: A4 縦の紙幅 595.28pt に対して、列は最低 600pt 要ります
(幅の指定がある 3 列で 600pt)。刷ると全体が縮められて、どの列も読めなくなります。刷る前に紙そのものを見るなら npx hatake paper <file>。列の並び・小計の位置・右寄せ・切れた文字が文字で読める(行を渡さなければ見本の行を作る)。
画面の width をそのまま持ってくると(px のつもりで 200 を3列)これに当たる。用紙の実寸は spec/papers.json が正で、刷る側と警告が同じ数を見ている。
余白・脚注・ページ番号は定義ではなく PrintStyle に書く。紙の体裁は業務ではなく印刷所の話なので、定義に持ち込まない。
reportPdf(page, rows, style: const PrintStyle(footer: '営業部 - {page}/{pages}'));日本語のフォントは埋め込まない(PDF が数KBで済み、どこで刷っても同じバイト列になる)。代わりに書体は開いた環境が決めるので、字面まで固定したい帳票には向かない。UI が無いところ(夜間バッチ・サーバ側)でも同じ1行で刷れるのは、この割り切りのおかげ。
書けるキー
| キー | 書く場所 | 型 | 必須 | 既定値 | 有効なページ種別 | 説明 |
|---|---|---|---|---|---|---|
report | reportPage | object → report | 任意 | — | report | The printing structure: paper, lines per sheet, control breaks and totals. Grouping is a control break over consecutive rows, so the repository must return them in the right order. |
paper | report | object → paper | 任意 | — | report | The sheet a report is laid out on. The renderer previews at this shape. |
size | paper | string (A4 / A3 / B5 / letter ほか) | 任意 | "A4" | report | Paper size. Open string; built-ins below. |
orientation | paper | string (portrait / landscape) | 任意 | "portrait" | report | — |
rowsPerPage | report | integer | 任意 | 40 | report | Lines per sheet. Group headings and total lines count as lines. |
groupBy | report | array → reportGroup | 任意 | — | report | Control breaks, outermost first. |
pageBreak | reportGroup | boolean | 任意 | false | report | Start a new sheet whenever this group changes. |
totals | report | array → reportTotal | 任意 | — | report | Figures on the subtotal / grand-total lines, in order. |
この表は spec/reference.json から生成している(JSON Schema が正)。手元では npx hatake reference <キー名> で同じものが引ける。
近い例
例は丸ごと写して直すのが一番速い。以下は CI で検証済み(そのまま動く形)。
| ファイル | 種別 | 画面 | どういうときに使うか |
|---|---|---|---|
sales_report.yaml | report | 売上明細表 | 一覧の印刷版が欲しい。顧客ごとに小計を出して、CSV も落としたい、紙にも刷りたい |
よくある間違い
groupBy を書いて sort を書かない
なぜ駄目か グループはコントロールブレイク(並んで来た行を上から見て、キーが変わったら小計)。Framework は並べ替えないので、同じ顧客が離れて届くとグループが分裂して小計が何度も出る。
こう直す report.sort に「印刷したい並び」を書く(Repository に渡る)。並べ替えは DB の責務。
page:
type: report
id: sales_report
title: 売上明細表
repository: orderRepository
table:
columns:
- { field: customer, label: 顧客名 }
- { field: amount, label: 金額, type: number, format: currency }
report:
sort: { field: customer }
groupBy: [ { field: customer, label: 顧客 } ]
totals: [ { field: amount, aggregate: sum } ]一覧の画面に type: print の印刷ボタンを置く
なぜ駄目か 紙の形(用紙・1枚の行数・グループ・小計)を決めているのは report なので、report の無い画面には刷るものが無い。定義は通り、ボタンも出るが、押すと「このページでは刷れません」と言われる(押すまで分からない)。
こう直す 印刷は帳票(type: report)に置く。一覧をそのまま持ち出したいなら type: export(CSV)で、こちらはどの画面でも動く。
page:
type: report
id: sales_report
title: 売上明細表
repository: orderRepository
table:
columns:
- { field: amount, label: 金額, type: number }
report:
paper: { size: A4 }
rowsPerPage: 30
actions:
- { id: printPdf, type: print, label: 印刷 }spec/pitfalls.json から生成。各項目は CI で検証済み(間違いは本当に落ち、正しい方は本当に通る)。手元では npx hatake pitfalls <キー名>。
実物を見る
デモアプリの「売上明細表」がこれを使っている。 デモを開く