Skip to content

使用Python实现发票统计功能

约 1280 字大约 4 分钟

2025-05-09

一、前言

由于近期项目开发任务紧张,我经常需要加班至晚上九点,随之产生了大量的加班餐费发票需进行报销。为了提高报销流程的效率,方便对每日发票金额和总报销金额进行统计与归档,我决定使用 Python 编写一个自动化处理工具,用于批量读取指定文件夹中的 PDF 发票文件,并自动生成结构化的统计数据和 Excel 报表。

二、pdf文件命名规则

文件名格式应为:{日期}_{金额}元_{姓名}.pdf 例如:05月12日_34.56元_张三.pdf。其中,日期应包含月份和日期(如“05月12日”),金额应为数字形式(可带小数点),并以“元”结尾,姓名可以是任意字符组合。

三、功能需求

  1. 统计文件夹内所有PDF文件的总金额、每人消费总额以及每天的消费总额。
  2. 将结果输出到Excel文件中,包括每份发票的具体信息(日期、金额、姓名)。
  3. 提供图形界面供用户选择要处理的文件夹路径。

四、技术栈

  • Python: 主要编程语言
  • os, re, collections, decimal: 内置库用于文件操作、正则表达式处理、数据结构定义及精确计算
  • openpyxl: 处理Excel文件
  • tkinter: 创建GUI应用程序
  • logging: 日志记录

五、实现代码

invoke

invoke.py

requirements.txt

icon.icon

invoke.py
import os  
import re  
from collections import defaultdict  
from decimal import Decimal  
import openpyxl  
from openpyxl.styles import Font  
import tkinter as tk  
from tkinter import filedialog, messagebox  
import platform  
import subprocess  
import logging  
  
# 配置日志  
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")  
  
  
def date_to_tuple(date_str):  
    """日期排序辅助函数"""  
    m, d = re.findall(r'\d+', date_str)  
    return (int(m), int(d))  
  
  
def parse_filename(filename):  
    """严格解析文件名"""  
    pattern = r'^(\d{1,2}\d{1,2})[_\-](\d+\.?\d*)[_\-]([^\.]+)\.pdf$'  
    match = re.match(pattern, filename)  
    if not match:  
        raise ValueError(f"无效的文件名格式: {filename}")  
    return match.groups()  
  
  
def process_folder(folder_path):  
    """处理文件夹及其子文件夹并返回结构化数据"""  
    records = []  
    date_stats = defaultdict(list)  
    person_stats = defaultdict(Decimal)  
    errors = []  
  
    # 使用os.walk递归遍历文件夹及其子文件夹  
    for root, dirs, files in os.walk(folder_path):  
        logging.info(f"正在处理文件夹: {root}")  
        pdf_files = [f for f in files if f.lower().endswith('.pdf')]  
  
        if not pdf_files:  
            logging.warning(f"文件夹 {root} 中未找到任何 .pdf 文件")  
            continue  
  
        for f in pdf_files:  
            file_path = os.path.join(root, f)  
            try:  
                date, amount, name = parse_filename(f)  
                amount = Decimal(amount)  
  
                records.append((date, amount, name))  
                date_stats[date].append(amount)  
                person_stats[name] += amount  
  
            except ValueError as ve:  
                errors.append(f"文件 {file_path} 格式错误: {str(ve)}")  
            except Exception as e:  
                errors.append(f"文件 {file_path} 处理失败: {str(e)}")  
  
    return {  
        "records": sorted(records, key=lambda x: date_to_tuple(x[0])),  
        "date_stats": date_stats,  
        "person_stats": person_stats,  
        "errors": errors  
    }  
  
  
