obuv-exam 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- obuv_exam-0.1.0/MANIFEST.in +4 -0
- obuv_exam-0.1.0/PKG-INFO +12 -0
- obuv_exam-0.1.0/README.md +3 -0
- obuv_exam-0.1.0/pyproject.toml +23 -0
- obuv_exam-0.1.0/setup.cfg +4 -0
- obuv_exam-0.1.0/src/obuv_exam/__init__.py +1 -0
- obuv_exam-0.1.0/src/obuv_exam/bundle.py +657 -0
- obuv_exam-0.1.0/src/obuv_exam/database/__init__.py +1 -0
- obuv_exam-0.1.0/src/obuv_exam/database/db.py +163 -0
- obuv_exam-0.1.0/src/obuv_exam/database/script.sql +132 -0
- obuv_exam-0.1.0/src/obuv_exam/main.py +31 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/icons/Icon.JPG +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/icons/Icon.ico +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/icons/Icon.png +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/1.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/10.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/2.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/3.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/4.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/5.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/6.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/7.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/8.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/9.jpg +0 -0
- obuv_exam-0.1.0/src/obuv_exam/resources/images/picture.png +0 -0
- obuv_exam-0.1.0/src/obuv_exam/styles/common.qss +19 -0
- obuv_exam-0.1.0/src/obuv_exam.egg-info/PKG-INFO +12 -0
- obuv_exam-0.1.0/src/obuv_exam.egg-info/SOURCES.txt +30 -0
- obuv_exam-0.1.0/src/obuv_exam.egg-info/dependency_links.txt +1 -0
- obuv_exam-0.1.0/src/obuv_exam.egg-info/entry_points.txt +2 -0
- obuv_exam-0.1.0/src/obuv_exam.egg-info/requires.txt +2 -0
- obuv_exam-0.1.0/src/obuv_exam.egg-info/top_level.txt +1 -0
obuv_exam-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obuv-exam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Demo exam project for shoe store UI
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: PyQt6>=6.0
|
|
8
|
+
Requires-Dist: PyMySQL>=1.1
|
|
9
|
+
|
|
10
|
+
# obuv-exam
|
|
11
|
+
|
|
12
|
+
Demo exam package for a shoe store UI project.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "obuv-exam"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Demo exam project for shoe store UI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
dependencies = ["PyQt6>=6.0", "PyMySQL>=1.1"]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
obuv-exam = "obuv_exam.main:main"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools]
|
|
17
|
+
package-dir = {"" = "src"}
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["src"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.package-data]
|
|
23
|
+
obuv_exam = ["resources/**/*", "database/script.sql", "styles/common.qss"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .main import main
|
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import random
|
|
5
|
+
from string import ascii_uppercase
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtCore import Qt, QDate, pyqtSignal
|
|
8
|
+
from PyQt6.QtGui import QPixmap
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QComboBox,
|
|
11
|
+
QDateEdit,
|
|
12
|
+
QDialog,
|
|
13
|
+
QDoubleSpinBox,
|
|
14
|
+
QFileDialog,
|
|
15
|
+
QFormLayout,
|
|
16
|
+
QFrame,
|
|
17
|
+
QHBoxLayout,
|
|
18
|
+
QLabel,
|
|
19
|
+
QLineEdit,
|
|
20
|
+
QMessageBox,
|
|
21
|
+
QPushButton,
|
|
22
|
+
QScrollArea,
|
|
23
|
+
QSpinBox,
|
|
24
|
+
QTabWidget,
|
|
25
|
+
QTextEdit,
|
|
26
|
+
QVBoxLayout,
|
|
27
|
+
QWidget,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .database.db import dao
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
BASE_DIR = Path(__file__).resolve().parent
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def generate_product_articule():
|
|
37
|
+
digits = "0123456789"
|
|
38
|
+
return (
|
|
39
|
+
random.choice(ascii_uppercase)
|
|
40
|
+
+ "".join(random.choices(digits, k=3))
|
|
41
|
+
+ random.choice(ascii_uppercase)
|
|
42
|
+
+ random.choice(digits)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AuthWindow(QDialog):
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
super().__init__()
|
|
49
|
+
self.main_window = None
|
|
50
|
+
self.setWindowTitle("Вход в систему")
|
|
51
|
+
self.setFixedSize(420, 220)
|
|
52
|
+
|
|
53
|
+
layout = QVBoxLayout(self)
|
|
54
|
+
title = QLabel("ООО «Обувь»")
|
|
55
|
+
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
56
|
+
layout.addWidget(title)
|
|
57
|
+
|
|
58
|
+
form = QFormLayout()
|
|
59
|
+
self.loginLineEdit = QLineEdit()
|
|
60
|
+
self.passwordLineEdit = QLineEdit()
|
|
61
|
+
self.passwordLineEdit.setEchoMode(QLineEdit.EchoMode.Password)
|
|
62
|
+
form.addRow("Логин", self.loginLineEdit)
|
|
63
|
+
form.addRow("Пароль", self.passwordLineEdit)
|
|
64
|
+
layout.addLayout(form)
|
|
65
|
+
|
|
66
|
+
buttons = QHBoxLayout()
|
|
67
|
+
self.loginButton = QPushButton("Войти")
|
|
68
|
+
self.guestButton = QPushButton("Гость")
|
|
69
|
+
buttons.addWidget(self.loginButton)
|
|
70
|
+
buttons.addWidget(self.guestButton)
|
|
71
|
+
layout.addLayout(buttons)
|
|
72
|
+
|
|
73
|
+
self.loginButton.clicked.connect(self.login)
|
|
74
|
+
self.guestButton.clicked.connect(self.guest_login)
|
|
75
|
+
|
|
76
|
+
def login(self):
|
|
77
|
+
login = self.loginLineEdit.text().strip()
|
|
78
|
+
password = self.passwordLineEdit.text().strip()
|
|
79
|
+
|
|
80
|
+
if not login or not password:
|
|
81
|
+
QMessageBox.warning(self, "Ошибка заполнения", "Введите логин и пароль!")
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
account = dao.login(login, password)
|
|
85
|
+
if account is None:
|
|
86
|
+
QMessageBox.critical(self, "Ошибка авторизации", "Неправильный логин или пароль!")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
self.main_window = MainWindow(account)
|
|
90
|
+
self.main_window.show()
|
|
91
|
+
self.close()
|
|
92
|
+
|
|
93
|
+
def guest_login(self):
|
|
94
|
+
self.main_window = MainWindow()
|
|
95
|
+
self.main_window.show()
|
|
96
|
+
self.close()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ProductDialog(QDialog):
|
|
100
|
+
def __init__(self, product: dict | None = None) -> None:
|
|
101
|
+
super().__init__()
|
|
102
|
+
self.product = product
|
|
103
|
+
self.image_path: str | None = None
|
|
104
|
+
self.setWindowTitle("Товар")
|
|
105
|
+
self.setMinimumSize(620, 560)
|
|
106
|
+
|
|
107
|
+
layout = QVBoxLayout(self)
|
|
108
|
+
self.imageLabel = QLabel()
|
|
109
|
+
self.imageLabel.setFixedSize(150, 150)
|
|
110
|
+
self.imageLabel.setScaledContents(True)
|
|
111
|
+
layout.addWidget(self.imageLabel)
|
|
112
|
+
|
|
113
|
+
form = QFormLayout()
|
|
114
|
+
self.nameLineEdit = QLineEdit()
|
|
115
|
+
self.categoryComboBox = QComboBox()
|
|
116
|
+
self.supplierComboBox = QComboBox()
|
|
117
|
+
self.manufactureComboBox = QComboBox()
|
|
118
|
+
self.unitComboBox = QComboBox()
|
|
119
|
+
self.discountSpinBox = QSpinBox()
|
|
120
|
+
self.discountSpinBox.setRange(0, 100)
|
|
121
|
+
self.priceDoubleSpinBox = QDoubleSpinBox()
|
|
122
|
+
self.priceDoubleSpinBox.setMaximum(1_000_000)
|
|
123
|
+
self.priceDoubleSpinBox.setDecimals(2)
|
|
124
|
+
self.amountSpinBox = QSpinBox()
|
|
125
|
+
self.amountSpinBox.setMaximum(1_000_000)
|
|
126
|
+
self.descriptionEdit = QTextEdit()
|
|
127
|
+
form.addRow("Название", self.nameLineEdit)
|
|
128
|
+
form.addRow("Категория", self.categoryComboBox)
|
|
129
|
+
form.addRow("Поставщик", self.supplierComboBox)
|
|
130
|
+
form.addRow("Производитель", self.manufactureComboBox)
|
|
131
|
+
form.addRow("Ед. изм.", self.unitComboBox)
|
|
132
|
+
form.addRow("Скидка", self.discountSpinBox)
|
|
133
|
+
form.addRow("Цена", self.priceDoubleSpinBox)
|
|
134
|
+
form.addRow("Количество", self.amountSpinBox)
|
|
135
|
+
form.addRow("Описание", self.descriptionEdit)
|
|
136
|
+
layout.addLayout(form)
|
|
137
|
+
|
|
138
|
+
row = QHBoxLayout()
|
|
139
|
+
self.upload_image_button = QPushButton("Загрузить изображение")
|
|
140
|
+
self.saveButton = QPushButton("Сохранить")
|
|
141
|
+
row.addWidget(self.upload_image_button)
|
|
142
|
+
row.addWidget(self.saveButton)
|
|
143
|
+
if self.product is not None:
|
|
144
|
+
self.deleteButton = QPushButton("Удалить")
|
|
145
|
+
row.addWidget(self.deleteButton)
|
|
146
|
+
layout.addLayout(row)
|
|
147
|
+
|
|
148
|
+
self.upload_image_button.clicked.connect(self.load_image)
|
|
149
|
+
self.saveButton.clicked.connect(self.on_save)
|
|
150
|
+
if self.product is not None:
|
|
151
|
+
self.deleteButton.clicked.connect(self.on_delete)
|
|
152
|
+
self.load_comboboxes()
|
|
153
|
+
self.load_product_info(product)
|
|
154
|
+
|
|
155
|
+
def load_comboboxes(self):
|
|
156
|
+
for item in dao.load_categories():
|
|
157
|
+
self.categoryComboBox.addItem(item["name"], item["id"])
|
|
158
|
+
for item in dao.load_suppliers():
|
|
159
|
+
self.supplierComboBox.addItem(item["name"], item["id"])
|
|
160
|
+
for item in dao.load_manufactures():
|
|
161
|
+
self.manufactureComboBox.addItem(item["name"], item["id"])
|
|
162
|
+
for item in dao.load_units():
|
|
163
|
+
self.unitComboBox.addItem(item["name"], item["id"])
|
|
164
|
+
|
|
165
|
+
def load_product_info(self, product: dict | None):
|
|
166
|
+
if product is None:
|
|
167
|
+
self.imageLabel.setPixmap(QPixmap(str(BASE_DIR / "resources/images/picture.png")).scaled(150, 150))
|
|
168
|
+
return
|
|
169
|
+
self.nameLineEdit.setText(product["name"])
|
|
170
|
+
self.discountSpinBox.setValue(product["discount"])
|
|
171
|
+
self.priceDoubleSpinBox.setValue(float(product["price"]))
|
|
172
|
+
self.amountSpinBox.setValue(product["warehouse_amount"])
|
|
173
|
+
self.descriptionEdit.setPlainText(product["description"] or "")
|
|
174
|
+
self.categoryComboBox.setCurrentText(product["category_name"])
|
|
175
|
+
self.supplierComboBox.setCurrentText(product["supplier_name"])
|
|
176
|
+
self.manufactureComboBox.setCurrentText(product["manufacture_name"])
|
|
177
|
+
self.unitComboBox.setCurrentText(product["unit_name"])
|
|
178
|
+
if product.get("image_name"):
|
|
179
|
+
self.image_path = product["image_name"]
|
|
180
|
+
self.imageLabel.setPixmap(QPixmap(str(BASE_DIR / "resources/images" / product["image_name"])).scaled(150, 150))
|
|
181
|
+
else:
|
|
182
|
+
self.imageLabel.setPixmap(QPixmap(str(BASE_DIR / "resources/images/picture.png")).scaled(150, 150))
|
|
183
|
+
|
|
184
|
+
def load_image(self):
|
|
185
|
+
file_path, _ = QFileDialog.getOpenFileName(self, "Выбор изображения", str(BASE_DIR / "resources/images"), "Изображения (*.png *.jpg *.jpeg)")
|
|
186
|
+
if file_path:
|
|
187
|
+
self.image_path = file_path
|
|
188
|
+
self.imageLabel.setPixmap(QPixmap(file_path).scaled(150, 150))
|
|
189
|
+
|
|
190
|
+
def validate_form(self):
|
|
191
|
+
if not self.nameLineEdit.text().strip():
|
|
192
|
+
raise ValueError("Введите название товара")
|
|
193
|
+
|
|
194
|
+
def on_save(self):
|
|
195
|
+
try:
|
|
196
|
+
self.validate_form()
|
|
197
|
+
self.accept()
|
|
198
|
+
except ValueError as error:
|
|
199
|
+
QMessageBox.warning(self, "Ошибка ввода", str(error))
|
|
200
|
+
|
|
201
|
+
def on_delete(self):
|
|
202
|
+
if QMessageBox.question(self, "Удаление", "Удалить товар?") == QMessageBox.StandardButton.Yes:
|
|
203
|
+
self.done(2)
|
|
204
|
+
|
|
205
|
+
def get_data(self):
|
|
206
|
+
return (
|
|
207
|
+
self.nameLineEdit.text().strip(),
|
|
208
|
+
self.categoryComboBox.currentData(),
|
|
209
|
+
self.supplierComboBox.currentData(),
|
|
210
|
+
self.manufactureComboBox.currentData(),
|
|
211
|
+
self.unitComboBox.currentData(),
|
|
212
|
+
self.discountSpinBox.value(),
|
|
213
|
+
self.priceDoubleSpinBox.value(),
|
|
214
|
+
self.image_path,
|
|
215
|
+
self.amountSpinBox.value(),
|
|
216
|
+
self.descriptionEdit.toPlainText().strip(),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class OrderDialog(QDialog):
|
|
221
|
+
def __init__(self, account: dict | None = None, order: dict | None = None) -> None:
|
|
222
|
+
super().__init__()
|
|
223
|
+
self.account = account
|
|
224
|
+
self.order = order
|
|
225
|
+
self.setWindowTitle("Заказ")
|
|
226
|
+
self.setMinimumSize(520, 420)
|
|
227
|
+
|
|
228
|
+
layout = QVBoxLayout(self)
|
|
229
|
+
form = QFormLayout()
|
|
230
|
+
|
|
231
|
+
self.articuleLineEdit = QLineEdit()
|
|
232
|
+
self.orderDateEdit = QDateEdit()
|
|
233
|
+
self.orderDateEdit.setCalendarPopup(True)
|
|
234
|
+
self.orderDateEdit.setDate(QDate.currentDate())
|
|
235
|
+
self.deliveryDateEdit = QDateEdit()
|
|
236
|
+
self.deliveryDateEdit.setCalendarPopup(True)
|
|
237
|
+
self.deliveryDateEdit.setDate(QDate.currentDate())
|
|
238
|
+
self.pickupPointComboBox = QComboBox()
|
|
239
|
+
self.userComboBox = QComboBox()
|
|
240
|
+
self.codeSpinBox = QSpinBox()
|
|
241
|
+
self.codeSpinBox.setMaximum(999999)
|
|
242
|
+
self.statusComboBox = QComboBox()
|
|
243
|
+
self.productComboBox = QComboBox()
|
|
244
|
+
self.amountSpinBox = QSpinBox()
|
|
245
|
+
self.amountSpinBox.setMaximum(100000)
|
|
246
|
+
|
|
247
|
+
form.addRow("Артикул", self.articuleLineEdit)
|
|
248
|
+
form.addRow("Дата заказа", self.orderDateEdit)
|
|
249
|
+
form.addRow("Дата доставки", self.deliveryDateEdit)
|
|
250
|
+
form.addRow("Пункт выдачи", self.pickupPointComboBox)
|
|
251
|
+
form.addRow("Пользователь", self.userComboBox)
|
|
252
|
+
form.addRow("Код", self.codeSpinBox)
|
|
253
|
+
form.addRow("Статус", self.statusComboBox)
|
|
254
|
+
form.addRow("Товар", self.productComboBox)
|
|
255
|
+
form.addRow("Количество", self.amountSpinBox)
|
|
256
|
+
layout.addLayout(form)
|
|
257
|
+
|
|
258
|
+
buttons = QHBoxLayout()
|
|
259
|
+
self.saveButton = QPushButton("Сохранить")
|
|
260
|
+
buttons.addWidget(self.saveButton)
|
|
261
|
+
if self.order is not None:
|
|
262
|
+
self.deleteButton = QPushButton("Удалить")
|
|
263
|
+
buttons.addWidget(self.deleteButton)
|
|
264
|
+
layout.addLayout(buttons)
|
|
265
|
+
|
|
266
|
+
self.saveButton.clicked.connect(self.on_save)
|
|
267
|
+
if self.order is not None:
|
|
268
|
+
self.deleteButton.clicked.connect(self.on_delete)
|
|
269
|
+
self.load_comboboxes()
|
|
270
|
+
self.load_order_info(order)
|
|
271
|
+
if self.account and self.account.get("role_name") == "Авторизованный клиент":
|
|
272
|
+
self.userComboBox.setCurrentIndex(0)
|
|
273
|
+
self.userComboBox.setEnabled(False)
|
|
274
|
+
|
|
275
|
+
def load_comboboxes(self):
|
|
276
|
+
for item in dao.load_pickup_points():
|
|
277
|
+
self.pickupPointComboBox.addItem(item["address"], item["id"])
|
|
278
|
+
for item in dao.select("users"):
|
|
279
|
+
full_name = f"{item['last_name']} {item['first_name']} {item['middle_name'] or ''}".strip()
|
|
280
|
+
self.userComboBox.addItem(full_name, item["id"])
|
|
281
|
+
for item in dao.load_order_statuses():
|
|
282
|
+
self.statusComboBox.addItem(item["status_name"], item["id"])
|
|
283
|
+
for item in dao.load_products():
|
|
284
|
+
self.productComboBox.addItem(f"{item['name']} | {item['articule']}", item["id"])
|
|
285
|
+
|
|
286
|
+
def load_order_info(self, order: dict | None):
|
|
287
|
+
if order is None:
|
|
288
|
+
return
|
|
289
|
+
self.articuleLineEdit.setText(order["articule"])
|
|
290
|
+
self.codeSpinBox.setValue(order["code"])
|
|
291
|
+
self.amountSpinBox.setValue(order.get("amount", 1))
|
|
292
|
+
self.userComboBox.setCurrentText(f"{order['user_last_name']} {order['user_first_name']} {order['user_middle_name'] or ''}".strip())
|
|
293
|
+
self.pickupPointComboBox.setCurrentText(order["address"])
|
|
294
|
+
self.statusComboBox.setCurrentText(order["status_name"])
|
|
295
|
+
self.productComboBox.setCurrentText(f"{order['product_name']} | {order['articule']}")
|
|
296
|
+
if order.get("order_date"):
|
|
297
|
+
self.orderDateEdit.setDate(QDate.fromString(str(order["order_date"]), "yyyy-MM-dd"))
|
|
298
|
+
if order.get("delivery_date"):
|
|
299
|
+
self.deliveryDateEdit.setDate(QDate.fromString(str(order["delivery_date"]), "yyyy-MM-dd"))
|
|
300
|
+
|
|
301
|
+
def validate_form(self):
|
|
302
|
+
if not self.articuleLineEdit.text().strip():
|
|
303
|
+
raise ValueError("Введите артикул")
|
|
304
|
+
if self.productComboBox.currentData() is None:
|
|
305
|
+
raise ValueError("Выберите товар")
|
|
306
|
+
|
|
307
|
+
def on_save(self):
|
|
308
|
+
try:
|
|
309
|
+
self.validate_form()
|
|
310
|
+
self.accept()
|
|
311
|
+
except ValueError as error:
|
|
312
|
+
QMessageBox.warning(self, "Ошибка ввода", str(error))
|
|
313
|
+
|
|
314
|
+
def on_delete(self):
|
|
315
|
+
if QMessageBox.question(self, "Удаление", "Удалить заказ?") == QMessageBox.StandardButton.Yes:
|
|
316
|
+
self.done(2)
|
|
317
|
+
|
|
318
|
+
def get_data(self):
|
|
319
|
+
return (
|
|
320
|
+
self.articuleLineEdit.text().strip(),
|
|
321
|
+
self.orderDateEdit.date().toString("yyyy-MM-dd"),
|
|
322
|
+
self.deliveryDateEdit.date().toString("yyyy-MM-dd"),
|
|
323
|
+
self.pickupPointComboBox.currentData(),
|
|
324
|
+
self.userComboBox.currentData(),
|
|
325
|
+
self.codeSpinBox.value(),
|
|
326
|
+
self.statusComboBox.currentData(),
|
|
327
|
+
self.productComboBox.currentData(),
|
|
328
|
+
self.amountSpinBox.value(),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class ProductCard(QFrame):
|
|
333
|
+
clicked = pyqtSignal(dict)
|
|
334
|
+
|
|
335
|
+
def __init__(self, product: dict, admin_mode: bool = False) -> None:
|
|
336
|
+
super().__init__()
|
|
337
|
+
self.product = product
|
|
338
|
+
self.setObjectName("ProductCard")
|
|
339
|
+
|
|
340
|
+
layout = QHBoxLayout(self)
|
|
341
|
+
self.image_frame = QFrame()
|
|
342
|
+
self.image_frame.setObjectName("cardBlock")
|
|
343
|
+
image_layout = QVBoxLayout(self.image_frame)
|
|
344
|
+
self.image = QLabel()
|
|
345
|
+
self.image.setFixedSize(130, 130)
|
|
346
|
+
self.image.setScaledContents(True)
|
|
347
|
+
image_layout.addWidget(self.image)
|
|
348
|
+
layout.addWidget(self.image_frame)
|
|
349
|
+
|
|
350
|
+
self.main_frame = QFrame()
|
|
351
|
+
self.main_frame.setObjectName("cardBlock")
|
|
352
|
+
content = QVBoxLayout(self.main_frame)
|
|
353
|
+
self.category_name_label = QLabel()
|
|
354
|
+
self.description_label = QLabel()
|
|
355
|
+
self.description_label.setWordWrap(True)
|
|
356
|
+
self.manufacture_label = QLabel()
|
|
357
|
+
self.supplier_label = QLabel()
|
|
358
|
+
self.price_label = QLabel()
|
|
359
|
+
self.unit_label = QLabel()
|
|
360
|
+
self.amount_label = QLabel()
|
|
361
|
+
for label in [self.category_name_label, self.description_label, self.manufacture_label, self.supplier_label, self.price_label, self.unit_label, self.amount_label]:
|
|
362
|
+
content.addWidget(label)
|
|
363
|
+
layout.addWidget(self.main_frame, 1)
|
|
364
|
+
|
|
365
|
+
self.discount_frame = QFrame()
|
|
366
|
+
self.discount_frame.setObjectName("cardBlock")
|
|
367
|
+
discount_layout = QVBoxLayout(self.discount_frame)
|
|
368
|
+
self.discount_label = QLabel()
|
|
369
|
+
self.discount_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
370
|
+
discount_layout.addWidget(self.discount_label)
|
|
371
|
+
layout.addWidget(self.discount_frame)
|
|
372
|
+
|
|
373
|
+
self.load_product(product)
|
|
374
|
+
self.adjust_styles()
|
|
375
|
+
|
|
376
|
+
def adjust_styles(self):
|
|
377
|
+
styles = "QFrame#ProductCard {border: 1px solid black; padding: 4px;} QFrame#ProductCard:hover {border: 1px solid #00FA9A;} QFrame#cardBlock {border: 1px solid black; padding: 6px;}"
|
|
378
|
+
if self.product["discount"] > 15:
|
|
379
|
+
styles += "QFrame#ProductCard {background-color: #2E8B57;}"
|
|
380
|
+
if self.product["warehouse_amount"] == 0:
|
|
381
|
+
styles += "#amount {color: blue;}"
|
|
382
|
+
self.setStyleSheet(styles)
|
|
383
|
+
|
|
384
|
+
def load_product(self, product: dict):
|
|
385
|
+
image_name = product.get("image_name")
|
|
386
|
+
if image_name:
|
|
387
|
+
image_path = BASE_DIR / "resources/images" / image_name
|
|
388
|
+
else:
|
|
389
|
+
image_path = BASE_DIR / "resources/images/picture.png"
|
|
390
|
+
self.image.setPixmap(QPixmap(str(image_path)).scaled(130, 130))
|
|
391
|
+
self.category_name_label.setText(f"<b>{product['category_name']} | {product['name']}</b>")
|
|
392
|
+
self.description_label.setText(f"Описание: {product['description']}")
|
|
393
|
+
self.manufacture_label.setText(f"Производитель: {product['manufacture_name']}")
|
|
394
|
+
self.supplier_label.setText(f"Поставщик: {product['supplier_name']}")
|
|
395
|
+
if product["discount"] > 0:
|
|
396
|
+
new_price = float(product["price"]) * (1 - product["discount"] / 100)
|
|
397
|
+
self.price_label.setText(f"Цена: <s style='color: red;'>{float(product['price']):.2f}</s> {new_price:.2f}")
|
|
398
|
+
else:
|
|
399
|
+
self.price_label.setText(f"Цена: {float(product['price']):.2f}")
|
|
400
|
+
self.unit_label.setText(f"Единица измерения: {product['unit_name']}")
|
|
401
|
+
self.amount_label.setText(f"Количество на складе: {product['warehouse_amount']}")
|
|
402
|
+
self.amount_label.setObjectName("amount")
|
|
403
|
+
self.discount_label.setText(f"Скидка: {product['discount']}%")
|
|
404
|
+
|
|
405
|
+
def mousePressEvent(self, event):
|
|
406
|
+
self.clicked.emit(self.product)
|
|
407
|
+
return super().mousePressEvent(event)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class OrderCard(QFrame):
|
|
411
|
+
clicked = pyqtSignal(dict)
|
|
412
|
+
|
|
413
|
+
def __init__(self, order: dict, admin_mode: bool = False) -> None:
|
|
414
|
+
super().__init__()
|
|
415
|
+
self.order = order
|
|
416
|
+
self.setObjectName("OrderCard")
|
|
417
|
+
|
|
418
|
+
layout = QHBoxLayout(self)
|
|
419
|
+
self.image_frame = QFrame()
|
|
420
|
+
self.image_frame.setObjectName("cardBlock")
|
|
421
|
+
image_layout = QVBoxLayout(self.image_frame)
|
|
422
|
+
self.image = QLabel()
|
|
423
|
+
self.image.setFixedSize(110, 110)
|
|
424
|
+
self.image.setScaledContents(True)
|
|
425
|
+
image_name = order.get("image_name") or "picture.png"
|
|
426
|
+
self.image.setPixmap(QPixmap(str(BASE_DIR / "resources/images" / image_name)).scaled(110, 110))
|
|
427
|
+
image_layout.addWidget(self.image)
|
|
428
|
+
layout.addWidget(self.image_frame)
|
|
429
|
+
|
|
430
|
+
self.main_frame = QFrame()
|
|
431
|
+
self.main_frame.setObjectName("cardBlock")
|
|
432
|
+
content = QVBoxLayout(self.main_frame)
|
|
433
|
+
self.articule_label = QLabel(order["articule"])
|
|
434
|
+
self.product_label = QLabel(f"Товар: {order.get('product_name', '')}")
|
|
435
|
+
self.order_date_label = QLabel(f"Дата заказа: {order['order_date']}")
|
|
436
|
+
self.pickup_address = QLabel(order["address"])
|
|
437
|
+
self.status_label = QLabel(order["status_name"])
|
|
438
|
+
for label in [self.articule_label, self.product_label, self.order_date_label, self.pickup_address, self.status_label]:
|
|
439
|
+
content.addWidget(label)
|
|
440
|
+
layout.addWidget(self.main_frame, 1)
|
|
441
|
+
|
|
442
|
+
self.delivery_frame = QFrame()
|
|
443
|
+
self.delivery_frame.setObjectName("cardBlock")
|
|
444
|
+
delivery_layout = QVBoxLayout(self.delivery_frame)
|
|
445
|
+
self.date_delivery_label = QLabel(f"Дата доставки: {order['delivery_date']}" if order["delivery_date"] else "Дата доставки: Не указано")
|
|
446
|
+
self.date_delivery_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
447
|
+
delivery_layout.addWidget(self.date_delivery_label)
|
|
448
|
+
layout.addWidget(self.delivery_frame)
|
|
449
|
+
|
|
450
|
+
self.adjust_styles()
|
|
451
|
+
|
|
452
|
+
def adjust_styles(self):
|
|
453
|
+
self.setStyleSheet("QFrame#OrderCard {border: 1px solid black; padding: 4px;} QFrame#OrderCard:hover {border: 1px solid #00FA9A;} QFrame#cardBlock {border: 1px solid black; padding: 6px;}")
|
|
454
|
+
|
|
455
|
+
def mousePressEvent(self, event):
|
|
456
|
+
self.clicked.emit(self.order)
|
|
457
|
+
return super().mousePressEvent(event)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class MainWindow(QWidget):
|
|
461
|
+
def __init__(self, account: dict | None = None) -> None:
|
|
462
|
+
super().__init__()
|
|
463
|
+
self.account = account
|
|
464
|
+
self.role = account["role_name"] if account else None
|
|
465
|
+
self.auth_window = None
|
|
466
|
+
self.setWindowTitle("ООО «Обувь»")
|
|
467
|
+
self.resize(1300, 900)
|
|
468
|
+
|
|
469
|
+
main_layout = QHBoxLayout(self)
|
|
470
|
+
sidebar = QVBoxLayout()
|
|
471
|
+
self.logo = QLabel()
|
|
472
|
+
self.logo.setPixmap(QPixmap(str(BASE_DIR / "resources/icons/Icon.png")).scaled(110, 110, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
|
473
|
+
self.logoutButton = QPushButton("Выйти")
|
|
474
|
+
sidebar.addWidget(self.logo)
|
|
475
|
+
sidebar.addStretch()
|
|
476
|
+
sidebar.addWidget(self.logoutButton)
|
|
477
|
+
|
|
478
|
+
content = QVBoxLayout()
|
|
479
|
+
top_row = QHBoxLayout()
|
|
480
|
+
self.fio_label = QLabel()
|
|
481
|
+
self.fio_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignTop)
|
|
482
|
+
top_row.addStretch()
|
|
483
|
+
top_row.addWidget(self.fio_label)
|
|
484
|
+
content.addLayout(top_row)
|
|
485
|
+
|
|
486
|
+
self.tabWidget = QTabWidget()
|
|
487
|
+
self.products_tab = QWidget()
|
|
488
|
+
self.orders_tab = QWidget()
|
|
489
|
+
self.tabWidget.addTab(self.products_tab, "Товары")
|
|
490
|
+
self.tabWidget.addTab(self.orders_tab, "Заказы")
|
|
491
|
+
content.addWidget(self.tabWidget)
|
|
492
|
+
|
|
493
|
+
main_layout.addLayout(sidebar, 0)
|
|
494
|
+
main_layout.addLayout(content, 1)
|
|
495
|
+
|
|
496
|
+
self._build_products_tab()
|
|
497
|
+
self._build_orders_tab()
|
|
498
|
+
self.load_common_parts()
|
|
499
|
+
self.adjust_to_role()
|
|
500
|
+
self.connect_slots()
|
|
501
|
+
self.load_filters()
|
|
502
|
+
self.load_products()
|
|
503
|
+
self.load_orders()
|
|
504
|
+
|
|
505
|
+
def _build_products_tab(self):
|
|
506
|
+
layout = QVBoxLayout(self.products_tab)
|
|
507
|
+
controls = QHBoxLayout()
|
|
508
|
+
self.searchEdit = QLineEdit()
|
|
509
|
+
self.searchEdit.setPlaceholderText("Поиск")
|
|
510
|
+
self.filterBox = QComboBox()
|
|
511
|
+
self.sortBox = QComboBox()
|
|
512
|
+
self.add_product_button = QPushButton("Добавить товар")
|
|
513
|
+
controls.addWidget(self.searchEdit)
|
|
514
|
+
controls.addWidget(self.filterBox)
|
|
515
|
+
controls.addWidget(self.sortBox)
|
|
516
|
+
controls.addWidget(self.add_product_button)
|
|
517
|
+
layout.addLayout(controls)
|
|
518
|
+
self.products_scroll = QScrollArea()
|
|
519
|
+
self.products_scroll.setWidgetResizable(True)
|
|
520
|
+
self.products_container = QWidget()
|
|
521
|
+
self.products_layout = QVBoxLayout(self.products_container)
|
|
522
|
+
self.products_scroll.setWidget(self.products_container)
|
|
523
|
+
layout.addWidget(self.products_scroll)
|
|
524
|
+
|
|
525
|
+
def _build_orders_tab(self):
|
|
526
|
+
layout = QVBoxLayout(self.orders_tab)
|
|
527
|
+
self.add_order_button = QPushButton("Добавить заказ")
|
|
528
|
+
layout.addWidget(self.add_order_button)
|
|
529
|
+
self.orders_scroll = QScrollArea()
|
|
530
|
+
self.orders_scroll.setWidgetResizable(True)
|
|
531
|
+
self.orders_container = QWidget()
|
|
532
|
+
self.orders_layout = QVBoxLayout(self.orders_container)
|
|
533
|
+
self.orders_scroll.setWidget(self.orders_container)
|
|
534
|
+
layout.addWidget(self.orders_scroll)
|
|
535
|
+
|
|
536
|
+
def connect_slots(self):
|
|
537
|
+
self.logoutButton.clicked.connect(self.logout)
|
|
538
|
+
self.searchEdit.textChanged.connect(self.load_products)
|
|
539
|
+
self.sortBox.currentIndexChanged.connect(self.load_products)
|
|
540
|
+
self.filterBox.currentIndexChanged.connect(self.load_products)
|
|
541
|
+
self.add_product_button.clicked.connect(self.on_add_product)
|
|
542
|
+
self.add_order_button.clicked.connect(self.on_add_order)
|
|
543
|
+
|
|
544
|
+
def load_common_parts(self):
|
|
545
|
+
if self.account:
|
|
546
|
+
fio = f"{self.account['last_name']} {self.account['first_name']} {self.account['middle_name'] or ''}".strip()
|
|
547
|
+
self.fio_label.setText(fio)
|
|
548
|
+
else:
|
|
549
|
+
self.fio_label.setText("Гость")
|
|
550
|
+
|
|
551
|
+
def adjust_to_role(self):
|
|
552
|
+
if self.account is None or self.role == "Авторизованный клиент":
|
|
553
|
+
self.tabWidget.setTabVisible(1, False)
|
|
554
|
+
self.searchEdit.hide()
|
|
555
|
+
self.sortBox.hide()
|
|
556
|
+
self.filterBox.hide()
|
|
557
|
+
self.add_product_button.hide()
|
|
558
|
+
elif self.role == "Менеджер":
|
|
559
|
+
self.add_product_button.hide()
|
|
560
|
+
self.add_order_button.hide()
|
|
561
|
+
|
|
562
|
+
def clear_layout(self, layout):
|
|
563
|
+
while layout.count():
|
|
564
|
+
item = layout.takeAt(0)
|
|
565
|
+
widget = item.widget()
|
|
566
|
+
if widget:
|
|
567
|
+
widget.setParent(None)
|
|
568
|
+
widget.deleteLater()
|
|
569
|
+
|
|
570
|
+
def load_filters(self):
|
|
571
|
+
self.filterBox.clear()
|
|
572
|
+
self.sortBox.clear()
|
|
573
|
+
self.filterBox.addItem("Все поставщики", None)
|
|
574
|
+
for supplier in dao.load_suppliers():
|
|
575
|
+
self.filterBox.addItem(supplier["name"], supplier["id"])
|
|
576
|
+
self.sortBox.addItem("По возрастанию (кол-во на складе)", "ASC")
|
|
577
|
+
self.sortBox.addItem("По убыванию (кол-во на складе)", "DESC")
|
|
578
|
+
|
|
579
|
+
def load_products(self):
|
|
580
|
+
self.clear_layout(self.products_layout)
|
|
581
|
+
search_text = self.searchEdit.text().strip() or None
|
|
582
|
+
supplier_id = self.filterBox.currentData()
|
|
583
|
+
sort_by = self.sortBox.currentData() or "ASC"
|
|
584
|
+
products = dao.load_products(search_text, supplier_id, sort_by) if self.role else dao.load_products()
|
|
585
|
+
for product in products:
|
|
586
|
+
card = ProductCard(product, admin_mode=self.role == "Администратор")
|
|
587
|
+
card.clicked.connect(self.on_update_product)
|
|
588
|
+
self.products_layout.addWidget(card)
|
|
589
|
+
self.products_layout.addStretch()
|
|
590
|
+
|
|
591
|
+
def load_orders(self):
|
|
592
|
+
self.clear_layout(self.orders_layout)
|
|
593
|
+
if self.role == "Авторизованный клиент":
|
|
594
|
+
return
|
|
595
|
+
orders = dao.load_orders()
|
|
596
|
+
for order in orders:
|
|
597
|
+
card = OrderCard(order, admin_mode=self.role == "Администратор")
|
|
598
|
+
card.clicked.connect(self.on_update_order)
|
|
599
|
+
self.orders_layout.addWidget(card)
|
|
600
|
+
self.orders_layout.addStretch()
|
|
601
|
+
|
|
602
|
+
def logout(self):
|
|
603
|
+
self.auth_window = AuthWindow()
|
|
604
|
+
self.auth_window.show()
|
|
605
|
+
self.close()
|
|
606
|
+
|
|
607
|
+
def save_image(self, image_path: str | None):
|
|
608
|
+
if not image_path:
|
|
609
|
+
return None
|
|
610
|
+
image_name = Path(image_path).name
|
|
611
|
+
destination = BASE_DIR / "resources/images" / image_name
|
|
612
|
+
if not destination.exists():
|
|
613
|
+
destination.write_bytes(Path(image_path).read_bytes())
|
|
614
|
+
return image_name
|
|
615
|
+
|
|
616
|
+
def on_add_product(self):
|
|
617
|
+
dialog = ProductDialog()
|
|
618
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
619
|
+
data = dialog.get_data()
|
|
620
|
+
image_name = self.save_image(data[7])
|
|
621
|
+
articule = generate_product_articule()
|
|
622
|
+
dao.insert_product(*data[:7], image_name, *data[8:], articule)
|
|
623
|
+
self.load_products()
|
|
624
|
+
|
|
625
|
+
def on_update_product(self, product: dict):
|
|
626
|
+
if self.role != "Администратор":
|
|
627
|
+
return
|
|
628
|
+
dialog = ProductDialog(product)
|
|
629
|
+
result = dialog.exec()
|
|
630
|
+
if result == QDialog.DialogCode.Accepted:
|
|
631
|
+
data = dialog.get_data()
|
|
632
|
+
image_name = self.save_image(data[7])
|
|
633
|
+
dao.update_product(*data[:7], image_name, *data[8:], product["id"])
|
|
634
|
+
self.load_products()
|
|
635
|
+
elif result == 2:
|
|
636
|
+
dao.delete_product(product["id"])
|
|
637
|
+
self.load_products()
|
|
638
|
+
|
|
639
|
+
def on_add_order(self):
|
|
640
|
+
dialog = OrderDialog(account=self.account)
|
|
641
|
+
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
642
|
+
articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id, amount = dialog.get_data()
|
|
643
|
+
dao.insert_order(articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id, amount)
|
|
644
|
+
self.load_orders()
|
|
645
|
+
|
|
646
|
+
def on_update_order(self, order: dict):
|
|
647
|
+
if self.role != "Администратор":
|
|
648
|
+
return
|
|
649
|
+
dialog = OrderDialog(account=self.account, order=order)
|
|
650
|
+
result = dialog.exec()
|
|
651
|
+
if result == QDialog.DialogCode.Accepted:
|
|
652
|
+
articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id, amount = dialog.get_data()
|
|
653
|
+
dao.update_order(order["id"], articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id, amount)
|
|
654
|
+
self.load_orders()
|
|
655
|
+
elif result == 2:
|
|
656
|
+
dao.delete_order(order["id"])
|
|
657
|
+
self.load_orders()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .db import dao
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pymysql
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Database:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self.conn = None
|
|
9
|
+
|
|
10
|
+
def connect(self):
|
|
11
|
+
if self.conn is None:
|
|
12
|
+
self.conn = pymysql.connect(
|
|
13
|
+
user="root",
|
|
14
|
+
password="",
|
|
15
|
+
database="shoes_sales",
|
|
16
|
+
host="localhost",
|
|
17
|
+
cursorclass=pymysql.cursors.DictCursor,
|
|
18
|
+
)
|
|
19
|
+
return self.conn
|
|
20
|
+
|
|
21
|
+
def commit(self):
|
|
22
|
+
self.connect().commit()
|
|
23
|
+
|
|
24
|
+
def rollback(self):
|
|
25
|
+
self.connect().rollback()
|
|
26
|
+
|
|
27
|
+
def cursor(self):
|
|
28
|
+
return self.connect().cursor()
|
|
29
|
+
|
|
30
|
+
def login(self, login: str, password: str):
|
|
31
|
+
cur = self.cursor()
|
|
32
|
+
cur.execute(
|
|
33
|
+
"SELECT users.*, roles.role_name FROM users INNER JOIN roles ON role_id = roles.id WHERE login = %s AND password = %s",
|
|
34
|
+
[login, password],
|
|
35
|
+
)
|
|
36
|
+
row = cur.fetchone()
|
|
37
|
+
cur.close()
|
|
38
|
+
return row
|
|
39
|
+
|
|
40
|
+
def select(self, table_name: str):
|
|
41
|
+
cur = self.cursor()
|
|
42
|
+
cur.execute(f"SELECT * FROM {table_name}")
|
|
43
|
+
rows = cur.fetchall()
|
|
44
|
+
cur.close()
|
|
45
|
+
return rows
|
|
46
|
+
|
|
47
|
+
def load_suppliers(self):
|
|
48
|
+
return self.select("suppliers")
|
|
49
|
+
|
|
50
|
+
def load_categories(self):
|
|
51
|
+
return self.select("product_categories")
|
|
52
|
+
|
|
53
|
+
def load_manufactures(self):
|
|
54
|
+
return self.select("manufactures")
|
|
55
|
+
|
|
56
|
+
def load_units(self):
|
|
57
|
+
return self.select("units")
|
|
58
|
+
|
|
59
|
+
def load_pickup_points(self):
|
|
60
|
+
return self.select("pickup_points")
|
|
61
|
+
|
|
62
|
+
def load_order_statuses(self):
|
|
63
|
+
return self.select("order_statuses")
|
|
64
|
+
|
|
65
|
+
def load_products(self, search: str | None = None, supplier_id: int | None = None, sort_by: str = "ASC"):
|
|
66
|
+
cur = self.cursor()
|
|
67
|
+
params = []
|
|
68
|
+
query = """
|
|
69
|
+
SELECT products.*, units.name AS unit_name, product_categories.name AS category_name,
|
|
70
|
+
suppliers.name AS supplier_name, manufactures.name AS manufacture_name
|
|
71
|
+
FROM products INNER JOIN units ON unit_id = units.id
|
|
72
|
+
INNER JOIN product_categories ON product_categories.id = category_id
|
|
73
|
+
INNER JOIN suppliers ON suppliers.id = supplier_id
|
|
74
|
+
INNER JOIN manufactures ON manufacture_id = manufactures.id
|
|
75
|
+
WHERE 1 = 1
|
|
76
|
+
"""
|
|
77
|
+
if search:
|
|
78
|
+
query += " AND (products.name LIKE %s OR units.name LIKE %s OR product_categories.name LIKE %s OR suppliers.name LIKE %s OR manufactures.name LIKE %s OR description LIKE %s OR articule LIKE %s)"
|
|
79
|
+
params.extend([f"%{search}%"] * 7)
|
|
80
|
+
if supplier_id:
|
|
81
|
+
query += " AND supplier_id = %s"
|
|
82
|
+
params.append(supplier_id)
|
|
83
|
+
query += f" ORDER BY warehouse_amount {sort_by}"
|
|
84
|
+
cur.execute(query, params)
|
|
85
|
+
rows = cur.fetchall()
|
|
86
|
+
cur.close()
|
|
87
|
+
return rows
|
|
88
|
+
|
|
89
|
+
def load_orders(self):
|
|
90
|
+
cur = self.cursor()
|
|
91
|
+
query = """
|
|
92
|
+
SELECT orders.*, pickup_points.address, order_statuses.status_name,
|
|
93
|
+
users.first_name AS user_first_name, users.last_name AS user_last_name,
|
|
94
|
+
users.middle_name AS user_middle_name,
|
|
95
|
+
products.name AS product_name, products.image_name, products.price,
|
|
96
|
+
products.discount, products.warehouse_amount,
|
|
97
|
+
order_items.amount
|
|
98
|
+
FROM orders
|
|
99
|
+
INNER JOIN pickup_points ON pickup_point_id = pickup_points.id
|
|
100
|
+
INNER JOIN order_statuses ON order_statuses.id = order_status_id
|
|
101
|
+
INNER JOIN users ON user_id = users.id
|
|
102
|
+
INNER JOIN products ON product_id = products.id
|
|
103
|
+
INNER JOIN order_items ON order_items.order_id = orders.id
|
|
104
|
+
"""
|
|
105
|
+
cur.execute(query)
|
|
106
|
+
rows = cur.fetchall()
|
|
107
|
+
cur.close()
|
|
108
|
+
return rows
|
|
109
|
+
|
|
110
|
+
def insert_product(self, name, category_id, supplier_id, manufacture_id, unit_id, discount, price, image_name, warehouse_amount, description, articule: str):
|
|
111
|
+
cur = self.cursor()
|
|
112
|
+
cur.execute(
|
|
113
|
+
"INSERT INTO products (articule, name, unit_id, price, supplier_id, manufacture_id, category_id, discount, warehouse_amount, description, image_name) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
|
114
|
+
[articule, name, unit_id, price, supplier_id, manufacture_id, category_id, discount, warehouse_amount, description, image_name],
|
|
115
|
+
)
|
|
116
|
+
cur.close()
|
|
117
|
+
self.commit()
|
|
118
|
+
|
|
119
|
+
def update_product(self, name, category_id, supplier_id, manufacture_id, unit_id, discount, price, image_name, warehouse_amount, description, id: int):
|
|
120
|
+
cur = self.cursor()
|
|
121
|
+
cur.execute(
|
|
122
|
+
"UPDATE products SET name = %s, unit_id = %s, price = %s, supplier_id = %s, manufacture_id = %s, category_id = %s, discount = %s, warehouse_amount = %s, description = %s, image_name = %s WHERE id = %s",
|
|
123
|
+
[name, unit_id, price, supplier_id, manufacture_id, category_id, discount, warehouse_amount, description, image_name, id],
|
|
124
|
+
)
|
|
125
|
+
cur.close()
|
|
126
|
+
self.commit()
|
|
127
|
+
|
|
128
|
+
def delete_product(self, product_id: int):
|
|
129
|
+
cur = self.cursor()
|
|
130
|
+
cur.execute("DELETE FROM products WHERE id = %s", [product_id])
|
|
131
|
+
cur.close()
|
|
132
|
+
self.commit()
|
|
133
|
+
|
|
134
|
+
def insert_order(self, articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id, amount):
|
|
135
|
+
cur = self.cursor()
|
|
136
|
+
cur.execute(
|
|
137
|
+
"INSERT INTO orders (articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
|
|
138
|
+
[articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id],
|
|
139
|
+
)
|
|
140
|
+
order_id = cur.lastrowid
|
|
141
|
+
cur.execute("INSERT INTO order_items (order_id, product_id, amount) VALUES (%s, %s, %s)", [order_id, product_id, amount])
|
|
142
|
+
cur.close()
|
|
143
|
+
self.commit()
|
|
144
|
+
|
|
145
|
+
def update_order(self, order_id, articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id, amount):
|
|
146
|
+
cur = self.cursor()
|
|
147
|
+
cur.execute(
|
|
148
|
+
"UPDATE orders SET articule = %s, order_date = %s, delivery_date = %s, pickup_point_id = %s, user_id = %s, code = %s, order_status_id = %s, product_id = %s WHERE id = %s",
|
|
149
|
+
[articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id, order_id],
|
|
150
|
+
)
|
|
151
|
+
cur.execute("UPDATE order_items SET product_id = %s, amount = %s WHERE order_id = %s", [product_id, amount, order_id])
|
|
152
|
+
cur.close()
|
|
153
|
+
self.commit()
|
|
154
|
+
|
|
155
|
+
def delete_order(self, order_id: int):
|
|
156
|
+
cur = self.cursor()
|
|
157
|
+
cur.execute("DELETE FROM order_items WHERE order_id = %s", [order_id])
|
|
158
|
+
cur.execute("DELETE FROM orders WHERE id = %s", [order_id])
|
|
159
|
+
cur.close()
|
|
160
|
+
self.commit()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
dao = Database()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
DROP DATABASE IF EXISTS shoes_sales;
|
|
2
|
+
CREATE DATABASE IF NOT EXISTS shoes_sales;
|
|
3
|
+
|
|
4
|
+
USE shoes_sales;
|
|
5
|
+
|
|
6
|
+
CREATE TABLE roles (
|
|
7
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
8
|
+
role_name VARCHAR(50) NOT NULL UNIQUE,
|
|
9
|
+
PRIMARY KEY (id)
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
CREATE TABLE units (
|
|
13
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
14
|
+
name VARCHAR(20) NOT NULL,
|
|
15
|
+
PRIMARY KEY (id)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE suppliers (
|
|
19
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
20
|
+
name VARCHAR(100) NOT NULL,
|
|
21
|
+
PRIMARY KEY (id)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
CREATE TABLE manufactures (
|
|
25
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
26
|
+
name VARCHAR(100) NOT NULL,
|
|
27
|
+
PRIMARY KEY (id)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE product_categories (
|
|
31
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
32
|
+
name VARCHAR(100) NOT NULL,
|
|
33
|
+
PRIMARY KEY (id)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE pickup_points (
|
|
37
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
38
|
+
address VARCHAR(300) NOT NULL UNIQUE,
|
|
39
|
+
PRIMARY KEY (id)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE order_statuses (
|
|
43
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
44
|
+
status_name VARCHAR(50) NOT NULL UNIQUE,
|
|
45
|
+
PRIMARY KEY (id)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE users (
|
|
49
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
50
|
+
role_id INTEGER NOT NULL,
|
|
51
|
+
first_name VARCHAR(50) NOT NULL,
|
|
52
|
+
last_name VARCHAR(50) NOT NULL,
|
|
53
|
+
middle_name VARCHAR(50) DEFAULT NULL,
|
|
54
|
+
login VARCHAR(200) NOT NULL UNIQUE,
|
|
55
|
+
password VARCHAR(30) NOT NULL,
|
|
56
|
+
PRIMARY KEY (id),
|
|
57
|
+
FOREIGN KEY (role_id) REFERENCES roles (id)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE products (
|
|
61
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
62
|
+
articule CHAR(6) NOT NULL UNIQUE,
|
|
63
|
+
name VARCHAR(100) NOT NULL,
|
|
64
|
+
unit_id INTEGER NOT NULL,
|
|
65
|
+
price DECIMAL(10,2) NOT NULL,
|
|
66
|
+
supplier_id INTEGER NOT NULL,
|
|
67
|
+
manufacture_id INTEGER NOT NULL,
|
|
68
|
+
category_id INTEGER NOT NULL,
|
|
69
|
+
discount INTEGER NOT NULL DEFAULT 0,
|
|
70
|
+
warehouse_amount INTEGER NOT NULL,
|
|
71
|
+
description TEXT,
|
|
72
|
+
image_name VARCHAR(255) DEFAULT NULL,
|
|
73
|
+
PRIMARY KEY (id),
|
|
74
|
+
FOREIGN KEY (unit_id) REFERENCES units (id),
|
|
75
|
+
FOREIGN KEY (supplier_id) REFERENCES suppliers (id),
|
|
76
|
+
FOREIGN KEY (manufacture_id) REFERENCES manufactures (id),
|
|
77
|
+
FOREIGN KEY (category_id) REFERENCES product_categories (id)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE orders (
|
|
81
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
82
|
+
articule VARCHAR(200) NOT NULL,
|
|
83
|
+
order_date DATE NOT NULL,
|
|
84
|
+
delivery_date DATE DEFAULT NULL,
|
|
85
|
+
pickup_point_id INTEGER NOT NULL,
|
|
86
|
+
user_id INTEGER NOT NULL,
|
|
87
|
+
code INTEGER NOT NULL,
|
|
88
|
+
order_status_id INTEGER NOT NULL,
|
|
89
|
+
product_id INTEGER NOT NULL,
|
|
90
|
+
PRIMARY KEY (id),
|
|
91
|
+
FOREIGN KEY (pickup_point_id) REFERENCES pickup_points (id),
|
|
92
|
+
FOREIGN KEY (user_id) REFERENCES users (id),
|
|
93
|
+
FOREIGN KEY (order_status_id) REFERENCES order_statuses (id),
|
|
94
|
+
FOREIGN KEY (product_id) REFERENCES products (id)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
CREATE TABLE order_items (
|
|
98
|
+
id INT NOT NULL AUTO_INCREMENT,
|
|
99
|
+
order_id INTEGER NOT NULL,
|
|
100
|
+
product_id INTEGER NOT NULL,
|
|
101
|
+
amount INTEGER NOT NULL,
|
|
102
|
+
PRIMARY KEY (id),
|
|
103
|
+
FOREIGN KEY (order_id) REFERENCES orders (id),
|
|
104
|
+
FOREIGN KEY (product_id) REFERENCES products (id)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
INSERT INTO roles (id, role_name) VALUES
|
|
108
|
+
(1, 'Администратор'),
|
|
109
|
+
(2, 'Менеджер'),
|
|
110
|
+
(3, 'Авторизованный клиент');
|
|
111
|
+
|
|
112
|
+
INSERT INTO units (id, name) VALUES (1, 'шт');
|
|
113
|
+
INSERT INTO suppliers (id, name) VALUES (1, 'Kari'), (2, 'Обувь для вас');
|
|
114
|
+
INSERT INTO manufactures (id, name) VALUES (1, 'Kari'), (2, 'Rieker'), (3, 'Poc'), (4, 'Marco Tozzi'), (5, 'CROSBY');
|
|
115
|
+
INSERT INTO product_categories (id, name) VALUES (1, 'Женская обувь'), (2, 'Мужская обувь');
|
|
116
|
+
INSERT INTO pickup_points (id, address) VALUES (1, '420151, г. Лесной, ул. Вишневая, 32');
|
|
117
|
+
INSERT INTO order_statuses (id, status_name) VALUES (1, 'Новый'), (2, 'Завершён');
|
|
118
|
+
INSERT INTO users (id, role_id, first_name, last_name, middle_name, login, password) VALUES
|
|
119
|
+
(1, 1, 'Иван', 'Иванов', 'Иванович', 'admin@gmail.com', 'admin'),
|
|
120
|
+
(2, 2, 'Пётр', 'Петров', 'Петрович', 'manager@gmail.com', 'manager'),
|
|
121
|
+
(3, 3, 'Денис', 'Денисов', 'Денисович', 'client@gmail.com', 'client');
|
|
122
|
+
|
|
123
|
+
INSERT INTO products (id, articule, name, unit_id, price, supplier_id, manufacture_id, category_id, discount, warehouse_amount, description, image_name) VALUES
|
|
124
|
+
(1, 'А112Т4', 'Ботинки', 1, 5590.00, 1, 1, 1, 16, 4, 'Женские ботинки демисезонные kari.', '7.jpg'),
|
|
125
|
+
(2, 'F635R4', 'Ботинки', 1, 2144.00, 1, 4, 1, 10, 3, 'Ботинки Marco Tozzi женские демисезонные, размер 39, цвет бежевый.', '2.jpg'),
|
|
126
|
+
(3, 'A674S9', 'Кроссовки', 1, 2200.00, 2, 5, 2, 10, 20, 'Универсальная модель для повседневной носки.', '6.jpg');
|
|
127
|
+
|
|
128
|
+
INSERT INTO orders (id, articule, order_date, delivery_date, pickup_point_id, user_id, code, order_status_id, product_id) VALUES
|
|
129
|
+
(1, 'А112Т4', '2025-02-27', '2025-04-20', 1, 1, 901, 1, 1);
|
|
130
|
+
|
|
131
|
+
INSERT INTO order_items (id, order_id, product_id, amount) VALUES
|
|
132
|
+
(1, 1, 1, 2);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from PyQt6.QtGui import QFont, QIcon
|
|
4
|
+
from PyQt6.QtWidgets import QApplication
|
|
5
|
+
|
|
6
|
+
from .bundle import AuthWindow
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
BASE_DIR = Path(__file__).resolve().parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
app = QApplication([])
|
|
14
|
+
app.setWindowIcon(QIcon(str(BASE_DIR / "resources/icons/Icon.ico")))
|
|
15
|
+
|
|
16
|
+
font = QFont()
|
|
17
|
+
font.setFamily("Times New Roman")
|
|
18
|
+
font.setPointSize(12)
|
|
19
|
+
app.setFont(font)
|
|
20
|
+
|
|
21
|
+
qss_path = BASE_DIR / "styles/common.qss"
|
|
22
|
+
if qss_path.exists():
|
|
23
|
+
app.setStyleSheet(qss_path.read_text(encoding="utf-8"))
|
|
24
|
+
|
|
25
|
+
window = AuthWindow()
|
|
26
|
+
window.show()
|
|
27
|
+
app.exec()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
QWidget {
|
|
2
|
+
background: #ffffff;
|
|
3
|
+
font-family: "Times New Roman";
|
|
4
|
+
font-size: 12pt;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
QPushButton {
|
|
8
|
+
background: #7fff00;
|
|
9
|
+
border: 1px solid #00fa9a;
|
|
10
|
+
padding: 6px 12px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
QPushButton:hover {
|
|
14
|
+
background: #00fa9a;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
QTabWidget::pane {
|
|
18
|
+
border: 1px solid #00fa9a;
|
|
19
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obuv-exam
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Demo exam project for shoe store UI
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: PyQt6>=6.0
|
|
8
|
+
Requires-Dist: PyMySQL>=1.1
|
|
9
|
+
|
|
10
|
+
# obuv-exam
|
|
11
|
+
|
|
12
|
+
Demo exam package for a shoe store UI project.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/obuv_exam/__init__.py
|
|
5
|
+
src/obuv_exam/bundle.py
|
|
6
|
+
src/obuv_exam/main.py
|
|
7
|
+
src/obuv_exam.egg-info/PKG-INFO
|
|
8
|
+
src/obuv_exam.egg-info/SOURCES.txt
|
|
9
|
+
src/obuv_exam.egg-info/dependency_links.txt
|
|
10
|
+
src/obuv_exam.egg-info/entry_points.txt
|
|
11
|
+
src/obuv_exam.egg-info/requires.txt
|
|
12
|
+
src/obuv_exam.egg-info/top_level.txt
|
|
13
|
+
src/obuv_exam/database/__init__.py
|
|
14
|
+
src/obuv_exam/database/db.py
|
|
15
|
+
src/obuv_exam/database/script.sql
|
|
16
|
+
src/obuv_exam/resources/icons/Icon.JPG
|
|
17
|
+
src/obuv_exam/resources/icons/Icon.ico
|
|
18
|
+
src/obuv_exam/resources/icons/Icon.png
|
|
19
|
+
src/obuv_exam/resources/images/1.jpg
|
|
20
|
+
src/obuv_exam/resources/images/10.jpg
|
|
21
|
+
src/obuv_exam/resources/images/2.jpg
|
|
22
|
+
src/obuv_exam/resources/images/3.jpg
|
|
23
|
+
src/obuv_exam/resources/images/4.jpg
|
|
24
|
+
src/obuv_exam/resources/images/5.jpg
|
|
25
|
+
src/obuv_exam/resources/images/6.jpg
|
|
26
|
+
src/obuv_exam/resources/images/7.jpg
|
|
27
|
+
src/obuv_exam/resources/images/8.jpg
|
|
28
|
+
src/obuv_exam/resources/images/9.jpg
|
|
29
|
+
src/obuv_exam/resources/images/picture.png
|
|
30
|
+
src/obuv_exam/styles/common.qss
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
obuv_exam
|