請求書作成アプリ
顧客・商品を選択してPDF請求書を自動生成するアプリ。reportlabライブラリでPDF出力を実装します。
1. アプリ概要
顧客・商品を選択してPDF請求書を自動生成するアプリ。reportlabライブラリでPDF出力を実装します。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ)・reportlab で、難易度は ★★★ です。
Pythonでは tkinter を使うことで、クロスプラットフォームなGUIアプリを簡単に作成できます。このアプリを通じて、ウィジェットの配置・イベント処理・データ管理など、GUI開発の実践的なスキルを習得できます。
ソースコードは完全な動作状態で提供しており、コピーしてそのまま実行できます。まずは実行して動作を確認し、その後コードを読んで仕組みを理解していきましょう。カスタマイズセクションでは機能拡張のアイデアも紹介しています。
GUIアプリ開発は、プログラミングの楽しさを実感できる最も効果的な学習方法のひとつです。アプリを作ることで、変数・関数・クラス・イベント処理など、プログラミングの重要な概念が自然と身についていきます。このアプリをきっかけに、オリジナルアプリの開発にも挑戦してみてください。
2. 機能一覧
- 請求書作成アプリのメイン機能
- 直感的なGUIインターフェース
- 入力値のバリデーション
- エラーハンドリング
- 結果の見やすい表示
- キーボードショートカット対応
3. 事前準備・環境
Python 3.10 以上 / Windows・Mac・Linux すべて対応
以下の環境で動作確認しています。
- Python 3.10 以上
- OS: Windows 10/11・macOS 12+・Ubuntu 20.04+
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
from datetime import datetime, date
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.platypus import (SimpleDocTemplate, Table, TableStyle,
Paragraph, Spacer)
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
REPORTLAB_AVAILABLE = True
except ImportError:
REPORTLAB_AVAILABLE = False
class App20:
"""請求書作成アプリ"""
DB_PATH = os.path.join(os.path.dirname(__file__), "invoices.db")
def __init__(self, root):
self.root = root
self.root.title("請求書作成アプリ")
self.root.geometry("900x620")
self.root.configure(bg="#f8f9fc")
self._init_db()
self._build_ui()
self._load_clients()
self._load_items()
self._new_invoice()
if not REPORTLAB_AVAILABLE:
messagebox.showwarning(
"ライブラリ未インストール",
"reportlab が必要です (PDF出力に必要)。\n"
"pip install reportlab でインストールしてください。\n\n"
"CSV形式での出力は可能です。")
def _init_db(self):
self.conn = sqlite3.connect(self.DB_PATH)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, address TEXT, email TEXT, phone TEXT
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, unit_price REAL, unit TEXT DEFAULT '式'
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invoice_no TEXT, client_id INTEGER, issue_date TEXT,
due_date TEXT, notes TEXT, created_at TEXT
)
""")
self.conn.commit()
# サンプルデータ
if not self.conn.execute("SELECT 1 FROM clients").fetchone():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('株式会社ABC','東京都千代田区1-1','abc@example.com','03-1234-5678')")
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('山田商事','大阪府大阪市2-2','yamada@example.com','06-9876-5432')")
self.conn.execute(
"INSERT INTO items (name,unit_price,unit) VALUES "
"('Webデザイン',50000,'式'),('システム開発',80000,'式'),"
"('コンサルティング',30000,'時間'),('保守費用',20000,'月')")
self.conn.commit()
def _build_ui(self):
title_frame = tk.Frame(self.root, bg="#f57f17", pady=10)
title_frame.pack(fill=tk.X)
tk.Label(title_frame, text="🧾 請求書作成アプリ",
font=("Noto Sans JP", 15, "bold"),
bg="#f57f17", fg="white").pack(side=tk.LEFT, padx=12)
ttk.Button(title_frame, text="📄 PDF出力",
command=self._export_pdf).pack(side=tk.RIGHT, padx=8)
ttk.Button(title_frame, text="📊 CSV出力",
command=self._export_csv).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="💾 保存",
command=self._save_invoice).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="🆕 新規",
command=self._new_invoice).pack(side=tk.RIGHT, padx=4)
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左: 請求書フォーム
form = ttk.LabelFrame(paned, text="請求書", padding=12)
# 請求書番号
row_f = tk.Frame(form, bg=form.cget("background"))
row_f.pack(fill=tk.X, pady=2)
tk.Label(row_f, text="請求書番号:", bg=row_f.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.inv_no_var = tk.StringVar()
ttk.Entry(row_f, textvariable=self.inv_no_var, width=18).pack(side=tk.LEFT, padx=4)
# 発行日
row_f2 = tk.Frame(form, bg=form.cget("background"))
row_f2.pack(fill=tk.X, pady=2)
tk.Label(row_f2, text="発行日:", bg=row_f2.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.issue_var = tk.StringVar(value=str(date.today()))
ttk.Entry(row_f2, textvariable=self.issue_var, width=12).pack(side=tk.LEFT, padx=4)
tk.Label(row_f2, text="支払期限:", bg=row_f2.cget("bg")).pack(side=tk.LEFT, padx=(12, 0))
self.due_var = tk.StringVar()
ttk.Entry(row_f2, textvariable=self.due_var, width=12).pack(side=tk.LEFT, padx=4)
# 顧客
row_f3 = tk.Frame(form, bg=form.cget("background"))
row_f3.pack(fill=tk.X, pady=2)
tk.Label(row_f3, text="顧客:", bg=row_f3.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.client_var = tk.StringVar()
self.client_cb = ttk.Combobox(row_f3, textvariable=self.client_var,
state="readonly", width=24)
self.client_cb.pack(side=tk.LEFT, padx=4)
ttk.Button(row_f3, text="✏️ 顧客管理",
command=self._manage_clients).pack(side=tk.LEFT, padx=4)
# 備考
row_f4 = tk.Frame(form, bg=form.cget("background"))
row_f4.pack(fill=tk.X, pady=2)
tk.Label(row_f4, text="備考:", bg=row_f4.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT, anchor="n")
self.notes_text = tk.Text(row_f4, height=2, width=30,
font=("Arial", 10))
self.notes_text.pack(side=tk.LEFT, padx=4)
# 明細テーブル
tk.Label(form, text="明細:", bg=form.cget("background"),
font=("Noto Sans JP", 11, "bold")).pack(anchor="w", pady=(8, 2))
item_f = tk.Frame(form, bg=form.cget("background"))
item_f.pack(fill=tk.X)
self.item_name_var = tk.StringVar()
self.item_cb = ttk.Combobox(item_f, textvariable=self.item_name_var,
width=20)
self.item_cb.pack(side=tk.LEFT, padx=2)
self.item_cb.bind("<<ComboboxSelected>>", self._on_item_select)
tk.Label(item_f, text="数量:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.qty_var = tk.IntVar(value=1)
ttk.Spinbox(item_f, from_=1, to=9999,
textvariable=self.qty_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(item_f, text="単価:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.price_var = tk.StringVar(value="0")
ttk.Entry(item_f, textvariable=self.price_var, width=10).pack(side=tk.LEFT, padx=2)
ttk.Button(item_f, text="➕ 追加",
command=self._add_line).pack(side=tk.LEFT, padx=4)
# 明細Treeview
cols = ("no", "name", "qty", "unit_price", "amount")
self.line_tree = ttk.Treeview(form, columns=cols,
show="headings", height=8)
for c, h, w in [("no", "#", 30), ("name", "品目", 200),
("qty", "数量", 50), ("unit_price", "単価", 90),
("amount", "金額", 90)]:
self.line_tree.heading(c, text=h)
self.line_tree.column(c, width=w, minwidth=30)
line_sb = ttk.Scrollbar(form, command=self.line_tree.yview)
self.line_tree.configure(yscrollcommand=line_sb.set)
line_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.line_tree.pack(fill=tk.BOTH, expand=True)
ttk.Button(form, text="🗑️ 選択行削除",
command=self._delete_line).pack(anchor="w", pady=2)
# 合計
total_f = tk.Frame(form, bg=form.cget("background"))
total_f.pack(fill=tk.X, pady=4)
self.subtotal_label = tk.Label(total_f, text="小計: ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.subtotal_label.pack(side=tk.LEFT)
self.tax_label = tk.Label(total_f, text=" 消費税(10%): ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.tax_label.pack(side=tk.LEFT)
self.total_label = tk.Label(total_f, text=" 合計: ¥0",
bg=form.cget("background"),
fg="#e65100",
font=("Arial", 13, "bold"))
self.total_label.pack(side=tk.LEFT)
paned.add(form, weight=3)
# 右: 請求書履歴
hist_frame = ttk.LabelFrame(paned, text="請求書履歴", padding=4)
hist_cols = ("inv_no", "client", "date", "total")
self.hist_tree = ttk.Treeview(hist_frame, columns=hist_cols,
show="headings", selectmode="browse")
for c, h, w in [("inv_no", "番号", 80), ("client", "顧客", 100),
("date", "発行日", 90), ("total", "金額", 80)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=30)
hist_sb = ttk.Scrollbar(hist_frame, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=hist_sb.set)
hist_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True)
paned.add(hist_frame, weight=1)
self.status_var = tk.StringVar(value="")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._line_items = []
def _load_clients(self):
rows = self.conn.execute(
"SELECT id, name FROM clients ORDER BY name").fetchall()
self._clients = {name: cid for cid, name in rows}
self.client_cb.configure(values=list(self._clients.keys()))
def _load_items(self):
rows = self.conn.execute(
"SELECT name, unit_price FROM items ORDER BY name").fetchall()
self._items = {name: price for name, price in rows}
self.item_cb.configure(values=list(self._items.keys()))
def _new_invoice(self):
now = datetime.now()
self.inv_no_var.set(f"INV-{now.strftime('%Y%m%d-%H%M')}")
self.issue_var.set(str(date.today()))
self.due_var.set("")
self.notes_text.delete("1.0", tk.END)
self.line_tree.delete(*self.line_tree.get_children())
self._line_items = []
self._update_totals()
def _on_item_select(self, event):
name = self.item_name_var.get()
price = self._items.get(name, 0)
self.price_var.set(str(price))
def _add_line(self):
name = self.item_name_var.get().strip()
if not name:
return
try:
qty = self.qty_var.get()
price = float(self.price_var.get())
except ValueError:
messagebox.showerror("エラー", "数量・単価を正しく入力してください")
return
amount = qty * price
no = len(self._line_items) + 1
self._line_items.append((name, qty, price, amount))
self.line_tree.insert("", "end",
values=(no, name, qty,
f"¥{price:,.0f}",
f"¥{amount:,.0f}"))
self._update_totals()
def _delete_line(self):
sel = self.line_tree.selection()
if sel:
idx = self.line_tree.index(sel[0])
self._line_items.pop(idx)
self.line_tree.delete(sel[0])
self._renumber()
self._update_totals()
def _renumber(self):
for idx, item in enumerate(self.line_tree.get_children()):
vals = list(self.line_tree.item(item)["values"])
vals[0] = idx + 1
self.line_tree.item(item, values=vals)
def _update_totals(self):
subtotal = sum(a for _, _, _, a in self._line_items)
tax = subtotal * 0.1
total = subtotal + tax
self.subtotal_label.config(text=f"小計: ¥{subtotal:,.0f}")
self.tax_label.config(text=f" 消費税(10%): ¥{tax:,.0f}")
self.total_label.config(text=f" 合計: ¥{total:,.0f}")
self._total = total
def _save_invoice(self):
client_name = self.client_var.get()
client_id = self._clients.get(client_name)
if not client_id:
messagebox.showwarning("警告", "顧客を選択してください")
return
self.conn.execute(
"INSERT OR REPLACE INTO invoices "
"(invoice_no, client_id, issue_date, due_date, notes, created_at) "
"VALUES (?,?,?,?,?,?)",
(self.inv_no_var.get(), client_id,
self.issue_var.get(), self.due_var.get(),
self.notes_text.get("1.0", tk.END).strip(),
datetime.now().isoformat()))
self.conn.commit()
self.status_var.set(f"保存完了: {self.inv_no_var.get()}")
def _export_csv(self):
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV", "*.csv"), ("すべて", "*.*")])
if not path:
return
import csv
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["請求書番号", "顧客", "発行日", "支払期限"])
w.writerow([self.inv_no_var.get(), self.client_var.get(),
self.issue_var.get(), self.due_var.get()])
w.writerow([])
w.writerow(["品目", "数量", "単価", "金額"])
for name, qty, price, amount in self._line_items:
w.writerow([name, qty, price, amount])
subtotal = sum(a for _, _, _, a in self._line_items)
w.writerow(["小計", "", "", subtotal])
w.writerow(["消費税(10%)", "", "", subtotal * 0.1])
w.writerow(["合計", "", "", subtotal * 1.1])
self.status_var.set(f"CSV出力: {path}")
def _export_pdf(self):
if not REPORTLAB_AVAILABLE:
messagebox.showwarning("警告",
"pip install reportlab が必要です")
return
path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF", "*.pdf")])
if not path:
return
try:
doc = SimpleDocTemplate(path, pagesize=A4)
styles = getSampleStyleSheet()
story = []
story.append(Paragraph(f"請求書 {self.inv_no_var.get()}",
styles["Title"]))
story.append(Spacer(1, 12))
story.append(Paragraph(
f"顧客: {self.client_var.get()} "
f"発行日: {self.issue_var.get()} "
f"支払期限: {self.due_var.get()}", styles["Normal"]))
story.append(Spacer(1, 20))
data = [["品目", "数量", "単価(円)", "金額(円)"]]
for name, qty, price, amount in self._line_items:
data.append([name, qty, f"{price:,.0f}", f"{amount:,.0f}"])
subtotal = sum(a for _, _, _, a in self._line_items)
data.append(["", "", "小計", f"{subtotal:,.0f}"])
data.append(["", "", "消費税(10%)", f"{subtotal*0.1:,.0f}"])
data.append(["", "", "合計", f"{subtotal*1.1:,.0f}"])
t = Table(data, colWidths=[200, 60, 100, 100])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565c0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ALIGN", (1, 1), (-1, -1), "RIGHT"),
("GRID", (0, 0), (-1, -2), 0.5, colors.grey),
("FONTSIZE", (0, 0), (-1, -1), 10),
]))
story.append(t)
if self.notes_text.get("1.0", tk.END).strip():
story.append(Spacer(1, 20))
story.append(Paragraph("備考: " + self.notes_text.get("1.0", tk.END).strip(),
styles["Normal"]))
doc.build(story)
self.status_var.set(f"PDF出力: {path}")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _manage_clients(self):
win = tk.Toplevel(self.root)
win.title("顧客管理")
win.geometry("500x350")
# 顧客一覧
cols = ("id", "name", "address", "email", "phone")
tree = ttk.Treeview(win, columns=cols, show="headings")
for c, h, w in [("id", "ID", 40), ("name", "名前", 120),
("address", "住所", 150), ("email", "メール", 120),
("phone", "電話", 100)]:
tree.heading(c, text=h)
tree.column(c, width=w)
tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
# 追加フォーム
form = tk.Frame(win)
form.pack(fill=tk.X, padx=4)
vars_ = {}
for i, key in enumerate(["名前", "住所", "メール", "電話"]):
tk.Label(form, text=f"{key}:").grid(row=i//2, column=(i%2)*2, sticky="w", pady=2)
v = tk.StringVar()
ttk.Entry(form, textvariable=v, width=18).grid(
row=i//2, column=(i%2)*2+1, padx=4)
vars_[key] = v
def add_client():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES (?,?,?,?)",
(vars_["名前"].get(), vars_["住所"].get(),
vars_["メール"].get(), vars_["電話"].get()))
self.conn.commit()
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
self._load_clients()
ttk.Button(form, text="追加", command=add_client).grid(
row=2, column=0, columnspan=4, pady=6)
if __name__ == "__main__":
root = tk.Tk()
app = App20(root)
root.mainloop()
5. コード解説
請求書作成アプリのコードを詳しく解説します。クラスベースの設計で各機能を整理して実装しています。
クラス設計とコンストラクタ
App20クラスにアプリの全機能をまとめています。__init__メソッドでウィンドウの基本設定を行い、_build_ui()でUI構築、process()でメイン処理を担当します。この分離により、各メソッドの責任が明確になりコードが読みやすくなります。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
from datetime import datetime, date
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.platypus import (SimpleDocTemplate, Table, TableStyle,
Paragraph, Spacer)
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
REPORTLAB_AVAILABLE = True
except ImportError:
REPORTLAB_AVAILABLE = False
class App20:
"""請求書作成アプリ"""
DB_PATH = os.path.join(os.path.dirname(__file__), "invoices.db")
def __init__(self, root):
self.root = root
self.root.title("請求書作成アプリ")
self.root.geometry("900x620")
self.root.configure(bg="#f8f9fc")
self._init_db()
self._build_ui()
self._load_clients()
self._load_items()
self._new_invoice()
if not REPORTLAB_AVAILABLE:
messagebox.showwarning(
"ライブラリ未インストール",
"reportlab が必要です (PDF出力に必要)。\n"
"pip install reportlab でインストールしてください。\n\n"
"CSV形式での出力は可能です。")
def _init_db(self):
self.conn = sqlite3.connect(self.DB_PATH)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, address TEXT, email TEXT, phone TEXT
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, unit_price REAL, unit TEXT DEFAULT '式'
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invoice_no TEXT, client_id INTEGER, issue_date TEXT,
due_date TEXT, notes TEXT, created_at TEXT
)
""")
self.conn.commit()
# サンプルデータ
if not self.conn.execute("SELECT 1 FROM clients").fetchone():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('株式会社ABC','東京都千代田区1-1','abc@example.com','03-1234-5678')")
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('山田商事','大阪府大阪市2-2','yamada@example.com','06-9876-5432')")
self.conn.execute(
"INSERT INTO items (name,unit_price,unit) VALUES "
"('Webデザイン',50000,'式'),('システム開発',80000,'式'),"
"('コンサルティング',30000,'時間'),('保守費用',20000,'月')")
self.conn.commit()
def _build_ui(self):
title_frame = tk.Frame(self.root, bg="#f57f17", pady=10)
title_frame.pack(fill=tk.X)
tk.Label(title_frame, text="🧾 請求書作成アプリ",
font=("Noto Sans JP", 15, "bold"),
bg="#f57f17", fg="white").pack(side=tk.LEFT, padx=12)
ttk.Button(title_frame, text="📄 PDF出力",
command=self._export_pdf).pack(side=tk.RIGHT, padx=8)
ttk.Button(title_frame, text="📊 CSV出力",
command=self._export_csv).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="💾 保存",
command=self._save_invoice).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="🆕 新規",
command=self._new_invoice).pack(side=tk.RIGHT, padx=4)
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左: 請求書フォーム
form = ttk.LabelFrame(paned, text="請求書", padding=12)
# 請求書番号
row_f = tk.Frame(form, bg=form.cget("background"))
row_f.pack(fill=tk.X, pady=2)
tk.Label(row_f, text="請求書番号:", bg=row_f.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.inv_no_var = tk.StringVar()
ttk.Entry(row_f, textvariable=self.inv_no_var, width=18).pack(side=tk.LEFT, padx=4)
# 発行日
row_f2 = tk.Frame(form, bg=form.cget("background"))
row_f2.pack(fill=tk.X, pady=2)
tk.Label(row_f2, text="発行日:", bg=row_f2.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.issue_var = tk.StringVar(value=str(date.today()))
ttk.Entry(row_f2, textvariable=self.issue_var, width=12).pack(side=tk.LEFT, padx=4)
tk.Label(row_f2, text="支払期限:", bg=row_f2.cget("bg")).pack(side=tk.LEFT, padx=(12, 0))
self.due_var = tk.StringVar()
ttk.Entry(row_f2, textvariable=self.due_var, width=12).pack(side=tk.LEFT, padx=4)
# 顧客
row_f3 = tk.Frame(form, bg=form.cget("background"))
row_f3.pack(fill=tk.X, pady=2)
tk.Label(row_f3, text="顧客:", bg=row_f3.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.client_var = tk.StringVar()
self.client_cb = ttk.Combobox(row_f3, textvariable=self.client_var,
state="readonly", width=24)
self.client_cb.pack(side=tk.LEFT, padx=4)
ttk.Button(row_f3, text="✏️ 顧客管理",
command=self._manage_clients).pack(side=tk.LEFT, padx=4)
# 備考
row_f4 = tk.Frame(form, bg=form.cget("background"))
row_f4.pack(fill=tk.X, pady=2)
tk.Label(row_f4, text="備考:", bg=row_f4.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT, anchor="n")
self.notes_text = tk.Text(row_f4, height=2, width=30,
font=("Arial", 10))
self.notes_text.pack(side=tk.LEFT, padx=4)
# 明細テーブル
tk.Label(form, text="明細:", bg=form.cget("background"),
font=("Noto Sans JP", 11, "bold")).pack(anchor="w", pady=(8, 2))
item_f = tk.Frame(form, bg=form.cget("background"))
item_f.pack(fill=tk.X)
self.item_name_var = tk.StringVar()
self.item_cb = ttk.Combobox(item_f, textvariable=self.item_name_var,
width=20)
self.item_cb.pack(side=tk.LEFT, padx=2)
self.item_cb.bind("<<ComboboxSelected>>", self._on_item_select)
tk.Label(item_f, text="数量:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.qty_var = tk.IntVar(value=1)
ttk.Spinbox(item_f, from_=1, to=9999,
textvariable=self.qty_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(item_f, text="単価:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.price_var = tk.StringVar(value="0")
ttk.Entry(item_f, textvariable=self.price_var, width=10).pack(side=tk.LEFT, padx=2)
ttk.Button(item_f, text="➕ 追加",
command=self._add_line).pack(side=tk.LEFT, padx=4)
# 明細Treeview
cols = ("no", "name", "qty", "unit_price", "amount")
self.line_tree = ttk.Treeview(form, columns=cols,
show="headings", height=8)
for c, h, w in [("no", "#", 30), ("name", "品目", 200),
("qty", "数量", 50), ("unit_price", "単価", 90),
("amount", "金額", 90)]:
self.line_tree.heading(c, text=h)
self.line_tree.column(c, width=w, minwidth=30)
line_sb = ttk.Scrollbar(form, command=self.line_tree.yview)
self.line_tree.configure(yscrollcommand=line_sb.set)
line_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.line_tree.pack(fill=tk.BOTH, expand=True)
ttk.Button(form, text="🗑️ 選択行削除",
command=self._delete_line).pack(anchor="w", pady=2)
# 合計
total_f = tk.Frame(form, bg=form.cget("background"))
total_f.pack(fill=tk.X, pady=4)
self.subtotal_label = tk.Label(total_f, text="小計: ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.subtotal_label.pack(side=tk.LEFT)
self.tax_label = tk.Label(total_f, text=" 消費税(10%): ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.tax_label.pack(side=tk.LEFT)
self.total_label = tk.Label(total_f, text=" 合計: ¥0",
bg=form.cget("background"),
fg="#e65100",
font=("Arial", 13, "bold"))
self.total_label.pack(side=tk.LEFT)
paned.add(form, weight=3)
# 右: 請求書履歴
hist_frame = ttk.LabelFrame(paned, text="請求書履歴", padding=4)
hist_cols = ("inv_no", "client", "date", "total")
self.hist_tree = ttk.Treeview(hist_frame, columns=hist_cols,
show="headings", selectmode="browse")
for c, h, w in [("inv_no", "番号", 80), ("client", "顧客", 100),
("date", "発行日", 90), ("total", "金額", 80)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=30)
hist_sb = ttk.Scrollbar(hist_frame, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=hist_sb.set)
hist_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True)
paned.add(hist_frame, weight=1)
self.status_var = tk.StringVar(value="")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._line_items = []
def _load_clients(self):
rows = self.conn.execute(
"SELECT id, name FROM clients ORDER BY name").fetchall()
self._clients = {name: cid for cid, name in rows}
self.client_cb.configure(values=list(self._clients.keys()))
def _load_items(self):
rows = self.conn.execute(
"SELECT name, unit_price FROM items ORDER BY name").fetchall()
self._items = {name: price for name, price in rows}
self.item_cb.configure(values=list(self._items.keys()))
def _new_invoice(self):
now = datetime.now()
self.inv_no_var.set(f"INV-{now.strftime('%Y%m%d-%H%M')}")
self.issue_var.set(str(date.today()))
self.due_var.set("")
self.notes_text.delete("1.0", tk.END)
self.line_tree.delete(*self.line_tree.get_children())
self._line_items = []
self._update_totals()
def _on_item_select(self, event):
name = self.item_name_var.get()
price = self._items.get(name, 0)
self.price_var.set(str(price))
def _add_line(self):
name = self.item_name_var.get().strip()
if not name:
return
try:
qty = self.qty_var.get()
price = float(self.price_var.get())
except ValueError:
messagebox.showerror("エラー", "数量・単価を正しく入力してください")
return
amount = qty * price
no = len(self._line_items) + 1
self._line_items.append((name, qty, price, amount))
self.line_tree.insert("", "end",
values=(no, name, qty,
f"¥{price:,.0f}",
f"¥{amount:,.0f}"))
self._update_totals()
def _delete_line(self):
sel = self.line_tree.selection()
if sel:
idx = self.line_tree.index(sel[0])
self._line_items.pop(idx)
self.line_tree.delete(sel[0])
self._renumber()
self._update_totals()
def _renumber(self):
for idx, item in enumerate(self.line_tree.get_children()):
vals = list(self.line_tree.item(item)["values"])
vals[0] = idx + 1
self.line_tree.item(item, values=vals)
def _update_totals(self):
subtotal = sum(a for _, _, _, a in self._line_items)
tax = subtotal * 0.1
total = subtotal + tax
self.subtotal_label.config(text=f"小計: ¥{subtotal:,.0f}")
self.tax_label.config(text=f" 消費税(10%): ¥{tax:,.0f}")
self.total_label.config(text=f" 合計: ¥{total:,.0f}")
self._total = total
def _save_invoice(self):
client_name = self.client_var.get()
client_id = self._clients.get(client_name)
if not client_id:
messagebox.showwarning("警告", "顧客を選択してください")
return
self.conn.execute(
"INSERT OR REPLACE INTO invoices "
"(invoice_no, client_id, issue_date, due_date, notes, created_at) "
"VALUES (?,?,?,?,?,?)",
(self.inv_no_var.get(), client_id,
self.issue_var.get(), self.due_var.get(),
self.notes_text.get("1.0", tk.END).strip(),
datetime.now().isoformat()))
self.conn.commit()
self.status_var.set(f"保存完了: {self.inv_no_var.get()}")
def _export_csv(self):
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV", "*.csv"), ("すべて", "*.*")])
if not path:
return
import csv
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["請求書番号", "顧客", "発行日", "支払期限"])
w.writerow([self.inv_no_var.get(), self.client_var.get(),
self.issue_var.get(), self.due_var.get()])
w.writerow([])
w.writerow(["品目", "数量", "単価", "金額"])
for name, qty, price, amount in self._line_items:
w.writerow([name, qty, price, amount])
subtotal = sum(a for _, _, _, a in self._line_items)
w.writerow(["小計", "", "", subtotal])
w.writerow(["消費税(10%)", "", "", subtotal * 0.1])
w.writerow(["合計", "", "", subtotal * 1.1])
self.status_var.set(f"CSV出力: {path}")
def _export_pdf(self):
if not REPORTLAB_AVAILABLE:
messagebox.showwarning("警告",
"pip install reportlab が必要です")
return
path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF", "*.pdf")])
if not path:
return
try:
doc = SimpleDocTemplate(path, pagesize=A4)
styles = getSampleStyleSheet()
story = []
story.append(Paragraph(f"請求書 {self.inv_no_var.get()}",
styles["Title"]))
story.append(Spacer(1, 12))
story.append(Paragraph(
f"顧客: {self.client_var.get()} "
f"発行日: {self.issue_var.get()} "
f"支払期限: {self.due_var.get()}", styles["Normal"]))
story.append(Spacer(1, 20))
data = [["品目", "数量", "単価(円)", "金額(円)"]]
for name, qty, price, amount in self._line_items:
data.append([name, qty, f"{price:,.0f}", f"{amount:,.0f}"])
subtotal = sum(a for _, _, _, a in self._line_items)
data.append(["", "", "小計", f"{subtotal:,.0f}"])
data.append(["", "", "消費税(10%)", f"{subtotal*0.1:,.0f}"])
data.append(["", "", "合計", f"{subtotal*1.1:,.0f}"])
t = Table(data, colWidths=[200, 60, 100, 100])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565c0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ALIGN", (1, 1), (-1, -1), "RIGHT"),
("GRID", (0, 0), (-1, -2), 0.5, colors.grey),
("FONTSIZE", (0, 0), (-1, -1), 10),
]))
story.append(t)
if self.notes_text.get("1.0", tk.END).strip():
story.append(Spacer(1, 20))
story.append(Paragraph("備考: " + self.notes_text.get("1.0", tk.END).strip(),
styles["Normal"]))
doc.build(story)
self.status_var.set(f"PDF出力: {path}")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _manage_clients(self):
win = tk.Toplevel(self.root)
win.title("顧客管理")
win.geometry("500x350")
# 顧客一覧
cols = ("id", "name", "address", "email", "phone")
tree = ttk.Treeview(win, columns=cols, show="headings")
for c, h, w in [("id", "ID", 40), ("name", "名前", 120),
("address", "住所", 150), ("email", "メール", 120),
("phone", "電話", 100)]:
tree.heading(c, text=h)
tree.column(c, width=w)
tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
# 追加フォーム
form = tk.Frame(win)
form.pack(fill=tk.X, padx=4)
vars_ = {}
for i, key in enumerate(["名前", "住所", "メール", "電話"]):
tk.Label(form, text=f"{key}:").grid(row=i//2, column=(i%2)*2, sticky="w", pady=2)
v = tk.StringVar()
ttk.Entry(form, textvariable=v, width=18).grid(
row=i//2, column=(i%2)*2+1, padx=4)
vars_[key] = v
def add_client():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES (?,?,?,?)",
(vars_["名前"].get(), vars_["住所"].get(),
vars_["メール"].get(), vars_["電話"].get()))
self.conn.commit()
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
self._load_clients()
ttk.Button(form, text="追加", command=add_client).grid(
row=2, column=0, columnspan=4, pady=6)
if __name__ == "__main__":
root = tk.Tk()
app = App20(root)
root.mainloop()
LabelFrameによるセクション分け
ttk.LabelFrame を使うことで、入力エリアと結果エリアを視覚的に分けられます。padding引数でフレーム内の余白を設定し、見やすいレイアウトを実現しています。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
from datetime import datetime, date
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.platypus import (SimpleDocTemplate, Table, TableStyle,
Paragraph, Spacer)
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
REPORTLAB_AVAILABLE = True
except ImportError:
REPORTLAB_AVAILABLE = False
class App20:
"""請求書作成アプリ"""
DB_PATH = os.path.join(os.path.dirname(__file__), "invoices.db")
def __init__(self, root):
self.root = root
self.root.title("請求書作成アプリ")
self.root.geometry("900x620")
self.root.configure(bg="#f8f9fc")
self._init_db()
self._build_ui()
self._load_clients()
self._load_items()
self._new_invoice()
if not REPORTLAB_AVAILABLE:
messagebox.showwarning(
"ライブラリ未インストール",
"reportlab が必要です (PDF出力に必要)。\n"
"pip install reportlab でインストールしてください。\n\n"
"CSV形式での出力は可能です。")
def _init_db(self):
self.conn = sqlite3.connect(self.DB_PATH)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, address TEXT, email TEXT, phone TEXT
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, unit_price REAL, unit TEXT DEFAULT '式'
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invoice_no TEXT, client_id INTEGER, issue_date TEXT,
due_date TEXT, notes TEXT, created_at TEXT
)
""")
self.conn.commit()
# サンプルデータ
if not self.conn.execute("SELECT 1 FROM clients").fetchone():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('株式会社ABC','東京都千代田区1-1','abc@example.com','03-1234-5678')")
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('山田商事','大阪府大阪市2-2','yamada@example.com','06-9876-5432')")
self.conn.execute(
"INSERT INTO items (name,unit_price,unit) VALUES "
"('Webデザイン',50000,'式'),('システム開発',80000,'式'),"
"('コンサルティング',30000,'時間'),('保守費用',20000,'月')")
self.conn.commit()
def _build_ui(self):
title_frame = tk.Frame(self.root, bg="#f57f17", pady=10)
title_frame.pack(fill=tk.X)
tk.Label(title_frame, text="🧾 請求書作成アプリ",
font=("Noto Sans JP", 15, "bold"),
bg="#f57f17", fg="white").pack(side=tk.LEFT, padx=12)
ttk.Button(title_frame, text="📄 PDF出力",
command=self._export_pdf).pack(side=tk.RIGHT, padx=8)
ttk.Button(title_frame, text="📊 CSV出力",
command=self._export_csv).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="💾 保存",
command=self._save_invoice).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="🆕 新規",
command=self._new_invoice).pack(side=tk.RIGHT, padx=4)
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左: 請求書フォーム
form = ttk.LabelFrame(paned, text="請求書", padding=12)
# 請求書番号
row_f = tk.Frame(form, bg=form.cget("background"))
row_f.pack(fill=tk.X, pady=2)
tk.Label(row_f, text="請求書番号:", bg=row_f.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.inv_no_var = tk.StringVar()
ttk.Entry(row_f, textvariable=self.inv_no_var, width=18).pack(side=tk.LEFT, padx=4)
# 発行日
row_f2 = tk.Frame(form, bg=form.cget("background"))
row_f2.pack(fill=tk.X, pady=2)
tk.Label(row_f2, text="発行日:", bg=row_f2.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.issue_var = tk.StringVar(value=str(date.today()))
ttk.Entry(row_f2, textvariable=self.issue_var, width=12).pack(side=tk.LEFT, padx=4)
tk.Label(row_f2, text="支払期限:", bg=row_f2.cget("bg")).pack(side=tk.LEFT, padx=(12, 0))
self.due_var = tk.StringVar()
ttk.Entry(row_f2, textvariable=self.due_var, width=12).pack(side=tk.LEFT, padx=4)
# 顧客
row_f3 = tk.Frame(form, bg=form.cget("background"))
row_f3.pack(fill=tk.X, pady=2)
tk.Label(row_f3, text="顧客:", bg=row_f3.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.client_var = tk.StringVar()
self.client_cb = ttk.Combobox(row_f3, textvariable=self.client_var,
state="readonly", width=24)
self.client_cb.pack(side=tk.LEFT, padx=4)
ttk.Button(row_f3, text="✏️ 顧客管理",
command=self._manage_clients).pack(side=tk.LEFT, padx=4)
# 備考
row_f4 = tk.Frame(form, bg=form.cget("background"))
row_f4.pack(fill=tk.X, pady=2)
tk.Label(row_f4, text="備考:", bg=row_f4.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT, anchor="n")
self.notes_text = tk.Text(row_f4, height=2, width=30,
font=("Arial", 10))
self.notes_text.pack(side=tk.LEFT, padx=4)
# 明細テーブル
tk.Label(form, text="明細:", bg=form.cget("background"),
font=("Noto Sans JP", 11, "bold")).pack(anchor="w", pady=(8, 2))
item_f = tk.Frame(form, bg=form.cget("background"))
item_f.pack(fill=tk.X)
self.item_name_var = tk.StringVar()
self.item_cb = ttk.Combobox(item_f, textvariable=self.item_name_var,
width=20)
self.item_cb.pack(side=tk.LEFT, padx=2)
self.item_cb.bind("<<ComboboxSelected>>", self._on_item_select)
tk.Label(item_f, text="数量:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.qty_var = tk.IntVar(value=1)
ttk.Spinbox(item_f, from_=1, to=9999,
textvariable=self.qty_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(item_f, text="単価:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.price_var = tk.StringVar(value="0")
ttk.Entry(item_f, textvariable=self.price_var, width=10).pack(side=tk.LEFT, padx=2)
ttk.Button(item_f, text="➕ 追加",
command=self._add_line).pack(side=tk.LEFT, padx=4)
# 明細Treeview
cols = ("no", "name", "qty", "unit_price", "amount")
self.line_tree = ttk.Treeview(form, columns=cols,
show="headings", height=8)
for c, h, w in [("no", "#", 30), ("name", "品目", 200),
("qty", "数量", 50), ("unit_price", "単価", 90),
("amount", "金額", 90)]:
self.line_tree.heading(c, text=h)
self.line_tree.column(c, width=w, minwidth=30)
line_sb = ttk.Scrollbar(form, command=self.line_tree.yview)
self.line_tree.configure(yscrollcommand=line_sb.set)
line_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.line_tree.pack(fill=tk.BOTH, expand=True)
ttk.Button(form, text="🗑️ 選択行削除",
command=self._delete_line).pack(anchor="w", pady=2)
# 合計
total_f = tk.Frame(form, bg=form.cget("background"))
total_f.pack(fill=tk.X, pady=4)
self.subtotal_label = tk.Label(total_f, text="小計: ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.subtotal_label.pack(side=tk.LEFT)
self.tax_label = tk.Label(total_f, text=" 消費税(10%): ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.tax_label.pack(side=tk.LEFT)
self.total_label = tk.Label(total_f, text=" 合計: ¥0",
bg=form.cget("background"),
fg="#e65100",
font=("Arial", 13, "bold"))
self.total_label.pack(side=tk.LEFT)
paned.add(form, weight=3)
# 右: 請求書履歴
hist_frame = ttk.LabelFrame(paned, text="請求書履歴", padding=4)
hist_cols = ("inv_no", "client", "date", "total")
self.hist_tree = ttk.Treeview(hist_frame, columns=hist_cols,
show="headings", selectmode="browse")
for c, h, w in [("inv_no", "番号", 80), ("client", "顧客", 100),
("date", "発行日", 90), ("total", "金額", 80)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=30)
hist_sb = ttk.Scrollbar(hist_frame, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=hist_sb.set)
hist_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True)
paned.add(hist_frame, weight=1)
self.status_var = tk.StringVar(value="")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._line_items = []
def _load_clients(self):
rows = self.conn.execute(
"SELECT id, name FROM clients ORDER BY name").fetchall()
self._clients = {name: cid for cid, name in rows}
self.client_cb.configure(values=list(self._clients.keys()))
def _load_items(self):
rows = self.conn.execute(
"SELECT name, unit_price FROM items ORDER BY name").fetchall()
self._items = {name: price for name, price in rows}
self.item_cb.configure(values=list(self._items.keys()))
def _new_invoice(self):
now = datetime.now()
self.inv_no_var.set(f"INV-{now.strftime('%Y%m%d-%H%M')}")
self.issue_var.set(str(date.today()))
self.due_var.set("")
self.notes_text.delete("1.0", tk.END)
self.line_tree.delete(*self.line_tree.get_children())
self._line_items = []
self._update_totals()
def _on_item_select(self, event):
name = self.item_name_var.get()
price = self._items.get(name, 0)
self.price_var.set(str(price))
def _add_line(self):
name = self.item_name_var.get().strip()
if not name:
return
try:
qty = self.qty_var.get()
price = float(self.price_var.get())
except ValueError:
messagebox.showerror("エラー", "数量・単価を正しく入力してください")
return
amount = qty * price
no = len(self._line_items) + 1
self._line_items.append((name, qty, price, amount))
self.line_tree.insert("", "end",
values=(no, name, qty,
f"¥{price:,.0f}",
f"¥{amount:,.0f}"))
self._update_totals()
def _delete_line(self):
sel = self.line_tree.selection()
if sel:
idx = self.line_tree.index(sel[0])
self._line_items.pop(idx)
self.line_tree.delete(sel[0])
self._renumber()
self._update_totals()
def _renumber(self):
for idx, item in enumerate(self.line_tree.get_children()):
vals = list(self.line_tree.item(item)["values"])
vals[0] = idx + 1
self.line_tree.item(item, values=vals)
def _update_totals(self):
subtotal = sum(a for _, _, _, a in self._line_items)
tax = subtotal * 0.1
total = subtotal + tax
self.subtotal_label.config(text=f"小計: ¥{subtotal:,.0f}")
self.tax_label.config(text=f" 消費税(10%): ¥{tax:,.0f}")
self.total_label.config(text=f" 合計: ¥{total:,.0f}")
self._total = total
def _save_invoice(self):
client_name = self.client_var.get()
client_id = self._clients.get(client_name)
if not client_id:
messagebox.showwarning("警告", "顧客を選択してください")
return
self.conn.execute(
"INSERT OR REPLACE INTO invoices "
"(invoice_no, client_id, issue_date, due_date, notes, created_at) "
"VALUES (?,?,?,?,?,?)",
(self.inv_no_var.get(), client_id,
self.issue_var.get(), self.due_var.get(),
self.notes_text.get("1.0", tk.END).strip(),
datetime.now().isoformat()))
self.conn.commit()
self.status_var.set(f"保存完了: {self.inv_no_var.get()}")
def _export_csv(self):
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV", "*.csv"), ("すべて", "*.*")])
if not path:
return
import csv
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["請求書番号", "顧客", "発行日", "支払期限"])
w.writerow([self.inv_no_var.get(), self.client_var.get(),
self.issue_var.get(), self.due_var.get()])
w.writerow([])
w.writerow(["品目", "数量", "単価", "金額"])
for name, qty, price, amount in self._line_items:
w.writerow([name, qty, price, amount])
subtotal = sum(a for _, _, _, a in self._line_items)
w.writerow(["小計", "", "", subtotal])
w.writerow(["消費税(10%)", "", "", subtotal * 0.1])
w.writerow(["合計", "", "", subtotal * 1.1])
self.status_var.set(f"CSV出力: {path}")
def _export_pdf(self):
if not REPORTLAB_AVAILABLE:
messagebox.showwarning("警告",
"pip install reportlab が必要です")
return
path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF", "*.pdf")])
if not path:
return
try:
doc = SimpleDocTemplate(path, pagesize=A4)
styles = getSampleStyleSheet()
story = []
story.append(Paragraph(f"請求書 {self.inv_no_var.get()}",
styles["Title"]))
story.append(Spacer(1, 12))
story.append(Paragraph(
f"顧客: {self.client_var.get()} "
f"発行日: {self.issue_var.get()} "
f"支払期限: {self.due_var.get()}", styles["Normal"]))
story.append(Spacer(1, 20))
data = [["品目", "数量", "単価(円)", "金額(円)"]]
for name, qty, price, amount in self._line_items:
data.append([name, qty, f"{price:,.0f}", f"{amount:,.0f}"])
subtotal = sum(a for _, _, _, a in self._line_items)
data.append(["", "", "小計", f"{subtotal:,.0f}"])
data.append(["", "", "消費税(10%)", f"{subtotal*0.1:,.0f}"])
data.append(["", "", "合計", f"{subtotal*1.1:,.0f}"])
t = Table(data, colWidths=[200, 60, 100, 100])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565c0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ALIGN", (1, 1), (-1, -1), "RIGHT"),
("GRID", (0, 0), (-1, -2), 0.5, colors.grey),
("FONTSIZE", (0, 0), (-1, -1), 10),
]))
story.append(t)
if self.notes_text.get("1.0", tk.END).strip():
story.append(Spacer(1, 20))
story.append(Paragraph("備考: " + self.notes_text.get("1.0", tk.END).strip(),
styles["Normal"]))
doc.build(story)
self.status_var.set(f"PDF出力: {path}")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _manage_clients(self):
win = tk.Toplevel(self.root)
win.title("顧客管理")
win.geometry("500x350")
# 顧客一覧
cols = ("id", "name", "address", "email", "phone")
tree = ttk.Treeview(win, columns=cols, show="headings")
for c, h, w in [("id", "ID", 40), ("name", "名前", 120),
("address", "住所", 150), ("email", "メール", 120),
("phone", "電話", 100)]:
tree.heading(c, text=h)
tree.column(c, width=w)
tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
# 追加フォーム
form = tk.Frame(win)
form.pack(fill=tk.X, padx=4)
vars_ = {}
for i, key in enumerate(["名前", "住所", "メール", "電話"]):
tk.Label(form, text=f"{key}:").grid(row=i//2, column=(i%2)*2, sticky="w", pady=2)
v = tk.StringVar()
ttk.Entry(form, textvariable=v, width=18).grid(
row=i//2, column=(i%2)*2+1, padx=4)
vars_[key] = v
def add_client():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES (?,?,?,?)",
(vars_["名前"].get(), vars_["住所"].get(),
vars_["メール"].get(), vars_["電話"].get()))
self.conn.commit()
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
self._load_clients()
ttk.Button(form, text="追加", command=add_client).grid(
row=2, column=0, columnspan=4, pady=6)
if __name__ == "__main__":
root = tk.Tk()
app = App20(root)
root.mainloop()
Entryウィジェットとイベントバインド
ttk.Entryで入力フィールドを作成します。bind('
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
from datetime import datetime, date
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.platypus import (SimpleDocTemplate, Table, TableStyle,
Paragraph, Spacer)
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
REPORTLAB_AVAILABLE = True
except ImportError:
REPORTLAB_AVAILABLE = False
class App20:
"""請求書作成アプリ"""
DB_PATH = os.path.join(os.path.dirname(__file__), "invoices.db")
def __init__(self, root):
self.root = root
self.root.title("請求書作成アプリ")
self.root.geometry("900x620")
self.root.configure(bg="#f8f9fc")
self._init_db()
self._build_ui()
self._load_clients()
self._load_items()
self._new_invoice()
if not REPORTLAB_AVAILABLE:
messagebox.showwarning(
"ライブラリ未インストール",
"reportlab が必要です (PDF出力に必要)。\n"
"pip install reportlab でインストールしてください。\n\n"
"CSV形式での出力は可能です。")
def _init_db(self):
self.conn = sqlite3.connect(self.DB_PATH)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, address TEXT, email TEXT, phone TEXT
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, unit_price REAL, unit TEXT DEFAULT '式'
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invoice_no TEXT, client_id INTEGER, issue_date TEXT,
due_date TEXT, notes TEXT, created_at TEXT
)
""")
self.conn.commit()
# サンプルデータ
if not self.conn.execute("SELECT 1 FROM clients").fetchone():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('株式会社ABC','東京都千代田区1-1','abc@example.com','03-1234-5678')")
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('山田商事','大阪府大阪市2-2','yamada@example.com','06-9876-5432')")
self.conn.execute(
"INSERT INTO items (name,unit_price,unit) VALUES "
"('Webデザイン',50000,'式'),('システム開発',80000,'式'),"
"('コンサルティング',30000,'時間'),('保守費用',20000,'月')")
self.conn.commit()
def _build_ui(self):
title_frame = tk.Frame(self.root, bg="#f57f17", pady=10)
title_frame.pack(fill=tk.X)
tk.Label(title_frame, text="🧾 請求書作成アプリ",
font=("Noto Sans JP", 15, "bold"),
bg="#f57f17", fg="white").pack(side=tk.LEFT, padx=12)
ttk.Button(title_frame, text="📄 PDF出力",
command=self._export_pdf).pack(side=tk.RIGHT, padx=8)
ttk.Button(title_frame, text="📊 CSV出力",
command=self._export_csv).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="💾 保存",
command=self._save_invoice).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="🆕 新規",
command=self._new_invoice).pack(side=tk.RIGHT, padx=4)
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左: 請求書フォーム
form = ttk.LabelFrame(paned, text="請求書", padding=12)
# 請求書番号
row_f = tk.Frame(form, bg=form.cget("background"))
row_f.pack(fill=tk.X, pady=2)
tk.Label(row_f, text="請求書番号:", bg=row_f.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.inv_no_var = tk.StringVar()
ttk.Entry(row_f, textvariable=self.inv_no_var, width=18).pack(side=tk.LEFT, padx=4)
# 発行日
row_f2 = tk.Frame(form, bg=form.cget("background"))
row_f2.pack(fill=tk.X, pady=2)
tk.Label(row_f2, text="発行日:", bg=row_f2.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.issue_var = tk.StringVar(value=str(date.today()))
ttk.Entry(row_f2, textvariable=self.issue_var, width=12).pack(side=tk.LEFT, padx=4)
tk.Label(row_f2, text="支払期限:", bg=row_f2.cget("bg")).pack(side=tk.LEFT, padx=(12, 0))
self.due_var = tk.StringVar()
ttk.Entry(row_f2, textvariable=self.due_var, width=12).pack(side=tk.LEFT, padx=4)
# 顧客
row_f3 = tk.Frame(form, bg=form.cget("background"))
row_f3.pack(fill=tk.X, pady=2)
tk.Label(row_f3, text="顧客:", bg=row_f3.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.client_var = tk.StringVar()
self.client_cb = ttk.Combobox(row_f3, textvariable=self.client_var,
state="readonly", width=24)
self.client_cb.pack(side=tk.LEFT, padx=4)
ttk.Button(row_f3, text="✏️ 顧客管理",
command=self._manage_clients).pack(side=tk.LEFT, padx=4)
# 備考
row_f4 = tk.Frame(form, bg=form.cget("background"))
row_f4.pack(fill=tk.X, pady=2)
tk.Label(row_f4, text="備考:", bg=row_f4.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT, anchor="n")
self.notes_text = tk.Text(row_f4, height=2, width=30,
font=("Arial", 10))
self.notes_text.pack(side=tk.LEFT, padx=4)
# 明細テーブル
tk.Label(form, text="明細:", bg=form.cget("background"),
font=("Noto Sans JP", 11, "bold")).pack(anchor="w", pady=(8, 2))
item_f = tk.Frame(form, bg=form.cget("background"))
item_f.pack(fill=tk.X)
self.item_name_var = tk.StringVar()
self.item_cb = ttk.Combobox(item_f, textvariable=self.item_name_var,
width=20)
self.item_cb.pack(side=tk.LEFT, padx=2)
self.item_cb.bind("<<ComboboxSelected>>", self._on_item_select)
tk.Label(item_f, text="数量:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.qty_var = tk.IntVar(value=1)
ttk.Spinbox(item_f, from_=1, to=9999,
textvariable=self.qty_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(item_f, text="単価:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.price_var = tk.StringVar(value="0")
ttk.Entry(item_f, textvariable=self.price_var, width=10).pack(side=tk.LEFT, padx=2)
ttk.Button(item_f, text="➕ 追加",
command=self._add_line).pack(side=tk.LEFT, padx=4)
# 明細Treeview
cols = ("no", "name", "qty", "unit_price", "amount")
self.line_tree = ttk.Treeview(form, columns=cols,
show="headings", height=8)
for c, h, w in [("no", "#", 30), ("name", "品目", 200),
("qty", "数量", 50), ("unit_price", "単価", 90),
("amount", "金額", 90)]:
self.line_tree.heading(c, text=h)
self.line_tree.column(c, width=w, minwidth=30)
line_sb = ttk.Scrollbar(form, command=self.line_tree.yview)
self.line_tree.configure(yscrollcommand=line_sb.set)
line_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.line_tree.pack(fill=tk.BOTH, expand=True)
ttk.Button(form, text="🗑️ 選択行削除",
command=self._delete_line).pack(anchor="w", pady=2)
# 合計
total_f = tk.Frame(form, bg=form.cget("background"))
total_f.pack(fill=tk.X, pady=4)
self.subtotal_label = tk.Label(total_f, text="小計: ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.subtotal_label.pack(side=tk.LEFT)
self.tax_label = tk.Label(total_f, text=" 消費税(10%): ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.tax_label.pack(side=tk.LEFT)
self.total_label = tk.Label(total_f, text=" 合計: ¥0",
bg=form.cget("background"),
fg="#e65100",
font=("Arial", 13, "bold"))
self.total_label.pack(side=tk.LEFT)
paned.add(form, weight=3)
# 右: 請求書履歴
hist_frame = ttk.LabelFrame(paned, text="請求書履歴", padding=4)
hist_cols = ("inv_no", "client", "date", "total")
self.hist_tree = ttk.Treeview(hist_frame, columns=hist_cols,
show="headings", selectmode="browse")
for c, h, w in [("inv_no", "番号", 80), ("client", "顧客", 100),
("date", "発行日", 90), ("total", "金額", 80)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=30)
hist_sb = ttk.Scrollbar(hist_frame, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=hist_sb.set)
hist_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True)
paned.add(hist_frame, weight=1)
self.status_var = tk.StringVar(value="")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._line_items = []
def _load_clients(self):
rows = self.conn.execute(
"SELECT id, name FROM clients ORDER BY name").fetchall()
self._clients = {name: cid for cid, name in rows}
self.client_cb.configure(values=list(self._clients.keys()))
def _load_items(self):
rows = self.conn.execute(
"SELECT name, unit_price FROM items ORDER BY name").fetchall()
self._items = {name: price for name, price in rows}
self.item_cb.configure(values=list(self._items.keys()))
def _new_invoice(self):
now = datetime.now()
self.inv_no_var.set(f"INV-{now.strftime('%Y%m%d-%H%M')}")
self.issue_var.set(str(date.today()))
self.due_var.set("")
self.notes_text.delete("1.0", tk.END)
self.line_tree.delete(*self.line_tree.get_children())
self._line_items = []
self._update_totals()
def _on_item_select(self, event):
name = self.item_name_var.get()
price = self._items.get(name, 0)
self.price_var.set(str(price))
def _add_line(self):
name = self.item_name_var.get().strip()
if not name:
return
try:
qty = self.qty_var.get()
price = float(self.price_var.get())
except ValueError:
messagebox.showerror("エラー", "数量・単価を正しく入力してください")
return
amount = qty * price
no = len(self._line_items) + 1
self._line_items.append((name, qty, price, amount))
self.line_tree.insert("", "end",
values=(no, name, qty,
f"¥{price:,.0f}",
f"¥{amount:,.0f}"))
self._update_totals()
def _delete_line(self):
sel = self.line_tree.selection()
if sel:
idx = self.line_tree.index(sel[0])
self._line_items.pop(idx)
self.line_tree.delete(sel[0])
self._renumber()
self._update_totals()
def _renumber(self):
for idx, item in enumerate(self.line_tree.get_children()):
vals = list(self.line_tree.item(item)["values"])
vals[0] = idx + 1
self.line_tree.item(item, values=vals)
def _update_totals(self):
subtotal = sum(a for _, _, _, a in self._line_items)
tax = subtotal * 0.1
total = subtotal + tax
self.subtotal_label.config(text=f"小計: ¥{subtotal:,.0f}")
self.tax_label.config(text=f" 消費税(10%): ¥{tax:,.0f}")
self.total_label.config(text=f" 合計: ¥{total:,.0f}")
self._total = total
def _save_invoice(self):
client_name = self.client_var.get()
client_id = self._clients.get(client_name)
if not client_id:
messagebox.showwarning("警告", "顧客を選択してください")
return
self.conn.execute(
"INSERT OR REPLACE INTO invoices "
"(invoice_no, client_id, issue_date, due_date, notes, created_at) "
"VALUES (?,?,?,?,?,?)",
(self.inv_no_var.get(), client_id,
self.issue_var.get(), self.due_var.get(),
self.notes_text.get("1.0", tk.END).strip(),
datetime.now().isoformat()))
self.conn.commit()
self.status_var.set(f"保存完了: {self.inv_no_var.get()}")
def _export_csv(self):
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV", "*.csv"), ("すべて", "*.*")])
if not path:
return
import csv
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["請求書番号", "顧客", "発行日", "支払期限"])
w.writerow([self.inv_no_var.get(), self.client_var.get(),
self.issue_var.get(), self.due_var.get()])
w.writerow([])
w.writerow(["品目", "数量", "単価", "金額"])
for name, qty, price, amount in self._line_items:
w.writerow([name, qty, price, amount])
subtotal = sum(a for _, _, _, a in self._line_items)
w.writerow(["小計", "", "", subtotal])
w.writerow(["消費税(10%)", "", "", subtotal * 0.1])
w.writerow(["合計", "", "", subtotal * 1.1])
self.status_var.set(f"CSV出力: {path}")
def _export_pdf(self):
if not REPORTLAB_AVAILABLE:
messagebox.showwarning("警告",
"pip install reportlab が必要です")
return
path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF", "*.pdf")])
if not path:
return
try:
doc = SimpleDocTemplate(path, pagesize=A4)
styles = getSampleStyleSheet()
story = []
story.append(Paragraph(f"請求書 {self.inv_no_var.get()}",
styles["Title"]))
story.append(Spacer(1, 12))
story.append(Paragraph(
f"顧客: {self.client_var.get()} "
f"発行日: {self.issue_var.get()} "
f"支払期限: {self.due_var.get()}", styles["Normal"]))
story.append(Spacer(1, 20))
data = [["品目", "数量", "単価(円)", "金額(円)"]]
for name, qty, price, amount in self._line_items:
data.append([name, qty, f"{price:,.0f}", f"{amount:,.0f}"])
subtotal = sum(a for _, _, _, a in self._line_items)
data.append(["", "", "小計", f"{subtotal:,.0f}"])
data.append(["", "", "消費税(10%)", f"{subtotal*0.1:,.0f}"])
data.append(["", "", "合計", f"{subtotal*1.1:,.0f}"])
t = Table(data, colWidths=[200, 60, 100, 100])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565c0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ALIGN", (1, 1), (-1, -1), "RIGHT"),
("GRID", (0, 0), (-1, -2), 0.5, colors.grey),
("FONTSIZE", (0, 0), (-1, -1), 10),
]))
story.append(t)
if self.notes_text.get("1.0", tk.END).strip():
story.append(Spacer(1, 20))
story.append(Paragraph("備考: " + self.notes_text.get("1.0", tk.END).strip(),
styles["Normal"]))
doc.build(story)
self.status_var.set(f"PDF出力: {path}")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _manage_clients(self):
win = tk.Toplevel(self.root)
win.title("顧客管理")
win.geometry("500x350")
# 顧客一覧
cols = ("id", "name", "address", "email", "phone")
tree = ttk.Treeview(win, columns=cols, show="headings")
for c, h, w in [("id", "ID", 40), ("name", "名前", 120),
("address", "住所", 150), ("email", "メール", 120),
("phone", "電話", 100)]:
tree.heading(c, text=h)
tree.column(c, width=w)
tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
# 追加フォーム
form = tk.Frame(win)
form.pack(fill=tk.X, padx=4)
vars_ = {}
for i, key in enumerate(["名前", "住所", "メール", "電話"]):
tk.Label(form, text=f"{key}:").grid(row=i//2, column=(i%2)*2, sticky="w", pady=2)
v = tk.StringVar()
ttk.Entry(form, textvariable=v, width=18).grid(
row=i//2, column=(i%2)*2+1, padx=4)
vars_[key] = v
def add_client():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES (?,?,?,?)",
(vars_["名前"].get(), vars_["住所"].get(),
vars_["メール"].get(), vars_["電話"].get()))
self.conn.commit()
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
self._load_clients()
ttk.Button(form, text="追加", command=add_client).grid(
row=2, column=0, columnspan=4, pady=6)
if __name__ == "__main__":
root = tk.Tk()
app = App20(root)
root.mainloop()
Textウィジェットでの結果表示
結果表示にはtk.Textウィジェットを使います。state=tk.DISABLEDでユーザーが直接編集できないようにし、表示前にNORMALに切り替えてからinsert()で内容を更新します。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
from datetime import datetime, date
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.platypus import (SimpleDocTemplate, Table, TableStyle,
Paragraph, Spacer)
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
REPORTLAB_AVAILABLE = True
except ImportError:
REPORTLAB_AVAILABLE = False
class App20:
"""請求書作成アプリ"""
DB_PATH = os.path.join(os.path.dirname(__file__), "invoices.db")
def __init__(self, root):
self.root = root
self.root.title("請求書作成アプリ")
self.root.geometry("900x620")
self.root.configure(bg="#f8f9fc")
self._init_db()
self._build_ui()
self._load_clients()
self._load_items()
self._new_invoice()
if not REPORTLAB_AVAILABLE:
messagebox.showwarning(
"ライブラリ未インストール",
"reportlab が必要です (PDF出力に必要)。\n"
"pip install reportlab でインストールしてください。\n\n"
"CSV形式での出力は可能です。")
def _init_db(self):
self.conn = sqlite3.connect(self.DB_PATH)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, address TEXT, email TEXT, phone TEXT
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, unit_price REAL, unit TEXT DEFAULT '式'
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invoice_no TEXT, client_id INTEGER, issue_date TEXT,
due_date TEXT, notes TEXT, created_at TEXT
)
""")
self.conn.commit()
# サンプルデータ
if not self.conn.execute("SELECT 1 FROM clients").fetchone():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('株式会社ABC','東京都千代田区1-1','abc@example.com','03-1234-5678')")
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('山田商事','大阪府大阪市2-2','yamada@example.com','06-9876-5432')")
self.conn.execute(
"INSERT INTO items (name,unit_price,unit) VALUES "
"('Webデザイン',50000,'式'),('システム開発',80000,'式'),"
"('コンサルティング',30000,'時間'),('保守費用',20000,'月')")
self.conn.commit()
def _build_ui(self):
title_frame = tk.Frame(self.root, bg="#f57f17", pady=10)
title_frame.pack(fill=tk.X)
tk.Label(title_frame, text="🧾 請求書作成アプリ",
font=("Noto Sans JP", 15, "bold"),
bg="#f57f17", fg="white").pack(side=tk.LEFT, padx=12)
ttk.Button(title_frame, text="📄 PDF出力",
command=self._export_pdf).pack(side=tk.RIGHT, padx=8)
ttk.Button(title_frame, text="📊 CSV出力",
command=self._export_csv).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="💾 保存",
command=self._save_invoice).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="🆕 新規",
command=self._new_invoice).pack(side=tk.RIGHT, padx=4)
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左: 請求書フォーム
form = ttk.LabelFrame(paned, text="請求書", padding=12)
# 請求書番号
row_f = tk.Frame(form, bg=form.cget("background"))
row_f.pack(fill=tk.X, pady=2)
tk.Label(row_f, text="請求書番号:", bg=row_f.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.inv_no_var = tk.StringVar()
ttk.Entry(row_f, textvariable=self.inv_no_var, width=18).pack(side=tk.LEFT, padx=4)
# 発行日
row_f2 = tk.Frame(form, bg=form.cget("background"))
row_f2.pack(fill=tk.X, pady=2)
tk.Label(row_f2, text="発行日:", bg=row_f2.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.issue_var = tk.StringVar(value=str(date.today()))
ttk.Entry(row_f2, textvariable=self.issue_var, width=12).pack(side=tk.LEFT, padx=4)
tk.Label(row_f2, text="支払期限:", bg=row_f2.cget("bg")).pack(side=tk.LEFT, padx=(12, 0))
self.due_var = tk.StringVar()
ttk.Entry(row_f2, textvariable=self.due_var, width=12).pack(side=tk.LEFT, padx=4)
# 顧客
row_f3 = tk.Frame(form, bg=form.cget("background"))
row_f3.pack(fill=tk.X, pady=2)
tk.Label(row_f3, text="顧客:", bg=row_f3.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.client_var = tk.StringVar()
self.client_cb = ttk.Combobox(row_f3, textvariable=self.client_var,
state="readonly", width=24)
self.client_cb.pack(side=tk.LEFT, padx=4)
ttk.Button(row_f3, text="✏️ 顧客管理",
command=self._manage_clients).pack(side=tk.LEFT, padx=4)
# 備考
row_f4 = tk.Frame(form, bg=form.cget("background"))
row_f4.pack(fill=tk.X, pady=2)
tk.Label(row_f4, text="備考:", bg=row_f4.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT, anchor="n")
self.notes_text = tk.Text(row_f4, height=2, width=30,
font=("Arial", 10))
self.notes_text.pack(side=tk.LEFT, padx=4)
# 明細テーブル
tk.Label(form, text="明細:", bg=form.cget("background"),
font=("Noto Sans JP", 11, "bold")).pack(anchor="w", pady=(8, 2))
item_f = tk.Frame(form, bg=form.cget("background"))
item_f.pack(fill=tk.X)
self.item_name_var = tk.StringVar()
self.item_cb = ttk.Combobox(item_f, textvariable=self.item_name_var,
width=20)
self.item_cb.pack(side=tk.LEFT, padx=2)
self.item_cb.bind("<<ComboboxSelected>>", self._on_item_select)
tk.Label(item_f, text="数量:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.qty_var = tk.IntVar(value=1)
ttk.Spinbox(item_f, from_=1, to=9999,
textvariable=self.qty_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(item_f, text="単価:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.price_var = tk.StringVar(value="0")
ttk.Entry(item_f, textvariable=self.price_var, width=10).pack(side=tk.LEFT, padx=2)
ttk.Button(item_f, text="➕ 追加",
command=self._add_line).pack(side=tk.LEFT, padx=4)
# 明細Treeview
cols = ("no", "name", "qty", "unit_price", "amount")
self.line_tree = ttk.Treeview(form, columns=cols,
show="headings", height=8)
for c, h, w in [("no", "#", 30), ("name", "品目", 200),
("qty", "数量", 50), ("unit_price", "単価", 90),
("amount", "金額", 90)]:
self.line_tree.heading(c, text=h)
self.line_tree.column(c, width=w, minwidth=30)
line_sb = ttk.Scrollbar(form, command=self.line_tree.yview)
self.line_tree.configure(yscrollcommand=line_sb.set)
line_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.line_tree.pack(fill=tk.BOTH, expand=True)
ttk.Button(form, text="🗑️ 選択行削除",
command=self._delete_line).pack(anchor="w", pady=2)
# 合計
total_f = tk.Frame(form, bg=form.cget("background"))
total_f.pack(fill=tk.X, pady=4)
self.subtotal_label = tk.Label(total_f, text="小計: ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.subtotal_label.pack(side=tk.LEFT)
self.tax_label = tk.Label(total_f, text=" 消費税(10%): ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.tax_label.pack(side=tk.LEFT)
self.total_label = tk.Label(total_f, text=" 合計: ¥0",
bg=form.cget("background"),
fg="#e65100",
font=("Arial", 13, "bold"))
self.total_label.pack(side=tk.LEFT)
paned.add(form, weight=3)
# 右: 請求書履歴
hist_frame = ttk.LabelFrame(paned, text="請求書履歴", padding=4)
hist_cols = ("inv_no", "client", "date", "total")
self.hist_tree = ttk.Treeview(hist_frame, columns=hist_cols,
show="headings", selectmode="browse")
for c, h, w in [("inv_no", "番号", 80), ("client", "顧客", 100),
("date", "発行日", 90), ("total", "金額", 80)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=30)
hist_sb = ttk.Scrollbar(hist_frame, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=hist_sb.set)
hist_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True)
paned.add(hist_frame, weight=1)
self.status_var = tk.StringVar(value="")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._line_items = []
def _load_clients(self):
rows = self.conn.execute(
"SELECT id, name FROM clients ORDER BY name").fetchall()
self._clients = {name: cid for cid, name in rows}
self.client_cb.configure(values=list(self._clients.keys()))
def _load_items(self):
rows = self.conn.execute(
"SELECT name, unit_price FROM items ORDER BY name").fetchall()
self._items = {name: price for name, price in rows}
self.item_cb.configure(values=list(self._items.keys()))
def _new_invoice(self):
now = datetime.now()
self.inv_no_var.set(f"INV-{now.strftime('%Y%m%d-%H%M')}")
self.issue_var.set(str(date.today()))
self.due_var.set("")
self.notes_text.delete("1.0", tk.END)
self.line_tree.delete(*self.line_tree.get_children())
self._line_items = []
self._update_totals()
def _on_item_select(self, event):
name = self.item_name_var.get()
price = self._items.get(name, 0)
self.price_var.set(str(price))
def _add_line(self):
name = self.item_name_var.get().strip()
if not name:
return
try:
qty = self.qty_var.get()
price = float(self.price_var.get())
except ValueError:
messagebox.showerror("エラー", "数量・単価を正しく入力してください")
return
amount = qty * price
no = len(self._line_items) + 1
self._line_items.append((name, qty, price, amount))
self.line_tree.insert("", "end",
values=(no, name, qty,
f"¥{price:,.0f}",
f"¥{amount:,.0f}"))
self._update_totals()
def _delete_line(self):
sel = self.line_tree.selection()
if sel:
idx = self.line_tree.index(sel[0])
self._line_items.pop(idx)
self.line_tree.delete(sel[0])
self._renumber()
self._update_totals()
def _renumber(self):
for idx, item in enumerate(self.line_tree.get_children()):
vals = list(self.line_tree.item(item)["values"])
vals[0] = idx + 1
self.line_tree.item(item, values=vals)
def _update_totals(self):
subtotal = sum(a for _, _, _, a in self._line_items)
tax = subtotal * 0.1
total = subtotal + tax
self.subtotal_label.config(text=f"小計: ¥{subtotal:,.0f}")
self.tax_label.config(text=f" 消費税(10%): ¥{tax:,.0f}")
self.total_label.config(text=f" 合計: ¥{total:,.0f}")
self._total = total
def _save_invoice(self):
client_name = self.client_var.get()
client_id = self._clients.get(client_name)
if not client_id:
messagebox.showwarning("警告", "顧客を選択してください")
return
self.conn.execute(
"INSERT OR REPLACE INTO invoices "
"(invoice_no, client_id, issue_date, due_date, notes, created_at) "
"VALUES (?,?,?,?,?,?)",
(self.inv_no_var.get(), client_id,
self.issue_var.get(), self.due_var.get(),
self.notes_text.get("1.0", tk.END).strip(),
datetime.now().isoformat()))
self.conn.commit()
self.status_var.set(f"保存完了: {self.inv_no_var.get()}")
def _export_csv(self):
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV", "*.csv"), ("すべて", "*.*")])
if not path:
return
import csv
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["請求書番号", "顧客", "発行日", "支払期限"])
w.writerow([self.inv_no_var.get(), self.client_var.get(),
self.issue_var.get(), self.due_var.get()])
w.writerow([])
w.writerow(["品目", "数量", "単価", "金額"])
for name, qty, price, amount in self._line_items:
w.writerow([name, qty, price, amount])
subtotal = sum(a for _, _, _, a in self._line_items)
w.writerow(["小計", "", "", subtotal])
w.writerow(["消費税(10%)", "", "", subtotal * 0.1])
w.writerow(["合計", "", "", subtotal * 1.1])
self.status_var.set(f"CSV出力: {path}")
def _export_pdf(self):
if not REPORTLAB_AVAILABLE:
messagebox.showwarning("警告",
"pip install reportlab が必要です")
return
path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF", "*.pdf")])
if not path:
return
try:
doc = SimpleDocTemplate(path, pagesize=A4)
styles = getSampleStyleSheet()
story = []
story.append(Paragraph(f"請求書 {self.inv_no_var.get()}",
styles["Title"]))
story.append(Spacer(1, 12))
story.append(Paragraph(
f"顧客: {self.client_var.get()} "
f"発行日: {self.issue_var.get()} "
f"支払期限: {self.due_var.get()}", styles["Normal"]))
story.append(Spacer(1, 20))
data = [["品目", "数量", "単価(円)", "金額(円)"]]
for name, qty, price, amount in self._line_items:
data.append([name, qty, f"{price:,.0f}", f"{amount:,.0f}"])
subtotal = sum(a for _, _, _, a in self._line_items)
data.append(["", "", "小計", f"{subtotal:,.0f}"])
data.append(["", "", "消費税(10%)", f"{subtotal*0.1:,.0f}"])
data.append(["", "", "合計", f"{subtotal*1.1:,.0f}"])
t = Table(data, colWidths=[200, 60, 100, 100])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565c0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ALIGN", (1, 1), (-1, -1), "RIGHT"),
("GRID", (0, 0), (-1, -2), 0.5, colors.grey),
("FONTSIZE", (0, 0), (-1, -1), 10),
]))
story.append(t)
if self.notes_text.get("1.0", tk.END).strip():
story.append(Spacer(1, 20))
story.append(Paragraph("備考: " + self.notes_text.get("1.0", tk.END).strip(),
styles["Normal"]))
doc.build(story)
self.status_var.set(f"PDF出力: {path}")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _manage_clients(self):
win = tk.Toplevel(self.root)
win.title("顧客管理")
win.geometry("500x350")
# 顧客一覧
cols = ("id", "name", "address", "email", "phone")
tree = ttk.Treeview(win, columns=cols, show="headings")
for c, h, w in [("id", "ID", 40), ("name", "名前", 120),
("address", "住所", 150), ("email", "メール", 120),
("phone", "電話", 100)]:
tree.heading(c, text=h)
tree.column(c, width=w)
tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
# 追加フォーム
form = tk.Frame(win)
form.pack(fill=tk.X, padx=4)
vars_ = {}
for i, key in enumerate(["名前", "住所", "メール", "電話"]):
tk.Label(form, text=f"{key}:").grid(row=i//2, column=(i%2)*2, sticky="w", pady=2)
v = tk.StringVar()
ttk.Entry(form, textvariable=v, width=18).grid(
row=i//2, column=(i%2)*2+1, padx=4)
vars_[key] = v
def add_client():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES (?,?,?,?)",
(vars_["名前"].get(), vars_["住所"].get(),
vars_["メール"].get(), vars_["電話"].get()))
self.conn.commit()
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
self._load_clients()
ttk.Button(form, text="追加", command=add_client).grid(
row=2, column=0, columnspan=4, pady=6)
if __name__ == "__main__":
root = tk.Tk()
app = App20(root)
root.mainloop()
例外処理とmessagebox
try-except で ValueError と Exception を捕捉し、messagebox.showerror() でユーザーにわかりやすいエラーメッセージを表示します。入力バリデーションは必ず実装しましょう。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sqlite3
import os
from datetime import datetime, date
try:
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.platypus import (SimpleDocTemplate, Table, TableStyle,
Paragraph, Spacer)
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
REPORTLAB_AVAILABLE = True
except ImportError:
REPORTLAB_AVAILABLE = False
class App20:
"""請求書作成アプリ"""
DB_PATH = os.path.join(os.path.dirname(__file__), "invoices.db")
def __init__(self, root):
self.root = root
self.root.title("請求書作成アプリ")
self.root.geometry("900x620")
self.root.configure(bg="#f8f9fc")
self._init_db()
self._build_ui()
self._load_clients()
self._load_items()
self._new_invoice()
if not REPORTLAB_AVAILABLE:
messagebox.showwarning(
"ライブラリ未インストール",
"reportlab が必要です (PDF出力に必要)。\n"
"pip install reportlab でインストールしてください。\n\n"
"CSV形式での出力は可能です。")
def _init_db(self):
self.conn = sqlite3.connect(self.DB_PATH)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, address TEXT, email TEXT, phone TEXT
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, unit_price REAL, unit TEXT DEFAULT '式'
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS invoices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
invoice_no TEXT, client_id INTEGER, issue_date TEXT,
due_date TEXT, notes TEXT, created_at TEXT
)
""")
self.conn.commit()
# サンプルデータ
if not self.conn.execute("SELECT 1 FROM clients").fetchone():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('株式会社ABC','東京都千代田区1-1','abc@example.com','03-1234-5678')")
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES "
"('山田商事','大阪府大阪市2-2','yamada@example.com','06-9876-5432')")
self.conn.execute(
"INSERT INTO items (name,unit_price,unit) VALUES "
"('Webデザイン',50000,'式'),('システム開発',80000,'式'),"
"('コンサルティング',30000,'時間'),('保守費用',20000,'月')")
self.conn.commit()
def _build_ui(self):
title_frame = tk.Frame(self.root, bg="#f57f17", pady=10)
title_frame.pack(fill=tk.X)
tk.Label(title_frame, text="🧾 請求書作成アプリ",
font=("Noto Sans JP", 15, "bold"),
bg="#f57f17", fg="white").pack(side=tk.LEFT, padx=12)
ttk.Button(title_frame, text="📄 PDF出力",
command=self._export_pdf).pack(side=tk.RIGHT, padx=8)
ttk.Button(title_frame, text="📊 CSV出力",
command=self._export_csv).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="💾 保存",
command=self._save_invoice).pack(side=tk.RIGHT, padx=4)
ttk.Button(title_frame, text="🆕 新規",
command=self._new_invoice).pack(side=tk.RIGHT, padx=4)
paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
# 左: 請求書フォーム
form = ttk.LabelFrame(paned, text="請求書", padding=12)
# 請求書番号
row_f = tk.Frame(form, bg=form.cget("background"))
row_f.pack(fill=tk.X, pady=2)
tk.Label(row_f, text="請求書番号:", bg=row_f.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.inv_no_var = tk.StringVar()
ttk.Entry(row_f, textvariable=self.inv_no_var, width=18).pack(side=tk.LEFT, padx=4)
# 発行日
row_f2 = tk.Frame(form, bg=form.cget("background"))
row_f2.pack(fill=tk.X, pady=2)
tk.Label(row_f2, text="発行日:", bg=row_f2.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.issue_var = tk.StringVar(value=str(date.today()))
ttk.Entry(row_f2, textvariable=self.issue_var, width=12).pack(side=tk.LEFT, padx=4)
tk.Label(row_f2, text="支払期限:", bg=row_f2.cget("bg")).pack(side=tk.LEFT, padx=(12, 0))
self.due_var = tk.StringVar()
ttk.Entry(row_f2, textvariable=self.due_var, width=12).pack(side=tk.LEFT, padx=4)
# 顧客
row_f3 = tk.Frame(form, bg=form.cget("background"))
row_f3.pack(fill=tk.X, pady=2)
tk.Label(row_f3, text="顧客:", bg=row_f3.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT)
self.client_var = tk.StringVar()
self.client_cb = ttk.Combobox(row_f3, textvariable=self.client_var,
state="readonly", width=24)
self.client_cb.pack(side=tk.LEFT, padx=4)
ttk.Button(row_f3, text="✏️ 顧客管理",
command=self._manage_clients).pack(side=tk.LEFT, padx=4)
# 備考
row_f4 = tk.Frame(form, bg=form.cget("background"))
row_f4.pack(fill=tk.X, pady=2)
tk.Label(row_f4, text="備考:", bg=row_f4.cget("bg"),
width=14, anchor="w").pack(side=tk.LEFT, anchor="n")
self.notes_text = tk.Text(row_f4, height=2, width=30,
font=("Arial", 10))
self.notes_text.pack(side=tk.LEFT, padx=4)
# 明細テーブル
tk.Label(form, text="明細:", bg=form.cget("background"),
font=("Noto Sans JP", 11, "bold")).pack(anchor="w", pady=(8, 2))
item_f = tk.Frame(form, bg=form.cget("background"))
item_f.pack(fill=tk.X)
self.item_name_var = tk.StringVar()
self.item_cb = ttk.Combobox(item_f, textvariable=self.item_name_var,
width=20)
self.item_cb.pack(side=tk.LEFT, padx=2)
self.item_cb.bind("<<ComboboxSelected>>", self._on_item_select)
tk.Label(item_f, text="数量:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.qty_var = tk.IntVar(value=1)
ttk.Spinbox(item_f, from_=1, to=9999,
textvariable=self.qty_var, width=5).pack(side=tk.LEFT, padx=2)
tk.Label(item_f, text="単価:", bg=form.cget("background")).pack(side=tk.LEFT, padx=(6, 2))
self.price_var = tk.StringVar(value="0")
ttk.Entry(item_f, textvariable=self.price_var, width=10).pack(side=tk.LEFT, padx=2)
ttk.Button(item_f, text="➕ 追加",
command=self._add_line).pack(side=tk.LEFT, padx=4)
# 明細Treeview
cols = ("no", "name", "qty", "unit_price", "amount")
self.line_tree = ttk.Treeview(form, columns=cols,
show="headings", height=8)
for c, h, w in [("no", "#", 30), ("name", "品目", 200),
("qty", "数量", 50), ("unit_price", "単価", 90),
("amount", "金額", 90)]:
self.line_tree.heading(c, text=h)
self.line_tree.column(c, width=w, minwidth=30)
line_sb = ttk.Scrollbar(form, command=self.line_tree.yview)
self.line_tree.configure(yscrollcommand=line_sb.set)
line_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.line_tree.pack(fill=tk.BOTH, expand=True)
ttk.Button(form, text="🗑️ 選択行削除",
command=self._delete_line).pack(anchor="w", pady=2)
# 合計
total_f = tk.Frame(form, bg=form.cget("background"))
total_f.pack(fill=tk.X, pady=4)
self.subtotal_label = tk.Label(total_f, text="小計: ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.subtotal_label.pack(side=tk.LEFT)
self.tax_label = tk.Label(total_f, text=" 消費税(10%): ¥0",
bg=form.cget("background"),
font=("Arial", 11))
self.tax_label.pack(side=tk.LEFT)
self.total_label = tk.Label(total_f, text=" 合計: ¥0",
bg=form.cget("background"),
fg="#e65100",
font=("Arial", 13, "bold"))
self.total_label.pack(side=tk.LEFT)
paned.add(form, weight=3)
# 右: 請求書履歴
hist_frame = ttk.LabelFrame(paned, text="請求書履歴", padding=4)
hist_cols = ("inv_no", "client", "date", "total")
self.hist_tree = ttk.Treeview(hist_frame, columns=hist_cols,
show="headings", selectmode="browse")
for c, h, w in [("inv_no", "番号", 80), ("client", "顧客", 100),
("date", "発行日", 90), ("total", "金額", 80)]:
self.hist_tree.heading(c, text=h)
self.hist_tree.column(c, width=w, minwidth=30)
hist_sb = ttk.Scrollbar(hist_frame, command=self.hist_tree.yview)
self.hist_tree.configure(yscrollcommand=hist_sb.set)
hist_sb.pack(side=tk.RIGHT, fill=tk.Y)
self.hist_tree.pack(fill=tk.BOTH, expand=True)
paned.add(hist_frame, weight=1)
self.status_var = tk.StringVar(value="")
tk.Label(self.root, textvariable=self.status_var,
bg="#dde", font=("Arial", 9), anchor="w", padx=8
).pack(fill=tk.X, side=tk.BOTTOM)
self._line_items = []
def _load_clients(self):
rows = self.conn.execute(
"SELECT id, name FROM clients ORDER BY name").fetchall()
self._clients = {name: cid for cid, name in rows}
self.client_cb.configure(values=list(self._clients.keys()))
def _load_items(self):
rows = self.conn.execute(
"SELECT name, unit_price FROM items ORDER BY name").fetchall()
self._items = {name: price for name, price in rows}
self.item_cb.configure(values=list(self._items.keys()))
def _new_invoice(self):
now = datetime.now()
self.inv_no_var.set(f"INV-{now.strftime('%Y%m%d-%H%M')}")
self.issue_var.set(str(date.today()))
self.due_var.set("")
self.notes_text.delete("1.0", tk.END)
self.line_tree.delete(*self.line_tree.get_children())
self._line_items = []
self._update_totals()
def _on_item_select(self, event):
name = self.item_name_var.get()
price = self._items.get(name, 0)
self.price_var.set(str(price))
def _add_line(self):
name = self.item_name_var.get().strip()
if not name:
return
try:
qty = self.qty_var.get()
price = float(self.price_var.get())
except ValueError:
messagebox.showerror("エラー", "数量・単価を正しく入力してください")
return
amount = qty * price
no = len(self._line_items) + 1
self._line_items.append((name, qty, price, amount))
self.line_tree.insert("", "end",
values=(no, name, qty,
f"¥{price:,.0f}",
f"¥{amount:,.0f}"))
self._update_totals()
def _delete_line(self):
sel = self.line_tree.selection()
if sel:
idx = self.line_tree.index(sel[0])
self._line_items.pop(idx)
self.line_tree.delete(sel[0])
self._renumber()
self._update_totals()
def _renumber(self):
for idx, item in enumerate(self.line_tree.get_children()):
vals = list(self.line_tree.item(item)["values"])
vals[0] = idx + 1
self.line_tree.item(item, values=vals)
def _update_totals(self):
subtotal = sum(a for _, _, _, a in self._line_items)
tax = subtotal * 0.1
total = subtotal + tax
self.subtotal_label.config(text=f"小計: ¥{subtotal:,.0f}")
self.tax_label.config(text=f" 消費税(10%): ¥{tax:,.0f}")
self.total_label.config(text=f" 合計: ¥{total:,.0f}")
self._total = total
def _save_invoice(self):
client_name = self.client_var.get()
client_id = self._clients.get(client_name)
if not client_id:
messagebox.showwarning("警告", "顧客を選択してください")
return
self.conn.execute(
"INSERT OR REPLACE INTO invoices "
"(invoice_no, client_id, issue_date, due_date, notes, created_at) "
"VALUES (?,?,?,?,?,?)",
(self.inv_no_var.get(), client_id,
self.issue_var.get(), self.due_var.get(),
self.notes_text.get("1.0", tk.END).strip(),
datetime.now().isoformat()))
self.conn.commit()
self.status_var.set(f"保存完了: {self.inv_no_var.get()}")
def _export_csv(self):
path = filedialog.asksaveasfilename(
defaultextension=".csv",
filetypes=[("CSV", "*.csv"), ("すべて", "*.*")])
if not path:
return
import csv
with open(path, "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(["請求書番号", "顧客", "発行日", "支払期限"])
w.writerow([self.inv_no_var.get(), self.client_var.get(),
self.issue_var.get(), self.due_var.get()])
w.writerow([])
w.writerow(["品目", "数量", "単価", "金額"])
for name, qty, price, amount in self._line_items:
w.writerow([name, qty, price, amount])
subtotal = sum(a for _, _, _, a in self._line_items)
w.writerow(["小計", "", "", subtotal])
w.writerow(["消費税(10%)", "", "", subtotal * 0.1])
w.writerow(["合計", "", "", subtotal * 1.1])
self.status_var.set(f"CSV出力: {path}")
def _export_pdf(self):
if not REPORTLAB_AVAILABLE:
messagebox.showwarning("警告",
"pip install reportlab が必要です")
return
path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF", "*.pdf")])
if not path:
return
try:
doc = SimpleDocTemplate(path, pagesize=A4)
styles = getSampleStyleSheet()
story = []
story.append(Paragraph(f"請求書 {self.inv_no_var.get()}",
styles["Title"]))
story.append(Spacer(1, 12))
story.append(Paragraph(
f"顧客: {self.client_var.get()} "
f"発行日: {self.issue_var.get()} "
f"支払期限: {self.due_var.get()}", styles["Normal"]))
story.append(Spacer(1, 20))
data = [["品目", "数量", "単価(円)", "金額(円)"]]
for name, qty, price, amount in self._line_items:
data.append([name, qty, f"{price:,.0f}", f"{amount:,.0f}"])
subtotal = sum(a for _, _, _, a in self._line_items)
data.append(["", "", "小計", f"{subtotal:,.0f}"])
data.append(["", "", "消費税(10%)", f"{subtotal*0.1:,.0f}"])
data.append(["", "", "合計", f"{subtotal*1.1:,.0f}"])
t = Table(data, colWidths=[200, 60, 100, 100])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565c0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ALIGN", (1, 1), (-1, -1), "RIGHT"),
("GRID", (0, 0), (-1, -2), 0.5, colors.grey),
("FONTSIZE", (0, 0), (-1, -1), 10),
]))
story.append(t)
if self.notes_text.get("1.0", tk.END).strip():
story.append(Spacer(1, 20))
story.append(Paragraph("備考: " + self.notes_text.get("1.0", tk.END).strip(),
styles["Normal"]))
doc.build(story)
self.status_var.set(f"PDF出力: {path}")
except Exception as e:
messagebox.showerror("エラー", str(e))
def _manage_clients(self):
win = tk.Toplevel(self.root)
win.title("顧客管理")
win.geometry("500x350")
# 顧客一覧
cols = ("id", "name", "address", "email", "phone")
tree = ttk.Treeview(win, columns=cols, show="headings")
for c, h, w in [("id", "ID", 40), ("name", "名前", 120),
("address", "住所", 150), ("email", "メール", 120),
("phone", "電話", 100)]:
tree.heading(c, text=h)
tree.column(c, width=w)
tree.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
# 追加フォーム
form = tk.Frame(win)
form.pack(fill=tk.X, padx=4)
vars_ = {}
for i, key in enumerate(["名前", "住所", "メール", "電話"]):
tk.Label(form, text=f"{key}:").grid(row=i//2, column=(i%2)*2, sticky="w", pady=2)
v = tk.StringVar()
ttk.Entry(form, textvariable=v, width=18).grid(
row=i//2, column=(i%2)*2+1, padx=4)
vars_[key] = v
def add_client():
self.conn.execute(
"INSERT INTO clients (name,address,email,phone) VALUES (?,?,?,?)",
(vars_["名前"].get(), vars_["住所"].get(),
vars_["メール"].get(), vars_["電話"].get()))
self.conn.commit()
for row in self.conn.execute("SELECT * FROM clients").fetchall():
tree.insert("", "end", values=row)
self._load_clients()
ttk.Button(form, text="追加", command=add_client).grid(
row=2, column=0, columnspan=4, pady=6)
if __name__ == "__main__":
root = tk.Tk()
app = App20(root)
root.mainloop()
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app20.py と保存します。
-
2クラスの骨格を作る
App20クラスを定義し、__init__とmainloop()の最小構成を作ります。
-
3タイトルバーを作る
Frameを使ってカラーバー付きのタイトルエリアを作ります。
-
4入力フォームを実装する
LabelFrameとEntryウィジェットで入力エリアを作ります。
-
5処理ロジックを実装する
_calculate()メソッドに計算・処理ロジックを実装します。
-
6結果表示を実装する
TextウィジェットかLabelに結果を表示する_show_result()を実装します。
-
7エラー処理を追加する
try-exceptとmessageboxでエラーハンドリングを追加します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 データのエクスポート機能
計算結果をCSV・TXTファイルに保存するエクスポート機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。
💡 入力履歴機能
以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。
8. よくある問題と解決法
❌ 日本語フォントが表示されない
原因:システムに日本語フォントが見つからない場合があります。
解決法:font引数を省略するかシステムに合ったフォントを指定してください。
❌ ウィンドウのサイズが変更できない
原因:resizable(False, False)が設定されています。
解決法:resizable(True, True)に変更してください。
9. 練習問題
アプリの理解を深めるための練習問題です。難易度順に挑戦してみてください。
-
課題1:機能拡張
請求書作成アプリに新しい機能を1つ追加してみましょう。どんな機能があると便利か考えてから実装してください。
-
課題2:UIの改善
色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしてみましょう。
-
課題3:保存機能の追加
入力値や計算結果をファイルに保存する機能を追加しましょう。jsonやcsvモジュールを使います。