main.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import pandas as pd
  2. import tkinter as tk
  3. from tkinter import ttk, filedialog, messagebox
  4. from datetime import datetime
  5. import os
  6. from openpyxl.styles import Font
  7. # -------------------------- 全局配置 --------------------------
  8. # 定义参数表的sheet名称(根据实际情况调整,若不同需修改)
  9. ACCOUNT_PARAM_SHEET = "账户信息参数" # 参数表中账户信息的sheet名
  10. CATEGORY_PARAM_SHEET = "收支分类参数" # 参数表中收支分类的sheet名
  11. # 固定匹配的对手户名(结算收支)
  12. SETTLEMENT_COMPANIES = ["成都合煦商贸有限公司", "四川泗讯科技有限公司"]
  13. # -------------------------- 核心数据处理函数 --------------------------
  14. def load_parameter_table(param_file_path):
  15. """加载参数表,返回账户信息和收支分类"""
  16. try:
  17. # 加载账户信息参数(列名在第1行,所以使用header=1)
  18. account_params = pd.read_excel(param_file_path, sheet_name=ACCOUNT_PARAM_SHEET, header=1)
  19. # 加载收支分类参数(列名在第1行,所以使用header=1)
  20. category_params = pd.read_excel(param_file_path, sheet_name=CATEGORY_PARAM_SHEET, header=1)
  21. return account_params, category_params
  22. except Exception as e:
  23. messagebox.showerror("错误", f"加载参数表失败:{str(e)}")
  24. return None, None
  25. def parse_ccb_flow(flow_file_path):
  26. """解析建行流水文件,返回标准化的流水数据"""
  27. try:
  28. # 读取建行流水(适配xls格式,列名在第9行)
  29. flow_df = pd.read_excel(flow_file_path, dtype=str, header=9) # 先以字符串读取避免格式丢失
  30. # 去除列名中的空格
  31. flow_df.columns = flow_df.columns.str.strip()
  32. # 标准化字段名(根据建行流水实际列名调整,以下是常见建行流水列名,需核对)
  33. # 你可根据实际流水列名修改key值
  34. field_mapping = {
  35. "交易时间": "交易时间",
  36. "对方户名": "对方户名",
  37. "摘要": "摘要",
  38. "贷方发生额/元(收入)": "收入",
  39. "借方发生额/元(支取)": "支出"
  40. }
  41. # 检查必要列是否存在
  42. missing_fields = [k for k in field_mapping.keys() if k not in flow_df.columns]
  43. if missing_fields:
  44. messagebox.showerror("错误", f"建行流水缺少必要列:{missing_fields}")
  45. return None
  46. # 保留所有需要的列,并重命名部分列
  47. required_columns = ["交易时间", "对方户名", "摘要", "贷方发生额/元(收入)", "借方发生额/元(支取)", "对方开户机构", "备注", "余额"]
  48. standard_flow = flow_df[required_columns].rename(columns=field_mapping)
  49. # 处理空值和格式
  50. standard_flow["收入"] = pd.to_numeric(standard_flow["收入"], errors='coerce').fillna(0)
  51. standard_flow["支出"] = pd.to_numeric(standard_flow["支出"], errors='coerce').fillna(0)
  52. return standard_flow
  53. except Exception as e:
  54. messagebox.showerror("错误", f"解析建行流水失败:{str(e)}")
  55. return None
  56. def parse_boc_flow(flow_file_path):
  57. """解析中国银行流水文件,返回标准化的流水数据"""
  58. try:
  59. # 读取中国银行流水(列名在第8行)
  60. flow_df = pd.read_excel(flow_file_path, dtype=str, header=8)
  61. # 去除列名中的空格
  62. flow_df.columns = flow_df.columns.str.strip()
  63. # 检查必要列是否存在
  64. required_columns = [
  65. "交易类型[ Transaction Type ]",
  66. "交易日期[ Transaction Date ]",
  67. "交易时间[ Transaction time ]",
  68. "交易金额[ Trade Amount ]",
  69. "交易后余额[ After-transaction balance ]",
  70. "摘要[ Reference ]",
  71. "收款人名称[ Payee's Name ]",
  72. "收款人开户行名[ Beneficiary account bank ]",
  73. "付款人名称[ Payer's Name ]",
  74. "付款人开户行名[ Payer account bank ]"
  75. ]
  76. missing_fields = [k for k in required_columns if k not in flow_df.columns]
  77. if missing_fields:
  78. messagebox.showerror("错误", f"中国银行流水缺少必要列:{missing_fields}")
  79. return None
  80. # 构建标准化流水数据
  81. standard_flow = []
  82. for _, row in flow_df.iterrows():
  83. transaction_type = str(row["交易类型[ Transaction Type ]"]).strip()
  84. transaction_date = str(row["交易日期[ Transaction Date ]"]).strip()
  85. transaction_time = str(row["交易时间[ Transaction time ]"]).strip()
  86. amount = pd.to_numeric(row["交易金额[ Trade Amount ]"], errors='coerce')
  87. balance = str(row["交易后余额[ After-transaction balance ]"]).strip()
  88. reference = str(row["摘要[ Reference ]"]).strip()
  89. payee_name = str(row["收款人名称[ Payee's Name ]"]).strip()
  90. payee_bank = str(row["收款人开户行名[ Beneficiary account bank ]"]).strip()
  91. payer_name = str(row["付款人名称[ Payer's Name ]"]).strip()
  92. payer_bank = str(row["付款人开户行名[ Payer account bank ]"]).strip()
  93. # 跳过空行
  94. if pd.isna(amount) or amount == 0:
  95. continue
  96. # 根据交易类型确定收支
  97. if transaction_type == "往账":
  98. income = 0
  99. expense = abs(amount)
  100. opponent_name = payee_name
  101. opponent_bank = payee_bank
  102. elif transaction_type == "来账":
  103. income = abs(amount)
  104. expense = 0
  105. opponent_name = payer_name
  106. opponent_bank = payer_bank
  107. else:
  108. continue
  109. # 合并日期和时间
  110. transaction_datetime = f"{transaction_date} {transaction_time}"
  111. standard_flow.append({
  112. "交易时间": transaction_datetime,
  113. "对方户名": opponent_name,
  114. "摘要": reference,
  115. "收入": income,
  116. "支出": expense,
  117. "对方开户机构": opponent_bank,
  118. "备注": opponent_bank,
  119. "余额": balance
  120. })
  121. return pd.DataFrame(standard_flow)
  122. except Exception as e:
  123. messagebox.showerror("错误", f"解析中国银行流水失败:{str(e)}")
  124. return None
  125. def generate_journal_data(company_name, bank_name, param_file, flow_file):
  126. """核心:生成日记账数据"""
  127. # 1. 加载参数表
  128. account_params, category_params = load_parameter_table(param_file)
  129. if account_params is None:
  130. return None
  131. # 2. 根据银行名称选择不同的解析函数
  132. if "中国银行" in bank_name:
  133. flow_df = parse_boc_flow(flow_file)
  134. elif "建设银行" in bank_name:
  135. flow_df = parse_ccb_flow(flow_file)
  136. else:
  137. messagebox.showerror("错误", f"不支持的银行:{bank_name}")
  138. return None
  139. if flow_df is None:
  140. return None
  141. # 3. 匹配账户编号和简称(按公司+开户行)
  142. # 先精确匹配公司,然后模糊匹配开户行
  143. account_filter = account_params["公司"] == company_name
  144. account_info = account_params[account_filter]
  145. # 如果开户行不是具体的支行名称,则进行模糊匹配
  146. if not account_info.empty and bank_name not in account_info["开户行"].values:
  147. # 模糊匹配:检查开户行是否包含银行名称
  148. account_filter = account_filter & account_params["开户行"].str.contains(bank_name, na=False)
  149. account_info = account_params[account_filter]
  150. if account_info.empty:
  151. messagebox.showerror("错误", f"参数表中未找到{company_name}-{bank_name}的账户信息")
  152. return None
  153. account_code = account_info["编号"].iloc[0]
  154. account_short_name = account_info["简称"].iloc[0]
  155. company_name_full = account_info["公司"].iloc[0]
  156. # 4. 构建日记账数据
  157. journal_data = []
  158. for _, row in flow_df.iterrows():
  159. # 跳过空行(交易时间为空)
  160. if pd.isna(row["交易时间"]) or str(row["交易时间"]).strip() == "":
  161. continue
  162. # 计算收支金额
  163. income = row["收入"] if pd.notna(row["收入"]) else 0
  164. expense = row["支出"] if pd.notna(row["支出"]) else 0
  165. # 确定收支类型和金额
  166. if income > 0:
  167. shouzhi_type = "收入"
  168. amount = income
  169. elif expense > 0:
  170. shouzhi_type = "支出"
  171. amount = expense
  172. else:
  173. shouzhi_type = ""
  174. amount = 0
  175. # 提取月份(从日期中提取)
  176. try:
  177. date_obj = pd.to_datetime(row["交易时间"], errors='coerce')
  178. month = date_obj.month if pd.notna(date_obj) else ""
  179. except:
  180. month = ""
  181. # 格式化日期为 2026/1/26 格式
  182. try:
  183. date_obj = pd.to_datetime(row["交易时间"], errors='coerce')
  184. if pd.notna(date_obj):
  185. formatted_date = f"{date_obj.year}/{date_obj.month}/{date_obj.day}"
  186. else:
  187. formatted_date = row["交易时间"]
  188. except:
  189. formatted_date = row["交易时间"]
  190. # 判断对方户名是否为个人(简单判断:如果对方户名较短且不包含"公司"等关键词)
  191. opponent_name = str(row["对方户名"]).strip()
  192. is_personal = len(opponent_name) < 5 or "公司" not in opponent_name
  193. # 构建备注:对方户名+对方开户机构,如果是个人则用对方户名+备注
  194. if is_personal:
  195. remark = f"{opponent_name}{row['备注']}"
  196. else:
  197. remark = f"{opponent_name}{row['对方开户机构']}"
  198. # 基础字段
  199. journal_row = {
  200. "编号": account_code,
  201. "简称": account_short_name,
  202. "企业": company_name_full,
  203. "日期": formatted_date,
  204. "月份": month,
  205. "收支": shouzhi_type,
  206. "资金分类-1级": "",
  207. "资金分类-2级": "",
  208. "资金分类-3级": "",
  209. "对手户": opponent_name,
  210. "备注": remark,
  211. "发生额": amount,
  212. "余额": row["余额"],
  213. "备注.1": ""
  214. }
  215. # 5. 匹配资金分类(核心规则)
  216. # 规则1:对手户是参数表中的公司 或 指定的两家公司 → 结算收入/支出
  217. if (opponent_name in account_params["公司"].values) or (opponent_name in SETTLEMENT_COMPANIES):
  218. if income > 0:
  219. # 结算收入
  220. journal_row["资金分类-1级"] = "结算收入"
  221. journal_row["资金分类-2级"] = "结算收入"
  222. journal_row["资金分类-3级"] = "结算收入"
  223. else:
  224. # 结算支出
  225. journal_row["资金分类-1级"] = "结算支出"
  226. journal_row["资金分类-2级"] = "结算支出"
  227. journal_row["资金分类-3级"] = "结算支出"
  228. # 规则2:摘要含"手续费"或"对公跨行转账汇款手续费" → 财务费用-财务费用-手续费
  229. elif "手续费" in str(row["摘要"]) or "对公跨行转账汇款手续费" in str(row["摘要"]):
  230. journal_row["资金分类-1级"] = "财务费用"
  231. journal_row["资金分类-2级"] = "财务费用"
  232. journal_row["资金分类-3级"] = "手续费"
  233. # 对手户和备注都只写成手续费
  234. journal_row["对手户"] = "手续费"
  235. journal_row["备注"] = "手续费"
  236. # 规则3:备注是"会长提现" → 结算支出-结算支出-会长提现支付
  237. elif "会长提现" in str(row["备注"]):
  238. journal_row["资金分类-1级"] = "结算支出"
  239. journal_row["资金分类-2级"] = "结算支出"
  240. journal_row["资金分类-3级"] = "会长提现支付"
  241. # 其他情况(可后续补充规则)
  242. else:
  243. journal_row["资金分类-1级"] = "待分类"
  244. journal_row["资金分类-2级"] = "待分类"
  245. journal_row["资金分类-3级"] = "待分类"
  246. journal_data.append(journal_row)
  247. # 转换为DataFrame
  248. journal_df = pd.DataFrame(journal_data)
  249. # 确保列顺序匹配参考日记账
  250. journal_columns = [
  251. "编号", "简称", "企业", "日期", "月份", "收支",
  252. "资金分类-1级", "资金分类-2级", "资金分类-3级", "对手户", "备注", "发生额", "余额", "备注.1"
  253. ]
  254. journal_df = journal_df[journal_columns]
  255. return journal_df
  256. # -------------------------- GUI界面 --------------------------
  257. def select_param_file():
  258. """选择参数表文件"""
  259. file_path = filedialog.askopenfilename(
  260. title="选择参数表文件",
  261. filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
  262. )
  263. if file_path:
  264. param_file_var.set(file_path)
  265. def select_flow_file():
  266. """选择银行流水文件"""
  267. bank = bank_var.get()
  268. file_path = filedialog.askopenfilename(
  269. title=f"选择{bank}流水文件",
  270. filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")]
  271. )
  272. if file_path:
  273. flow_file_var.set(file_path)
  274. def generate_journal():
  275. """生成日记账"""
  276. # 获取界面输入
  277. company = company_var.get()
  278. bank = bank_var.get()
  279. param_file = param_file_var.get()
  280. flow_file = flow_file_var.get()
  281. # 校验输入
  282. if not param_file or not flow_file:
  283. messagebox.showwarning("提示", "请先选择参数表和流水文件")
  284. return
  285. # 生成日记账数据
  286. journal_df = generate_journal_data(company, bank, param_file, flow_file)
  287. if journal_df is None:
  288. return
  289. # 保存文件
  290. today = datetime.now().strftime("%Y年%m月%d日")
  291. save_path = filedialog.asksaveasfilename(
  292. title="保存日记账",
  293. defaultextension=".xlsx",
  294. initialfile=f"{company}-{bank}-日记账-{today}.xlsx",
  295. filetypes=[("Excel文件", "*.xlsx"), ("所有文件", "*.*")]
  296. )
  297. if save_path:
  298. # 保存时保持格式(匹配参考日记账)
  299. with pd.ExcelWriter(save_path, engine="openpyxl") as writer:
  300. journal_df.to_excel(writer, index=False, sheet_name="日记账")
  301. # 设置表头加粗
  302. workbook = writer.book
  303. worksheet = writer.sheets["日记账"]
  304. # 创建加粗字体样式
  305. bold_font = Font(bold=True)
  306. # 设置表头行加粗
  307. for cell in worksheet[1]:
  308. cell.font = bold_font
  309. messagebox.showinfo("成功", f"日记账已生成:{save_path}")
  310. # 初始化GUI
  311. root = tk.Tk()
  312. root.title("出纳日记账生成工具")
  313. root.geometry("600x300")
  314. # 变量定义
  315. company_var = tk.StringVar(value="成都锦高量科科技有限公司")
  316. bank_var = tk.StringVar(value="建设银行")
  317. param_file_var = tk.StringVar()
  318. flow_file_var = tk.StringVar()
  319. # 界面布局
  320. # 1. 公司选择
  321. frame_company = ttk.Frame(root, padding="10")
  322. frame_company.pack(fill=tk.X)
  323. ttk.Label(frame_company, text="公司名称:").pack(side=tk.LEFT)
  324. company_combo = ttk.Combobox(frame_company, textvariable=company_var, state="readonly")
  325. company_combo["values"] = ["成都锦高量科科技有限公司", "成都云启寰宇科技有限公司", "成都橙风有量科技有限公司"]
  326. company_combo.pack(side=tk.LEFT, padx=5)
  327. # 2. 银行选择
  328. frame_bank = ttk.Frame(root, padding="10")
  329. frame_bank.pack(fill=tk.X)
  330. ttk.Label(frame_bank, text="开户银行:").pack(side=tk.LEFT)
  331. bank_combo = ttk.Combobox(frame_bank, textvariable=bank_var, state="readonly")
  332. bank_combo["values"] = ["建设银行", "中国银行"]
  333. bank_combo.pack(side=tk.LEFT, padx=5)
  334. # 3. 参数表选择
  335. frame_param = ttk.Frame(root, padding="10")
  336. frame_param.pack(fill=tk.X)
  337. ttk.Label(frame_param, text="参数表文件:").pack(side=tk.LEFT)
  338. ttk.Entry(frame_param, textvariable=param_file_var, width=50).pack(side=tk.LEFT, padx=5)
  339. ttk.Button(frame_param, text="选择", command=select_param_file).pack(side=tk.LEFT)
  340. # 4. 流水文件选择
  341. frame_flow = ttk.Frame(root, padding="10")
  342. frame_flow.pack(fill=tk.X)
  343. ttk.Label(frame_flow, text="流水文件:").pack(side=tk.LEFT)
  344. ttk.Entry(frame_flow, textvariable=flow_file_var, width=50).pack(side=tk.LEFT, padx=5)
  345. ttk.Button(frame_flow, text="选择", command=select_flow_file).pack(side=tk.LEFT)
  346. # 5. 生成按钮
  347. frame_generate = ttk.Frame(root, padding="10")
  348. frame_generate.pack(fill=tk.X)
  349. ttk.Button(frame_generate, text="生成日记账", command=generate_journal, style="Accent.TButton").pack()
  350. # 启动GUI
  351. root.mainloop()