Pythonを使うとExcelデータをもとに、PDFに出力・変換することができます。
ここでは実務の事例として、請求書を自動作成するPythonプログラムを紹介していきます。
今回は、openpyxlではなくxlwingsを使用します。
その目的はPDFへの変換です。openpyxlではpdfへの変換が難しいので、xlwingsでPDFへの変換を行います。
・テンプレートのエクセルへデータを出力する
・PDFへ変換して請求書として保存する
それでは以下で詳しく紹介していきます。
目次
- 1 PythonでExcelデータから請求書PDFへ変換するプログラムの概要
- 2 Pythonプログラムを実行するための準備
- 3 Pythonプログラム解説
- 3.1 プログラム1|ライブラリの設定
- 3.2 プログラム2|対象エクセルのファイルパスを指定
- 3.3 プログラム3|エクセルを読み込み、日付を変換
- 3.4 プログラム4|取引先のリストを作成
- 3.5 プログラム5|エクセルを新しいインスタンスで作成
- 3.6 プログラム6|取引先ごとに処理
- 3.7 プログラム7|テンプレートエクセルを開く
- 3.8 プログラム8|対象期間を設定
- 3.9 プログラム9|情報を設定
- 3.10 プログラム10|取引先ごとにフィルターしてリストに変換
- 3.11 プログラム11|取引先ごとのデータの内、対象期間に含まれるものだけを処理
- 3.12 プログラム12|各データをテンプレートエクセルへ流し込む
- 3.13 プログラム13|行に罫線を引く
- 3.14 プログラム14|合計金額と対象行を累算する
- 3.15 プログラム15|テンプレートエクセルに各情報を出力
- 3.16 プログラム16|テンプレートエクセルをPDFとして保存
- 3.17 プログラム17|テンプレートエクセルを新しいエクセルとして保存
- 3.18 プログラム18|エクセルをアプリケーションごと閉じる
- 4 Pythonについて詳しく理解したいなら
PythonでExcelデータから請求書PDFへ変換するプログラムの概要
今回は以下の作業をpythonで行うことを目指します。
1.元のデータが格納されているエクセルファイルを読み込む
2.Pandasでデータを取引先ごとに分ける
3.取引先ごとに分析した情報をテンプレートエクセルに出力する
4.PDFとして保存する
詳しいプログラム解説は後半で行います。
Pythonプログラムを実行するための準備
事前の準備として、作成したいフォルダをテキストファイルに記載しておきます。
準備1|作成した請求書PDFを保管するフォルダの作成
今回は上記のように請求書PDFを保管するフォルダを作成しました。
作成した請求書PDFはこのフォルダに保管するようにプログラムを組んでいきます。
準備2|テンプレートエクセルの作成
今回の事例では、請求書フォーマットが記入されたエクセルを事前に準備しておきます。
ちなみに以下のエクセルを請求書テンプレートとして使用します。
特筆すべき点は以下です。
1.セルJ2に準備1で指定したPDF保管用フォルダパスを入れる
2.セルJ4~L4に対象期間(開始日)、セルJ5~L5に対象期間(終了日)を入力
3.補足
カンタンに補足しておきます。
1.セルJ2に準備1で指定したPDF保管用フォルダパスを入れる
今回作成するPDFはセルJ2で指定したフォルダに自動で保存されます。
後半で記述するプログラム内で、このパスを取得するようにしています。
2.セルJ4~L4に対象期間(開始日)、セルJ5~L5に対象期間(終了日)を入力
今回の請求書作成では、請求書の対象とする期間を設定しています。
J4からL4に記載されている「年、月、日」を開始日として、J5からL5に記載されている「年、月、日」を終了日としています。
この期間に含まれるデータを請求書の対象とするようにしています。
3.補足
これはご自身の仕事に合わせたテンプレートがあるはずなので、そちらを使うことで良いかと思います。
しかしながら、今回は紹介するテンプレートで出力するセルを指定しています。
つまり本記事で紹介しているテンプレート以外を使用する場合は、エクセルに出力する部分のプログラムを調整する必要があります。
準備3|データベース用のエクセル
今回は「sample.xlsx」というファイル名で以下のようなデータを使います。
1行目のヘッダーはpandasで読み込むときに重要なキーとなります。
もしプログラムをそのまま使う場合は、ヘッダーの記載とプログラムの記述を合わせるように調整が必要です。
準備4|必要ライブラリのインストール
今回は以下の2つのライブラリをインストールします。
pip install xlwings
pip install pandas
以下のプログラムでは、上記の2つをインストールしておかないと動かないので、注意が必要です。
Pythonプログラム解説
この記事で紹介するプログラムを解説しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
#プログラム1|ライブラリの設定 from datetime import datetime, timedelta import xlwings as xw import pandas as pd import os #プログラム2|対象エクセルのファイルパスを指定 samplepath = 'sample.xlsx' templatepath = 'template.xlsx' #プログラム3|エクセルを読み込み、日付を変換 df = pd.read_excel(samplepath) df['納品日'] = pd.to_datetime(df['納品日']).dt.strftime("%Y-%m-%d") #プログラム4|取引先のリストを作成 torihiki_list = sorted(list(df['取引先'].unique())) #プログラム5|エクセルを新しいインスタンスで作成(エクセルのアプリケーションを開く) App = xw.App() #プログラム6|取引先ごとに処理 for torihiki in torihiki_list: # プログラム7|テンプレートエクセルを開く wb = App.books.open(templatepath) ws = wb.sheets('テンプレート') # プログラム8|対象期間を設定 startdate = datetime(int(ws['J4'].value), int(ws['K4'].value), int(ws['L4'].value)) enddate = datetime(int(ws['J5'].value), int(ws['K5'].value), int(ws['L5'].value)) # プログラム9|情報を設定 goukei = 0 gyo = 14 # プログラム10|取引先ごとにフィルターしてリストに変換 filtered = df[df['取引先'] == f'{torihiki}'] values = filtered.values.tolist() # プログラム11|取引先ごとのデータの内、対象期間に含まれるものだけを処理 for rows in values: torihikidate = datetime.strptime(rows[2], '%Y-%m-%d') if startdate <= torihikidate <= enddate: # プログラム12|各データをテンプレートエクセルへ流し込む for x, cell in enumerate(rows): if x == 0: ws.range(gyo, 2 + x).value = cell elif x == 1: ws.range((gyo, 2 + x),(gyo, 4 + x) ).merge() ws.range(gyo, 2 + x).value = cell elif x == 2: ws.range(gyo, 4 + x).value = cell ws.range(gyo, 4 + x).number_format = 'yyyy-mm-dd' elif x == 3: ws.range(gyo, 4 + x).value = cell ws.range(gyo, 4 + x).number_format = '¥#,##0;¥-#,##0' # プログラム13|行に罫線を引く ws.range((gyo, 2),(gyo, 7) ).api.Borders.LineStyle = 1 # プログラム14|合計金額と対象行を累算する goukei += rows[3] gyo+=1 # プログラム15|テンプレートエクセルに各情報を出力 ws.range('B4').value = torihiki kikan = startdate.strftime('%Y-%m-%d') + '~' + enddate.strftime('%Y-%m-%d') + 'の請求書' ws.range('B6').value = kikan ws.range('C9').value = goukei ws.range('C9').number_format = '¥#,##0;¥-#,##0' now = datetime.now() seikyusho_id = now.strftime('%Y%m%d') + '_' + torihiki ws.range('G3').value = seikyusho_id hiduke = now.strftime('%Y-%m-%d') ws.range('G4').value = hiduke kigen = now + timedelta(days=15) ws.range('C11').value = kigen.strftime('%Y-%m-%d') ws.name = torihiki # プログラム16|テンプレートエクセルをPDFとして保存 savepath = ws.range('J2').value pdf_path = os.path.join(savepath, f'{torihiki}_report.pdf') wb.api.ExportAsFixedFormat(0, pdf_path) #os.startfile(pdf_path) # プログラム17|テンプレートエクセルを新しいエクセルとして保存 filename = hiduke + '_' + torihiki + '.xlsx' wb.save(filename) wb.close() # プログラム18|エクセルをアプリケーションごと閉じる App.quit() |
以下で詳しく説明しています。
プログラム1|ライブラリの設定
1 2 3 4 5 |
#プログラム1|ライブラリの設定 from datetime import datetime, timedelta import xlwings as xw import pandas as pd import os |
1 2 3 4 5 |
# プログラム解説 datetime, timedelta|日付を取得したり比較したりするときに使用 xlwings|エクセル操作に使用 pandas|読み込んだエクセルの分析に使用 os|PDFを開いたりファイルを保存したりするときに使用 |
プログラム2|対象エクセルのファイルパスを指定
1 2 3 |
#プログラム2|対象エクセルのファイルパスを指定 samplepath = 'sample.xlsx' templatepath = 'template.xlsx' |
1 2 3 |
#プログラム解説 変数samplepathを「sample.xlsx」とする 変数templatepathを「template.xlsx」とする |
Pythonファイルと同じフォルダにある「sample.xlsx」と「template.xlsx」を指定します。
sample.xlsxには元データが存在し、template.xlsxには請求書フォーマットが存在します。
プログラム3|エクセルを読み込み、日付を変換
1 2 3 |
#プログラム3|エクセルを読み込み、日付を変換 df = pd.read_excel(samplepath) df['納品日'] = pd.to_datetime(df['納品日']).dt.strftime("%Y-%m-%d") |
1 2 3 |
# プログラム解説 変数dfでsample.xlsxを読み込む sample.xlsxの「納品日」列の日付データを「YYYY-mm-dd」型に変換 |
pandasでデータを取得してデータを変換していきます。
プログラム4|取引先のリストを作成
1 2 |
#プログラム4|取引先のリストを作成 torihiki_list = sorted(list(df['取引先'].unique())) |
1 2 |
#プログラム解説 取引先の重複を除外したリストを作成 |
ちなみにこの時点で、以下のprintで表示してみると画像のようになります。
プログラム5|エクセルを新しいインスタンスで作成
1 2 |
#プログラム5|エクセルを新しいインスタンスで作成 App = xw.App() |
1 2 |
#プログラム解説 エクセルのアプリケーションを新しく開く |
ここでエクセルを新しく立ち上げます。
プログラム6|取引先ごとに処理
1 2 |
#プログラム6|取引先ごとに処理 for torihiki in torihiki_list: |
1 2 |
#プログラム解説 torihiki_listに含まれる要素で繰り返し処理を行う |
プログラム4で紹介したとおり、今回の事例では、torihiki_list=[‘愛媛不動産’, ‘愛知製本’, ‘愛知販売’, ‘茨城信託’, ‘茨城物産’]で5つの要素が入っています。
したがって、この5つに対して処理を行います。
プログラム7|テンプレートエクセルを開く
1 2 3 |
# プログラム7|テンプレートエクセルを開く wb = App.books.open(templatepath) ws = wb.sheets('テンプレート') |
1 2 3 |
#プログラム解説 変数wbで「template.xlsx」のエクセルを開く 変数wsで「template.xlsx」の「テンプレート」シートを指定する |
プログラム8|対象期間を設定
1 2 3 |
# プログラム8|対象期間を設定 startdate = datetime(int(ws['J4'].value), int(ws['K4'].value), int(ws['L4'].value)) enddate = datetime(int(ws['J5'].value), int(ws['K5'].value), int(ws['L5'].value)) |
1 2 3 |
# プログラム解説 startdateに対象期間の開始日をインプットする enddateに対象期間の終了日をインプットする |
startdate(対象期間の開始日)は以下のように取得しています。
対象期間
セルJ4|開始年
セルK4|開始月
セルL4|開始日
enddate(対象期間の終了日)は以下のように取得しています。
対象期間
セルJ5|終了年
セルK5|終了月
セルL5|終了日
ここは敢えてそれぞれ3つのセルから情報を取得するようにしています。
その理由は、担当者によるITリテラシーの違いや誤入力を避けるためです。
しかしながら、必ず同じようにする必要はありません。
自分の状況に合わせて、やりやすい形で問題ないです。
プログラム9|情報を設定
1 2 3 |
# プログラム9|情報を設定 goukei = 0 gyo = 14 |
1 2 3 |
# プログラム解説 変数goukeiをゼロリセット 変数gyoを14にする(テンプレートエクセルのデータ入力開始行) |
変数goukeiは、取引金額の合計を算出するときに使う変数です。
変数gyoは、テンプレートシートにデータを流し込むとき、14行目から流し込みたいので、gyo=14としています。
したがってテンプレートシートのデータ入力開始行が14行目以外であれば、変更する必要があります。
プログラム10|取引先ごとにフィルターしてリストに変換
1 2 3 |
# プログラム10|取引先ごとにフィルターしてリストに変換 filtered = df[df['取引先'] == f'{torihiki}'] values = filtered.values.tolist() |
1 2 3 |
# プログラム解説 変数filteredに「取引先」列を各取引先でフィルターしたデータを入れる filtered(pandasデータ)をリスト型へ変換して、valuesとする |
filteredには各取引先ごとにソートされたデータが格納されます。
このデータはpandasのデータなので、これをリストに変換してエクセルにカンタンに書き出せるようにします。
プログラム11|取引先ごとのデータの内、対象期間に含まれるものだけを処理
1 2 3 4 |
# プログラム11|取引先ごとのデータの内、対象期間に含まれるものだけを処理 for rows in values: torihikidate = datetime.strptime(rows[2], '%Y-%m-%d') if startdate <= torihikidate <= enddate: |
1 2 3 4 |
# プログラム解説 変数valuesを一行ずつ調べる 変数torihikidateをrow[2]をdatetime(YYYY-mm-dd型)に変換する もしtorihikidateが、startdateとenddateの対象期間内であれば |
ちなみにこの時点で、valuesを以下のprintで表示してみると画像のようになります。
このrow[2]を日付として取得し、プログラム8で設定した期間内に入っているかどうかを確認します。
もしTrue(期間に入っている)のであれば、プログラム12~14を実行します。False(期間に入っていない)のであれば、プログラム12~14はスキップします。
プログラム12|各データをテンプレートエクセルへ流し込む
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# プログラム12|各データをテンプレートエクセルへ流し込む for x, cell in enumerate(rows): if x == 0: ws.range(gyo, 2 + x).value = cell elif x == 1: ws.range((gyo, 2 + x),(gyo, 4 + x) ).merge() ws.range(gyo, 2 + x).value = cell elif x == 2: ws.range(gyo, 4 + x).value = cell ws.range(gyo, 4 + x).number_format = 'yyyy-mm-dd' elif x == 3: ws.range(gyo, 4 + x).value = cell ws.range(gyo, 4 + x).number_format = '¥#,##0;¥-#,##0' |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# プログラム解説 rows内の要素を一つずつ検証する もしxが0であれば 行(gyo)、列(2+x)の値をcellとする もしxが1であれば 行(gyo)、列(2+x)から列(4+x)までのセルを結合する 行(gyo)、列(2+x)の値をcellとする もしxが2であれば 行(gyo)、列(4+x)の値をcellとする 行(gyo)、列(4+x)の書式を「yyyy-mm-dd」型にする もしxが3であれば: 行(gyo)、列(4+x)の値をcellとする 行(gyo)、列(4+x)の書式を「¥#,##0;¥-#,##0」型にする |
ここで各行にデータを入れ込んでいきます。
gyoは行数を、(2+x)や(4+x)は列数をそれぞれ表します。
なおセルの指定は以下の2つのパターンを使用しています。
ws.range(gyo, 2 + x).value = cell
1つのセルを指定する場合は、range(行, 列)という順番でセルの位置を指定します。
ws.range((gyo, 2 + x),(gyo, 4 + x) ).merge()
複数のセルを指定する場合は、range(行1, 列1)という順番でセルの位置を指定します。
たとえば、以下のようにセルB3からセルE6を指定する場合、以下のように記載します。
これでxlwingsで複数のセルを指定することができるので覚えておくと役出つかもしれません。
プログラム13|行に罫線を引く
1 2 |
# プログラム13|行に罫線を引く ws.range((gyo, 2),(gyo, 7) ).api.Borders.LineStyle = 1 |
1 2 |
# プログラム解説 対象行に罫線を引く |
プログラム14|合計金額と対象行を累算する
1 2 3 |
# プログラム14|合計金額と対象行を累算する goukei += rows[3] gyo+=1 |
1 2 3 |
# プログラム解説 goukeiに取引金額を累算 gyoに1加算 |
取引金額の合計は、row[3]に格納されている値なので、これを累算することで、計算していきます。
対象行は14行目に始まり、1行ずつデータを入れていきます。
したがってgyo+=1とすることで、データを転記していく行を下にずらしていきます。
プログラム15|テンプレートエクセルに各情報を出力
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# プログラム15|テンプレートエクセルに各情報を出力 ws.range('B4').value = torihiki kikan = startdate.strftime('%Y-%m-%d') + '~' + enddate.strftime('%Y-%m-%d') + 'の請求書' ws.range('B6').value = kikan ws.range('C9').value = goukei ws.range('C9').number_format = '¥#,##0;¥-#,##0' seikyusho_id = now.strftime('%Y%m%d') + '_' + torihiki ws.range('G3').value = seikyusho_id now = datetime.now() hiduke = now.strftime('%Y-%m-%d') ws.range('G4').value = hiduke kigen = now + timedelta(days=15) ws.range('C11').value = kigen.strftime('%Y-%m-%d') ws.name = torihiki |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# プログラム解説 セルB4にtorihikiの値を出力する kikanを「startdateを「YYYY-mm-dd」型に変換した値 ~ enddateを「YYYY-mm-dd」型に変換した値 + 'の請求書'」とする セルB5にkikanの値を出力する セルC9にgoukeiの値を出力する セルC9の書式を「¥#,##0;¥-#,##0」にする(お金の表示形式) nowで今の日時を取得 seikyusho_idを「nowの日付を「YYYYmmdd」型に変換した値_torihiki」とする セルG3にseikyusho_idの値を出力する hidukeをnowを「YYYY-mm-dd」型に変換した値にする セルG4にhidukeの値を出力する kigenをnowに15日加算した日付とする セルC11にkigenを「YYYY-mm-dd」型に変換した値にする シートの名前をtorihikiにする |
プログラム16|テンプレートエクセルをPDFとして保存
1 2 3 4 5 |
# プログラム16|テンプレートエクセルをPDFとして保存 savepath = ws.range('J2').value pdf_path = os.path.join(savepath, f'{torihiki}_report.pdf') wb.api.ExportAsFixedFormat(0, pdf_path) #os.startfile(pdf_path) |
1 2 3 4 5 |
# プログラム解説 savepathをセルJ2の値にする pdf_pathを「savepath + torihiki_report.pdf」をくっつけたパスにする エクセルをPDFに変換してpdf_pathに保存する #保存したPDFを開く(コメントアウトしているためプログラムは起動しない) |
PDFを保存するファイルはあらかじめ、セルJ2に指定してあるため、そのパスに保存を行います。(上の画像のとおり)
セルJ2に指定しておかなくてもよいのですが、担当者がプログラムを操作できる人は限らないため、敢えてセルJ2にフォルダパスを入力しておきます。
なお、5行目はコメントアウトしてありますが、PDFを開くプログラムを入れてあります。
これにより保存したファイルが正しく出力されているかどうかを目視で確認できるような工夫も入れます。
ただし全員が必要なプログラムではないので、コメントアウトしてあります。
プログラム17|テンプレートエクセルを新しいエクセルとして保存
1 2 3 4 |
# プログラム17|テンプレートエクセルを新しいエクセルとして保存 filename = hiduke + '_' + torihiki + '.xlsx' wb.save(filename) wb.close() |
1 2 3 4 |
# プログラム解説 filenameを「hiduke_torihiki.xlsx」というパスを入れる テンプレートエクセルをfilenameで新しく保存する エクセルを閉じる |
作成したエクセルはバックアップとして保存しておきます。
プログラム18|エクセルをアプリケーションごと閉じる
1 2 |
# プログラム18|エクセルをアプリケーションごと閉じる App.quit() |
1 2 |
# プログラム解説 エクセルをアプリケーションごと閉じる |
最後にアプリケーションごとエクセルを閉じてプログラムを終了します。
プログラムの解説はここまでです。
Pythonについて詳しく理解したいなら
Pythonを活用すると、仕事を効率化できる幅を広げることができます。
たとえば私が実際にPythonを活用して効率化してきた作業は以下の記事で紹介しています。
興味がある人は以下の記事もご覧ください。
Pythonの難易度などについては、以下で紹介しています。
勉強の参考になれば幸いです。