PySide6 SQLite3 做的 电脑组装报价系统

一、数据库结构说明

1. 配件类别表 (component_categories)

字段名 类型 说明 约束
category_id INTEGER 类别ID PRIMARY KEY, AUTOINCREMENT
category_name TEXT 类别名称 NOT NULL, UNIQUE
description TEXT 类别描述

2. 配件表 (components)

字段名 类型 说明 约束
component_id INTEGER 配件ID PRIMARY KEY, AUTOINCREMENT
category_id INTEGER 类别ID NOT NULL, FOREIGN KEY
name TEXT 配件名称 NOT NULL
stock INTEGER 库存数量 DEFAULT 0
purchase_price REAL 采购价格 NOT NULL
selling_price REAL 销售价格 NOT NULL

3. 客户表 (customers)

字段名 类型 说明 约束
customer_id INTEGER 客户ID PRIMARY KEY, AUTOINCREMENT
name TEXT 客户名称 NOT NULL
phone TEXT 电话号码
wechat TEXT 微信号

4. 报价单表 (quotations)

字段名 类型 说明 约束
quotation_id INTEGER 报价单ID PRIMARY KEY, AUTOINCREMENT
customer_id INTEGER 客户ID NOT NULL, FOREIGN KEY
quotation_date DATE 报价日期 NOT NULL
total_amount REAL 总金额 DEFAULT 0
total_cost REAL 总成本 DEFAULT 0
pricing_type TEXT 定价类型 DEFAULT 'percentage'
markup_value REAL 加价值 DEFAULT 15
final_amount REAL 最终金额 DEFAULT 0
valid_days INTEGER 有效天数 DEFAULT 7
notes TEXT 备注
status TEXT 状态 DEFAULT '待确认'

5. 报价单明细表 (quotation_details)

字段名 类型 说明 约束
detail_id INTEGER 明细ID PRIMARY KEY, AUTOINCREMENT
quotation_id INTEGER 报价单ID NOT NULL, FOREIGN KEY
component_id INTEGER 配件ID NOT NULL, FOREIGN KEY
quantity INTEGER 数量 DEFAULT 1
pricing_type TEXT 定价类型 NOT NULL, DEFAULT 'fixed'
markup_value REAL 加价值 DEFAULT 0
unit_price REAL 单价 NOT NULL
cost_price REAL 成本价 NOT NULL
subtotal REAL 小计 NOT NULL

表关系说明

  1. components -> component_categories:通过 category_id 关联

  2. quotations -> customers:通过 customer_id 关联

  3. quotation_details -> quotations:通过 quotation_id 关联

  4. quotation_details -> components:通过 component_id 关联

枚举值说明

pricing_type (定价类型)

  • fixed:固定价格

  • percentage:百分比加价

  • markup:固定加价

status (报价单状态)

  • 待确认

  • 已接受

  • 已拒绝

  • 已过期

  • 已完成

二、 程序中SQLite3的数据库表结构:

  1. 配件类别表 (component_categories)
复制代码
CREATE TABLE component_categories (
    category_id INTEGER PRIMARY KEY AUTOINCREMENT,
    category_name TEXT NOT NULL UNIQUE,
    description TEXT
)
  1. 配件表 (components)
复制代码
CREATE TABLE components (
    component_id INTEGER PRIMARY KEY AUTOINCREMENT,
    category_id INTEGER NOT NULL,
    name TEXT NOT NULL,
    stock INTEGER DEFAULT 0,
    purchase_price REAL NOT NULL,
    selling_price REAL NOT NULL,
    FOREIGN KEY (category_id) REFERENCES component_categories (category_id)
)
  1. 客户表 (customers)
复制代码
CREATE TABLE customers (
    customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    phone TEXT,
    wechat TEXT
)
  1. 报价单表 (quotations)
复制代码
CREATE TABLE quotations (
    quotation_id INTEGER PRIMARY KEY AUTOINCREMENT,
    customer_id INTEGER NOT NULL,
    quotation_date DATE NOT NULL,
    total_amount REAL DEFAULT 0,
    total_cost REAL DEFAULT 0,
    pricing_type TEXT DEFAULT 'percentage',
    markup_value REAL DEFAULT 15,
    final_amount REAL DEFAULT 0,
    valid_days INTEGER DEFAULT 7,
    notes TEXT,
    status TEXT DEFAULT '待确认',
    FOREIGN KEY (customer_id) REFERENCES customers (customer_id)
)
  1. 报价单明细表 (quotation_details)
复制代码
CREATE TABLE quotation_details (
    detail_id INTEGER PRIMARY KEY AUTOINCREMENT,
    quotation_id INTEGER NOT NULL,
    component_id INTEGER NOT NULL,
    quantity INTEGER DEFAULT 1,
    pricing_type TEXT NOT NULL DEFAULT 'fixed',
    markup_value REAL DEFAULT 0,
    unit_price REAL NOT NULL,
    cost_price REAL NOT NULL,
    subtotal REAL NOT NULL,
    FOREIGN KEY (quotation_id) REFERENCES quotations (quotation_id),
    FOREIGN KEY (component_id) REFERENCES components (component_id)
)

表之间的关系:

  1. components 表通过 category_id 关联到 component_categories 表

  2. quotations 表通过 customer_id 关联到 customers 表

  3. quotation_details 表通过 quotation_id 关联到 quotations 表

  4. quotation_details 表通过 component_id 关联到 components 表

主要字段说明:

  • pricing_type: 'fixed'(固定价格), 'percentage'(百分比加价), 'markup'(固定加价)

  • status: '待确认', '已接受', '已拒绝', '已过期', '已完成'

三、Pyside6 代码

main.py

python 复制代码
import sys
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import QFile
from views.main_window import MainWindow
from database.db_manager import DatabaseManager
from utils.excel_exporter import QuoteExporter

def main():
    # 初始化数据库
    db = DatabaseManager('db/computer_quote.db')
    
    # 创建QT应用
    app = QApplication(sys.argv)
    
    # 加载样式表
    style_file = QFile("style/style.qss")
    if style_file.open(QFile.ReadOnly | QFile.Text):
        style_sheet = str(style_file.readAll(), encoding='utf-8')
        app.setStyleSheet(style_sheet)
    
    # 创建主窗口
    window = MainWindow(db)
    window.show()
    
    sys.exit(app.exec())

if __name__ == '__main__':
    main() 

views\main_window.py

python 复制代码
from PySide6.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, 
                              QTabWidget, QPushButton, QMessageBox, QFileDialog,
                              QToolBar)
from PySide6.QtGui import QAction, QIcon
from .components import ComponentsTab
from .quotations import QuotationsTab
from .customers import CustomersTab
from utils.excel_exporter import QuoteExporter
from datetime import datetime

class MainWindow(QMainWindow):
    def __init__(self, db_manager):
        super().__init__()
        self.db = db_manager
        self.init_ui()
        
    def init_ui(self):
        """初始化主窗口UI"""
        self.setWindowTitle('电脑组装报价系统')
        self.setGeometry(100, 100, 1200, 800)
        
        # 创建中央部件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 创建主布局
        layout = QVBoxLayout(central_widget)
        
        # 创建选项卡
        tabs = QTabWidget()
        tabs.addTab(ComponentsTab(self.db), '配件管理')
        tabs.addTab(QuotationsTab(self.db), '报价单')
        tabs.addTab(CustomersTab(self.db), '客户管理')
        
        layout.addWidget(tabs) 

views\components.py

python 复制代码
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, 
                              QTableWidget, QPushButton, QComboBox,
                              QLabel, QLineEdit, QSpinBox, QDoubleSpinBox,
                              QTableWidgetItem, QMessageBox, QDialog,
                              QDialogButtonBox, QFormLayout, QHeaderView,
                              QFileDialog)
from PySide6.QtCore import Qt
from datetime import datetime

