logo

Python实现半自动账单分类

2024年09月03日 编辑

从支付宝和微信导出了账单,为了统一管理,所以必须对分类进行统一,但是分类又杂乱无章,因此自定义一级分类一级子类,采用dropdown的方式直接点选,个人觉得比excel好点一些,因为我完全没研究过那玩意儿。
sorting.webp

Features:
1.可以存档,没做完的下次再做;
2.无错误数据直接跳过;
3.可随时终止提交导出数据;
4.右侧原excel绑定右侧操作数据高亮展示;

import pandas as pd
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import json
import os

categories = {
    '收入': ['退款', '红包', '网站', '转账'],
    '支出': {
        '衣': [],
        '食': ['吃饭', '饮料酒水', '烟'],
        '住': ['酒店', '房租','日用百货'],
        '行': ['飞机', '火车', '公交', '电动车费用'],
        '娱乐': ['网费','电影','会员'],
        '礼': ['人情', '红包', '请吃饭','孝敬家长'],
        '其他': ['话费', '理发','其他']
    }
}

class App:
    def __init__(self, root):
        self.root = root
        self.root.title("Excel 类别修改工具")
        self.df = None
        self.current_index = 0
        self.progress_file = os.path.abspath("progress.json")
        self.create_widgets()
        self.load_progress()  # Load progress on initialization

    def create_widgets(self):
        main_frame = tk.Frame(self.root)
        main_frame.pack(expand=True, fill=tk.BOTH)
        
        # 创建表格视图
        self.table_view = self.create_table_view(main_frame)

        # 创建信息框架
        self.info_frame = tk.Frame(main_frame)
        self.info_frame.pack(side=tk.RIGHT, padx=10, pady=10, fill=tk.BOTH, expand=True)
        self.create_info_frame(self.info_frame)

    def create_table_view(self, parent):
        table_frame = tk.Frame(parent)
        table_frame.pack(side=tk.LEFT, padx=10, pady=10, fill=tk.BOTH, expand=True)
        
        tree = ttk.Treeview(table_frame, columns=("Index", "日期", "金额", "收支类型", "类别", "子类", "账户", "备注"), show="headings")
        for col in tree["columns"]:
            tree.heading(col, text=col)
            tree.column(col, width=100, anchor=tk.W)
        
        scrollbar = tk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        tree.configure(yscrollcommand=scrollbar.set)
        tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        return tree

    def create_info_frame(self, parent):
        self.category_combobox = self.create_combobox(parent, "选择类别:", 0, self.update_subcategories)
        self.subcategory_combobox = self.create_combobox(parent, "选择子类:", 1, self.update_category_based_on_subcategory)
        self.info_labels = self.create_info_labels(parent)
        self.create_buttons(parent)

    def create_combobox(self, parent, text, row, select_func):
        tk.Label(parent, text=text, font=("微软雅黑", 10)).grid(row=row, column=0, padx=5, pady=5, sticky=tk.W)
        combobox = ttk.Combobox(parent, font=("微软雅黑", 10))
        combobox.grid(row=row, column=1, padx=5, pady=5, sticky=tk.W)
        combobox.bind("<<ComboboxSelected>>", select_func)
        return combobox

    def create_info_labels(self, parent):
        labels = {}
        for i, text in enumerate(["日期", "收支类型", "金额", "类别", "子类", "账户", "备注"], start=2):
            tk.Label(parent, text=text + ":", font=("微软雅黑", 10, "bold")).grid(row=i, column=0, padx=5, pady=2, sticky=tk.W)
            labels[text] = tk.Label(parent, text="", width=30, relief="solid", anchor="w", font=("微软雅黑", 10))
            labels[text].grid(row=i, column=1, padx=5, pady=2, sticky=tk.W)
        return labels

    def create_buttons(self, parent):
        buttons_frame = tk.Frame(parent)
        buttons_frame.grid(row=9, column=0, columnspan=2, pady=10, sticky=tk.W+tk.E)
        tk.Button(buttons_frame, text="上一条", command=self.prev_row, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=5)
        tk.Button(buttons_frame, text="下一条", command=self.next_row, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=5)
        tk.Button(buttons_frame, text="保存进度", command=self.save_progress, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=5)
        tk.Button(buttons_frame, text="终止录入并导出", command=self.stop_and_export, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=5)

        tk.Button(parent, text="上传 Excel 文件", command=self.upload_file, font=("微软雅黑", 10)).grid(row=10, column=0, columnspan=2, pady=10)
        self.status_label = tk.Label(parent, text="请上传文件以开始", font=("微软雅黑", 10))
        self.status_label.grid(row=11, column=0, columnspan=2, pady=10)

    def upload_file(self):
        file_path = filedialog.askopenfilename(filetypes=[("Excel files", "*.xlsx")])
        if file_path:
            self.df = pd.read_excel(file_path)
            self.status_label.config(text="文件已加载,请选择类别和子类")
            self.update_table_view()
            self.update_row_info()

    def update_table_view(self):
        if self.df is not None:
            self.table_view.delete(*self.table_view.get_children())
            for index, row in self.df.iterrows():
                tag = 'highlight' if index == self.current_index else ''
                self.table_view.insert("", tk.END, iid=index, tags=(tag,), values=(
                    index + 1, row.get('日期', ''), row.get('金额', ''), row.get('收支类型', ''),
                    row.get('类别', ''), row.get('子类', ''), row.get('账户', ''), row.get('备注', '')
                ))
            self.table_view.tag_configure('highlight', background='lightgreen')
            self.table_view.see(self.current_index)  # 自动滚动到当前行

    def update_subcategories(self, event=None):
        if self.df is not None and self.current_index < len(self.df):
            category_type = self.df.loc[self.current_index, '收支类型']
            category = self.category_combobox.get()
            if category_type == '收入':
                self.subcategory_combobox.config(values=[], state="disabled")
            elif category_type == '支出':
                subcategories = categories['支出'].get(category, [])
                self.subcategory_combobox.config(values=subcategories, state="normal")

    def update_category_based_on_subcategory(self, event=None):
        selected_subcategory = self.subcategory_combobox.get()
        if selected_subcategory:
            for main_category, subcategories in categories['支出'].items():
                if selected_subcategory in subcategories:
                    self.category_combobox.set(main_category)
                    break

    def update_row_info(self):
        if self.df is not None and self.current_index < len(self.df):
            row = self.df.iloc[self.current_index]
            self.category_combobox.set(row.get('类别', ''))
            self.subcategory_combobox.set(row.get('子类', ''))

            if row.get('收支类型', '') == '收入':
                self.category_combobox.config(values=categories['收入'])
                self.subcategory_combobox.config(values=[], state="disabled")
            else:
                self.category_combobox.config(values=list(categories['支出'].keys()))
                self.update_subcategories()

            for key, value in row.items():
                if key in self.info_labels:
                    self.info_labels[key].config(text=value)

    def next_row(self):
        if self.df is not None and self.current_index < len(self.df) - 1:
            self.df.at[self.current_index, '类别'] = self.category_combobox.get()
            self.df.at[self.current_index, '子类'] = self.subcategory_combobox.get()
            self.current_index += 1
            self.update_table_view()
            self.update_row_info()
            self.save_progress()  # Save progress without notification

    def prev_row(self):
        if self.df is not None and self.current_index > 0:
            self.df.at[self.current_index, '类别'] = self.category_combobox.get()
            self.df.at[self.current_index, '子类'] = self.subcategory_combobox.get()
            self.current_index -= 1
            self.update_table_view()
            self.update_row_info()
            self.save_progress()  # Save progress without notification

    def save_progress(self):
        if self.df is not None:
            progress = {"current_index": self.current_index}
            try:
                with open(self.progress_file, "w") as file:
                    json.dump(progress, file)
            except IOError as e:
                print(f"无法保存进度: {e}")  # Use print to log errors instead of showing a message

    def load_progress(self):
        if os.path.exists(self.progress_file):
            try:
                with open(self.progress_file, "r") as file:
                    progress = json.load(file)
                    self.current_index = progress.get("current_index", 0)
            except (IOError, json.JSONDecodeError) as e:
                print(f"无法加载进度: {e}")  # Use print to log errors instead of showing a message
                self.current_index = 0

    def submit(self):
        if self.df is not None and self.current_index < len(self.df):
            category = self.category_combobox.get()
            subcategory = self.subcategory_combobox.get()
            self.df.at[self.current_index, '类别'] = category
            self.df.at[self.current_index, '子类'] = subcategory
            self.update_table_view()
            self.next_row()

    def stop_and_export(self):
        if self.df is not None:
            export_file_path = filedialog.asksaveasfilename(defaultextension=".xlsx", filetypes=[("Excel files", "*.xlsx")])
            if export_file_path:
                self.df.to_excel(export_file_path, index=False)
                messagebox.showinfo("导出成功", f"数据已成功导出到文件:{export_file_path}")

# 创建主窗口
root = tk.Tk()
app = App(root)
root.protocol("WM_DELETE_WINDOW", app.save_progress)  # Ensure progress is saved on window close
root.mainloop()