Perlゼミ

  1. Perl
  2. 帳票作成
  3. here

請求書の作成 - PDF::API2で帳票作成

PDF::API2で請求書を作成してみました。見積書、申込書、レシート、サービス利用明細など、本格的な帳票の作成の基本的なひな型として利用できます。

  • A4用紙サイズ
  • 日本語対応
  • 太字対応
  • 背景色対応
  • フォントサイズ対応
  • 下線対応
  • 表の表示
  • 表の偶奇カラーリング
  • 単価、数量、価格
  • 価格合計
  • 印鑑押印用の四角
  • 会社ロゴの挿入
  • 金額桁区切り
  • 25項目

作成した請求書のPDFのサンプルは、こちらからダウンロードできます。

請求書の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 = '木本システム株式会社';
$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
木本システム株式会社
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の書籍
  • 業務に役立つPerl

    ログ解析など日本語を含むテキスト処理の実践!
    この私、Perlゼミの作者が執筆しています。
    ご購入、口コミ歓迎。
  • Perlの書籍 »
自己紹介
木本裕紀

「今日も元気だ、Perlで元気。
ゆとりあるITエンジニアライフのために
Perl情報を前向きに発信中!」

Twitter
フォロー、いいね、リツート、コメント歓迎

Youtube

チャンネル登録、いいね、コメント歓迎

kimoto.yuki@gmail.com
応援メッセージ、質問、間違い報告歓迎

木本システム株式会社
ご紹介キャンペーン実施中です。契約金額の10%をご紹介料としてお支払い。

(例)30万円のシステム開発委託契約が、1年続いたら、36万円がもらえる!!!