5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

新規開発や新技術の検証、導入にまつわる記事を投稿しよう!

Python の PDF 作成ライブラリ fpdf2 で見積書を作ってみた

Last updated at Posted at 2023-07-03

以下段階の人が書いています

  1. ドキュメントをパッと見た (done)
  2. ドキュメント読みながら手を動かしてみる (←今ここ)
  3. 運用レベルで使ってみる (まだ)

fpdf2 とは

依存が Python のみのライブラリで、 this project is mature and actively maintained. と述べられています。

Python には reportlab という実績のある pdf 作成ライブラリ があります。一方で、 PyPDF2 のメンテナーの方が以下のような紹介文を書かれていたのが気になって触ってみました。

edit: As the maintainer of pypdf and PyPDF2, I will work more closely with fpdf2 in future. They seem to have a good community and the project seems well-managed!

作ってみた見積書と書いたコード

以下のような見積書を作成しました。

スクリーンショット 2023-07-03 8.57.43.png

NotoSansJP の install が必要です。
NotoSansJP へのパスを指定する必要があります。

import datetime
import math

from fpdf import FPDF
from fpdf.fonts import FontFace

tax_rate = 0.1


class PDF(FPDF):
    def 見積もり日(self):  # noqa
        base_x = -70
        base_y = 10
        self.set_xy(base_x, base_y)
        self.cell(txt=f"見積もり日 : {format(datetime.date.today(), '%Y年%m月%d日')}")

    def 見積もり番号(self):  # noqa
        base_x = -70
        base_y = 15
        self.set_xy(base_x, base_y)
        self.cell(txt=f"見積もり番号 : {format(datetime.date.today(), '%Y%m%d')}-001")

    def 見積書タイトル(self):  # noqa
        base_x = 95
        base_y = 30
        self.set_font(size=20)
        self.set_xy(95, 30)
        self.set_xy(base_x, base_y)
        self.cell(txt="見積書", align="C")

    def 顧客宛名(self, company_name: str):  # noqa
        base_x = 10
        base_y = 50
        self.set_xy(base_x, base_y)
        atena = f"{company_name} 御中"
        cell_width = atena.__len__() * 4
        cell_height = 20
        self.set_draw_color(0, 0, 0)
        self.set_line_width(0.3)
        self.cell(cell_width, cell_height, txt=atena)
        self.line(
            self.get_x() - cell_width,
            self.get_y() + cell_height - 5,
            self.get_x(),
            self.get_y() + cell_height - 5,
        )

    def 見積もり件名(self, title: str):  # noqa
        base_x = 10
        base_y = 70
        self.set_xy(base_x, base_y)
        self.cell(txt="件名:" + title)

    def 見積もり期限(self, interbal):  # noqa
        base_x = 10
        base_y = 75
        self.set_xy(base_x, base_y)
        self.cell(
            txt=f"有効期限 : {format(datetime.date.today() + datetime.timedelta(days=interbal), '%Y年%m月%d日')}"
        )

    def 見積もり金額(self, price):  # noqa
        base_x = 10
        base_y = 100
        self.set_xy(base_x, base_y)
        price_str = f"お見積もり金額   ¥{price:,}"  # price を 3桁区切りにする
        cell_width = price_str.__len__() * 3
        cell_height = 20
        self.set_draw_color(0, 0, 0)
        self.set_line_width(0.3)
        self.cell(cell_width, cell_height, txt=price_str)
        self.line(
            self.get_x() - cell_width,
            self.get_y() + cell_height - 5,
            self.get_x(),
            self.get_y() + cell_height - 5,
        )

    def 見積もり会社(
        self, my_company_name: str, company_adress: str, account_user_name: str
    ):  # noqa
        base_x = 150
        base_y = 60
        self.set_xy(base_x, base_y)
        self.cell(txt=my_company_name)
        self.set_xy(base_x, base_y + 5)
        self.cell(txt=company_adress)
        self.set_xy(base_x, base_y + 10)
        self.cell(txt=account_user_name)

    def 見積もり明細(self, data: list[dict]) -> int:  # noqa
        if len(data) == 0:
            return 0
        base_x = 10
        base_y = 150
        self.set_xy(base_x, base_y)
        self.set_draw_color(255, 0, 0)
        self.set_line_width(0.3)
        headings_style = FontFace(color=255, fill_color=(255, 100, 0))
        with pdf.table(
            borders_layout="NO_HORIZONTAL_LINES",
            cell_fill_color=(224, 235, 255),
            col_widths=(42, 39, 35, 42),
            cell_fill_mode="ROWS",
            headings_style=headings_style,
            line_height=6,
            text_align=("LEFT", "CENTER", "RIGHT", "RIGHT"),
            width=160,
        ) as table:
            sub_total_amount = 0
            for page, data_row in enumerate(data):
                if page == 0:
                    row = table.row()
                    row.cell("項目")
                    row.cell("数量")
                    row.cell("単価")
                    row.cell("金額")
                row = table.row()
                row.cell(data_row["項目"])
                row.cell(data_row["数量"])
                row.cell(f"¥{int(data_row['単価']):,}")
                row.cell(f"¥{int(data_row['金額']):,}")
                sub_total_amount += int(data_row["金額"])
            row = table.row()
            row.cell("小計")
            row.cell("")
            row.cell("")
            row.cell(f"¥{sub_total_amount:,}")

            row = table.row()
            row.cell("消費税")
            row.cell("")
            row.cell("")
            row.cell(f"¥{int(sub_total_amount * tax_rate):,}")

            row = table.row()
            row.cell("合計")
            row.cell("")
            row.cell("")
            total_amount = sub_total_amount + (sub_total_amount * tax_rate)
            row.cell(f"¥{int(total_amount):,}")
            # total_amount を切り上げ
            return math.ceil(total_amount)


