請求書の作成 - PDF::API2で帳票作成
PDF::API2で請求書を作成してみました。見積書、申込書、レシート、サービス利用明細など、本格的な帳票の作成の基本的なひな型として利用できます。
- A4用紙サイズ
- 日本語対応
- 太字対応
- 背景色対応
- フォントサイズ対応
- 下線対応
- 表の表示
- 表の偶奇カラーリング
- 単価、数量、価格
- 価格合計
- 印鑑押印用の四角
- 会社ロゴの挿入
- 金額桁区切り
- 25項目
作成した請求書のPDFのサンプルは、こちらからダウンロードできます。
PDF::API2で書いた請求書のソースコード
PDF::API2で書いた請求書のソースコードです。
注意点
会社ロゴは、同一ディレクトリに「logo.png」という名前で、配置してください。
日本語用フォントは、テキストを描画するで紹介した、あおぞら明朝フォントを使っています。通常フォント「AozoraMinchoRegular.ttf」と太字フォント「AozoraMincho-bold.ttf」を同一ディレクトリに配置してください。
レイアウトのアルゴリズムは、表の構造になっています。1つの縦幅を指定して、行を進めています。列は、100分割されていて、その割合を示した位置に表示しています。
PDF::API2で書いた請求書のソースコード
use strict;
use warnings;
use utf8;
use FindBin;
use PDF::API2;
# 商品データ(本)
my $books = [
{
name => '本1',
unit_price => 1000,
count => 3,
},
{
name => '本2',
unit_price => 2000,
count => 6,
},
{
name => '本3',
unit_price => 1500,
count => 5,
}
];
# 金額小計
my $price_total_no_tax = 0;
for my $book (@$books) {
$price_total_no_tax += $book->{unit_price} * $book->{count};
}
my $price_total_no_tax_disp = price_disp($price_total_no_tax);
# 消費税
my $tax_rate = 0.1;
my $tax = int($price_total_no_tax * $tax_rate);
my $tax_disp = price_disp($tax);
# 税込金額
my $price_total = $price_total_no_tax + $tax;
my $price_total_disp = price_disp($price_total);
# PDF
my $pdf = PDF::API2->new;
# 用紙サイズ A4設定
$pdf->mediabox('A4');
# 用紙サイズの取得
my @page_size_infos = $pdf->mediabox;
my $page_width = $page_size_infos[2];
my $page_height = $page_size_infos[3];
# ページ余白
my $page_top_margin = 48;
my $page_bottom_margin = 48;
my $page_left_margin = 49;
my $page_right_margin = 49;
# 日本語に対応したTrueTypeフォントの読み込み - 通常フォントと太字フォント
my $true_type_font_file = "$FindBin::Bin/AozoraMinchoRegular.ttf";
my $font = $pdf->ttfont($true_type_font_file);
my $true_type_font_bold_file = "$FindBin::Bin/AozoraMincho-bold.ttf";
my $font_bold = $pdf->ttfont($true_type_font_bold_file);
# フォントサイズ - デフォルト
my $font_size_default = 10;
# 描画開始x座標
my $render_start_x = $page_left_margin;
# 描画開始y座標
my $render_start_y = $page_height - 1 - $page_top_margin;
# 描画終了x座標
my $render_end_x = $page_width - 1 - $page_right_margin;
# 描画終了y座標
my $render_end_y = $page_bottom_margin;
# レイアウトは、テーブル構造になっていて
# 縦幅と横幅の最小単位を設定、横幅は、100分割したものが最小単位
my $unit_height = 14;
my $unit_width = ($render_end_x - $render_start_x) / 100;
# テキスト描画のパディング
my $text_bottom_padding = 3;
my $text_left_padding = 3;
# 受取企業欄の幅
my $receive_company_end_tds_count = 55;
# 請求企業欄の開始位置
my $send_company_start_tds_count = 66.5;
# 色の一覧
my $color_black = '#000';
# 線の幅
my $line_width_basic = 0.3;
my $line_width_bold = 2;
# ページ
my $page = $pdf->page;
# グラフィック描画
my $gfx = $page->gfx;
# テキスト描画
my $text = $page->text;
# 現在の行
my $cur_row = 0;
# 一番上太線
$gfx->move($render_start_x, $render_start_y);
$gfx->hline($render_end_x);
$gfx->linewidth(10);
$gfx->stroke;
$cur_row += 1.5;
# 申し込み日付
$text->translate($render_end_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->strokecolor($color_black);
$text->fillcolor($color_black);
$text->text_right('2019年10月14日');
$gfx->move($render_start_x + $send_company_start_tds_count * $unit_width);
$gfx->hline($render_end_x);
$gfx->linewidth($line_width_basic);
$gfx->stroke;
$cur_row += 2;
# ロゴの開始位置を保存
my $cur_logo_row = $cur_row - 1.5;
# 請求書タイトル
$text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font_bold, 20);
$text->text('請求書');
$cur_row += 3;
# 右列の開始位置を保存
my $cur_right_row = $cur_row + 1;
# 納品先企業名
my $receive_company_name = 'XXXXX株式会社';
$text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text($receive_company_name);
$text->translate(
$render_start_x + $receive_company_end_tds_count * $unit_width,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->text_right('御中');
$gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row);
$gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width);
$gfx->linewidth($line_width_basic);
$gfx->stroke;
$cur_row += 4;
# 件名
my $subject_label = '件 名:';
$text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text($subject_label);
my $subject = '2019年3月3日の商品売買契約';
$text->translate(
$render_start_x + 10 * $unit_width,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($subject);
$gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row);
$gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width);
$gfx->linewidth($line_width_basic);
$gfx->stroke;
$cur_row += 1;
# 納品日
my $delivery_date_label = '納品日:';
$text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text($delivery_date_label);
my $delivery_date = '2019年3月30日';
$text->translate(
$render_start_x + 10 * $unit_width,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($delivery_date);
$gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row);
$gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width);
$gfx->linewidth($line_width_basic);
$gfx->stroke;
$cur_row += 2.8;
# 請求メッセージ
$text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text('下記の通り、ご請求申し上げます。');
$cur_row += 2;
# 金額
my $price_total_top_label = '金額';
$text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text($price_total_top_label);
my $price_total_top = "¥${price_total_disp}円";
$text->translate(
$render_start_x + 48 * $unit_width,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->font($font, $font_size_default + 5);
$text->text_right($price_total_top);
my $price_total_top_zeikomi = '(税込)';
$text->translate(
$render_start_x + 50 * $unit_width,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($price_total_top_zeikomi);
$gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row);
$gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width);
$gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row - 2);
$gfx->hline($render_start_x + $receive_company_end_tds_count * $unit_width);
$gfx->linewidth($line_width_basic);
$gfx->stroke;
$cur_row += 0.5;
# 会社ロゴ挿入
my $logo_image_file = "$FindBin::Bin/logo.png";
my $logo_image_object = $pdf->image_png($logo_image_file);
my $logo_image_width = 60;
$gfx->image(
$logo_image_object,
$render_start_x + $send_company_start_tds_count * $unit_width,
$render_start_y - $unit_height * $cur_logo_row - $logo_image_width,
$logo_image_width,
$logo_image_width,
);
# 請求者
my $sender_company_name = 'Perlクラブ';
$text->translate(
$render_start_x + $send_company_start_tds_count * $unit_width,
$render_start_y - $unit_height * $cur_right_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($sender_company_name);
$cur_right_row += 1;
# 郵便番号
my $sender_zip_code = '住所:123-4567';
$text->translate(
$render_start_x + $send_company_start_tds_count * $unit_width,
$render_start_y - $unit_height * $cur_right_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($sender_zip_code);
$cur_right_row += 1;
# 住所
my $sender_address = '東京都港区〇〇 123-4';
$text->translate(
$render_start_x + $send_company_start_tds_count * $unit_width,
$render_start_y - $unit_height * $cur_right_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($sender_address);
$cur_right_row += 1;
# 電話番号
my $sender_tel = 'TEL:090-1234-5678';
$text->translate(
$render_start_x + $send_company_start_tds_count * $unit_width,
$render_start_y - $unit_height * $cur_right_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($sender_tel);
$cur_right_row += 1;
# FAX
my $sender_fax = 'FAX:090-1234-5679';
$text->translate(
$render_start_x + $send_company_start_tds_count * $unit_width,
$render_start_y - $unit_height * $cur_right_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($sender_fax);
$cur_right_row += 1;
# 担当
my $sender_staff = '担当:田中太郎';
$text->translate(
$render_start_x + $send_company_start_tds_count * $unit_width,
$render_start_y - $unit_height * $cur_right_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($sender_staff);
# 印鑑押印四角形
my $sleal_width = 55;
$gfx->rectxy(
$render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 2,
$render_start_y - $unit_height * ($cur_right_row - 1) + $sleal_width,
$render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 3,
$render_start_y - $unit_height * ($cur_right_row - 1)
);
$gfx->rectxy(
$render_start_x + $send_company_start_tds_count * $unit_width,
$render_start_y - $unit_height * $cur_right_row ,
$render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 3,
$render_start_y - $unit_height * $cur_right_row - $sleal_width
);
$gfx->poly(
$render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width,
$render_start_y - $unit_height * $cur_right_row ,
$render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width,
$render_start_y - $unit_height * $cur_right_row - $sleal_width
);
$gfx->poly(
$render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 2,
$render_start_y - $unit_height * $cur_right_row ,
$render_start_x + $send_company_start_tds_count * $unit_width + $sleal_width * 2,
$render_start_y - $unit_height * $cur_right_row - $sleal_width
);
$gfx->strokecolor('#bbb');
$gfx->stroke;
# 見積書見出し背景
$gfx->rectxy(
$render_start_x, $render_start_y - $unit_height * $cur_row,
$render_end_x, $render_start_y - $unit_height * ($cur_row + 1)
);
$gfx->fillcolor('#eee');
$gfx->fill;
# 見積書見出し上飾り太線
$gfx->move($render_start_x, $render_start_y - $unit_height * $cur_row);
$gfx->hline($render_end_x);
$gfx->linewidth($line_width_bold);
$gfx->strokecolor($color_black);
$gfx->stroke;
$cur_row += 1;
# 見積書見出し下飾り太線の位置を保存
my $header_bottom_line_row = $cur_row;
my $cur_column_units_count = 0;
# No
my $no_units_count = 10;
$text->translate($render_start_x + $cur_column_units_count * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text('No.');
$cur_column_units_count += $no_units_count;
# 項目
my $name_units_count = 34;
$text->translate($render_start_x + $cur_column_units_count * $unit_width + ($name_units_count * $unit_width / 2), $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text_center('項目');
$cur_column_units_count += $name_units_count;
# 数量
my $count_units_count = 14;
$text->translate($render_start_x + $cur_column_units_count * $unit_width + ($count_units_count * $unit_width / 2), $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text_center('数量');
$cur_column_units_count += $count_units_count;
# 単価
my $unit_price_units_count = 14;
$text->translate($render_start_x + $cur_column_units_count * $unit_width + ($unit_price_units_count * $unit_width / 2), $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text_center('単価');
$cur_column_units_count += $unit_price_units_count;
# 金額
my $price_units_count = 28;
$text->translate($render_start_x + $cur_column_units_count * $unit_width + ($price_units_count * $unit_width / 2), $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text_center('金額');
$cur_column_units_count = 0;
$cur_row++;
my $rows_count = 25;
# 各項目の枠の描画
for (my $row = 0; $row < $rows_count; $row++) {
my $book = $books->[$row];
# 行を交互に塗り分ける
if ($row % 2 == 1) {
$gfx->rectxy(
$render_start_x,
$render_start_y - $unit_height * $cur_row,
$render_end_x,
$render_start_y - $unit_height * ($cur_row - 1),
);
$gfx->fillcolor('#eee');
$gfx->fill;
}
# No
if ($book) {
$text->translate(
$render_start_x + $cur_column_units_count * $unit_width + $text_left_padding,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($row + 1);
}
$cur_column_units_count += $no_units_count;
# 項目
$gfx->poly(
$render_start_x + $cur_column_units_count * $unit_width,
$render_start_y - $unit_height * $cur_row,
$render_start_x + $cur_column_units_count * $unit_width,
$render_start_y - $unit_height * ($cur_row - 1)
);
$gfx->linewidth(1.5);
$gfx->strokecolor('#ccc');
$gfx->stroke;
if ($book) {
$text->translate(
$render_start_x + $cur_column_units_count * $unit_width + $text_left_padding,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text($book->{name});
}
$cur_column_units_count += $name_units_count;
# 数量
$gfx->poly(
$render_start_x + $cur_column_units_count * $unit_width,
$render_start_y - $unit_height * $cur_row,
$render_start_x + $cur_column_units_count * $unit_width,
$render_start_y - $unit_height * ($cur_row - 1)
);
$gfx->linewidth(1.5);
$gfx->strokecolor('#ccc');
$gfx->stroke;
if ($book) {
$text->translate(
$render_start_x + $cur_column_units_count * $unit_width + ($count_units_count * $unit_width) - $text_left_padding,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
$text->text_right($book->{count});
}
$cur_column_units_count += $count_units_count;
# 単価
$gfx->poly(
$render_start_x + $cur_column_units_count * $unit_width,
$render_start_y - $unit_height * $cur_row,
$render_start_x + $cur_column_units_count * $unit_width,
$render_start_y - $unit_height * ($cur_row - 1)
);
$gfx->linewidth(1.5);
$gfx->strokecolor('#ccc');
$gfx->stroke;
if ($book) {
$text->translate(
$render_start_x + $cur_column_units_count * $unit_width + ($unit_price_units_count * $unit_width) - $text_left_padding,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
my $unit_price_disp = price_disp($book->{unit_price});
$text->text_right($unit_price_disp);
}
$cur_column_units_count += $unit_price_units_count;
# 金額
$gfx->poly(
$render_start_x + $cur_column_units_count * $unit_width,
$render_start_y - $unit_height * $cur_row,
$render_start_x + $cur_column_units_count * $unit_width,
$render_start_y - $unit_height * ($cur_row - 1)
);
$gfx->linewidth(1.5);
$gfx->strokecolor('#ccc');
$gfx->stroke;
if ($book) {
$text->translate(
$render_start_x + $cur_column_units_count * $unit_width + ($price_units_count * $unit_width) - $text_left_padding,
$render_start_y - $unit_height * $cur_row + $text_bottom_padding
);
$text->font($font, $font_size_default);
my $price_disp = price_disp($book->{unit_price} * $book->{count});
$text->text_right($price_disp);
}
$cur_column_units_count = 0;
$cur_row++;
}
# 見積書見出し下飾り太線(灰色の縦線の上に書くためこの位置で描画)
$gfx->move($render_start_x, $render_start_y - $unit_height * $header_bottom_line_row);
$gfx->hline($render_end_x);
$gfx->linewidth($line_width_bold);
$gfx->strokecolor($color_black);
$gfx->stroke;
# 太線
$gfx->move($render_start_x, $render_start_y - $unit_height * ($cur_row - 1));
$gfx->hline($render_end_x);
$gfx->linewidth($line_width_bold);
$gfx->strokecolor($color_black);
$gfx->stroke;
# 小計
my $price_total_no_tax_label = '小計';
$text->translate($render_start_x + ($no_units_count + $name_units_count + $count_units_count) * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text($price_total_no_tax_label);
$text->translate($render_end_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text_right($price_total_no_tax_disp);
$cur_row += 1;
# 消費税
my $tax_label = '消費税';
$text->translate($render_start_x + ($no_units_count + $name_units_count + $count_units_count) * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text($tax_label);
$text->translate($render_end_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text_right($tax_disp);
$cur_row += 1;
# 消費税
my $price_total_label = '税込合計';
$text->translate($render_start_x + ($no_units_count + $name_units_count + $count_units_count) * $unit_width + $text_left_padding, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text($price_total_label);
$text->translate($render_end_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text_right("¥$price_total_disp");
$cur_row += 1;
# 太線
$gfx->move($render_start_x, $render_start_y - $unit_height * ($cur_row - 1));
$gfx->hline($render_end_x);
$gfx->linewidth($line_width_bold);
$gfx->strokecolor($color_black);
$gfx->stroke;
# 振込先
my $furikomi_disp = <<"EOS";
お振込先
みずほ銀行芝支店
普通口座 34521xx
Perlクラブ
EOS
for my $line (split /\n/, $furikomi_disp) {
$text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text($line);
$cur_row += 1;
}
# 細い線
$gfx->move($render_start_x, $render_start_y - $unit_height * ($cur_row - 1));
$gfx->hline($render_end_x);
$gfx->linewidth($line_width_basic);
$gfx->strokecolor($color_black);
$gfx->stroke;
# 備考
my $bikou_disp = <<"EOS";
納品日の翌月の末日までのお支払いをお願いいたします。
EOS
for my $line (split /\n/, $bikou_disp) {
$text->translate($render_start_x, $render_start_y - $unit_height * $cur_row + $text_bottom_padding);
$text->font($font, $font_size_default);
$text->text($line);
$cur_row += 1;
}
# 太線
$gfx->move($render_start_x, $render_end_y);
$gfx->hline($render_end_x);
$gfx->linewidth($line_width_bold);
$gfx->strokecolor($color_black);
$gfx->stroke;
# 3桁区切りで金額を表示してくれるサブルーチン
sub price_disp {
my ($price) = @_;
1 while $price =~ s/(.*\d)(\d\d\d)/$1,$2/;
return $price;
}
my $pdf_file = 'invoice.pdf';
$pdf->saveas($pdf_file);
Perlゼミ

