python+tkinter智能實(shí)現(xiàn)票據(jù)生成
效果如下


使用python與tkinter實(shí)現(xiàn)的一個(gè)簡(jiǎn)單的票據(jù)生成,
鍵入收款方,通過(guò)“添加明細(xì)”鍵入項(xiàng)目詳細(xì)信息,生成的收據(jù)將有pdf格式保存。
完整代碼如下
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from fpdf import FPDF
from fpdf.enums import XPos, YPos # 添加缺失的導(dǎo)入
from datetime import datetime
import platform
from pathlib import Path
import logging
from tkcalendar import DateEntry
# 日志配置
logging.basicConfig(filename='receipt.log', level=logging.INFO)
class AmountUtils:
"""金額處理工具類"""
@staticmethod
def to_chinese(num: float) -> str:
"""精確實(shí)現(xiàn)中文大寫金額轉(zhuǎn)換"""
units = ['', '拾', '佰', '仟']
big_units = ['', '萬(wàn)', '億']
digits = ('零', '壹', '貳', '叁', '肆', '伍', '陸', '柒', '捌', '玖')
num = round(num, 2)
integer_part = int(num)
decimal_part = int(round(num - integer_part, 2) * 100)
result = []
# 處理整數(shù)部分
if integer_part > 0:
unit_index = 0
while integer_part > 0:
section = integer_part % 10000
integer_part = integer_part // 10000
section_str = ''
for i in range(4):
digit = section % 10
section = section // 10
if digit != 0:
section_str = digits[digit] + units[i] + section_str
elif section_str and section_str[0] != '零':
section_str = '零' + section_str
if section_str:
section_str += big_units[unit_index]
result.append(section_str)
unit_index += 1
result = list(reversed(result))
result_str = ''.join(result).replace('零零', '零').rstrip('零') + '元'
result_str = result_str.replace('零零', '零')
result = [result_str]
else:
result.append('零元')
# 處理小數(shù)部分
if decimal_part > 0:
jiao = decimal_part // 10
fen = decimal_part % 10
if jiao > 0:
result.append(digits[jiao] + '角')
if fen > 0:
result.append(digits[fen] + '分')
else:
result.append('整')
return ''.join(result).replace('零元', '').replace('零零', '零')
class PDFGenerator:
"""嚴(yán)格按照?qǐng)D片樣式的PDF生成器"""
def __init__(self, font_config):
self.font_config = font_config
def generate(self, data: dict, save_path: str):
"""生成PDF文件"""
pdf = FPDF()
pdf.add_page()
pdf.set_auto_page_break(auto=False)
# 加載字體
if self.font_config['path'] and Path(self.font_config['path']).exists():
pdf.add_font(self.font_config['name'], fname=self.font_config['path'])
pdf.set_font(self.font_config['name'], size=12)
else:
pdf.set_font("Arial", size=12)
# 標(biāo)題
pdf.set_font_size(18)
pdf.cell(0, 15, "收 款 收 據(jù)", align='C', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
# 交款方和日期
pdf.set_font_size(12)
pdf.cell(50, 10, f"交款方:{data['payer']}", new_x=XPos.LMARGIN)
pdf.cell(0, 10, f"日期:{data['date']}", align='R', new_x=XPos.LMARGIN, new_y=YPos.NEXT)
# 繪制表格
self._draw_table(pdf, data)
# 合計(jì)人民幣
self._draw_total(pdf, data['total'])
pdf.output(save_path)
def _draw_table(self, pdf, data):
"""繪制與圖片完全一致的表格"""
# 列寬(根據(jù)圖片比例精確計(jì)算)
col_width = [40, 20, 20, 25, 30, 35]
headers = ['項(xiàng)目', '單位', '數(shù)量', '單價(jià)', '金額', '備注']
# 表頭
pdf.set_xy(25, 45)
pdf.set_fill_color(240, 240, 240)
for i, header in enumerate(headers):
pdf.cell(col_width[i], 10, header, border=1, fill=True, align='C')
pdf.ln()
# 表格內(nèi)容
pdf.set_font_size(12)
y_position = 55
for item in data['items']:
if y_position > 250: # 處理分頁(yè)
pdf.add_page()
y_position = 45
self._draw_table_header(pdf)
y_position = 55
pdf.set_xy(25, y_position)
# 項(xiàng)目列左對(duì)齊,其他列居中/右對(duì)齊
alignments = ['L', 'C', 'R', 'R', 'R', 'L']
values = [
item['name'],
item['unit'],
f"{item['quantity']:.2f}",
f"¥{item['price']:.2f}",
f"¥{item['quantity'] * item['price']:.2f}",
item['remark']
]
for i in range(6):
pdf.cell(col_width[i], 10, values[i], border=1, align=alignments[i])
y_position += 10
def _draw_table_header(self, pdf):
"""繪制表頭(分頁(yè)時(shí)使用)"""
col_width = [40, 20, 20, 25, 30, 35]
headers = ['項(xiàng)目', '單位', '數(shù)量', '單價(jià)', '金額', '備注']
pdf.set_xy(25, 45)
pdf.set_fill_color(240, 240, 240)
for i, header in enumerate(headers):
pdf.cell(col_width[i], 10, header, border=1, fill=True, align='C')
pdf.ln()
def _draw_total(self, pdf, total):
"""繪制合計(jì)行(精確復(fù)制圖片樣式)"""
pdf.set_xy(25, 105)
# 第一行
pdf.cell(100, 10, "合計(jì)人民幣", border=1)
pdf.cell(45, 10, f"¥{total:.2f}", border=1, align='R')
pdf.cell(35, 10, "", border=1) # 備注列
# 第二行
pdf.set_xy(25, 115)
pdf.cell(100, 10, "", border=1)
pdf.cell(80, 10, f"(大寫){AmountUtils.to_chinese(total)}", border=1, align='L')
class ReceiptSystem:
"""主界面系統(tǒng)"""
def __init__(self):
self.root = tk.Tk()
self.root.title("智能收據(jù)系統(tǒng)")
self.root.geometry("800x600")
self.font_config = self._get_font_config()
self.items = []
self.total = 0.0
self._create_ui()
self.reset() # 初始化表單數(shù)據(jù)
def _get_font_config(self):
"""獲取系統(tǒng)字體配置"""
system = platform.system()
font_config = {'name': 'Arial', 'path': None}
if system == 'Windows':
font_path = 'C:/Windows/Fonts/simhei.ttf'
if Path(font_path).exists():
font_config.update(name='SimHei', path=font_path)
elif system == 'Linux':
font_path = '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'
if Path(font_path).exists():
font_config.update(name='WenQuanYi Zen Hei', path=font_path)
elif system == 'Darwin':
font_path = '/System/Library/Fonts/STHeiti Light.ttc'
if Path(font_path).exists():
font_config.update(name='STHeiti', path=font_path)
return font_config
def _create_ui(self):
"""創(chuàng)建與圖片一致的UI布局"""
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
# 標(biāo)題
ttk.Label(main_frame, text="收 款 收 據(jù)", font=('', 16, 'bold')).pack(pady=10)
# 交款方和日期
info_frame = ttk.Frame(main_frame)
info_frame.pack(fill=tk.X, pady=10)
ttk.Label(info_frame, text="交款方:").grid(row=0, column=0, sticky='e')
self.payer_entry = ttk.Entry(info_frame, width=25)
self.payer_entry.grid(row=0, column=1, padx=5, sticky='ew')
ttk.Label(info_frame, text="日期:").grid(row=0, column=2, padx=10)
self.date_entry = DateEntry(info_frame,
locale='zh_CN',
date_pattern='yyyy-MM-dd',
width=12)
self.date_entry.grid(row=0, column=3, padx=5, sticky='ew')
info_frame.columnconfigure(1, weight=1)
info_frame.columnconfigure(3, weight=1)
# 表格(精確列寬)
self.tree = ttk.Treeview(main_frame,
columns=('項(xiàng)目', '單位', '數(shù)量', '單價(jià)', '金額', '備注'),
show='headings',
height=5)
# 嚴(yán)格按照?qǐng)D片比例設(shè)置列寬
columns = [
('項(xiàng)目', 160), ('單位', 80),
('數(shù)量', 80), ('單價(jià)', 100),
('金額', 100), ('備注', 100)
]
for col, width in columns:
self.tree.heading(col, text=col)
self.tree.column(col, width=width, anchor='center')
self.tree.pack(fill=tk.BOTH, expand=True, pady=10)
# 操作按鈕
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="添加明細(xì)", command=self.add_item).grid(row=0, column=0, padx=5)
ttk.Button(btn_frame, text="生成收據(jù)", command=self.generate_pdf).grid(row=0, column=1, padx=5)
ttk.Button(btn_frame, text="重置", command=self.reset).grid(row=0, column=2, padx=5)
def add_item(self):
"""添加明細(xì)(包含備注字段)"""
if len(self.items) >= 5:
messagebox.showwarning("提示", "最多只能添加5條明細(xì)")
return
input_win = tk.Toplevel(self.root)
input_win.title("輸入明細(xì)")
fields = [
('項(xiàng)目', 0), ('單位', 1),
('數(shù)量', 2), ('單價(jià)', 3),
('備注', 4)
]
entries = {}
for idx, (label, row) in enumerate(fields):
ttk.Label(input_win, text=f"{label}:").grid(row=row, column=0, padx=5, pady=5)
entry = ttk.Entry(input_win)
entry.grid(row=row, column=1, padx=5, pady=5)
entries[label] = entry
ttk.Button(input_win, text="確認(rèn)",
command=lambda: self._validate_entry(entries, input_win)).grid(row=5, columnspan=2)
def _validate_entry(self, entries, window):
"""驗(yàn)證輸入"""
try:
data = {
'name': entries['項(xiàng)目'].get().strip(),
'unit': entries['單位'].get().strip(),
'quantity': float(entries['數(shù)量'].get()),
'price': float(entries['單價(jià)'].get()),
'remark': entries['備注'].get().strip()
}
if not data['name'] or not data['unit']:
raise ValueError("項(xiàng)目和單位必須填寫")
if data['quantity'] <= 0 or data['price'] <= 0:
raise ValueError("數(shù)量和單價(jià)必須大于零")
# 更新表格
amount = data['quantity'] * data['price']
self.tree.insert('', 'end', values=(
data['name'],
data['unit'],
f"{data['quantity']:.2f}",
f"¥{data['price']:.2f}",
f"¥{amount:.2f}",
data['remark']
))
self.items.append(data)
self.total += amount
window.destroy()
except Exception as e:
messagebox.showerror("輸入錯(cuò)誤", str(e))
def generate_pdf(self):
"""生成PDF"""
try:
if not self.items:
raise ValueError("請(qǐng)先添加明細(xì)項(xiàng)目")
if not self.payer_entry.get().strip():
raise ValueError("交款方不能為空")
data = {
'payer': self.payer_entry.get(),
'date': self.date_entry.get(),
'items': self.items,
'total': self.total
}
save_path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF文件", "*.pdf")]
)
if save_path:
# 確保目錄存在
path = Path(save_path)
path.parent.mkdir(parents=True, exist_ok=True)
PDFGenerator(self.font_config).generate(data, save_path)
messagebox.showinfo("成功", f"文件已保存至:{save_path}")
self.root.update()
except Exception as e:
messagebox.showerror("生成錯(cuò)誤", str(e))
logging.error(f"生成錯(cuò)誤: {str(e)}")
def reset(self):
"""重置表單"""
self.items = []
self.total = 0.0
self.tree.delete(*self.tree.get_children())
self.payer_entry.delete(0, tk.END)
self.date_entry.set_date(datetime.now())
if __name__ == "__main__":
app = ReceiptSystem()
app.root.mainloop()此為簡(jiǎn)單示例,如果針對(duì)一些固定商品,在增添項(xiàng)目過(guò)程中引用數(shù)據(jù)庫(kù)商品通過(guò)檢索方式會(huì)更加的高效。
到此這篇關(guān)于python+tkinter智能實(shí)現(xiàn)票據(jù)生成的文章就介紹到這了,更多相關(guān)python票據(jù)生成內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
圖解Python中淺拷貝copy()和深拷貝deepcopy()的區(qū)別
這篇文章主要介紹了Python中淺拷貝copy()和深拷貝deepcopy()的區(qū)別,淺拷貝和深拷貝想必大家在學(xué)習(xí)中遇到很多次,這也是面試中常常被問(wèn)到的問(wèn)題,本文就帶你詳細(xì)了解一下2023-05-05
Python+Turtle實(shí)現(xiàn)繪制可愛的小倉(cāng)鼠
肉嘟嘟的小動(dòng)物很是可愛,這篇文章主要為大家介紹一下如何運(yùn)用Python中的turtle庫(kù)控制函數(shù)繪制小倉(cāng)鼠,文中的實(shí)現(xiàn)方法講解詳細(xì),感興趣的可以嘗試一下2022-10-10
Jupyter Notebook運(yùn)行Python代碼實(shí)現(xiàn)傳參方式
這篇文章主要介紹了Jupyter Notebook運(yùn)行Python代碼實(shí)現(xiàn)傳參方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
python實(shí)現(xiàn)MD5進(jìn)行文件去重的示例代碼
工作中偶爾會(huì)收到一大堆文件,名稱各不相同,分析文件的時(shí)候發(fā)現(xiàn)有不少重復(fù)的文件,導(dǎo)致工作效率低下,那么,這里就寫了一個(gè)python腳本實(shí)現(xiàn)文件去重功能,感興趣的就一起來(lái)了解一下2021-07-07
使用pyqt5 實(shí)現(xiàn)ComboBox的鼠標(biāo)點(diǎn)擊觸發(fā)事件
這篇文章主要介紹了使用pyqt5 實(shí)現(xiàn)ComboBox的鼠標(biāo)點(diǎn)擊觸發(fā)事件,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03
Python count()函數(shù)實(shí)例詳解
count() 是Python的內(nèi)置函數(shù),可以「統(tǒng)計(jì)」字符串里指定「字符」或指定字符串出現(xiàn)的「次數(shù)」,這篇文章主要介紹了Python count()函數(shù)詳解,需要的朋友可以參考下2023-07-07
python計(jì)算階乘和的方法(1!+2!+3!+...+n!)
今天小編就為大家分享一篇python計(jì)算階乘和的方法(1!+2!+3!+...+n!),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-02-02
Pytorch實(shí)現(xiàn)簡(jiǎn)單自定義網(wǎng)絡(luò)層的方法
這篇文章主要給大家介紹了關(guān)于Pytorch實(shí)現(xiàn)簡(jiǎn)單自定義網(wǎng)絡(luò)層的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-05-05