def create_excel_report(data, output_path):  
    """生成标准格式Excel报表"""  
    wb = openpyxl.Workbook()  
  
    # 发票明细页  
    ws1 = wb.active  
    ws1.title = "发票明细"  
    headers = ["加班日期", "发票金额", "报销人"]  
    ws1.append(headers)  
    for cell in ws1[1]:  
        cell.font = Font(bold=True)  
  
    for row in data["records"]:  
        ws1.append([row[0], f"{row[1]:.2f}", row[2]])  
  
    # 统计汇总页  
    ws2 = wb.create_sheet("统计汇总")  
  
    # 日期统计  
    ws2.append(["日期统计"])  
    ws2.append(["日期", "发票数量", "总金额"])  
    for date in sorted(data["date_stats"], key=date_to_tuple):  
        amounts = data["date_stats"][date]  
        ws2.append([date, len(amounts), f"{sum(amounts):.2f}"])  
  
    # 人员统计  
    ws2.append([])  
    ws2.append(["人员统计"])  
    ws2.append(["姓名", "总金额"])  
    for name, total in sorted(data["person_stats"].items(), key=lambda x: -x[1]):  
        ws2.append([name, f"{total:.2f}"])  
  
    # 设置列宽  
    for sheet in [ws1, ws2]:  
        sheet.column_dimensions['A'].width = 15  
        sheet.column_dimensions['B'].width = 15  
        sheet.column_dimensions['C'].width = 15  
  
    wb.save(output_path)  
  
  
def open_folder(folder_path):  
    """跨平台打开文件夹"""  
    try:  
        if platform.system() == "Windows":  
            os.startfile(folder_path)  
        elif platform.system() == "Darwin":  
            subprocess.Popen(['open', folder_path])  
        else:  
            subprocess.Popen(['xdg-open', folder_path])  
    except Exception as e:  
        logging.error(f"无法打开文件夹:{str(e)}")  
        messagebox.showwarning("警告", f"无法打开文件夹:{str(e)}")  
  
  
def main_gui():  
    """图形界面主函数"""  
    root = tk.Tk()  
    root.title("发票助手 v1.0.1")  
    root.geometry("300x180")  
  
    def process_directory():  
        folder_path = filedialog.askdirectory()  
        if not folder_path:  
            return  
  
        try:  
            # 处理数据  
            report_data = process_folder(folder_path)  
            if not report_data["records"]:  
                messagebox.showwarning("警告", "未找到有效的发票文件!")  
                return  
  
            # 生成报表  
            output_file = os.path.join(folder_path, "发票汇总.xlsx")  
            if os.path.exists(output_file):  
                overwrite = messagebox.askyesno("确认", f"文件 {output_file} 已存在,是否覆盖?")  
                if not overwrite:  
                    return  
  
            create_excel_report(report_data, output_file)  
  
            # 构建结果信息  
            success_msg = (  
                f"成功生成报表: {output_file}\n\n"  
                f"处理统计:\n"  
                f"• 处理发票总数: {len(report_data['records'])}\n"  
                f"• 涉及日期数量: {len(report_data['date_stats'])}\n"  
                f"• 涉及人员数量: {len(report_data['person_stats'])}"  
            )  
  
            # 显示错误信息(如果有)  
            if report_data["errors"]:  
                error_msg = "以下文件处理时发现问题:\n" + "\n".join(report_data["errors"])  
                messagebox.showwarning("部分错误", error_msg)  
  
            messagebox.showinfo("处理完成", success_msg)  
            open_folder(folder_path)  
  
        except Exception as e:  
            logging.error(f"处理过程中发生错误: {str(e)}")  
            messagebox.showerror("严重错误", f"处理过程中发生错误:\n{str(e)}")  
  
    # 界面布局  
    frame = tk.Frame(root, padx=20, pady=20)  
    frame.pack(expand=True)  
  
    label = tk.Label(frame, text="请选择包含发票PDF的文件夹")  
    label.pack(pady=10)  
  
    btn = tk.Button(frame,  
                    text="选择文件夹",  
                    command=process_directory,  
                    padx=10,  
                    pady=5,  
                    bg="#4CAF50",  
                    fg="white")  
    btn.pack()  
  
    # 添加版权信息  
    copyright_label = tk.Label(root, text="版权所有 © 2025 系统运营部", font=("Arial", 8))  
    copyright_label.pack(side=tk.BOTTOM, pady=4)  
  
    root.mainloop()  
  
  
if __name__ == "__main__":  
    main_gui()
requirements.txt
openpyxl
icon.icon
使用自己喜欢的ico图标替换即可

使用以下命令将代码打包成可执行文件:

pyinstaller --onefile -w -i icon.ico invoke.py

Power by VuePress & vuepress-theme-plume