class AddCategoryDialog(QDialog):
    """添加配件类别的对话框"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("添加配件类别")
        self.setup_ui()

    def setup_ui(self):
        layout = QFormLayout(self)
        
        # 创建输入框
        self.name_input = QLineEdit()
        self.description_input = QLineEdit()
        
        # 添加到布局
        layout.addRow("类别名称:", self.name_input)
        layout.addRow("描述:", self.description_input)
        
        # 添加按钮
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            parent=self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addRow(buttons)

class EditComponentDialog(QDialog):
    """编辑配件的对话框"""
    def __init__(self, db_manager, component_data, parent=None):
        super().__init__(parent)
        self.db = db_manager
        self.component_data = component_data  # (id, category_name, name, stock, purchase_price, selling_price)
        self.setWindowTitle("编辑配件")
        self.setup_ui()

    def setup_ui(self):
        layout = QFormLayout(self)
        
        # 类别选择
        self.category_combo = QComboBox()
        categories = self.db.get_all_categories()
        current_category = None
        for category in categories:
            self.category_combo.addItem(category[1], category[0])
            if category[1] == self.component_data[1]:  # 匹配当前类别
                current_category = self.category_combo.count() - 1
        if current_category is not None:
            self.category_combo.setCurrentIndex(current_category)
        
        # 名称输入
        self.name_input = QLineEdit(self.component_data[2])
        
        # 库存输入
        self.stock_input = QSpinBox()
        self.stock_input.setRange(0, 9999)
        self.stock_input.setValue(self.component_data[3])
        
        # 价格输入
        self.purchase_price_input = QDoubleSpinBox()
        self.purchase_price_input.setRange(0, 999999)
        self.purchase_price_input.setPrefix("¥")
        self.purchase_price_input.setValue(self.component_data[4])
        
        self.selling_price_input = QDoubleSpinBox()
        self.selling_price_input.setRange(0, 999999)
        self.selling_price_input.setPrefix("¥")
        self.selling_price_input.setValue(self.component_data[5])
        
        # 添加到布局
        layout.addRow("类别:", self.category_combo)
        layout.addRow("名称:", self.name_input)
        layout.addRow("库存:", self.stock_input)
        layout.addRow("采购价:", self.purchase_price_input)
        layout.addRow("销售价:", self.selling_price_input)
        
        # 添加按钮
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            parent=self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addRow(buttons)

class ComponentsTab(QWidget):
    def __init__(self, db_manager):
        super().__init__()
        self.db = db_manager
        self.init_ui()
        
    def init_ui(self):
        """初始化配件管理界面"""
        layout = QVBoxLayout(self)
        
        
        # 添加搜索区域
        search_layout = QHBoxLayout()
        
        # 搜索输入框
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText("输入配件名称搜索...")
        self.search_input.textChanged.connect(self.search_components)
        
        # 搜索按钮
        search_btn = QPushButton("搜索")
        search_btn.clicked.connect(self.search_components)
        
        # 清除搜索按钮
        clear_btn = QPushButton("清除搜索")
        clear_btn.clicked.connect(self.clear_search)
        
        search_layout.addWidget(QLabel("搜索:"))
        search_layout.addWidget(self.search_input)
        search_layout.addWidget(search_btn)
        search_layout.addWidget(clear_btn)
        search_layout.addStretch()
        
        layout.addLayout(search_layout)
        
        # 添加导入导出按钮
        import_export_layout = QHBoxLayout()
        
        # 导出按钮
        export_btn = QPushButton("导出配件")
        export_btn.clicked.connect(self.export_components)
        
        # 导入按钮
        import_btn = QPushButton("导入配件")
        import_btn.clicked.connect(self.import_components)
        
        import_export_layout.addWidget(export_btn)
        import_export_layout.addWidget(import_btn)
        import_export_layout.addStretch()
        
        layout.addLayout(import_export_layout)
        
        
        # 类别管理区域
        category_layout = QHBoxLayout()
        category_layout.addWidget(QLabel("配件类别:"))
        self.category_combo = QComboBox()
        category_layout.addWidget(self.category_combo)
        
        add_category_btn = QPushButton("添加类别")
        add_category_btn.clicked.connect(self.add_category)
        category_layout.addWidget(add_category_btn)
        
        delete_category_btn = QPushButton("删除类别")
        delete_category_btn.clicked.connect(self.delete_category)
        category_layout.addWidget(delete_category_btn)
        
        layout.addLayout(category_layout)
        
        # 添加配件区域
        add_component_layout = QHBoxLayout()
        
        # 配件信息输入
        self.name_input = QLineEdit()
        self.name_input.setPlaceholderText("配件名称")
        add_component_layout.addWidget(QLabel("名称:"))
        add_component_layout.addWidget(self.name_input)
        
        # 采购价输入框改为普通输入框
        self.purchase_price_input = QLineEdit()
        self.purchase_price_input.setPlaceholderText("采购价")
        add_component_layout.addWidget(QLabel("采购价:"))
        add_component_layout.addWidget(self.purchase_price_input)
        
        # 销售价输入框改为普通输入框
        self.selling_price_input = QLineEdit()
        self.selling_price_input.setPlaceholderText("销售价")
        add_component_layout.addWidget(QLabel("销售价:"))
        add_component_layout.addWidget(self.selling_price_input)
        
        # 库存输入框改为普通输入框
        self.stock_input = QLineEdit()
        self.stock_input.setPlaceholderText("库存")
        add_component_layout.addWidget(QLabel("库存:"))
        add_component_layout.addWidget(self.stock_input)
        
        # 添加按钮
        add_btn = QPushButton("添加配件")
        add_btn.clicked.connect(self.add_component)
        add_component_layout.addWidget(add_btn)
        
        layout.addLayout(add_component_layout)
        
        # 配件列表
        self.components_table = QTableWidget()
        self.components_table.setColumnCount(7)
        self.components_table.setHorizontalHeaderLabels([
            "ID", "类别", "名称", "库存", "采购价", "销售价", "操作"
        ])
        layout.addWidget(self.components_table)
        
        # 刷新数据
        self.refresh_categories()
        self.refresh_components()
        
        # 在表格初始化后添加
        self.components_table.verticalHeader().setDefaultSectionSize(60)
        self.components_table.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.components_table.setColumnWidth(6, 250)  # 操作列宽度改为250像素
        self.components_table.horizontalHeader().setFixedHeight(35)
    
    def refresh_categories(self):
        """刷新类别下拉框"""
        self.category_combo.clear()
        categories = self.db.get_all_categories()  # 需要在 DatabaseManager 中实现
        for category in categories:
            self.category_combo.addItem(category[1], category[0])  # 显示名称,存储ID
    
    def refresh_components(self):
        """刷新配件列表"""
        self.components_table.setRowCount(0)
        components = self.db.get_all_components()  # 需要在 DatabaseManager 中实现
        
        for row, comp in enumerate(components):
            self.components_table.insertRow(row)
            
            # 添加数据
            self.components_table.setItem(row, 0, QTableWidgetItem(str(comp[0])))  # ID
            self.components_table.setItem(row, 1, QTableWidgetItem(comp[1]))  # 类别名称
            self.components_table.setItem(row, 2, QTableWidgetItem(comp[2]))  # 配件名称
            self.components_table.setItem(row, 3, QTableWidgetItem(str(comp[3])))  # 库存
            self.components_table.setItem(row, 4, QTableWidgetItem(f"¥{comp[4]:.2f}"))  # 采购价
            self.components_table.setItem(row, 5, QTableWidgetItem(f"¥{comp[5]:.2f}"))  # 销售价
            
            # 添加操作按钮
            btn_layout = QHBoxLayout()
            edit_btn = QPushButton("编辑")
            delete_btn = QPushButton("删除")
            
            edit_btn.clicked.connect(lambda checked, r=row: self.edit_component(r))
            delete_btn.clicked.connect(lambda checked, r=row: self.delete_component(r))
            
            btn_widget = QWidget()
            btn_layout.addWidget(edit_btn)
            btn_layout.addWidget(delete_btn)
            btn_widget.setLayout(btn_layout)
            
            self.components_table.setCellWidget(row, 6, btn_widget)
    
    def add_category(self):
        """添加新类别"""
        dialog = AddCategoryDialog(self)
        if dialog.exec_():
            name = dialog.name_input.text().strip()
            description = dialog.description_input.text().strip()
            
            if name:
                try:
                    self.db.add_component_category(name, description)
                    self.refresh_categories()
                    QMessageBox.information(self, "成功", "类别添加成功!")
                except Exception as e:
                    QMessageBox.warning(self, "错误", f"添加类别失败:{str(e)}")
            else:
                QMessageBox.warning(self, "错误", "类别名称不能为空!")
    
    def delete_category(self):
        """删除类别"""
        current_category_id = self.category_combo.currentData()
        if current_category_id is None:
            QMessageBox.warning(self, "错误", "请先选择要删除的类别!")
            return
            
        reply = QMessageBox.question(self, "确认删除", 
                                   "确定要删除这个类别吗?这将同时删除该类别下的所有配件!",
                                   QMessageBox.Yes | QMessageBox.No)
        
        if reply == QMessageBox.Yes:
            try:
                self.db.delete_category(current_category_id)  # 需要在 DatabaseManager 中实现
                self.refresh_categories()
                self.refresh_components()
                QMessageBox.information(self, "成功", "类别删除成功!")
            except Exception as e:
                QMessageBox.warning(self, "错误", f"删除类别失败:{str(e)}")
    
    def add_component(self):
        """添加新配件"""
        category_id = self.category_combo.currentData()
        name = self.name_input.text().strip()
        
        # 获取并验证价格和库存
        try:
            purchase_price = float(self.purchase_price_input.text().strip() or '0')
            selling_price = float(self.selling_price_input.text().strip() or '0')
            stock = int(self.stock_input.text().strip() or '0')
            
            if purchase_price < 0 or selling_price < 0 or stock < 0:
                raise ValueError("价格和库存不能为负数")
            
        except ValueError as e:
            QMessageBox.warning(self, "错误", f"请输入有效的价格和库存!\n{str(e)}")
            return
        
        if not all([category_id, name]):
            QMessageBox.warning(self, "错误", "请填写所有必要信息!")
            return
        
        try:
            self.db.add_component(category_id, name, purchase_price, selling_price, stock)
            
            # 清空输入
            self.name_input.clear()
            self.purchase_price_input.clear()
            self.selling_price_input.clear()
            self.stock_input.clear()
            
            # 刷新列表
            self.refresh_components()
            QMessageBox.information(self, "成功", "配件添加成功!")
        except Exception as e:
            QMessageBox.warning(self, "错误", f"添加配件失败:{str(e)}")
    
    def edit_component(self, row):
        """编辑配件"""
        component_id = int(self.components_table.item(row, 0).text())
        component_data = (
            component_id,
            self.components_table.item(row, 1).text(),  # category_name
            self.components_table.item(row, 2).text(),  # name
            int(self.components_table.item(row, 3).text()),  # stock
            float(self.components_table.item(row, 4).text().replace('¥', '')),  # purchase_price
            float(self.components_table.item(row, 5).text().replace('¥', ''))   # selling_price
        )
        
        dialog = EditComponentDialog(self.db, component_data, self)
        if dialog.exec_():
            try:
                # 获取编辑后的数据
                category_id = dialog.category_combo.currentData()
                name = dialog.name_input.text().strip()
                stock = dialog.stock_input.value()
                purchase_price = dialog.purchase_price_input.value()
                selling_price = dialog.selling_price_input.value()
                
                if not all([category_id, name, purchase_price, selling_price]):
                    QMessageBox.warning(self, "错误", "请填写所有必要信息!")
                    return
                
                # 更新数据库
                self.db.update_component(
                    component_id, category_id, name, 
                    purchase_price, selling_price, stock
                )
                
                # 刷新显示
                self.refresh_components()
                QMessageBox.information(self, "成功", "配件更新成功!")
                
            except Exception as e:
                QMessageBox.warning(self, "错误", f"更新配件失败:{str(e)}")
    
    def delete_component(self, row):
        """删除配件"""
        component_id = int(self.components_table.item(row, 0).text())
        reply = QMessageBox.question(self, "确认删除", 
                                   "确定要删除这个配件吗?",
                                   QMessageBox.Yes | QMessageBox.No)
        
        if reply == QMessageBox.Yes:
            try:
                self.db.delete_component(component_id)  # 需要在 DatabaseManager 中实现
                self.refresh_components()
                QMessageBox.information(self, "成功", "配件删除成功!")
            except Exception as e:
                QMessageBox.warning(self, "错误", f"删除配件失败:{str(e)}")
    
    def search_components(self):
        """搜索配件"""
        search_text = self.search_input.text().strip().lower()
        
        # 遍历所有行
        for row in range(self.components_table.rowCount()):
            # 获取配件名称
            name_item = self.components_table.item(row, 2)  # 假设名称在第3列
            if name_item:
                name = name_item.text().lower()
                # 如果搜索文本为空或者名称包含搜索文本,则显示该行
                should_hide = bool(search_text and search_text not in name)  # 转换为 bool 类型
                self.components_table.setRowHidden(row, should_hide)
    
    def clear_search(self):
        """清除搜索"""
        self.search_input.clear()
        # 显示所有行
        for row in range(self.components_table.rowCount()):
            self.components_table.setRowHidden(row, False) 
    
    def export_components(self):
        """导出配件到Excel"""
        file_name, _ = QFileDialog.getSaveFileName(
            self,
            "导出配件",
            f"配件列表_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
            "Excel Files (*.xlsx)"
        )
        
        if file_name:
            try:
                # 获取所有配件数据
                components = self.db.get_all_components()
                # 转换为字典列表
                components_data = []
                for comp in components:
                    components_data.append({
                        'category_name': comp[1],
                        'name': comp[2],
                        'stock': comp[3],
                        'purchase_price': comp[4],
                        'selling_price': comp[5]
                    })
                
                # 导出到Excel
                from utils.component_excel_handler import ComponentExcelHandler
                ComponentExcelHandler.export_to_excel(components_data, file_name)
                QMessageBox.information(self, "成功", "配件导出成功!")
            except Exception as e:
                QMessageBox.critical(self, "错误", f"导出失败:{str(e)}")
    
    def import_components(self):
        """从Excel导入配件"""
        file_name, _ = QFileDialog.getOpenFileName(
            self,
            "导入配件",
            "",
            "Excel Files (*.xlsx)"
        )
        
        if file_name:
            try:
                # 从Excel读取数据
                from utils.component_excel_handler import ComponentExcelHandler
                components = ComponentExcelHandler.import_from_excel(file_name)
                
                # 确认导入
                reply = QMessageBox.question(
                    self,
                    "确认导入",
                    f"确定要导入 {len(components)} 个配件吗?",
                    QMessageBox.Yes | QMessageBox.No
                )
                
                if reply == QMessageBox.Yes:
                    # 导入每个配件
                    for comp in components:
                        # 先查找或创建类别
                        category_id = self._get_or_create_category(comp['category_name'])
                        # 添加配件
                        self.db.add_component(
                            category_id,
                            comp['name'],
                            comp['purchase_price'],
                            comp['selling_price'],
                            comp['stock']
                        )
                    
                    # 刷新显示
                    self.refresh_categories()
                    self.refresh_components()
                    QMessageBox.information(self, "成功", "配件导入成功!")
            except Exception as e:
                QMessageBox.critical(self, "错误", f"导入失败:{str(e)}")
    
    def _get_or_create_category(self, category_name):
        """获取或创建类别"""
        # 查找现有类别
        categories = self.db.get_all_categories()
        for category in categories:
            if category[1] == category_name:
                return category[0]
        
        # 如果不存在,创建新类别
        return self.db.add_component_category(category_name) 

views\customers.py

python 复制代码
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout,
                              QTableWidget, QPushButton, QLineEdit,
                              QLabel, QMessageBox, QTableWidgetItem,
                              QDialog, QDialogButtonBox, QFormLayout,
                              QHeaderView, QFileDialog)
from datetime import datetime

class EditCustomerDialog(QDialog):
    """编辑客户的对话框"""
    def __init__(self, customer_data, parent=None):
        super().__init__(parent)
        self.customer_data = customer_data  # (id, name, phone, wechat)
        self.setWindowTitle("编辑客户")
        self.setup_ui()

    def setup_ui(self):
        layout = QFormLayout(self)
        
        # 创建输入框
        self.name_input = QLineEdit(self.customer_data[1])
        self.phone_input = QLineEdit(self.customer_data[2])
        self.wechat_input = QLineEdit(self.customer_data[3])
        
        # 添加到布局
        layout.addRow("客户名称:", self.name_input)
        layout.addRow("电话:", self.phone_input)
        layout.addRow("微信:", self.wechat_input)
        
        # 添加按钮
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            parent=self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addRow(buttons)

class CustomersTab(QWidget):
    def __init__(self, db_manager):
        super().__init__()
        self.db = db_manager
        self.init_ui()
        
    def init_ui(self):
        """初始化客户管理界面"""
        layout = QVBoxLayout(self)
        
        # 添加搜索区域
        search_layout = QHBoxLayout()
        
        # 搜索输入框
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText("输入客户姓名、电话或微信搜索...")
        self.search_input.textChanged.connect(self.search_customers)
        
        # 搜索按钮
        search_btn = QPushButton("搜索")
        search_btn.clicked.connect(self.search_customers)
        
        # 清除搜索按钮
        clear_btn = QPushButton("清除搜索")
        clear_btn.clicked.connect(self.clear_search)
        
        search_layout.addWidget(QLabel("搜索:"))
        search_layout.addWidget(self.search_input)
        search_layout.addWidget(search_btn)
        search_layout.addWidget(clear_btn)
        search_layout.addStretch()
        
        layout.addLayout(search_layout)
        
        # 添加导入导出按钮
        import_export_layout = QHBoxLayout()
        
        # 导出按钮
        export_btn = QPushButton("导出客户")
        export_btn.clicked.connect(self.export_customers)
        
        # 导入按钮
        import_btn = QPushButton("导入客户")
        import_btn.clicked.connect(self.import_customers)
        
        import_export_layout.addWidget(export_btn)
        import_export_layout.addWidget(import_btn)
        import_export_layout.addStretch()
        
        layout.addLayout(import_export_layout)
        
        # 添加客户区域
        add_customer_layout = QHBoxLayout()
        
        # 客户信息输入
        self.name_input = QLineEdit()
        self.name_input.setPlaceholderText('客户名称')
        
        self.phone_input = QLineEdit()
        self.phone_input.setPlaceholderText('电话')
        
        self.wechat_input = QLineEdit()
        self.wechat_input.setPlaceholderText('微信')
        
        # 添加按钮
        add_btn = QPushButton('添加客户')
        add_btn.clicked.connect(self.add_customer)
        
        # 将控件添加到水平布局
        add_customer_layout.addWidget(QLabel('客户名称:'))
        add_customer_layout.addWidget(self.name_input)
        add_customer_layout.addWidget(QLabel('电话:'))
        add_customer_layout.addWidget(self.phone_input)
        add_customer_layout.addWidget(QLabel('微信:'))
        add_customer_layout.addWidget(self.wechat_input)
        add_customer_layout.addWidget(add_btn)
        
        # 客户列表
        self.customers_table = QTableWidget()
        self.customers_table.setColumnCount(5)
        self.customers_table.setHorizontalHeaderLabels([
            "ID", "客户名称", "电话", "微信", "操作"
        ])
        
        # 添加到主布局
        layout.addLayout(add_customer_layout)
        layout.addWidget(self.customers_table)
        
        # 刷新数据
        self.refresh_customers()
        
        # 在表格初始化后添加
        self.customers_table.verticalHeader().setDefaultSectionSize(60)
        self.customers_table.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)
        self.customers_table.setColumnWidth(4, 250)  # 操作列宽度改为250像素
        self.customers_table.horizontalHeader().setFixedHeight(35)
    
    def refresh_customers(self):
        """刷新客户列表"""
        self.customers_table.setRowCount(0)
        customers = self.db.get_all_customers()
        
        for row, cust in enumerate(customers):
            self.customers_table.insertRow(row)
            
            # 添加数据
            self.customers_table.setItem(row, 0, QTableWidgetItem(str(cust[0])))  # ID
            self.customers_table.setItem(row, 1, QTableWidgetItem(cust[1]))  # 名称
            self.customers_table.setItem(row, 2, QTableWidgetItem(cust[2]))  # 电话
            self.customers_table.setItem(row, 3, QTableWidgetItem(cust[3]))  # 微信
            
            # 添加操作按钮
            btn_layout = QHBoxLayout()
            edit_btn = QPushButton("编辑")
            delete_btn = QPushButton("删除")
            
            edit_btn.clicked.connect(lambda checked, r=row: self.edit_customer(r))
            delete_btn.clicked.connect(lambda checked, r=row: self.delete_customer(r))
            
            btn_widget = QWidget()
            btn_layout.addWidget(edit_btn)
            btn_layout.addWidget(delete_btn)
            btn_widget.setLayout(btn_layout)
            
            self.customers_table.setCellWidget(row, 4, btn_widget)
    
    def add_customer(self):
        """添加新客户"""
        name = self.name_input.text().strip()
        phone = self.phone_input.text().strip()
        wechat = self.wechat_input.text().strip()
        
        if not name:
            QMessageBox.warning(self, "错误", "客户名称不能为空!")
            return
        
        try:
            self.db.add_customer(name, phone, wechat)
            
            # 清空输入
            self.name_input.clear()
            self.phone_input.clear()
            self.wechat_input.clear()
            
            # 刷新列表
            self.refresh_customers()
            QMessageBox.information(self, "成功", "客户添加成功!")
        except Exception as e:
            QMessageBox.warning(self, "错误", f"添加客户失败:{str(e)}")
    
    def edit_customer(self, row):
        """编辑客户"""
        customer_data = (
            int(self.customers_table.item(row, 0).text()),  # id
            self.customers_table.item(row, 1).text(),       # name
            self.customers_table.item(row, 2).text(),       # phone
            self.customers_table.item(row, 3).text()        # wechat
        )
        
        dialog = EditCustomerDialog(customer_data, self)
        if dialog.exec_():
            try:
                # 获取编辑后的数据
                name = dialog.name_input.text().strip()
                phone = dialog.phone_input.text().strip()
                wechat = dialog.wechat_input.text().strip()
                
                if not name:
                    QMessageBox.warning(self, "错误", "客户名称不能为空!")
                    return
                
                # 更新数据库
                self.db.update_customer(customer_data[0], name, phone, wechat)
                
                # 刷新显示
                self.refresh_customers()
                QMessageBox.information(self, "成功", "客户信息更新成功!")
            except Exception as e:
                QMessageBox.warning(self, "错误", f"更新客户信息失败:{str(e)}")
    
    def delete_customer(self, row):
        """删除客户"""
        customer_id = int(self.customers_table.item(row, 0).text())
        reply = QMessageBox.question(self, "确认删除", 
                                   "确定要删除这个客户吗?",
                                   QMessageBox.Yes | QMessageBox.No)
        
        if reply == QMessageBox.Yes:
            try:
                self.db.delete_customer(customer_id)
                self.refresh_customers()
                QMessageBox.information(self, "成功", "客户删除成功!")
            except Exception as e:
                QMessageBox.warning(self, "错误", f"删除客户失败:{str(e)}") 
    
    def search_customers(self):
        """搜索客户"""
        search_text = self.search_input.text().strip().lower()
        
        # 遍历所有行
        for row in range(self.customers_table.rowCount()):
            show_row = False
            if not search_text:
                show_row = True
            else:
                # 检查姓名、电话和微信
                name_item = self.customers_table.item(row, 1)  # 姓名列
                phone_item = self.customers_table.item(row, 2)  # 电话列
                wechat_item = self.customers_table.item(row, 3)  # 微信列
                
                if name_item and search_text in name_item.text().lower():
                    show_row = True
                elif phone_item and search_text in phone_item.text().lower():
                    show_row = True
                elif wechat_item and search_text in wechat_item.text().lower():
                    show_row = True
            
            self.customers_table.setRowHidden(row, not show_row)
    
    def clear_search(self):
        """清除搜索"""
        self.search_input.clear()
        # 显示所有行
        for row in range(self.customers_table.rowCount()):
            self.customers_table.setRowHidden(row, False) 
    
    def export_customers(self):
        """导出客户到Excel"""
        file_name, _ = QFileDialog.getSaveFileName(
            self,
            "导出客户",
            f"客户列表_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
            "Excel Files (*.xlsx)"
        )
        
        if file_name:
            try:
                # 获取所有客户数据
                customers = self.db.get_all_customers()
                # 转换为字典列表
                customers_data = []
                for cust in customers:
                    customers_data.append({
                        'name': cust[1],
                        'phone': cust[2],
                        'wechat': cust[3]
                    })
                
                # 导出到Excel
                from utils.customer_excel_handler import CustomerExcelHandler
                CustomerExcelHandler.export_to_excel(customers_data, file_name)
                QMessageBox.information(self, "成功", "客户导出成功!")
            except Exception as e:
                QMessageBox.critical(self, "错误", f"导出失败:{str(e)}")
    
    def import_customers(self):
        """从Excel导入客户"""
        file_name, _ = QFileDialog.getOpenFileName(
            self,
            "导入客户",
            "",
            "Excel Files (*.xlsx)"
        )
        
        if file_name:
            try:
                # 从Excel读取数据
                from utils.customer_excel_handler import CustomerExcelHandler
                customers = CustomerExcelHandler.import_from_excel(file_name)
                
                # 确认导入
                reply = QMessageBox.question(
                    self,
                    "确认导入",
                    f"确定要导入 {len(customers)} 个客户吗?",
                    QMessageBox.Yes | QMessageBox.No
                )
                
                if reply == QMessageBox.Yes:
                    # 导入每个客户
                    for cust in customers:
                        self.db.add_customer(
                            cust['name'],
                            cust['phone'],
                            cust['wechat']
                        )
                    
                    # 刷新显示
                    self.refresh_customers()
                    QMessageBox.information(self, "成功", "客户导入成功!")
            except Exception as e:
                QMessageBox.critical(self, "错误", f"导入失败:{str(e)}") 

views\quotations.py

python 复制代码
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, 
                              QTableWidget, QPushButton, QComboBox,
                              QLabel, QLineEdit, QSpinBox, QDoubleSpinBox,
                              QTableWidgetItem, QMessageBox, QDialog,
                              QDialogButtonBox, QFormLayout, QDateEdit,
                              QHeaderView, QFileDialog)
from PySide6.QtCore import Qt
from datetime import date, datetime
from utils.excel_exporter import QuoteExporter

class CreateQuotationDialog(QDialog):
    """创建报价单对话框"""
    def __init__(self, db_manager, parent=None):
        super().__init__(parent)
        self.db = db_manager
        self.quotation_id = None
        self.setWindowTitle("创建报价单")
        # 设置窗口大小 可放大
        self.setMinimumSize(950, 600)
        self.setup_ui()

    def setup_ui(self):
        layout = QVBoxLayout(self)
        
        # 基本信息区域
        form_layout = QFormLayout()
        
        # 客户选择
        self.customer_combo = QComboBox()
        customers = self.db.get_all_customers()
        for customer in customers:
            self.customer_combo.addItem(customer[1], customer[0])
        form_layout.addRow("客户:", self.customer_combo)
        
        # 日期选择
        self.date_edit = QDateEdit()
        self.date_edit.setDate(date.today())
        form_layout.addRow("报价日期:", self.date_edit)
        
        # 有效期
        self.valid_days = QSpinBox()
        self.valid_days.setRange(1, 30)
        self.valid_days.setValue(7)
        form_layout.addRow("有效天数:", self.valid_days)
        
        # 备注
        self.notes_input = QLineEdit()
        form_layout.addRow("备注:", self.notes_input)
        
        layout.addLayout(form_layout)
        
        # 添加总金额定价方式
        pricing_layout = QHBoxLayout()
        self.pricing_type = QComboBox()
        self.pricing_type.addItems(['固定价格', '百分比加价', '固定加价'])
        self.pricing_type.setCurrentText('百分比加价')  # 设置默认值
        
        self.markup_value = QDoubleSpinBox()
        self.markup_value.setRange(0, 999999)
        self.markup_value.setValue(15)  # 设置默认值为15
        self.markup_value.setPrefix("¥")
        
        pricing_layout.addWidget(QLabel("总金额定价方式:"))
        pricing_layout.addWidget(self.pricing_type)
        pricing_layout.addWidget(QLabel("加价值:"))
        pricing_layout.addWidget(self.markup_value)
        layout.addLayout(pricing_layout)
        
        # 总金额显示区域
        total_layout = QHBoxLayout()
        self.cost_label = QLabel("总成本: ¥0.00")  # 初始显示为0
        self.amount_label = QLabel("明细总额: ¥0.00")
        self.final_label = QLabel("最终金额: ¥0.00")
        total_layout.addWidget(self.cost_label)
        total_layout.addWidget(self.amount_label)
        total_layout.addWidget(self.final_label)
        layout.addLayout(total_layout)
        
        # 创建报价单按钮
        create_btn = QPushButton("明细列表")
        create_btn.clicked.connect(self.create_quotation)
        layout.addWidget(create_btn)
        
        # 明细区域(初始隐藏)
        self.detail_widget = QWidget()
        self.detail_widget.setVisible(False)
        detail_layout = QVBoxLayout(self.detail_widget)
        
        # 明细列表
        detail_layout.addWidget(QLabel("报价单明细:"))
        self.details_table = QTableWidget()
        self.details_table.setColumnCount(9)
        self.details_table.setHorizontalHeaderLabels([
            "ID", "配件", "数量", "定价方式", "加价值", 
            "采购单价", "采购总价", "销售单价", "销售总价"
        ])
        
        # 设置列宽
        self.details_table.setColumnWidth(0, 50)   # ID
        self.details_table.setColumnWidth(1, 200)  # 配件
        self.details_table.setColumnWidth(2, 60)   # 数量
        self.details_table.setColumnWidth(3, 100)  # 定价方式
        self.details_table.setColumnWidth(4, 80)   # 加价值
        self.details_table.setColumnWidth(5, 100)  # 采购单价
        self.details_table.setColumnWidth(6, 100)  # 采购总价
        self.details_table.setColumnWidth(7, 100)  # 销售单价
        self.details_table.setColumnWidth(8, 100)  # 销售总价
        
        # 设置表格样式
        self.details_table.setAlternatingRowColors(True)  # 交替行颜色
        self.details_table.setSelectionBehavior(QTableWidget.SelectRows)  # 整行选择
        self.details_table.setEditTriggers(QTableWidget.NoEditTriggers)  # 禁止编辑
        detail_layout.addWidget(self.details_table)
        
        # 明细操作按钮
        btn_layout = QHBoxLayout()
        add_detail_btn = QPushButton("添加配件")
        add_detail_btn.clicked.connect(self.add_detail)
        delete_detail_btn = QPushButton("删除配件")
        delete_detail_btn.clicked.connect(self.delete_detail)
        
        btn_layout.addWidget(add_detail_btn)
        btn_layout.addWidget(delete_detail_btn)
        btn_layout.addStretch()
        detail_layout.addLayout(btn_layout)
        
        # 添加明细区域到主布局
        layout.addWidget(self.detail_widget)
        
        # 确定取消按钮
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            parent=self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)
    
    def create_quotation(self):
        """创建报价单基本信息"""
        try:
            customer_id = self.customer_combo.currentData()
            quotation_date = self.date_edit.date().toPython()
            valid_days = self.valid_days.value()
            notes = self.notes_input.text().strip()
            
            # 获取定价信息
            pricing_type = self.pricing_type.currentText()
            if pricing_type == '固定价格':
                pricing_type = 'fixed'
            elif pricing_type == '百分比加价':
                pricing_type = 'percentage'
            else:
                pricing_type = 'markup'
            markup_value = self.markup_value.value()
            
            # 创建报价单
            self.quotation_id = self.db.create_quotation(
                customer_id, quotation_date, valid_days, notes,
                pricing_type, markup_value
            )
            
            # 显示明细区域
            self.detail_widget.setVisible(True)
            # 禁用基本信息区域和创建按钮
            self.customer_combo.setEnabled(False)
            self.date_edit.setEnabled(False)
            self.valid_days.setEnabled(False)
            self.notes_input.setEnabled(False)
            self.sender().setEnabled(False)
            
            QMessageBox.information(self, "成功", "明细列表创建成功,请添加配件明细!")
            
        except Exception as e:
            QMessageBox.warning(self, "错误", f"明细列表单失败:{str(e)}")
    
    def refresh_details(self):
        """刷新明细列表"""
        self.details_table.setRowCount(0)
        if self.quotation_id:
            details = self.db.get_quotation_details(self.quotation_id)
            
            for row, detail in enumerate(details):
                self.details_table.insertRow(row)
                
                # 添加数据
                self.details_table.setItem(row, 0, QTableWidgetItem(str(detail[0])))  # ID
                self.details_table.setItem(row, 1, QTableWidgetItem(detail[1]))  # 配件名称
                self.details_table.setItem(row, 2, QTableWidgetItem(str(detail[2])))  # 数量
                
                # 定价方式转换显示
                pricing_type = detail[3]
                if pricing_type == 'fixed':
                    pricing_type = '固定价格'
                elif pricing_type == 'percentage':
                    pricing_type = '百分比加价'
                else:
                    pricing_type = '固定加价'
                self.details_table.setItem(row, 3, QTableWidgetItem(pricing_type))
                
                self.details_table.setItem(row, 4, QTableWidgetItem(str(detail[4])))  # 加价值
                self.details_table.setItem(row, 5, QTableWidgetItem(f"¥{detail[7]:.2f}"))  # 采购单价
                self.details_table.setItem(row, 6, QTableWidgetItem(f"¥{detail[8]:.2f}"))  # 采购总价
                self.details_table.setItem(row, 7, QTableWidgetItem(f"¥{detail[5]:.2f}"))  # 销售单价
                self.details_table.setItem(row, 8, QTableWidgetItem(f"¥{detail[6]:.2f}"))  # 销售总价
            
            # 更新总金额显示
            quotation = self.db.get_quotation(self.quotation_id)
            if quotation:
                self.cost_label.setText(f"总成本: ¥{quotation[7]:.2f}")  # total_cost
                self.amount_label.setText(f"明细总额: ¥{quotation[8]:.2f}")  # total_amount
                self.final_label.setText(f"最终金额: ¥{quotation[3]:.2f}")  # final_amount
    
    def add_detail(self):
        """添加明细"""
        dialog = AddQuotationDetailDialog(self.db, self)
        if dialog.exec_():
            try:
                component_id = dialog.component_combo.currentData()
                quantity = dialog.quantity_input.value()
                pricing_type = dialog.pricing_type.currentText()
                markup_value = dialog.markup_value.value()
                
                # 转换定价类型
                if pricing_type == '固定价格':
                    pricing_type = 'fixed'
                elif pricing_type == '百分比加价':
                    pricing_type = 'percentage'
                else:
                    pricing_type = 'markup'
                
                self.db.add_quotation_detail(
                    self.quotation_id, component_id, quantity,
                    pricing_type, markup_value
                )
                
                # 更新报价单总金额
                self.db.update_quotation_total(self.quotation_id)
                
                # 刷新显示
                self.refresh_details()
                
            except Exception as e:
                QMessageBox.warning(self, "错误", f"添加配件失败:{str(e)}")
    
    def delete_detail(self):
        """删除明细"""
        current_row = self.details_table.currentRow()
        if current_row < 0:
            QMessageBox.warning(self, "错误", "请先选择要删除的配件!")
            return
            
        detail_id = int(self.details_table.item(current_row, 0).text())
        
        reply = QMessageBox.question(
            self, "确认删除", 
            "确定要删除这个配件吗?",
            QMessageBox.Yes | QMessageBox.No
        )
        
        if reply == QMessageBox.Yes:
            try:
                self.db.delete_quotation_detail(detail_id)
                # 更新报价单总金额
                self.db.update_quotation_total(self.quotation_id)
                self.refresh_details()
            except Exception as e:
                QMessageBox.warning(self, "错误", f"删除配件失败:{str(e)}") 

class AddQuotationDetailDialog(QDialog):
    """添加报价单明细对话框"""
    def __init__(self, db_manager, parent=None):
        super().__init__(parent)
        self.db = db_manager
        self.setWindowTitle("添加配件")
        self.setup_ui()

    def setup_ui(self):
        layout = QFormLayout(self)
        
        # 配件选择
        self.component_combo = QComboBox()
        components = self.db.get_all_components()
        for comp in components:
            self.component_combo.addItem(f"{comp[1]} - {comp[2]}", comp[0])
        
        # 数量
        self.quantity_input = QSpinBox()
        self.quantity_input.setRange(1, 999)
        self.quantity_input.setValue(1)
        
        # 定价方式
        self.pricing_type = QComboBox()
        self.pricing_type.addItems(['固定价格', '百分比加价', '固定加价'])
        self.pricing_type.setCurrentText('百分比加价')  # 设置默认值
        
        # 加价值
        self.markup_value = QDoubleSpinBox()
        self.markup_value.setRange(0, 999999)
        self.markup_value.setValue(15)  # 设置默认值为15
        
        # 添加到布局
        layout.addRow("配件:", self.component_combo)
        layout.addRow("数量:", self.quantity_input)
        layout.addRow("定价方式:", self.pricing_type)
        layout.addRow("加价值:", self.markup_value)
        
        # 添加按钮
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            parent=self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addRow(buttons)

class QuotationsTab(QWidget):
    def __init__(self, db_manager):
        super().__init__()
        self.db = db_manager
        self.init_ui()
        
    def init_ui(self):
        """初始化报价单界面"""
        layout = QVBoxLayout()
        
        # 添加搜索区域
        search_layout = QHBoxLayout()
        
        # 搜索输入框
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText("输入客户名称或备注搜索...")
        self.search_input.textChanged.connect(self.search_quotations)
        
        # 搜索按钮
        search_btn = QPushButton("搜索")
        search_btn.clicked.connect(self.search_quotations)
        
        # 清除搜索按钮
        clear_btn = QPushButton("清除搜索")
        clear_btn.clicked.connect(self.clear_search)
        
        search_layout.addWidget(QLabel("搜索:"))
        search_layout.addWidget(self.search_input)
        search_layout.addWidget(search_btn)
        search_layout.addWidget(clear_btn)
        search_layout.addStretch()
        
        layout.addLayout(search_layout)
        
        # 添加按钮布局
        button_layout = QHBoxLayout()
        
        # 添加现有的按钮
        self.add_btn = QPushButton('新建报价单')
        self.add_btn.clicked.connect(self.create_new_quotation)
        
        # 添加导出按钮
        self.export_btn = QPushButton('导出报价单')
        self.export_btn.clicked.connect(self.export_quotation)
        
        # 添加打印按钮
        self.print_btn = QPushButton('打印报价单')
        self.print_btn.clicked.connect(self.print_quotation)
        
        button_layout.addWidget(self.add_btn)
        button_layout.addWidget(self.export_btn)
        button_layout.addWidget(self.print_btn)
        button_layout.addStretch()
        
        layout.addLayout(button_layout)
        
        # 添加报价单表格
        self.table = QTableWidget()
        self.table.setColumnCount(8)
        self.table.setHorizontalHeaderLabels([
            "ID", "客户", "日期", "总金额", "有效期", "状态", "备注", "操作"
        ])
        
        # 设置表格样式
        self.table.setAlternatingRowColors(True)  # 交替行颜色
        self.table.setSelectionBehavior(QTableWidget.SelectRows)  # 整行选择
        self.table.setSelectionMode(QTableWidget.SingleSelection)  # 单行选择
        self.table.setEditTriggers(QTableWidget.NoEditTriggers)  # 禁止编辑
        
        # 设置列宽
        self.table.setColumnWidth(0, 60)   # ID
        self.table.setColumnWidth(1, 150)  # 客户
        self.table.setColumnWidth(2, 100)  # 日期
        self.table.setColumnWidth(3, 100)  # 总金额
        self.table.setColumnWidth(4, 80)   # 有效期
        self.table.setColumnWidth(5, 100)  # 状态
        self.table.setColumnWidth(6, 200)  # 备注
        self.table.setColumnWidth(7, 150)  # 操作
        
        # 设置表头样式
        header = self.table.horizontalHeader()
        header.setStretchLastSection(True)  # 最后一列自动填充
        header.setSectionResizeMode(QHeaderView.Fixed)  # 禁止调整列宽
        
        # 设置行高
        self.table.verticalHeader().setDefaultSectionSize(60)  # 设置默认行高为60
        self.table.verticalHeader().setVisible(False)  # 隐藏行号
        
        layout.addWidget(self.table)
        
        self.setLayout(layout)
        self.refresh_table()
    
    def export_quotation(self):
        """导出当前选中的报价单"""
        # 获取当前选中的行
        current_row = self.table.currentRow()
        if current_row < 0:
            QMessageBox.warning(self, "警告", "请先选择要导出的报价单!")
            return
            
        # 获取报价单ID和客户名称
        quotation_id = int(self.table.item(current_row, 0).text())
        customer_name = self.table.item(current_row, 1).text()
        
        # 选择保存位置
        file_name, _ = QFileDialog.getSaveFileName(
            self,
            "导出报价单",
            f"电脑配置报价单_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
            "Excel Files (*.xlsx)"
        )
        
        if file_name:
            try:
                # 获取报价单数据
                quote_data = self.db.get_quotation_items(quotation_id)
                QuoteExporter.export_to_excel(quote_data, customer_name, file_name)
                QMessageBox.information(self, "成功", "报价单已成功导出!")
            except Exception as e:
                QMessageBox.critical(self, "错误", f"导出失败:{str(e)}")
    
    def refresh_table(self):
        """刷新报价单列表"""
        self.table.setRowCount(0)
        quotations = self.db.get_all_quotations()
        
        for row, quot in enumerate(quotations):
            self.table.insertRow(row)
            
            # 添加数据
            self.table.setItem(row, 0, QTableWidgetItem(str(quot[0])))  # ID
            self.table.setItem(row, 1, QTableWidgetItem(quot[1]))  # 客户
            self.table.setItem(row, 2, QTableWidgetItem(str(quot[2])))  # 日期
            self.table.setItem(row, 3, QTableWidgetItem(f"¥{quot[3]:.2f}"))  # 最终金额
            self.table.setItem(row, 4, QTableWidgetItem(f"{quot[4]}天"))  # 有效期
            
            # 状态(使用下拉框)
            status_combo = QComboBox()
            status_combo.addItems(self.db.QUOTATION_STATUS.values())
            status_combo.setCurrentText(quot[5])
            status_combo.currentTextChanged.connect(
                lambda status, qid=quot[0]: self.update_quotation_status(qid, status)
            )
            self.table.setCellWidget(row, 5, status_combo)
            
            self.table.setItem(row, 6, QTableWidgetItem(quot[6]))  # 备注
            
            # 添加操作按钮
            btn_layout = QHBoxLayout()
            view_btn = QPushButton("查看")
            delete_btn = QPushButton("删除")
            
            view_btn.clicked.connect(lambda checked, r=row: self.view_quotation(r))
            delete_btn.clicked.connect(lambda checked, r=row: self.delete_quotation(r))
            
            btn_widget = QWidget()
            btn_layout.addWidget(view_btn)
            btn_layout.addWidget(delete_btn)
            btn_widget.setLayout(btn_layout)
            
            self.table.setCellWidget(row, 7, btn_widget)
    
    def create_new_quotation(self):
        """创建新报价单"""
        dialog = CreateQuotationDialog(self.db, self)
        if dialog.exec_():
            self.refresh_table()
    
    def add_quotation_detail(self, quotation_id):
        """添加报价单明细"""
        dialog = AddQuotationDetailDialog(self.db, self)
        if dialog.exec_():
            try:
                component_id = dialog.component_combo.currentData()
                quantity = dialog.quantity_input.value()
                pricing_type = dialog.pricing_type.currentText()
                markup_value = dialog.markup_value.value()
                
                # 转换定价类型
                if pricing_type == '固定价格':
                    pricing_type = 'fixed'
                elif pricing_type == '百分比加价':
                    pricing_type = 'percentage'
                else:
                    pricing_type = 'markup'
                
                self.db.add_quotation_detail(
                    quotation_id, component_id, quantity,
                    pricing_type, markup_value
                )
                
                # 询问是否继续添加
                reply = QMessageBox.question(
                    self, "继续添加?", 
                    "是否继续添加配件?",
                    QMessageBox.Yes | QMessageBox.No
                )
                
                if reply == QMessageBox.Yes:
                    self.add_quotation_detail(quotation_id)
                else:
                    self.refresh_table()
                    
            except Exception as e:
                QMessageBox.warning(self, "错误", f"添加配件失败:{str(e)}")
    
    def view_quotation(self, row):
        """查看/编辑报价单"""
        quotation_id = int(self.table.item(row, 0).text())
        
        self.setMinimumSize(950, 600)
        quotation = self.db.get_quotation(quotation_id)
        
        quotation_data = (
            quotation[0],  # id
            quotation[1],  # customer_name
            quotation[2],  # date
            quotation[3],  # final_amount
            quotation[4],  # valid_days
            quotation[5],  # status
            quotation[6],  # notes
            quotation[7],  # total_cost
            quotation[8],  # total_amount
            quotation[9],  # pricing_type
            quotation[10]  # markup_value
        )
        
        dialog = EditQuotationDialog(self.db, quotation_data, self)
        if dialog.exec_():
            try:
                # 获取定价信息
                pricing_type = dialog.pricing_type.currentText()
                if pricing_type == '固定价格':
                    pricing_type = 'fixed'
                elif pricing_type == '百分比加价':
                    pricing_type = 'percentage'
                else:
                    pricing_type = 'markup'
                markup_value = dialog.markup_value.value()
                
                # 更新报价单基本信息
                self.db.update_quotation(
                    quotation_id,
                    dialog.date_edit.date().toPython(),
                    dialog.valid_days.value(),
                    dialog.notes_input.text().strip(),
                    pricing_type,
                    markup_value
                )
                
                # 刷新显示
                self.refresh_table()
                QMessageBox.information(self, "成功", "报价单更新成功!")
            except Exception as e:
                QMessageBox.warning(self, "错误", f"更新报价单失败:{str(e)}")
    
    def delete_quotation(self, row):
        """删除报价单"""
        quotation_id = int(self.table.item(row, 0).text())
        reply = QMessageBox.question(
            self, "确认删除", 
            "确定要删除这个报价单吗?",
            QMessageBox.Yes | QMessageBox.No
        )
        
        if reply == QMessageBox.Yes:
            try:
                self.db.delete_quotation(quotation_id)
                self.refresh_table()
                QMessageBox.information(self, "成功", "报价单删除成功!")
            except Exception as e:
                QMessageBox.warning(self, "错误", f"删除报价单失败:{str(e)}") 
    
    def update_quotation_status(self, quotation_id, new_status):
        """更新报价单状态"""
        try:
            self.db.update_quotation_status(quotation_id, new_status)
            QMessageBox.information(self, "成功", "状态更新成功!")
        except Exception as e:
            QMessageBox.warning(self, "错误", f"更新状态失败:{str(e)}") 

    def search_quotations(self):
        """搜索报价单"""
        search_text = self.search_input.text().strip().lower()
        
        # 遍历所有行
        for row in range(self.table.rowCount()):
            show_row = False
            if not search_text:
                show_row = True
            else:
                # 检查客户名称和备注
                customer_item = self.table.item(row, 1)  # 客户列
                notes_item = self.table.item(row, 6)     # 备注列
                
                if customer_item and search_text in customer_item.text().lower():
                    show_row = True
                elif notes_item and search_text in notes_item.text().lower():
                    show_row = True
            
            self.table.setRowHidden(row, not show_row)

    def clear_search(self):
        """清除搜索"""
        self.search_input.clear()
        # 显示所有行
        for row in range(self.table.rowCount()):
            self.table.setRowHidden(row, False)

    def print_quotation(self):
        """打印当前选中的报价单"""
        # 获取当前选中的行
        current_row = self.table.currentRow()
        if current_row < 0:
            QMessageBox.warning(self, "警告", "请先选择要打印的报价单!")
            return
        
        # 获取报价单ID和客户名称
        quotation_id = int(self.table.item(current_row, 0).text())
        customer_name = self.table.item(current_row, 1).text()
        
        try:
            # 获取报价单数据
            quote_data = self.db.get_quotation_items(quotation_id)
            # 调用打印处理
            from utils.print_handler import QuotePrinter
            QuotePrinter.print_quote(quote_data, customer_name, self)
        except Exception as e:
            QMessageBox.critical(self, "错误", f"打印失败:{str(e)}")

class EditQuotationDialog(QDialog):
    """编辑报价单对话框"""
    def __init__(self, db_manager, quotation_data, parent=None):
        super().__init__(parent)
        self.db = db_manager
        self.quotation_data = quotation_data  # (id, customer_name, date, total, valid_days, status, notes)
        self.setWindowTitle("编辑报价单")
        # 窗口大小
        self.resize(950, 600)
        self.setup_ui()

    def setup_ui(self):
        layout = QVBoxLayout(self)
        
        # 基本信息区域
        form_layout = QFormLayout()
        
        # 客户信息(只读)
        customer_label = QLabel(self.quotation_data[1])
        form_layout.addRow("客户:", customer_label)
        
        # 日期选择
        self.date_edit = QDateEdit()
        self.date_edit.setDate(datetime.strptime(self.quotation_data[2], '%Y-%m-%d').date())
        form_layout.addRow("报价日期:", self.date_edit)
        
        # 有效期
        self.valid_days = QSpinBox()
        self.valid_days.setRange(1, 30)
        self.valid_days.setValue(self.quotation_data[4])
        form_layout.addRow("有效天数:", self.valid_days)
        
        # 备注
        self.notes_input = QLineEdit(self.quotation_data[6])
        form_layout.addRow("备注:", self.notes_input)
        
        layout.addLayout(form_layout)
        
        # 添加总金额定价方式
        pricing_layout = QHBoxLayout()
        self.pricing_type = QComboBox()
        self.pricing_type.addItems(['固定价格', '百分比加价', '固定加价'])
        # 设置当前定价方式
        current_pricing = self.quotation_data[9]  # pricing_type
        if current_pricing == 'fixed':
            self.pricing_type.setCurrentText('固定价格')
        elif current_pricing == 'percentage':
            self.pricing_type.setCurrentText('百分比加价')
        else:
            self.pricing_type.setCurrentText('固定加价')
            
        self.markup_value = QDoubleSpinBox()
        self.markup_value.setRange(0, 999999)
        self.markup_value.setPrefix("¥")
        self.markup_value.setValue(self.quotation_data[10])  # markup_value
        
        pricing_layout.addWidget(QLabel("总金额定价方式:"))
        pricing_layout.addWidget(self.pricing_type)
        pricing_layout.addWidget(QLabel("加价值:"))
        pricing_layout.addWidget(self.markup_value)
        layout.addLayout(pricing_layout)
        
        # 总金额显示区域
        total_layout = QHBoxLayout()
        self.cost_label = QLabel(f"总成本: ¥{self.quotation_data[7]:.2f}")  # total_cost
        self.amount_label = QLabel(f"明细总额: ¥{self.quotation_data[8]:.2f}")  # total_amount
        self.final_label = QLabel(f"最终金额: ¥{self.quotation_data[3]:.2f}")  # final_amount
        total_layout.addWidget(self.cost_label)
        total_layout.addWidget(self.amount_label)
        total_layout.addWidget(self.final_label)
        layout.addLayout(total_layout)
        
        # 明细列表
        layout.addWidget(QLabel("报价单明细:"))
        self.details_table = QTableWidget()
        self.details_table.setColumnCount(9)
        self.details_table.setHorizontalHeaderLabels([
            "ID", "配件", "数量", "定价方式", "加价值", 
            "采购单价", "采购总价", "销售单价", "销售总价"
        ])
        
        # 设置列宽
        self.details_table.setColumnWidth(0, 50)   # ID
        self.details_table.setColumnWidth(1, 200)  # 配件
        self.details_table.setColumnWidth(2, 60)   # 数量
        self.details_table.setColumnWidth(3, 100)  # 定价方式
        self.details_table.setColumnWidth(4, 80)   # 加价值
        self.details_table.setColumnWidth(5, 100)  # 采购单价
        self.details_table.setColumnWidth(6, 100)  # 采购总价
        self.details_table.setColumnWidth(7, 100)  # 销售单价
        self.details_table.setColumnWidth(8, 100)  # 销售总价
        
        # 设置表格样式
        self.details_table.setAlternatingRowColors(True)  # 交替行颜色
        self.details_table.setSelectionBehavior(QTableWidget.SelectRows)  # 整行选择
        self.details_table.setEditTriggers(QTableWidget.NoEditTriggers)  # 禁止编辑
        layout.addWidget(self.details_table)
        
        # 明细操作按钮
        btn_layout = QHBoxLayout()
        add_detail_btn = QPushButton("添加配件")
        add_detail_btn.clicked.connect(self.add_detail)
        delete_detail_btn = QPushButton("删除配件")
        delete_detail_btn.clicked.connect(self.delete_detail)
        
        btn_layout.addWidget(add_detail_btn)
        btn_layout.addWidget(delete_detail_btn)
        btn_layout.addStretch()
        layout.addLayout(btn_layout)
        
        # 确定取消按钮
        buttons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
            parent=self)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)
        
        # 加载明细数据
        self.refresh_details()
    
    def refresh_details(self):
        """刷新明细列表"""
        self.details_table.setRowCount(0)
        details = self.db.get_quotation_details(self.quotation_data[0])
        
        for row, detail in enumerate(details):
            self.details_table.insertRow(row)
            
            # 添加数据
            self.details_table.setItem(row, 0, QTableWidgetItem(str(detail[0])))  # ID
            self.details_table.setItem(row, 1, QTableWidgetItem(detail[1]))  # 配件名称
            self.details_table.setItem(row, 2, QTableWidgetItem(str(detail[2])))  # 数量
            
            # 定价方式转换显示
            pricing_type = detail[3]
            if pricing_type == 'fixed':
                pricing_type = '固定价格'
            elif pricing_type == 'percentage':
                pricing_type = '百分比加价'
            else:
                pricing_type = '固定加价'
            self.details_table.setItem(row, 3, QTableWidgetItem(pricing_type))
            
            self.details_table.setItem(row, 4, QTableWidgetItem(str(detail[4])))  # 加价值
            self.details_table.setItem(row, 5, QTableWidgetItem(f"¥{detail[7]:.2f}"))  # 采购单价
            self.details_table.setItem(row, 6, QTableWidgetItem(f"¥{detail[8]:.2f}"))  # 采购总价
            self.details_table.setItem(row, 7, QTableWidgetItem(f"¥{detail[5]:.2f}"))  # 销售单价
            self.details_table.setItem(row, 8, QTableWidgetItem(f"¥{detail[6]:.2f}"))  # 销售总价
        
        # 更新总金额显示
        quotation = self.db.get_quotation(self.quotation_data[0])
        if quotation:
            self.cost_label.setText(f"总成本: ¥{quotation[7]:.2f}")  # total_cost
            self.amount_label.setText(f"明细总额: ¥{quotation[8]:.2f}")  # total_amount
            self.final_label.setText(f"最终金额: ¥{quotation[3]:.2f}")  # final_amount
    
    def add_detail(self):
        """添加明细"""
        dialog = AddQuotationDetailDialog(self.db, self)
        if dialog.exec_():
            try:
                component_id = dialog.component_combo.currentData()
                quantity = dialog.quantity_input.value()
                pricing_type = dialog.pricing_type.currentText()
                markup_value = dialog.markup_value.value()
                
                # 转换定价类型
                if pricing_type == '固定价格':
                    pricing_type = 'fixed'
                elif pricing_type == '百分比加价':
                    pricing_type = 'percentage'
                else:
                    pricing_type = 'markup'
                
                self.db.add_quotation_detail(
                    self.quotation_data[0], component_id, quantity,
                    pricing_type, markup_value
                )
                
                # 刷新显示
                self.refresh_details()
                # 更新总金额显示
                quotation = self.db.get_quotation(self.quotation_data[0])
                self.final_label.setText(f"最终金额: ¥{quotation[3]:.2f}")
                
            except Exception as e:
                QMessageBox.warning(self, "错误", f"添加配件失败:{str(e)}")
    
    def delete_detail(self):
        """删除明细"""
        current_row = self.details_table.currentRow()
        if current_row < 0:
            QMessageBox.warning(self, "错误", "请先选择要删除的配件!")
            return
            
        detail_id = int(self.details_table.item(current_row, 0).text())
        
        reply = QMessageBox.question(
            self, "确认删除", 
            "确定要删除这个配件吗?",
            QMessageBox.Yes | QMessageBox.No
        )
        
        if reply == QMessageBox.Yes:
            try:
                self.db.delete_quotation_detail(detail_id)
                self.refresh_details()
                # 更新总金额显示
                quotation = self.db.get_quotation(self.quotation_data[0])
                self.final_label.setText(f"最终金额: ¥{quotation[3]:.2f}")
            except Exception as e:
                QMessageBox.warning(self, "错误", f"删除配件失败:{str(e)}") 

utils\print_handler.py --->打印

python 复制代码
from PySide6.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PySide6.QtGui import QTextDocument, QPageSize, QPageLayout
from PySide6.QtCore import Qt, QDateTime, QSizeF
from PySide6.QtWidgets import QDialog

class QuotePrinter:
    @staticmethod
    def print_quote(quote_data, customer_name, parent=None):
        """打印报价单"""
        # 创建文档
        document = QTextDocument()
        
        # 构建HTML内容
        html = f"""
        <html>
        <head>
            <style>
                body {{ font-family: Arial, sans-serif; margin: 20px; }}
                table {{ border-collapse: collapse; width: 100%; }}
                th, td {{ border: 1px solid black; padding: 8px; text-align: center; }}
                th {{ background-color: #f2f2f2; }}
                .header {{ text-align: center; margin-bottom: 20px; }}
                .customer-info {{ margin-bottom: 20px; }}
                .total {{ margin-top: 20px; text-align: right; }}
                .footer {{ margin-top: 30px; text-align: left; }}
            </style>
        </head>
        <body>
            <div class="header">
                <h1>电脑配置报价单</h1>
            </div>
            
            <div class="customer-info">
                <p><strong>客户名称:</strong>{customer_name}</p>
                <p><strong>日期:</strong>{QDateTime.currentDateTime().toString('yyyy-MM-dd')}</p>
            </div>
            
            <table>
                <tr>
                    <th>配件类型</th>
                    <th>名称</th>
                    <th>数量</th>
                    <th>单价</th>
                    <th>总价</th>
                </tr>
        """
        
        # 添加数据行
        total_price = 0
        for item in quote_data:
            subtotal = item['quantity'] * item['price']
            total_price += subtotal
            html += f"""
                <tr>
                    <td>{item['component_type']}</td>
                    <td>{item['name']}</td>
                    <td>{item['quantity']}</td>
                    <td>¥{item['price']:.2f}</td>
                    <td>¥{subtotal:.2f}</td>
                </tr>
            """
        
        # 添加总计和页脚
        html += f"""
            </table>
            
            <div class="total">
                <p><strong>总计:</strong>¥{total_price:.2f}</p>
            </div>
            
            <div class="footer">
                <p>打印时间:{QDateTime.currentDateTime().toString('yyyy-MM-dd hh:mm:ss')}</p>
            </div>
        </body>
        </html>
        """
        
        # 设置文档内容
        document.setHtml(html)
        
        # 创建打印机
        printer = QPrinter(QPrinter.HighResolution)
        printer.setPageSize(QPageSize.A4)
        
        # 创建打印预览对话框
        preview = QPrintPreviewDialog(printer, parent)
        preview.setWindowTitle("打印预览")
        # 设置窗口属性
        preview.setWindowFlags(
            Qt.Dialog |
            Qt.WindowMaximizeButtonHint |  # 添加最大化按钮
            Qt.WindowMinimizeButtonHint |  # 添加最小化按钮
            Qt.WindowCloseButtonHint       # 添加关闭按钮
        )
        # 设置初始大小
        preview.resize(800, 600)
        
        # 连接打印请求
        def handle_print(printer):
            document.print_(printer)
            
        preview.paintRequested.connect(handle_print)
        preview.exec() 

utils\excel_exporter.py -->报价单导出 excel文件

python 复制代码
from openpyxl import Workbook
from datetime import datetime

class QuoteExporter:
    @staticmethod
    def export_to_excel(quote_data, customer_name, file_path):
        wb = Workbook()
        ws = wb.active
        ws.title = "电脑配置报价单"
        
        # 添加客户信息
        ws.cell(row=1, column=1, value="客户名称:")
        ws.cell(row=1, column=2, value=customer_name)
        
        # 空一行
        current_row = 3
        
        # 设置表头
        headers = ["配件类型", "名称", "数量", "单价", "总价"]
        for col, header in enumerate(headers, 1):
            ws.cell(row=current_row, column=col, value=header)
        
        # 写入数据
        current_row += 1
        total_price = 0
        
        for item in quote_data:
            ws.cell(row=current_row, column=1, value=item['component_type'])
            ws.cell(row=current_row, column=2, value=item['name'])
            ws.cell(row=current_row, column=3, value=item['quantity'])
            ws.cell(row=current_row, column=4, value=item['price'])
            subtotal = item['quantity'] * item['price']
            ws.cell(row=current_row, column=5, value=subtotal)
            total_price += subtotal
            current_row += 1
        
        # 添加总计行
        ws.cell(row=current_row + 1, column=1, value="总计")
        ws.cell(row=current_row + 1, column=5, value=total_price)
        
        # 添加导出时间
        ws.cell(row=current_row + 3, column=1, 
                value=f"导出时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        
        # 保存文件
        wb.save(file_path) 

utils\component_excel_handler.py--》配件 导出导入 excel

python 复制代码
from openpyxl import Workbook, load_workbook
from datetime import datetime

class ComponentExcelHandler:
    @staticmethod
    def export_to_excel(components_data, file_path):
        """导出配件数据到Excel"""
        wb = Workbook()
        ws = wb.active
        ws.title = "配件列表"
        
        # 设置表头
        headers = ["类别", "名称", "库存", "采购价", "销售价"]
        for col, header in enumerate(headers, 1):
            ws.cell(row=1, column=col, value=header)
        
        # 写入数据
        current_row = 2
        for item in components_data:
            ws.cell(row=current_row, column=1, value=item['category_name'])
            ws.cell(row=current_row, column=2, value=item['name'])
            ws.cell(row=current_row, column=3, value=item['stock'])
            ws.cell(row=current_row, column=4, value=item['purchase_price'])
            ws.cell(row=current_row, column=5, value=item['selling_price'])
            current_row += 1
          
        # 保存文件
        wb.save(file_path)
    
    @staticmethod
    def import_from_excel(file_path):
        """从Excel导入配件数据"""
        wb = load_workbook(file_path)
        ws = wb.active
        
        # 读取数据(跳过表头)
        components = []
        for row in range(2, ws.max_row + 1):
            # 检查是否是空行
            if not ws.cell(row=row, column=1).value:
                continue
                
            component = {
                'category_name': ws.cell(row=row, column=1).value,
                'name': ws.cell(row=row, column=2).value,
                'stock': int(ws.cell(row=row, column=3).value or 0),
                'purchase_price': float(ws.cell(row=row, column=4).value or 0),
                'selling_price': float(ws.cell(row=row, column=5).value or 0)
            }
            components.append(component)
        
        return components 

utils\customer_excel_handler.py 客户导出导入

python 复制代码
from openpyxl import Workbook, load_workbook

class CustomerExcelHandler:
    @staticmethod
    def export_to_excel(customers_data, file_path):
        """导出客户数据到Excel"""
        wb = Workbook()
        ws = wb.active
        ws.title = "客户列表"
        
        # 设置表头
        headers = ["客户名称", "电话", "微信"]
        for col, header in enumerate(headers, 1):
            ws.cell(row=1, column=col, value=header)
        
        # 写入数据
        current_row = 2
        for item in customers_data:
            ws.cell(row=current_row, column=1, value=item['name'])
            ws.cell(row=current_row, column=2, value=item['phone'])
            ws.cell(row=current_row, column=3, value=item['wechat'])
            current_row += 1
        
        # 保存文件
        wb.save(file_path)
    
    @staticmethod
    def import_from_excel(file_path):
        """从Excel导入客户数据"""
        wb = load_workbook(file_path)
        ws = wb.active
        
        # 读取数据(跳过表头)
        customers = []
        for row in range(2, ws.max_row + 1):
            # 检查是否是空行
            if not ws.cell(row=row, column=1).value:
                continue
                
            customer = {
                'name': ws.cell(row=row, column=1).value,
                'phone': str(ws.cell(row=row, column=2).value or ''),
                'wechat': str(ws.cell(row=row, column=3).value or '')
            }
            customers.append(customer)
        
        return customers 

style\style.qss 样式

css 复制代码
/* 全局样式 */
QWidget {
    font-family: "Microsoft YaHei", "Segoe UI";
    font-size: 12px;
}

/* 主窗口样式 */
QMainWindow {
    background-color: #f5f5f5;
}

/* 标签样式 */
QLabel {
    color: #333333;
}

/* 按钮样式 */
QPushButton {
    background-color: #2196F3;
    color: white;
    border: none;
    padding: 5px 10px;
    border-radius: 3px;
    min-width: 30px;
}

QPushButton:hover {
    background-color: #1976D2;
}

QPushButton:pressed {
    background-color: #0D47A1;
}

/* 删除按钮特殊样式 */
QPushButton[text="删除"], QPushButton[text="删除类别"], QPushButton[text="删除配件"] {
    background-color: #F44336;
}

QPushButton[text="删除"]:hover, QPushButton[text="删除类别"]:hover, QPushButton[text="删除配件"]:hover {
    background-color: #D32F2F;
}

/* 输入框样式 */
QLineEdit, QSpinBox, QDoubleSpinBox {
    padding: 5px;
    border: 1px solid #BDBDBD;
    border-radius: 3px;
    background-color: white;
}

QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus {
    border: 1px solid #2196F3;
}

/* 下拉框样式 */
QComboBox {
    padding: 5px;
    border: 1px solid #BDBDBD;
    border-radius: 3px;
    background-color: white;
}

QComboBox::drop-down {
    border: none;
    width: 20px;
}

QComboBox::down-arrow {
    image: url(icons/down-arrow.png);
    width: 12px;
    height: 12px;
}

/* 表格样式 */
QTableWidget {
    background-color: white;
    alternate-background-color: #F5F5F5;
    border: 1px solid #E0E0E0;
    gridline-color: #E0E0E0;
}

QTableWidget::item {
    padding: 8px;
    min-height: 60px;
}

QTableWidget::item:selected {
    background-color: #2196F3;
    color: white;
}

QHeaderView::section {
    background-color: #FAFAFA;
    padding: 8px;
    border: none;
    border-right: 1px solid #E0E0E0;
    border-bottom: 1px solid #E0E0E0;
    min-height: 25px;
}

QHeaderView::section:horizontal {
    border-right: 1px solid #E0E0E0;
}

QHeaderView::section:horizontal:hover {
    background-color: #FAFAFA;
}

/* 设置表格行高 */
QTableWidget::item:first-column {
    min-height: 60px;
}

/* 禁止调整列宽 */
QHeaderView::section {
    background-color: #FAFAFA;
    padding: 8px;
    border: none;
    border-right: 1px solid #E0E0E0;
    border-bottom: 1px solid #E0E0E0;
}

/* 选项卡样式 */
QTabWidget::pane {
    border: 1px solid #E0E0E0;
    background-color: white;
}

QTabBar::tab {
    background-color: #FAFAFA;
    border: 1px solid #E0E0E0;
    padding: 8px 15px;
    margin-right: 2px;
}

QTabBar::tab:selected {
    background-color: white;
    border-bottom: none;
}

/* 对话框样式 */
QDialog {
    background-color: white;
}

/* 分组框样式 */
QGroupBox {
    border: 1px solid #E0E0E0;
    border-radius: 3px;
    margin-top: 10px;
    padding-top: 15px;
}

QGroupBox::title {
    subcontrol-origin: margin;
    subcontrol-position: top left;
    padding: 0 5px;
    color: #424242;
}

/* 滚动条样式 */
QScrollBar:vertical {
    border: none;
    background: #F5F5F5;
    width: 10px;
    margin: 0px;
}

QScrollBar::handle:vertical {
    background: #BDBDBD;
    min-height: 20px;
    border-radius: 5px;
}

QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
    height: 0px;
} 

database\db_manager.py --》数据生成

python 复制代码
import sqlite3
from datetime import datetime

class DatabaseManager:
    # 报价单状态常量
    QUOTATION_STATUS = {
        'PENDING': '待确认',
        'ACCEPTED': '已接受',
        'REJECTED': '已拒绝',
        'EXPIRED': '已过期',
        'COMPLETED': '已完成'
    }
    
    def __init__(self, db_name):
        self.conn = sqlite3.connect(db_name)
        self.cursor = self.conn.cursor()
        self.create_tables()
    
    def create_tables(self):
        """创建数据库表"""
        try:
            # 创建配件类别表
            self.cursor.execute("""
                CREATE TABLE IF NOT EXISTS component_categories (
                    category_id INTEGER PRIMARY KEY AUTOINCREMENT,
                    category_name TEXT NOT NULL UNIQUE,
                    description TEXT
                )
            """)

            # 创建配件表
            self.cursor.execute("""
                CREATE TABLE IF NOT EXISTS components (
                    component_id INTEGER PRIMARY KEY AUTOINCREMENT,
                    category_id INTEGER NOT NULL,
                    name TEXT NOT NULL,
                    stock INTEGER DEFAULT 0,
                    purchase_price REAL NOT NULL,
                    selling_price REAL NOT NULL,
                    FOREIGN KEY (category_id) REFERENCES component_categories (category_id)
                )
            """)

            # 创建客户表
            self.cursor.execute("""
                CREATE TABLE IF NOT EXISTS customers (
                    customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
                    name TEXT NOT NULL,
                    phone TEXT,
                    wechat TEXT
                )
            """)

            # 创建报价单表
            self.cursor.execute("""
                CREATE TABLE IF NOT EXISTS quotations (
                    quotation_id INTEGER PRIMARY KEY AUTOINCREMENT,
                    customer_id INTEGER NOT NULL,
                    quotation_date DATE NOT NULL,
                    total_amount REAL DEFAULT 0,
                    total_cost REAL DEFAULT 0,
                    pricing_type TEXT DEFAULT 'percentage',
                    markup_value REAL DEFAULT 15,
                    final_amount REAL DEFAULT 0,
                    valid_days INTEGER DEFAULT 7,
                    notes TEXT,
                    status TEXT DEFAULT '待确认',
                    FOREIGN KEY (customer_id) REFERENCES customers (customer_id)
                )
            """)

            # 创建报价单明细表
            self.cursor.execute("""
                CREATE TABLE IF NOT EXISTS quotation_details (
                    detail_id INTEGER PRIMARY KEY AUTOINCREMENT,
                    quotation_id INTEGER NOT NULL,
                    component_id INTEGER NOT NULL,
                    quantity INTEGER DEFAULT 1,
                    pricing_type TEXT NOT NULL DEFAULT 'fixed',
                    markup_value REAL DEFAULT 0,
                    unit_price REAL NOT NULL,
                    cost_price REAL NOT NULL,
                    subtotal REAL NOT NULL,
                    FOREIGN KEY (quotation_id) REFERENCES quotations (quotation_id),
                    FOREIGN KEY (component_id) REFERENCES components (component_id)
                )
            """)

            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise Exception(f"创建数据库表失败:{str(e)}")

    def add_component_category(self, name, description=""):
        """添加配件类别"""
        try:
            self.cursor.execute("""
                INSERT INTO component_categories (category_name, description)
                VALUES (?, ?)
            """, (name, description))
            self.conn.commit()
            return self.cursor.lastrowid
        except Exception as e:
            self.conn.rollback()
            raise e

    def add_component(self, category_id, name, purchase_price, selling_price, stock=0):
        """添加配件"""
        try:
            self.cursor.execute("""
                INSERT INTO components (category_id, name, purchase_price, selling_price, stock)
                VALUES (?, ?, ?, ?, ?)
            """, (category_id, name, purchase_price, selling_price, stock))
            self.conn.commit()
            return self.cursor.lastrowid
        except Exception as e:
            self.conn.rollback()
            raise e

    def get_all_categories(self):
        """获取所有配件类别"""
        self.cursor.execute("""
            SELECT category_id, category_name, description 
            FROM component_categories
            ORDER BY category_name
        """)
        return self.cursor.fetchall()

    def get_all_components(self):
        """获取所有配件(包含类别名称)"""
        self.cursor.execute("""
            SELECT c.component_id, cc.category_name, c.name, 
                   c.stock, c.purchase_price, c.selling_price
            FROM components c
            JOIN component_categories cc ON c.category_id = cc.category_id
            ORDER BY cc.category_name, c.name
        """)
        return self.cursor.fetchall()

    def delete_category(self, category_id):
        """删除配件类别(同时删除该类别下的所有配件)"""
        try:
            # 首先删除该类别下的所有配件
            self.cursor.execute("DELETE FROM components WHERE category_id = ?", (category_id,))
            # 然后删除类别
            self.cursor.execute("DELETE FROM component_categories WHERE category_id = ?", (category_id,))
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def delete_component(self, component_id):
        """删除配件"""
        try:
            self.cursor.execute("DELETE FROM components WHERE component_id = ?", (component_id,))
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def update_component(self, component_id, category_id, name, purchase_price, selling_price, stock):
        """更新配件信息"""
        try:
            self.cursor.execute("""
                UPDATE components 
                SET category_id = ?,
                    name = ?,
                    stock = ?,
                    purchase_price = ?,
                    selling_price = ?
                WHERE component_id = ?
            """, (category_id, name, stock, purchase_price, selling_price, component_id))
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def add_customer(self, name, phone="", wechat=""):
        """添加客户"""
        try:
            self.cursor.execute("""
                INSERT INTO customers (name, phone, wechat)
                VALUES (?, ?, ?)
            """, (name, phone, wechat))
            self.conn.commit()
            return self.cursor.lastrowid
        except Exception as e:
            self.conn.rollback()
            raise e

    def get_all_customers(self):
        """获取所有客户"""
        self.cursor.execute("""
            SELECT customer_id, name, phone, wechat
            FROM customers
            ORDER BY name
        """)
        return self.cursor.fetchall()

    def update_customer(self, customer_id, name, phone, wechat):
        """更新客户信息"""
        try:
            self.cursor.execute("""
                UPDATE customers 
                SET name = ?, phone = ?, wechat = ?
                WHERE customer_id = ?
            """, (name, phone, wechat, customer_id))
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def delete_customer(self, customer_id):
        """删除客户"""
        try:
            self.cursor.execute("DELETE FROM customers WHERE customer_id = ?", (customer_id,))
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def create_quotation(self, customer_id, quotation_date, valid_days=7, notes="", pricing_type='percentage', markup_value=15):
        """创建新报价单"""
        try:
            self.cursor.execute("""
                INSERT INTO quotations (
                    customer_id, quotation_date, valid_days, notes, 
                    pricing_type, markup_value
                )
                VALUES (?, ?, ?, ?, ?, ?)
            """, (customer_id, quotation_date, valid_days, notes, pricing_type, markup_value))
            self.conn.commit()
            return self.cursor.lastrowid
        except Exception as e:
            self.conn.rollback()
            raise e

    def add_quotation_detail(self, quotation_id, component_id, quantity, pricing_type='fixed', markup_value=0):
        """添加报价单明细"""
        try:
            # 获取配件信息
            self.cursor.execute("""
                SELECT purchase_price, selling_price 
                FROM components 
                WHERE component_id = ?
            """, (component_id,))
            comp = self.cursor.fetchone()
            if not comp:
                raise Exception("配件不存在")
            
            cost_price = comp[0]
            # 根据定价类型计算单价
            if pricing_type == 'fixed':
                unit_price = comp[1]  # 使用配件的销售价
            elif pricing_type == 'percentage':
                unit_price = cost_price * (1 + markup_value / 100)  # 成本价加百分比
            else:  # markup
                unit_price = cost_price + markup_value  # 成本价加固定金额
                
            subtotal = unit_price * quantity
            
            # 插入明细记录
            self.cursor.execute("""
                INSERT INTO quotation_details 
                (quotation_id, component_id, quantity, pricing_type, markup_value, 
                 unit_price, cost_price, subtotal)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            """, (quotation_id, component_id, quantity, pricing_type, markup_value,
                  unit_price, cost_price, subtotal))
            
            # 更新报价单总金额
            self.cursor.execute("""
                UPDATE quotations 
                SET total_amount = (
                    SELECT SUM(subtotal) 
                    FROM quotation_details 
                    WHERE quotation_id = ?
                )
                WHERE quotation_id = ?
            """, (quotation_id, quotation_id))
            
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def get_all_quotations(self):
        """获取所有报价单(包含客户信息)"""
        self.cursor.execute("""
            SELECT q.quotation_id, c.name, q.quotation_date, 
                   q.final_amount, q.valid_days, q.status, q.notes,
                   q.total_cost, q.total_amount, q.pricing_type, q.markup_value
            FROM quotations q
            JOIN customers c ON q.customer_id = c.customer_id
            ORDER BY q.quotation_date DESC
        """)
        return self.cursor.fetchall()

    def get_quotation_details(self, quotation_id):
        """获取报价单明细"""
        self.cursor.execute("""
            SELECT qd.detail_id, c.name, qd.quantity, 
                   qd.pricing_type, qd.markup_value,
                   qd.unit_price, qd.subtotal, qd.cost_price,
                   (qd.cost_price * qd.quantity) as total_cost
            FROM quotation_details qd
            JOIN components c ON qd.component_id = c.component_id
            WHERE qd.quotation_id = ?
            ORDER BY qd.detail_id
        """, (quotation_id,))
        return self.cursor.fetchall()

    def update_quotation_status(self, quotation_id, status):
        """更新报价单状态"""
        try:
            self.cursor.execute("""
                UPDATE quotations 
                SET status = ?
                WHERE quotation_id = ?
            """, (status, quotation_id))
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def delete_quotation(self, quotation_id):
        """删除报价单及其明细"""
        try:
            # 首先删除明细
            self.cursor.execute("DELETE FROM quotation_details WHERE quotation_id = ?", (quotation_id,))
            # 然后删除报价单
            self.cursor.execute("DELETE FROM quotations WHERE quotation_id = ?", (quotation_id,))
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def get_quotation(self, quotation_id):
        """获取报价单信息"""
        self.cursor.execute("""
            SELECT q.quotation_id, c.name, q.quotation_date, 
                   q.total_amount, q.valid_days, q.status, q.notes,
                   q.total_cost, q.final_amount, q.pricing_type, q.markup_value
            FROM quotations q
            JOIN customers c ON q.customer_id = c.customer_id
            WHERE q.quotation_id = ?
        """, (quotation_id,))
        return self.cursor.fetchone()

    def delete_quotation_detail(self, detail_id):
        """删除报价单明细"""
        try:
            # 获取报价单ID
            self.cursor.execute("""
                SELECT quotation_id FROM quotation_details WHERE detail_id = ?
            """, (detail_id,))
            quotation_id = self.cursor.fetchone()[0]
            
            # 删除明细
            self.cursor.execute("DELETE FROM quotation_details WHERE detail_id = ?", (detail_id,))
            
            # 更新报价单总金额
            self.cursor.execute("""
                UPDATE quotations 
                SET total_amount = (
                    SELECT SUM(subtotal) 
                    FROM quotation_details 
                    WHERE quotation_id = ?
                )
                WHERE quotation_id = ?
            """, (quotation_id, quotation_id))
            
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def update_quotation(self, quotation_id, quotation_date, valid_days, notes, pricing_type=None, markup_value=None):
        """更新报价单基本信息"""
        try:
            if pricing_type is not None and markup_value is not None:
                self.cursor.execute("""
                    UPDATE quotations 
                    SET quotation_date = ?,
                        valid_days = ?,
                        notes = ?,
                        pricing_type = ?,
                        markup_value = ?
                    WHERE quotation_id = ?
                """, (quotation_date, valid_days, notes, pricing_type, markup_value, quotation_id))
            else:
                self.cursor.execute("""
                    UPDATE quotations 
                    SET quotation_date = ?,
                        valid_days = ?,
                        notes = ?
                    WHERE quotation_id = ?
                """, (quotation_date, valid_days, notes, quotation_id))
            
            # 更新总金额
            self.update_quotation_total(quotation_id)
            
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def update_quotation_total(self, quotation_id):
        """更新报价单总金额"""
        try:
            # 计算明细总金额和总成本
            self.cursor.execute("""
                UPDATE quotations 
                SET total_amount = (
                        SELECT SUM(subtotal) 
                        FROM quotation_details 
                        WHERE quotation_id = ?
                    ),
                    total_cost = (
                        SELECT SUM(cost_price * quantity)
                        FROM quotation_details
                        WHERE quotation_id = ?
                    )
                WHERE quotation_id = ?
            """, (quotation_id, quotation_id, quotation_id))

            # 获取报价单信息
            self.cursor.execute("""
                SELECT total_amount, total_cost, pricing_type, markup_value
                FROM quotations
                WHERE quotation_id = ?
            """, (quotation_id,))
            total_amount, total_cost, pricing_type, markup_value = self.cursor.fetchone()

            # 计算最终金额
            if pricing_type == 'fixed':
                final_amount = total_amount
            elif pricing_type == 'percentage':
                final_amount = total_cost * (1 + markup_value / 100)
            else:  # markup
                final_amount = total_cost + markup_value

            # 更新最终金额
            self.cursor.execute("""
                UPDATE quotations 
                SET final_amount = ?
                WHERE quotation_id = ?
            """, (final_amount, quotation_id))

            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
            raise e

    def get_current_quote_items(self):
        """获取当前报价单的所有项目"""
        try:
            # 获取最新的报价单ID
            self.cursor.execute("""
                SELECT quotation_id 
                FROM quotations 
                ORDER BY quotation_date DESC 
                LIMIT 1
            """)
            quote_id = self.cursor.fetchone()[0]
            
            # 获取该报价单的所有项目
            self.cursor.execute("""
                SELECT 
                    cc.category_name as component_type,
                    c.name,
                    qd.quantity,
                    qd.unit_price as price
                FROM quotation_details qd
                JOIN components c ON qd.component_id = c.component_id
                JOIN component_categories cc ON c.category_id = cc.category_id
                WHERE qd.quotation_id = ?
            """, (quote_id,))
            
            # 将结果转换为字典列表
            items = []
            for row in self.cursor.fetchall():
                items.append({
                    'component_type': row[0],
                    'name': row[1],
                    'quantity': row[2],
                    'price': row[3]
                })
            
            return items
            
        except Exception as e:
            print(f"获取报价单项目时出错: {str(e)}")
            return []

    def get_quotation_items(self, quotation_id):
        """获取指定报价单的所有项目"""
        try:
            self.cursor.execute("""
                SELECT 
                    cc.category_name as component_type,
                    c.name,
                    qd.quantity,
                    qd.unit_price as price
                FROM quotation_details qd
                JOIN components c ON qd.component_id = c.component_id
                JOIN component_categories cc ON c.category_id = cc.category_id
                WHERE qd.quotation_id = ?
            """, (quotation_id,))
            
            items = []
            for row in self.cursor.fetchall():
                items.append({
                    'component_type': row[0],
                    'name': row[1],
                    'quantity': row[2],
                    'price': row[3]
                })
            
            return items
            
        except Exception as e:
            print(f"获取报价单项目时出错: {str(e)}")
            return []

    # 其他数据库操作方法... 

db\computer_quote.db--》由程序生成

相关推荐
盐盐88769032 分钟前
【信息系统项目管理师】【综合知识】【备考知识点】【思维导图】第十一章 项目成本管理
数据库·经验分享·笔记·运维开发·学习方法
Edward-tan35 分钟前
【玩转全栈】----Django连接MySQL
python·mysql·django
油头少年_w44 分钟前
Python数据容器
python
有杨既安然1 小时前
Python爬虫入门指南:从零开始抓取数据
开发语言·爬虫·python·信息可视化·数据分析·excel
Grovvy_Deng1 小时前
使用rust加速python的tgz解压
开发语言·python·rust
Tiandaren1 小时前
医学图像分析工具02:3D Slicer || 医学影像可视化与分析工具 支持第三方插件
c++·人工智能·python·深度学习·3d·开源
Run Out Of Brain1 小时前
使用MySQL APT源在Linux上安装MySQL
linux·数据库·mysql
EnochChen_1 小时前
PyTorch快速入门教程【小土堆】之Sequential使用和小实战
人工智能·pytorch·python
深圳行云创新1 小时前
ChatBI来啦!NBAI 正式上线 NL2SQL 功能
数据库·人工智能·chatgpt·ai智能体
睿思达DBA_WGX2 小时前
Oracle 11g rac + Dataguard 环境调整 redo log 大小
数据库·oracle