customer_company_name = "梨木製作所"
my_company_name = "Hoge Inc."
quotations_title = "各種加工見積もりの件"
quotation_items = [
    {"項目": "アイテム1", "数量": "1", "単価": "1000", "金額": "1000"},
    {"項目": "アイテム2", "数量": "2", "単価": "10000", "金額": "20000"},
    {"項目": "アイテム3", "数量": "10", "単価": "1000", "金額": "10000"},
]
account_user_name = "梨木太郎"
company_address = "東京都渋谷区渋谷1-1-1"
font_family = "NotoSansJP"
font_path = "<path_to_font>"

pdf = PDF()
pdf.add_font(font_family, fname=font_path)
pdf.set_font(font_family, size=10)

pdf.set_title(f"{customer_company_name}様 お見積もり書")
pdf.set_author(my_company_name)

pdf.add_page()
pdf.見積もり日()
pdf.見積もり番号()
pdf.見積書タイトル()
pdf.set_font(font_family, size=10)
pdf.顧客宛名(customer_company_name)
pdf.見積もり件名(quotations_title)
pdf.見積もり期限(30)
pdf.見積もり会社(my_company_name, company_address, account_user_name)
total_amount = pdf.見積もり明細(data=quotation_items)
pdf.見積もり金額(total_amount)

pdf.output("quotation.pdf")

実装する上でのポイント

cell は描画した後に、デフォルトで移動するのがポイントです。

スクリーンショット 2023-07-03 8.44.41.png

文字の下にアンダーラインを作る場合は以下のように実装しました。
(markdown style でアンダーラインを作る方法もあるみたいですが、あえて line メソッドを使っています。)

スクリーンショット 2023-07-03 8.46.19.png

self.cell(cell_width, cell_height, txt=price_str)
        self.line(
            self.get_x() - cell_width,  # cell描画前に戻る
            self.get_y() + cell_height - 5,  # -5 は適当な調整です
            self.get_x(),
            self.get_y() + cell_height - 5,  # -5 は適当な調整です
        )

以上。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?