一、数据库结构说明
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 | 电话号码 | |
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 |
表关系说明
-
components -> component_categories:通过 category_id 关联
-
quotations -> customers:通过 customer_id 关联
-
quotation_details -> quotations:通过 quotation_id 关联
-
quotation_details -> components:通过 component_id 关联
枚举值说明
pricing_type (定价类型)
-
fixed:固定价格
-
percentage:百分比加价
-
markup:固定加价
status (报价单状态)
-
待确认
-
已接受
-
已拒绝
-
已过期
-
已完成
二、 程序中SQLite3的数据库表结构:
- 配件类别表 (component_categories)
CREATE TABLE component_categories ( category_id INTEGER PRIMARY KEY AUTOINCREMENT, category_name TEXT NOT NULL UNIQUE, description TEXT )
- 配件表 (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) )
- 客户表 (customers)
CREATE TABLE customers ( customer_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, wechat TEXT )
- 报价单表 (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) )
- 报价单明细表 (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) )
表之间的关系:
-
components 表通过 category_id 关联到 component_categories 表
-
quotations 表通过 customer_id 关联到 customers 表
-
quotation_details 表通过 quotation_id 关联到 quotations 表
-
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 []
# 其他数据库操作方法...