mirror of
https://github.com/goldenfishs/MRobot.git
synced 2026-03-31 21:07:14 +08:00
暂存运营
This commit is contained in:
814
app/finance_interface.py
Normal file
814
app/finance_interface.py
Normal file
@@ -0,0 +1,814 @@
|
||||
"""
|
||||
财务做账应用主界面
|
||||
包含做账、查询、导出三个功能标签页
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QStackedLayout,
|
||||
QLabel, QLineEdit, QDateEdit, QSpinBox, QDoubleSpinBox,
|
||||
QPushButton, QTableWidget, QTableWidgetItem, QHeaderView,
|
||||
QFileDialog, QMessageBox, QScrollArea, QTabWidget, QFrame,
|
||||
QComboBox, QCheckBox, QInputDialog, QDialog, QTextEdit)
|
||||
from PyQt5.QtCore import Qt, QDate, pyqtSignal, QMimeData, QRect, QSize
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QDrag, QFont, QColor
|
||||
from PyQt5.QtCore import Qt as QtEnum
|
||||
from PyQt5.QtWidgets import QApplication, QListWidget, QListWidgetItem
|
||||
from qfluentwidgets import (TitleLabel, BodyLabel, SubtitleLabel, StrongBodyLabel,
|
||||
HorizontalSeparator, CardWidget, PushButton, LineEdit,
|
||||
SpinBox, CheckBox, TextEdit, PrimaryPushButton,
|
||||
InfoBar, InfoBarPosition, FluentIcon as FIF, ComboBox,
|
||||
DoubleSpinBox, DateEdit, SearchLineEdit, StateToolTip)
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import os
|
||||
|
||||
from .tools.finance_manager import FinanceManager, TransactionType, Transaction, Account
|
||||
|
||||
|
||||
class CreateTransactionDialog(QDialog):
|
||||
"""创建/编辑交易记录对话框"""
|
||||
|
||||
def __init__(self, parent=None, transaction: Optional[Transaction] = None, account_id: Optional[str] = None):
|
||||
super().__init__(parent)
|
||||
self.transaction = transaction
|
||||
self.account_id = account_id
|
||||
self.finance_manager = FinanceManager()
|
||||
|
||||
self.setWindowTitle("新建交易记录" if not transaction else "编辑交易记录")
|
||||
self.setGeometry(100, 100, 600, 500)
|
||||
self.init_ui()
|
||||
|
||||
if transaction:
|
||||
self.load_transaction_data(transaction)
|
||||
|
||||
def init_ui(self):
|
||||
"""初始化UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# 日期
|
||||
date_layout = QHBoxLayout()
|
||||
date_layout.addWidget(QLabel("日期:"))
|
||||
self.date_edit = DateEdit()
|
||||
self.date_edit.setDate(QDate.currentDate())
|
||||
date_layout.addWidget(self.date_edit)
|
||||
date_layout.addStretch()
|
||||
layout.addLayout(date_layout)
|
||||
|
||||
# 金额
|
||||
amount_layout = QHBoxLayout()
|
||||
amount_layout.addWidget(QLabel("金额 (元):"))
|
||||
self.amount_spin = DoubleSpinBox()
|
||||
self.amount_spin.setRange(0, 999999999)
|
||||
self.amount_spin.setDecimals(2)
|
||||
amount_layout.addWidget(self.amount_spin)
|
||||
amount_layout.addStretch()
|
||||
layout.addLayout(amount_layout)
|
||||
|
||||
# 交易人
|
||||
trader_layout = QHBoxLayout()
|
||||
trader_layout.addWidget(QLabel("交易人:"))
|
||||
self.trader_edit = LineEdit()
|
||||
trader_layout.addWidget(self.trader_edit)
|
||||
layout.addLayout(trader_layout)
|
||||
|
||||
# 备注
|
||||
notes_layout = QHBoxLayout()
|
||||
notes_layout.addWidget(QLabel("备注:"))
|
||||
self.notes_edit = TextEdit()
|
||||
self.notes_edit.setMaximumHeight(80)
|
||||
notes_layout.addWidget(self.notes_edit)
|
||||
layout.addLayout(notes_layout)
|
||||
|
||||
# 图片部分
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
layout.addWidget(SubtitleLabel("相关文件 (可选)"))
|
||||
|
||||
# 发票
|
||||
invoice_layout = QHBoxLayout()
|
||||
invoice_layout.addWidget(QLabel("发票图片:"))
|
||||
self.invoice_label = QLabel("未选择")
|
||||
invoice_layout.addWidget(self.invoice_label)
|
||||
invoice_btn = PushButton("选择")
|
||||
invoice_btn.clicked.connect(lambda: self.select_image("invoice"))
|
||||
invoice_layout.addWidget(invoice_btn)
|
||||
layout.addLayout(invoice_layout)
|
||||
|
||||
# 支付记录
|
||||
payment_layout = QHBoxLayout()
|
||||
payment_layout.addWidget(QLabel("支付记录:"))
|
||||
self.payment_label = QLabel("未选择")
|
||||
payment_layout.addWidget(self.payment_label)
|
||||
payment_btn = PushButton("选择")
|
||||
payment_btn.clicked.connect(lambda: self.select_image("payment"))
|
||||
payment_layout.addWidget(payment_btn)
|
||||
layout.addLayout(payment_layout)
|
||||
|
||||
# 购买记录
|
||||
purchase_layout = QHBoxLayout()
|
||||
purchase_layout.addWidget(QLabel("购买记录:"))
|
||||
self.purchase_label = QLabel("未选择")
|
||||
purchase_layout.addWidget(self.purchase_label)
|
||||
purchase_btn = PushButton("选择")
|
||||
purchase_btn.clicked.connect(lambda: self.select_image("purchase"))
|
||||
purchase_layout.addWidget(purchase_btn)
|
||||
layout.addLayout(purchase_layout)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
# 按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.addStretch()
|
||||
|
||||
cancel_btn = PushButton("取消")
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
btn_layout.addWidget(cancel_btn)
|
||||
|
||||
save_btn = PrimaryPushButton("保存")
|
||||
save_btn.clicked.connect(self.save_transaction)
|
||||
btn_layout.addWidget(save_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
# 存储图片路径
|
||||
self.selected_images: dict = {
|
||||
'invoice': None,
|
||||
'payment': None,
|
||||
'purchase': None
|
||||
}
|
||||
|
||||
def select_image(self, image_type: str):
|
||||
"""选择图片"""
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self, f"选择{image_type}图片",
|
||||
"", "图片文件 (*.png *.jpg *.jpeg *.bmp);;所有文件 (*)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
self.selected_images[image_type] = file_path
|
||||
filename = Path(file_path).name
|
||||
|
||||
if image_type == 'invoice':
|
||||
self.invoice_label.setText(filename)
|
||||
elif image_type == 'payment':
|
||||
self.payment_label.setText(filename)
|
||||
elif image_type == 'purchase':
|
||||
self.purchase_label.setText(filename)
|
||||
|
||||
def load_transaction_data(self, transaction: Transaction):
|
||||
"""加载交易记录数据到表单"""
|
||||
self.date_edit.setDate(QDate.fromString(transaction.date, "yyyy-MM-dd"))
|
||||
self.amount_spin.setValue(transaction.amount)
|
||||
self.trader_edit.setText(transaction.trader)
|
||||
self.notes_edit.setText(transaction.notes)
|
||||
|
||||
if transaction.invoice_path:
|
||||
self.invoice_label.setText(Path(transaction.invoice_path).name)
|
||||
if transaction.payment_path:
|
||||
self.payment_label.setText(Path(transaction.payment_path).name)
|
||||
if transaction.purchase_path:
|
||||
self.purchase_label.setText(Path(transaction.purchase_path).name)
|
||||
|
||||
def save_transaction(self):
|
||||
"""保存交易记录"""
|
||||
if not self.account_id:
|
||||
QMessageBox.warning(self, "错误", "账户ID未设置")
|
||||
return
|
||||
|
||||
date_str = self.date_edit.date().toString("yyyy-MM-dd")
|
||||
amount = self.amount_spin.value()
|
||||
trader = self.trader_edit.text().strip()
|
||||
notes = self.notes_edit.toPlainText().strip()
|
||||
|
||||
if not trader:
|
||||
QMessageBox.warning(self, "验证错误", "请输入交易人")
|
||||
return
|
||||
|
||||
if amount <= 0:
|
||||
QMessageBox.warning(self, "验证错误", "金额必须大于0")
|
||||
return
|
||||
|
||||
if self.transaction:
|
||||
# 编辑现有交易记录
|
||||
trans_id = self.transaction.id
|
||||
self.finance_manager.update_transaction(
|
||||
self.account_id, trans_id,
|
||||
date=date_str, amount=amount,
|
||||
trader=trader, notes=notes
|
||||
)
|
||||
else:
|
||||
# 创建新交易记录
|
||||
transaction = Transaction(
|
||||
date=date_str, amount=amount,
|
||||
trader=trader, notes=notes
|
||||
)
|
||||
self.finance_manager.add_transaction(self.account_id, transaction)
|
||||
trans_id = transaction.id
|
||||
|
||||
# 保存图片
|
||||
for image_type_str, image_path in self.selected_images.items():
|
||||
if image_path:
|
||||
image_type = TransactionType[image_type_str.upper()]
|
||||
relative_path = self.finance_manager.save_image_for_transaction(
|
||||
self.account_id, trans_id, image_type, image_path
|
||||
)
|
||||
if relative_path:
|
||||
self.finance_manager.update_transaction(
|
||||
self.account_id, trans_id,
|
||||
**{f"{image_type_str}_path": relative_path}
|
||||
)
|
||||
|
||||
self.accept()
|
||||
|
||||
|
||||
class RecordViewDialog(QDialog):
|
||||
"""查看和编辑交易记录详情对话框"""
|
||||
|
||||
def __init__(self, parent=None, account_id: Optional[str] = None, transaction: Optional[Transaction] = None):
|
||||
super().__init__(parent)
|
||||
self.account_id = account_id
|
||||
self.transaction = transaction
|
||||
self.finance_manager = FinanceManager()
|
||||
|
||||
self.setWindowTitle("记录详情")
|
||||
self.setGeometry(100, 100, 700, 600)
|
||||
self.init_ui()
|
||||
|
||||
if transaction:
|
||||
self.load_transaction_data()
|
||||
|
||||
def init_ui(self):
|
||||
"""初始化UI"""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# 基本信息
|
||||
info_layout = QVBoxLayout()
|
||||
|
||||
date_layout = QHBoxLayout()
|
||||
date_layout.addWidget(QLabel("日期:"))
|
||||
self.date_label = QLabel()
|
||||
date_layout.addWidget(self.date_label)
|
||||
date_layout.addStretch()
|
||||
info_layout.addLayout(date_layout)
|
||||
|
||||
amount_layout = QHBoxLayout()
|
||||
amount_layout.addWidget(QLabel("金额:"))
|
||||
self.amount_label = QLabel()
|
||||
amount_layout.addWidget(self.amount_label)
|
||||
amount_layout.addStretch()
|
||||
info_layout.addLayout(amount_layout)
|
||||
|
||||
trader_layout = QHBoxLayout()
|
||||
trader_layout.addWidget(QLabel("交易人:"))
|
||||
self.trader_label = QLabel()
|
||||
trader_layout.addWidget(self.trader_label)
|
||||
trader_layout.addStretch()
|
||||
info_layout.addLayout(trader_layout)
|
||||
|
||||
notes_layout = QHBoxLayout()
|
||||
notes_layout.addWidget(QLabel("备注:"))
|
||||
self.notes_label = QLabel()
|
||||
self.notes_label.setWordWrap(True)
|
||||
notes_layout.addWidget(self.notes_label)
|
||||
info_layout.addLayout(notes_layout)
|
||||
|
||||
layout.addLayout(info_layout)
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 图片预览
|
||||
layout.addWidget(SubtitleLabel("相关文件预览"))
|
||||
|
||||
preview_layout = QHBoxLayout()
|
||||
|
||||
# 发票
|
||||
invoice_layout = QVBoxLayout()
|
||||
invoice_layout.addWidget(QLabel("发票:"))
|
||||
self.invoice_preview = QLabel("无")
|
||||
self.invoice_preview.setMinimumSize(150, 150)
|
||||
self.invoice_preview.setAlignment(Qt.AlignCenter)
|
||||
self.invoice_preview.setStyleSheet("border: 1px solid #ddd;")
|
||||
invoice_layout.addWidget(self.invoice_preview)
|
||||
preview_layout.addLayout(invoice_layout)
|
||||
|
||||
# 支付记录
|
||||
payment_layout = QVBoxLayout()
|
||||
payment_layout.addWidget(QLabel("支付记录:"))
|
||||
self.payment_preview = QLabel("无")
|
||||
self.payment_preview.setMinimumSize(150, 150)
|
||||
self.payment_preview.setAlignment(Qt.AlignCenter)
|
||||
self.payment_preview.setStyleSheet("border: 1px solid #ddd;")
|
||||
payment_layout.addWidget(self.payment_preview)
|
||||
preview_layout.addLayout(payment_layout)
|
||||
|
||||
# 购买记录
|
||||
purchase_layout = QVBoxLayout()
|
||||
purchase_layout.addWidget(QLabel("购买记录:"))
|
||||
self.purchase_preview = QLabel("无")
|
||||
self.purchase_preview.setMinimumSize(150, 150)
|
||||
self.purchase_preview.setAlignment(Qt.AlignCenter)
|
||||
self.purchase_preview.setStyleSheet("border: 1px solid #ddd;")
|
||||
purchase_layout.addWidget(self.purchase_preview)
|
||||
preview_layout.addLayout(purchase_layout)
|
||||
|
||||
layout.addLayout(preview_layout)
|
||||
layout.addStretch()
|
||||
|
||||
# 按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.addStretch()
|
||||
|
||||
close_btn = PushButton("关闭")
|
||||
close_btn.clicked.connect(self.reject)
|
||||
btn_layout.addWidget(close_btn)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
def load_transaction_data(self):
|
||||
"""加载交易记录数据"""
|
||||
if not self.transaction:
|
||||
return
|
||||
|
||||
self.date_label.setText(self.transaction.date)
|
||||
self.amount_label.setText(f"¥ {self.transaction.amount:.2f}")
|
||||
self.trader_label.setText(self.transaction.trader)
|
||||
self.notes_label.setText(self.transaction.notes or "无")
|
||||
|
||||
# 加载图片预览
|
||||
self._load_image_preview('invoice', self.transaction.invoice_path if self.transaction else None, self.invoice_preview)
|
||||
self._load_image_preview('payment', self.transaction.payment_path if self.transaction else None, self.payment_preview)
|
||||
self._load_image_preview('purchase', self.transaction.purchase_path if self.transaction else None, self.purchase_preview)
|
||||
|
||||
def _load_image_preview(self, image_type: str, relative_path: Optional[str], label: QLabel):
|
||||
"""加载并显示图片预览"""
|
||||
if not relative_path:
|
||||
return
|
||||
|
||||
full_path = self.finance_manager.get_transaction_image_path(self.account_id or "", relative_path)
|
||||
if full_path and full_path.exists():
|
||||
pixmap = QPixmap(str(full_path))
|
||||
scaled_pixmap = pixmap.scaledToWidth(150)
|
||||
label.setPixmap(scaled_pixmap)
|
||||
|
||||
|
||||
class FinanceInterface(QWidget):
|
||||
"""财务做账主界面"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("financeInterface")
|
||||
self.finance_manager = FinanceManager()
|
||||
|
||||
self.layout_main = QVBoxLayout(self)
|
||||
self.layout_main.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout_main.setSpacing(0)
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
"""初始化UI"""
|
||||
# 标签页
|
||||
self.tab_widget = QTabWidget()
|
||||
self.tab_widget.setStyleSheet("""
|
||||
QTabBar::tab {
|
||||
padding: 8px 20px;
|
||||
}
|
||||
""")
|
||||
|
||||
# 做账标签页
|
||||
self.bookkeeping_tab = self.create_bookkeeping_tab()
|
||||
self.tab_widget.addTab(self.bookkeeping_tab, "做账")
|
||||
|
||||
# 查询标签页
|
||||
self.query_tab = self.create_query_tab()
|
||||
self.tab_widget.addTab(self.query_tab, "查询")
|
||||
|
||||
# 导出标签页
|
||||
self.export_tab = self.create_export_tab()
|
||||
self.tab_widget.addTab(self.export_tab, "导出")
|
||||
|
||||
self.layout_main.addWidget(self.tab_widget)
|
||||
|
||||
# 初始化时获取默认账户
|
||||
self.init_default_account()
|
||||
|
||||
def create_bookkeeping_tab(self) -> QWidget:
|
||||
"""创建做账标签页"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# 标题和操作按钮
|
||||
title_layout = QHBoxLayout()
|
||||
title_layout.addWidget(SubtitleLabel("交易记录"))
|
||||
title_layout.addStretch()
|
||||
|
||||
new_record_btn = PrimaryPushButton("新建记录")
|
||||
new_record_btn.clicked.connect(self.create_new_record)
|
||||
title_layout.addWidget(new_record_btn)
|
||||
|
||||
layout.addLayout(title_layout)
|
||||
|
||||
# 记录表格
|
||||
self.records_table = QTableWidget()
|
||||
self.records_table.setColumnCount(6)
|
||||
self.records_table.setHorizontalHeaderLabels(["日期", "交易人", "金额 (元)", "备注", "操作", ""])
|
||||
header = self.records_table.horizontalHeader()
|
||||
if header:
|
||||
header.setStretchLastSection(False)
|
||||
self.records_table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
self.records_table.setAlternatingRowColors(True)
|
||||
self.records_table.setMaximumHeight(600)
|
||||
|
||||
layout.addWidget(self.records_table)
|
||||
|
||||
# 统计信息
|
||||
stats_layout = QHBoxLayout()
|
||||
stats_layout.addWidget(QLabel("总额:"))
|
||||
self.total_amount_label = QLabel("¥ 0.00")
|
||||
self.total_amount_label.setStyleSheet("font-weight: bold; font-size: 14px; color: #d32f2f;")
|
||||
stats_layout.addWidget(self.total_amount_label)
|
||||
|
||||
stats_layout.addSpacing(30)
|
||||
stats_layout.addWidget(QLabel("记录数:"))
|
||||
self.record_count_label = QLabel("0")
|
||||
self.record_count_label.setStyleSheet("font-weight: bold; font-size: 14px;")
|
||||
stats_layout.addWidget(self.record_count_label)
|
||||
|
||||
stats_layout.addStretch()
|
||||
layout.addLayout(stats_layout)
|
||||
|
||||
return widget
|
||||
|
||||
def create_query_tab(self) -> QWidget:
|
||||
"""创建查询标签页"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# 搜索过滤区
|
||||
filter_layout = QHBoxLayout()
|
||||
|
||||
filter_layout.addWidget(QLabel("日期范围:"))
|
||||
self.query_date_start = DateEdit()
|
||||
self.query_date_start.setDate(QDate.currentDate().addMonths(-1))
|
||||
filter_layout.addWidget(self.query_date_start)
|
||||
|
||||
filter_layout.addWidget(QLabel("至"))
|
||||
self.query_date_end = DateEdit()
|
||||
self.query_date_end.setDate(QDate.currentDate())
|
||||
filter_layout.addWidget(self.query_date_end)
|
||||
|
||||
filter_layout.addSpacing(20)
|
||||
filter_layout.addWidget(QLabel("金额范围:"))
|
||||
self.query_amount_min = DoubleSpinBox()
|
||||
self.query_amount_min.setRange(0, 999999999)
|
||||
self.query_amount_min.setPrefix("¥ ")
|
||||
filter_layout.addWidget(self.query_amount_min)
|
||||
|
||||
filter_layout.addWidget(QLabel("至"))
|
||||
self.query_amount_max = DoubleSpinBox()
|
||||
self.query_amount_max.setRange(0, 999999999)
|
||||
self.query_amount_max.setValue(999999999)
|
||||
self.query_amount_max.setPrefix("¥ ")
|
||||
filter_layout.addWidget(self.query_amount_max)
|
||||
|
||||
layout.addLayout(filter_layout)
|
||||
|
||||
# 交易人搜索
|
||||
trader_layout = QHBoxLayout()
|
||||
trader_layout.addWidget(QLabel("交易人:"))
|
||||
self.query_trader_edit = SearchLineEdit()
|
||||
self.query_trader_edit.setPlaceholderText("输入交易人名称...")
|
||||
trader_layout.addWidget(self.query_trader_edit)
|
||||
|
||||
query_btn = PrimaryPushButton("查询")
|
||||
query_btn.clicked.connect(self.perform_query)
|
||||
trader_layout.addWidget(query_btn)
|
||||
|
||||
layout.addLayout(trader_layout)
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 查询结果表格
|
||||
self.query_result_table = QTableWidget()
|
||||
self.query_result_table.setColumnCount(6)
|
||||
self.query_result_table.setHorizontalHeaderLabels(["日期", "交易人", "金额 (元)", "备注", "查看详情", ""])
|
||||
header = self.query_result_table.horizontalHeader()
|
||||
if header:
|
||||
header.setStretchLastSection(False)
|
||||
self.query_result_table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
self.query_result_table.setAlternatingRowColors(True)
|
||||
|
||||
layout.addWidget(self.query_result_table)
|
||||
|
||||
return widget
|
||||
|
||||
def create_export_tab(self) -> QWidget:
|
||||
"""创建导出标签页"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
layout.setSpacing(20)
|
||||
|
||||
layout.addWidget(TitleLabel("数据导出和导入"))
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 导出选项
|
||||
layout.addWidget(SubtitleLabel("导出选项"))
|
||||
|
||||
export_layout = QVBoxLayout()
|
||||
|
||||
# 导出账户为压缩包
|
||||
account_export_layout = QHBoxLayout()
|
||||
account_export_layout.addWidget(QLabel("导出当前账户:"))
|
||||
account_export_layout.addStretch()
|
||||
export_account_btn = PrimaryPushButton("导出为ZIP包")
|
||||
export_account_btn.clicked.connect(self.export_account)
|
||||
account_export_layout.addWidget(export_account_btn)
|
||||
export_layout.addLayout(account_export_layout)
|
||||
|
||||
# 导出为CSV
|
||||
csv_export_layout = QHBoxLayout()
|
||||
csv_export_layout.addWidget(QLabel("导出为Excel格式:"))
|
||||
csv_export_layout.addStretch()
|
||||
export_csv_btn = PrimaryPushButton("导出CSV")
|
||||
export_csv_btn.clicked.connect(self.export_csv)
|
||||
csv_export_layout.addWidget(export_csv_btn)
|
||||
export_layout.addLayout(csv_export_layout)
|
||||
|
||||
# 备份所有账户
|
||||
backup_layout = QHBoxLayout()
|
||||
backup_layout.addWidget(QLabel("备份所有账户:"))
|
||||
backup_layout.addStretch()
|
||||
backup_btn = PrimaryPushButton("创建备份")
|
||||
backup_btn.clicked.connect(self.backup_all)
|
||||
backup_layout.addWidget(backup_btn)
|
||||
export_layout.addLayout(backup_layout)
|
||||
|
||||
layout.addLayout(export_layout)
|
||||
layout.addWidget(HorizontalSeparator())
|
||||
|
||||
# 导入选项
|
||||
layout.addWidget(SubtitleLabel("导入选项"))
|
||||
|
||||
import_layout = QVBoxLayout()
|
||||
|
||||
# 导入账户
|
||||
account_import_layout = QHBoxLayout()
|
||||
account_import_layout.addWidget(QLabel("导入账户ZIP包:"))
|
||||
account_import_layout.addStretch()
|
||||
import_account_btn = PrimaryPushButton("导入账户")
|
||||
import_account_btn.clicked.connect(self.import_account)
|
||||
account_import_layout.addWidget(import_account_btn)
|
||||
import_layout.addLayout(account_import_layout)
|
||||
|
||||
layout.addLayout(import_layout)
|
||||
layout.addStretch()
|
||||
|
||||
return widget
|
||||
|
||||
def init_default_account(self):
|
||||
"""初始化默认账户"""
|
||||
accounts = self.finance_manager.get_all_accounts()
|
||||
if accounts:
|
||||
self.default_account_id = accounts[0].id
|
||||
self.refresh_records_display()
|
||||
else:
|
||||
self.default_account_id = None
|
||||
self.clear_records_table()
|
||||
|
||||
def refresh_account_list(self):
|
||||
"""刷新账户列表(已移除,保留兼容性)"""
|
||||
pass
|
||||
|
||||
def get_current_account_id(self) -> Optional[str]:
|
||||
"""获取当前账户ID"""
|
||||
if hasattr(self, 'default_account_id'):
|
||||
return self.default_account_id
|
||||
|
||||
# 备用:如果还没初始化,从财务管理器获取第一个账户
|
||||
accounts = self.finance_manager.get_all_accounts()
|
||||
if accounts:
|
||||
return accounts[0].id
|
||||
return None
|
||||
|
||||
def on_account_changed(self):
|
||||
"""账户改变时刷新显示(已移除,保留兼容性)"""
|
||||
pass
|
||||
|
||||
def refresh_records_display(self):
|
||||
"""刷新记录显示"""
|
||||
account_id = self.get_current_account_id()
|
||||
if not account_id:
|
||||
return
|
||||
|
||||
account = self.finance_manager.get_account(account_id)
|
||||
if not account:
|
||||
return
|
||||
|
||||
self.clear_records_table()
|
||||
|
||||
for transaction in account.transactions:
|
||||
row = self.records_table.rowCount()
|
||||
self.records_table.insertRow(row)
|
||||
|
||||
self.records_table.setItem(row, 0, QTableWidgetItem(transaction.date))
|
||||
self.records_table.setItem(row, 1, QTableWidgetItem(transaction.trader))
|
||||
self.records_table.setItem(row, 2, QTableWidgetItem(f"¥ {transaction.amount:.2f}"))
|
||||
self.records_table.setItem(row, 3, QTableWidgetItem(transaction.notes or ""))
|
||||
|
||||
# 操作按钮
|
||||
btn_layout = QHBoxLayout()
|
||||
edit_btn = PushButton("编辑")
|
||||
edit_btn.clicked.connect(lambda checked, tid=transaction.id: self.edit_record(tid))
|
||||
btn_layout.addWidget(edit_btn)
|
||||
|
||||
delete_btn = PushButton("删除")
|
||||
delete_btn.clicked.connect(lambda checked, tid=transaction.id: self.delete_record(tid))
|
||||
btn_layout.addWidget(delete_btn)
|
||||
|
||||
view_btn = PushButton("查看")
|
||||
view_btn.clicked.connect(lambda checked, tid=transaction.id: self.view_record(tid))
|
||||
btn_layout.addWidget(view_btn)
|
||||
|
||||
btn_widget = QWidget()
|
||||
btn_widget.setLayout(btn_layout)
|
||||
self.records_table.setCellWidget(row, 4, btn_widget)
|
||||
|
||||
# 更新统计信息
|
||||
total_amount = sum(t.amount for t in account.transactions)
|
||||
self.total_amount_label.setText(f"¥ {total_amount:.2f}")
|
||||
self.record_count_label.setText(str(len(account.transactions)))
|
||||
|
||||
def clear_records_table(self):
|
||||
"""清空记录表格"""
|
||||
self.records_table.setRowCount(0)
|
||||
self.total_amount_label.setText("¥ 0.00")
|
||||
self.record_count_label.setText("0")
|
||||
|
||||
def create_new_account(self):
|
||||
"""创建新账户(已移除)"""
|
||||
pass
|
||||
|
||||
def delete_current_account(self):
|
||||
"""删除当前账户(已移除)"""
|
||||
pass
|
||||
|
||||
def create_new_record(self):
|
||||
"""创建新记录"""
|
||||
account_id = self.get_current_account_id()
|
||||
if not account_id:
|
||||
QMessageBox.warning(self, "错误", "请先创建或选择一个账户")
|
||||
return
|
||||
|
||||
dialog = CreateTransactionDialog(self, account_id=account_id)
|
||||
if dialog.exec_():
|
||||
self.refresh_records_display()
|
||||
InfoBar.success("记录已添加", "", duration=2000, parent=self)
|
||||
|
||||
def edit_record(self, trans_id: str):
|
||||
"""编辑记录"""
|
||||
account_id = self.get_current_account_id()
|
||||
if not account_id:
|
||||
return
|
||||
|
||||
transaction = self.finance_manager.get_transaction(account_id, trans_id)
|
||||
if not transaction:
|
||||
return
|
||||
|
||||
dialog = CreateTransactionDialog(self, transaction=transaction, account_id=account_id)
|
||||
if dialog.exec_():
|
||||
self.refresh_records_display()
|
||||
InfoBar.success("记录已更新", "", duration=2000, parent=self)
|
||||
|
||||
def delete_record(self, trans_id: str):
|
||||
"""删除记录"""
|
||||
account_id = self.get_current_account_id()
|
||||
if not account_id:
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
self, "确认删除",
|
||||
"确定要删除这条记录吗?",
|
||||
QMessageBox.Yes | QMessageBox.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
self.finance_manager.delete_transaction(account_id, trans_id)
|
||||
self.refresh_records_display()
|
||||
InfoBar.success("记录已删除", "", duration=2000, parent=self)
|
||||
|
||||
def view_record(self, trans_id: str):
|
||||
"""查看记录详情"""
|
||||
account_id = self.get_current_account_id()
|
||||
if not account_id:
|
||||
return
|
||||
|
||||
transaction = self.finance_manager.get_transaction(account_id, trans_id)
|
||||
if not transaction:
|
||||
return
|
||||
|
||||
dialog = RecordViewDialog(self, account_id=account_id, transaction=transaction)
|
||||
dialog.exec_()
|
||||
|
||||
def perform_query(self):
|
||||
"""执行查询"""
|
||||
account_id = self.get_current_account_id()
|
||||
if not account_id:
|
||||
QMessageBox.warning(self, "错误", "请先创建或选择一个账户")
|
||||
return
|
||||
|
||||
date_start = self.query_date_start.date().toString("yyyy-MM-dd")
|
||||
date_end = self.query_date_end.date().toString("yyyy-MM-dd")
|
||||
amount_min = self.query_amount_min.value() if self.query_amount_min.value() > 0 else None
|
||||
amount_max = self.query_amount_max.value() if self.query_amount_max.value() < 999999999 else None
|
||||
trader = self.query_trader_edit.text().strip() or None
|
||||
|
||||
results = self.finance_manager.query_transactions(
|
||||
account_id,
|
||||
date_start=date_start, date_end=date_end,
|
||||
amount_min=amount_min, amount_max=amount_max,
|
||||
trader=trader
|
||||
)
|
||||
|
||||
self.query_result_table.setRowCount(0)
|
||||
|
||||
for transaction in results:
|
||||
row = self.query_result_table.rowCount()
|
||||
self.query_result_table.insertRow(row)
|
||||
|
||||
self.query_result_table.setItem(row, 0, QTableWidgetItem(transaction.date))
|
||||
self.query_result_table.setItem(row, 1, QTableWidgetItem(transaction.trader))
|
||||
self.query_result_table.setItem(row, 2, QTableWidgetItem(f"¥ {transaction.amount:.2f}"))
|
||||
self.query_result_table.setItem(row, 3, QTableWidgetItem(transaction.notes or ""))
|
||||
|
||||
view_btn = PushButton("查看详情")
|
||||
view_btn.clicked.connect(lambda checked, tid=transaction.id: self.view_record(tid))
|
||||
self.query_result_table.setCellWidget(row, 4, view_btn)
|
||||
|
||||
InfoBar.success(f"找到 {len(results)} 条记录", "", duration=2000, parent=self)
|
||||
|
||||
def export_account(self):
|
||||
"""导出账户为ZIP包"""
|
||||
account_id = self.get_current_account_id()
|
||||
if not account_id:
|
||||
QMessageBox.warning(self, "错误", "请先选择一个账户")
|
||||
return
|
||||
|
||||
export_dir = QFileDialog.getExistingDirectory(self, "选择导出目录")
|
||||
if export_dir:
|
||||
if self.finance_manager.export_account_package(account_id, export_dir):
|
||||
InfoBar.success("账户导出成功", "", duration=2000, parent=self)
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "导出账户失败")
|
||||
|
||||
def import_account(self):
|
||||
"""导入账户ZIP包"""
|
||||
zip_file, _ = QFileDialog.getOpenFileName(
|
||||
self, "选择要导入的账户文件",
|
||||
"", "ZIP文件 (*.zip)"
|
||||
)
|
||||
|
||||
if zip_file:
|
||||
account_id = self.finance_manager.import_account_package(zip_file)
|
||||
if account_id:
|
||||
# 重新初始化默认账户
|
||||
self.init_default_account()
|
||||
InfoBar.success("账户导入成功", "", duration=2000, parent=self)
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "导入账户失败")
|
||||
|
||||
def export_csv(self):
|
||||
"""导出为CSV"""
|
||||
account_id = self.get_current_account_id()
|
||||
if not account_id:
|
||||
QMessageBox.warning(self, "错误", "请先选择一个账户")
|
||||
return
|
||||
|
||||
account = self.finance_manager.get_account(account_id)
|
||||
if not account:
|
||||
return
|
||||
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self, "保存CSV文件",
|
||||
f"{account.name}.csv",
|
||||
"CSV文件 (*.csv)"
|
||||
)
|
||||
|
||||
if file_path:
|
||||
if self.finance_manager.export_to_csv(account_id, file_path):
|
||||
InfoBar.success("已导出为CSV", "", duration=2000, parent=self)
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "导出CSV失败")
|
||||
|
||||
def backup_all(self):
|
||||
"""备份所有账户"""
|
||||
if self.finance_manager.backup_all_accounts():
|
||||
InfoBar.success("备份创建成功", "已保存到 assets/Finance_Data/backups", duration=3000, parent=self)
|
||||
else:
|
||||
QMessageBox.warning(self, "错误", "创建备份失败")
|
||||
@@ -14,6 +14,7 @@ from .part_library_interface import PartLibraryInterface
|
||||
from .data_interface import DataInterface
|
||||
from .mini_tool_interface import MiniToolInterface
|
||||
from .code_configuration_interface import CodeConfigurationInterface
|
||||
from .finance_interface import FinanceInterface
|
||||
from .about_interface import AboutInterface
|
||||
import base64
|
||||
|
||||
@@ -52,6 +53,7 @@ class MainWindow(FluentWindow):
|
||||
# self.dataInterface = DataInterface(self)
|
||||
self.miniToolInterface = MiniToolInterface(self)
|
||||
self.codeConfigurationInterface = CodeConfigurationInterface(self)
|
||||
self.financeInterface = FinanceInterface(self)
|
||||
|
||||
|
||||
def initNavigation(self):
|
||||
@@ -60,6 +62,7 @@ class MainWindow(FluentWindow):
|
||||
self.addSubInterface(self.codeConfigurationInterface, FIF.CODE, self.tr('代码生成'))
|
||||
self.addSubInterface(self.serialTerminalInterface, FIF.COMMAND_PROMPT,self.tr('串口助手'))
|
||||
self.addSubInterface(self.partLibraryInterface, FIF.DOWNLOAD, self.tr('零件库'))
|
||||
self.addSubInterface(self.financeInterface, FIF.DOCUMENT, self.tr('财务做账'))
|
||||
self.addSubInterface(self.miniToolInterface, FIF.LIBRARY, self.tr('迷你工具箱'))
|
||||
self.addSubInterface(AboutInterface(self), FIF.INFO, self.tr('关于'), position=NavigationItemPosition.BOTTOM)
|
||||
|
||||
|
||||
542
app/tools/finance_manager.py
Normal file
542
app/tools/finance_manager.py
Normal file
@@ -0,0 +1,542 @@
|
||||
"""
|
||||
财务做账模块 - 数据管理系统
|
||||
管理所有财务账目、图片、文件等数据的存储和检索
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import zipfile
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Tuple, Union
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TransactionType(Enum):
|
||||
"""交易类型"""
|
||||
INVOICE = "invoice" # 发票
|
||||
PAYMENT = "payment" # 支付记录
|
||||
PURCHASE = "purchase" # 购买记录
|
||||
|
||||
|
||||
class Transaction:
|
||||
"""单个交易记录数据模型"""
|
||||
|
||||
def __init__(self, trans_id: Optional[str] = None, date: Optional[str] = None, amount: float = 0.0,
|
||||
trader: str = "", notes: str = "", invoice_path: Optional[str] = None,
|
||||
payment_path: Optional[str] = None, purchase_path: Optional[str] = None):
|
||||
self.id = trans_id or str(uuid.uuid4())
|
||||
self.date = date or datetime.now().strftime("%Y-%m-%d")
|
||||
self.amount = amount
|
||||
self.trader = trader
|
||||
self.notes = notes
|
||||
self.invoice_path = invoice_path # 相对路径
|
||||
self.payment_path = payment_path
|
||||
self.purchase_path = purchase_path
|
||||
self.created_at = datetime.now().isoformat()
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典格式(用于JSON序列化)"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'date': self.date,
|
||||
'amount': self.amount,
|
||||
'trader': self.trader,
|
||||
'notes': self.notes,
|
||||
'invoice_path': self.invoice_path,
|
||||
'payment_path': self.payment_path,
|
||||
'purchase_path': self.purchase_path,
|
||||
'created_at': self.created_at,
|
||||
'updated_at': self.updated_at
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> 'Transaction':
|
||||
"""从字典创建Transaction对象"""
|
||||
trans = cls(
|
||||
trans_id=data.get('id'),
|
||||
date=data.get('date'),
|
||||
amount=data.get('amount', 0.0),
|
||||
trader=data.get('trader', ''),
|
||||
notes=data.get('notes', ''),
|
||||
invoice_path=data.get('invoice_path'),
|
||||
payment_path=data.get('payment_path'),
|
||||
purchase_path=data.get('purchase_path')
|
||||
)
|
||||
if 'created_at' in data:
|
||||
trans.created_at = data['created_at']
|
||||
if 'updated_at' in data:
|
||||
trans.updated_at = data['updated_at']
|
||||
return trans
|
||||
|
||||
|
||||
class Account:
|
||||
"""账户数据模型"""
|
||||
|
||||
def __init__(self, account_id: Optional[str] = None, account_name: str = "", description: str = ""):
|
||||
self.id = account_id or str(uuid.uuid4())
|
||||
self.name = account_name
|
||||
self.description = description
|
||||
self.transactions: List[Transaction] = []
|
||||
self.created_at = datetime.now().isoformat()
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
|
||||
def add_transaction(self, transaction: Transaction) -> None:
|
||||
"""添加交易记录"""
|
||||
self.transactions.append(transaction)
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
|
||||
def remove_transaction(self, trans_id: str) -> bool:
|
||||
"""移除交易记录"""
|
||||
original_len = len(self.transactions)
|
||||
self.transactions = [t for t in self.transactions if t.id != trans_id]
|
||||
if len(self.transactions) < original_len:
|
||||
self.updated_at = datetime.now().isoformat()
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_transaction(self, trans_id: str) -> Optional[Transaction]:
|
||||
"""获取单个交易记录"""
|
||||
for t in self.transactions:
|
||||
if t.id == trans_id:
|
||||
return t
|
||||
return None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'transactions': [t.to_dict() for t in self.transactions],
|
||||
'created_at': self.created_at,
|
||||
'updated_at': self.updated_at
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> 'Account':
|
||||
"""从字典创建Account对象"""
|
||||
account = cls(
|
||||
account_id=data.get('id'),
|
||||
account_name=data.get('name', ''),
|
||||
description=data.get('description', '')
|
||||
)
|
||||
account.transactions = [Transaction.from_dict(t) for t in data.get('transactions', [])]
|
||||
if 'created_at' in data:
|
||||
account.created_at = data['created_at']
|
||||
if 'updated_at' in data:
|
||||
account.updated_at = data['updated_at']
|
||||
return account
|
||||
|
||||
|
||||
class FinanceManager:
|
||||
"""财务管理系统 - 处理所有数据操作和文件管理"""
|
||||
|
||||
def __init__(self, data_root: Optional[str] = None):
|
||||
"""初始化财务管理系统
|
||||
|
||||
Args:
|
||||
data_root: 数据存储根目录,默认为 assets/Finance_Data
|
||||
"""
|
||||
if data_root:
|
||||
self.data_root = Path(data_root)
|
||||
else:
|
||||
# 获取项目根目录
|
||||
import os
|
||||
current_dir = Path(os.getcwd())
|
||||
self.data_root = current_dir / "assets" / "Finance_Data"
|
||||
|
||||
self._ensure_directory_structure()
|
||||
self.accounts: Dict[str, Account] = {}
|
||||
self.load_all_accounts()
|
||||
|
||||
def _ensure_directory_structure(self) -> None:
|
||||
"""确保目录结构完整"""
|
||||
self.data_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 创建子目录
|
||||
subdirs = ['accounts', 'backups', 'images', 'invoices', 'payments', 'purchases']
|
||||
for subdir in subdirs:
|
||||
(self.data_root / subdir).mkdir(exist_ok=True)
|
||||
|
||||
def _get_account_dir(self, account_id: str) -> Path:
|
||||
"""获取账户目录"""
|
||||
account_dir = self.data_root / 'accounts' / account_id
|
||||
account_dir.mkdir(parents=True, exist_ok=True)
|
||||
return account_dir
|
||||
|
||||
def _get_transaction_dir(self, account_id: str, trans_id: str) -> Path:
|
||||
"""获取交易记录目录"""
|
||||
trans_dir = self._get_account_dir(account_id) / trans_id
|
||||
trans_dir.mkdir(parents=True, exist_ok=True)
|
||||
return trans_dir
|
||||
|
||||
def _save_account_metadata(self, account: Account) -> None:
|
||||
"""保存账户元数据(不包含交易详情)"""
|
||||
account_dir = self._get_account_dir(account.id)
|
||||
metadata_file = account_dir / 'metadata.json'
|
||||
|
||||
metadata = {
|
||||
'id': account.id,
|
||||
'name': account.name,
|
||||
'description': account.description,
|
||||
'created_at': account.created_at,
|
||||
'updated_at': account.updated_at
|
||||
}
|
||||
|
||||
with open(metadata_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def _load_account_metadata(self, account_id: str) -> Optional[dict]:
|
||||
"""加载账户元数据"""
|
||||
metadata_file = self._get_account_dir(account_id) / 'metadata.json'
|
||||
if not metadata_file.exists():
|
||||
return None
|
||||
|
||||
with open(metadata_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def _save_transaction_data(self, account_id: str, transaction: Transaction) -> None:
|
||||
"""保存交易记录数据"""
|
||||
trans_dir = self._get_transaction_dir(account_id, transaction.id)
|
||||
data_file = trans_dir / 'data.json'
|
||||
|
||||
with open(data_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(transaction.to_dict(), f, ensure_ascii=False, indent=2)
|
||||
|
||||
def _load_transaction_data(self, account_id: str, trans_id: str) -> Optional[Transaction]:
|
||||
"""加载交易记录数据"""
|
||||
data_file = self._get_transaction_dir(account_id, trans_id) / 'data.json'
|
||||
if not data_file.exists():
|
||||
return None
|
||||
|
||||
with open(data_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return Transaction.from_dict(data)
|
||||
|
||||
def create_account(self, account_name: str, description: str = "") -> Account:
|
||||
"""创建新账户"""
|
||||
account = Account(account_name=account_name, description=description)
|
||||
self.accounts[account.id] = account
|
||||
self._save_account_metadata(account)
|
||||
return account
|
||||
|
||||
def get_account(self, account_id: str) -> Optional[Account]:
|
||||
"""获取账户"""
|
||||
return self.accounts.get(account_id)
|
||||
|
||||
def get_all_accounts(self) -> List[Account]:
|
||||
"""获取所有账户"""
|
||||
return list(self.accounts.values())
|
||||
|
||||
def delete_account(self, account_id: str) -> bool:
|
||||
"""删除账户及其所有数据"""
|
||||
if account_id not in self.accounts:
|
||||
return False
|
||||
|
||||
account_dir = self._get_account_dir(account_id)
|
||||
try:
|
||||
shutil.rmtree(account_dir)
|
||||
del self.accounts[account_id]
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"删除账户出错: {e}")
|
||||
return False
|
||||
|
||||
def update_account(self, account_id: str, account_name: Optional[str] = None, description: Optional[str] = None) -> bool:
|
||||
"""更新账户信息"""
|
||||
account = self.accounts.get(account_id)
|
||||
if not account:
|
||||
return False
|
||||
|
||||
if account_name:
|
||||
account.name = account_name
|
||||
if description is not None:
|
||||
account.description = description
|
||||
|
||||
account.updated_at = datetime.now().isoformat()
|
||||
self._save_account_metadata(account)
|
||||
return True
|
||||
|
||||
def add_transaction(self, account_id: str, transaction: Transaction) -> bool:
|
||||
"""添加交易记录到账户"""
|
||||
account = self.accounts.get(account_id)
|
||||
if not account:
|
||||
return False
|
||||
|
||||
account.add_transaction(transaction)
|
||||
self._save_transaction_data(account_id, transaction)
|
||||
return True
|
||||
|
||||
def save_image_for_transaction(self, account_id: str, trans_id: str,
|
||||
image_type: TransactionType, image_path: str) -> Optional[str]:
|
||||
"""保存交易相关的图片,返回相对路径"""
|
||||
trans_dir = self._get_transaction_dir(account_id, trans_id)
|
||||
|
||||
source_path = Path(image_path)
|
||||
if not source_path.exists():
|
||||
return None
|
||||
|
||||
# 保存图片到对应的图片目录
|
||||
dest_dir = trans_dir / image_type.value
|
||||
dest_dir.mkdir(exist_ok=True)
|
||||
|
||||
dest_path = dest_dir / source_path.name
|
||||
shutil.copy2(source_path, dest_path)
|
||||
|
||||
# 返回相对于data_root的路径
|
||||
relative_path = str(dest_path.relative_to(self.data_root))
|
||||
return relative_path
|
||||
|
||||
def get_transaction_image_path(self, account_id: str, relative_path: str) -> Optional[Path]:
|
||||
"""获取交易图片的完整路径"""
|
||||
if not relative_path:
|
||||
return None
|
||||
return self.data_root / relative_path
|
||||
|
||||
def delete_transaction(self, account_id: str, trans_id: str) -> bool:
|
||||
"""删除交易记录"""
|
||||
account = self.accounts.get(account_id)
|
||||
if not account:
|
||||
return False
|
||||
|
||||
# 删除磁盘上的文件
|
||||
trans_dir = self._get_transaction_dir(account_id, trans_id)
|
||||
try:
|
||||
shutil.rmtree(trans_dir)
|
||||
except Exception as e:
|
||||
print(f"删除交易记录文件出错: {e}")
|
||||
|
||||
# 从账户中移除
|
||||
return account.remove_transaction(trans_id)
|
||||
|
||||
def update_transaction(self, account_id: str, trans_id: str, **kwargs) -> bool:
|
||||
"""更新交易记录"""
|
||||
account = self.accounts.get(account_id)
|
||||
if not account:
|
||||
return False
|
||||
|
||||
transaction = account.get_transaction(trans_id)
|
||||
if not transaction:
|
||||
return False
|
||||
|
||||
# 更新允许的字段
|
||||
allowed_fields = ['date', 'amount', 'trader', 'notes', 'invoice_path',
|
||||
'payment_path', 'purchase_path']
|
||||
for field, value in kwargs.items():
|
||||
if field in allowed_fields:
|
||||
setattr(transaction, field, value)
|
||||
|
||||
transaction.updated_at = datetime.now().isoformat()
|
||||
self._save_transaction_data(account_id, transaction)
|
||||
return True
|
||||
|
||||
def get_transaction(self, account_id: str, trans_id: str) -> Optional[Transaction]:
|
||||
"""获取单个交易记录"""
|
||||
account = self.accounts.get(account_id)
|
||||
if not account:
|
||||
return None
|
||||
return account.get_transaction(trans_id)
|
||||
|
||||
def query_transactions(self, account_id: str, date_start: Optional[str] = None,
|
||||
date_end: Optional[str] = None, amount_min: Optional[float] = None,
|
||||
amount_max: Optional[float] = None, trader: Optional[str] = None) -> List[Transaction]:
|
||||
"""查询交易记录(支持多条件筛选)"""
|
||||
account = self.accounts.get(account_id)
|
||||
if not account:
|
||||
return []
|
||||
|
||||
results = []
|
||||
for trans in account.transactions:
|
||||
# 日期范围筛选
|
||||
if date_start and trans.date < date_start:
|
||||
continue
|
||||
if date_end and trans.date > date_end:
|
||||
continue
|
||||
|
||||
# 金额范围筛选
|
||||
if amount_min is not None and trans.amount < amount_min:
|
||||
continue
|
||||
if amount_max is not None and trans.amount > amount_max:
|
||||
continue
|
||||
|
||||
# 交易人筛选(模糊匹配)
|
||||
if trader and trader.lower() not in trans.trader.lower():
|
||||
continue
|
||||
|
||||
results.append(trans)
|
||||
|
||||
# 按日期排序
|
||||
results.sort(key=lambda x: x.date, reverse=True)
|
||||
return results
|
||||
|
||||
def get_account_summary(self, account_id: str) -> Optional[dict]:
|
||||
"""获取账户汇总信息"""
|
||||
account = self.accounts.get(account_id)
|
||||
if not account:
|
||||
return None
|
||||
|
||||
total_amount = sum(t.amount for t in account.transactions)
|
||||
transaction_count = len(account.transactions)
|
||||
|
||||
return {
|
||||
'account_id': account_id,
|
||||
'account_name': account.name,
|
||||
'total_amount': total_amount,
|
||||
'transaction_count': transaction_count,
|
||||
'created_at': account.created_at,
|
||||
'updated_at': account.updated_at
|
||||
}
|
||||
|
||||
def export_account_package(self, account_id: str, export_path: str) -> bool:
|
||||
"""导出账户为可转移的压缩包"""
|
||||
account_dir = self._get_account_dir(account_id)
|
||||
if not account_dir.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
export_file = Path(export_path) / f"{self.accounts[account_id].name}_{account_id}.zip"
|
||||
|
||||
with zipfile.ZipFile(export_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(account_dir):
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
arcname = file_path.relative_to(account_dir)
|
||||
zipf.write(file_path, arcname)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"导出账户出错: {e}")
|
||||
return False
|
||||
|
||||
def import_account_package(self, zip_path: str) -> Optional[str]:
|
||||
"""导入账户压缩包,返回导入的账户ID"""
|
||||
try:
|
||||
zip_path = Path(zip_path)
|
||||
if not zip_path.exists():
|
||||
return None
|
||||
|
||||
# 先加载元数据以获取账户ID
|
||||
with zipfile.ZipFile(zip_path, 'r') as zipf:
|
||||
metadata_content = zipf.read('metadata.json')
|
||||
metadata = json.loads(metadata_content)
|
||||
account_id = metadata['id']
|
||||
|
||||
# 如果账户已存在,创建新ID
|
||||
if account_id in self.accounts:
|
||||
account_id = str(uuid.uuid4())
|
||||
# 更新元数据中的ID
|
||||
metadata['id'] = account_id
|
||||
|
||||
# 解压到临时目录
|
||||
temp_dir = self.data_root / f"_temp_{uuid.uuid4()}"
|
||||
with zipfile.ZipFile(zip_path, 'r') as zipf:
|
||||
zipf.extractall(temp_dir)
|
||||
|
||||
# 更新临时目录中的元数据文件
|
||||
metadata_file = temp_dir / 'metadata.json'
|
||||
with open(metadata_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# 移动到正式目录
|
||||
account_dir = self._get_account_dir(account_id)
|
||||
if account_dir.exists():
|
||||
shutil.rmtree(account_dir)
|
||||
|
||||
shutil.move(str(temp_dir), str(account_dir))
|
||||
|
||||
# 重新加载账户
|
||||
self.load_all_accounts()
|
||||
return account_id
|
||||
except Exception as e:
|
||||
print(f"导入账户出错: {e}")
|
||||
return None
|
||||
|
||||
def backup_all_accounts(self) -> bool:
|
||||
"""备份所有账户"""
|
||||
try:
|
||||
backup_dir = self.data_root / 'backups'
|
||||
backup_dir.mkdir(exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_file = backup_dir / f"backup_{timestamp}.zip"
|
||||
|
||||
accounts_dir = self.data_root / 'accounts'
|
||||
with zipfile.ZipFile(backup_file, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for root, dirs, files in os.walk(accounts_dir):
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
arcname = file_path.relative_to(self.data_root / 'accounts')
|
||||
zipf.write(file_path, arcname)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"备份账户出错: {e}")
|
||||
return False
|
||||
|
||||
def load_all_accounts(self) -> None:
|
||||
"""加载所有账户"""
|
||||
self.accounts.clear()
|
||||
accounts_dir = self.data_root / 'accounts'
|
||||
|
||||
if not accounts_dir.exists():
|
||||
return
|
||||
|
||||
for account_dir in accounts_dir.iterdir():
|
||||
if not account_dir.is_dir():
|
||||
continue
|
||||
|
||||
metadata = self._load_account_metadata(account_dir.name)
|
||||
if not metadata:
|
||||
continue
|
||||
|
||||
account = Account(
|
||||
account_id=metadata['id'],
|
||||
account_name=metadata['name'],
|
||||
description=metadata.get('description', '')
|
||||
)
|
||||
account.created_at = metadata.get('created_at')
|
||||
account.updated_at = metadata.get('updated_at')
|
||||
|
||||
# 加载该账户的所有交易记录
|
||||
trans_dirs = [d for d in account_dir.iterdir()
|
||||
if d.is_dir() and d.name not in ['invoice', 'payment', 'purchase']]
|
||||
|
||||
for trans_dir in trans_dirs:
|
||||
transaction = self._load_transaction_data(account_dir.name, trans_dir.name)
|
||||
if transaction:
|
||||
account.transactions.append(transaction)
|
||||
|
||||
# 按日期排序
|
||||
account.transactions.sort(key=lambda x: x.date, reverse=True)
|
||||
|
||||
self.accounts[account.id] = account
|
||||
|
||||
def export_to_csv(self, account_id: str, csv_path: str) -> bool:
|
||||
"""导出账户数据为CSV格式"""
|
||||
account = self.accounts.get(account_id)
|
||||
if not account:
|
||||
return False
|
||||
|
||||
try:
|
||||
import csv
|
||||
with open(csv_path, 'w', newline='', encoding='utf-8-sig') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['日期', '金额', '交易人', '备注', '创建时间'])
|
||||
|
||||
for trans in account.transactions:
|
||||
writer.writerow([
|
||||
trans.date,
|
||||
trans.amount,
|
||||
trans.trader,
|
||||
trans.notes,
|
||||
trans.created_at
|
||||
])
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"导出CSV出错: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user