qt-datastore 1.0.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.
- qt_datastore-1.0.0/PKG-INFO +21 -0
- qt_datastore-1.0.0/README.md +11 -0
- qt_datastore-1.0.0/pyproject.toml +22 -0
- qt_datastore-1.0.0/qt_datastore/__init__.py +1 -0
- qt_datastore-1.0.0/qt_datastore/__main__.py +4 -0
- qt_datastore-1.0.0/qt_datastore/app.py +661 -0
- qt_datastore-1.0.0/qt_datastore.egg-info/PKG-INFO +21 -0
- qt_datastore-1.0.0/qt_datastore.egg-info/SOURCES.txt +11 -0
- qt_datastore-1.0.0/qt_datastore.egg-info/dependency_links.txt +1 -0
- qt_datastore-1.0.0/qt_datastore.egg-info/entry_points.txt +2 -0
- qt_datastore-1.0.0/qt_datastore.egg-info/requires.txt +3 -0
- qt_datastore-1.0.0/qt_datastore.egg-info/top_level.txt +1 -0
- qt_datastore-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qt-datastore
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Qt datastore and form utilities for desktop applications
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pymysql
|
|
8
|
+
Requires-Dist: PyQt6
|
|
9
|
+
Requires-Dist: cryptography
|
|
10
|
+
|
|
11
|
+
# qt-datastore
|
|
12
|
+
|
|
13
|
+
Qt datastore utilities for desktop applications with MySQL backend.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
qtds-ui
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m qt_datastore
|
|
21
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qt-datastore"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Qt datastore and form utilities for desktop applications"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pymysql",
|
|
13
|
+
"PyQt6",
|
|
14
|
+
"cryptography",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
qtds-ui = "qt_datastore.app:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["."]
|
|
22
|
+
include = ["qt_datastore*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import pymysql
|
|
5
|
+
from PyQt6.QtWidgets import *
|
|
6
|
+
from PyQt6.QtCore import *
|
|
7
|
+
from PyQt6.QtGui import *
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def apply_light_theme(app):
|
|
11
|
+
app.setStyle("Fusion")
|
|
12
|
+
p = QPalette()
|
|
13
|
+
p.setColor(QPalette.ColorRole.Window, QColor(245, 245, 245))
|
|
14
|
+
p.setColor(QPalette.ColorRole.WindowText, QColor(33, 33, 33))
|
|
15
|
+
p.setColor(QPalette.ColorRole.Base, QColor(255, 255, 255))
|
|
16
|
+
p.setColor(QPalette.ColorRole.AlternateBase, QColor(240, 240, 240))
|
|
17
|
+
p.setColor(QPalette.ColorRole.ToolTipBase, QColor(255, 255, 255))
|
|
18
|
+
p.setColor(QPalette.ColorRole.ToolTipText, QColor(33, 33, 33))
|
|
19
|
+
p.setColor(QPalette.ColorRole.Text, QColor(33, 33, 33))
|
|
20
|
+
p.setColor(QPalette.ColorRole.Button, QColor(240, 240, 240))
|
|
21
|
+
p.setColor(QPalette.ColorRole.ButtonText, QColor(33, 33, 33))
|
|
22
|
+
p.setColor(QPalette.ColorRole.BrightText, QColor(200, 0, 0))
|
|
23
|
+
p.setColor(QPalette.ColorRole.Link, QColor(0, 102, 204))
|
|
24
|
+
p.setColor(QPalette.ColorRole.Highlight, QColor(0, 120, 215))
|
|
25
|
+
p.setColor(QPalette.ColorRole.HighlightedText, QColor(255, 255, 255))
|
|
26
|
+
app.setPalette(p)
|
|
27
|
+
app.setStyleSheet("""
|
|
28
|
+
QMainWindow, QWidget, QDialog { background-color: #f5f5f5; color: #212121; }
|
|
29
|
+
QLineEdit, QTextEdit, QComboBox {
|
|
30
|
+
background-color: #ffffff; color: #212121;
|
|
31
|
+
border: 1px solid #cccccc; padding: 4px;
|
|
32
|
+
}
|
|
33
|
+
QPushButton {
|
|
34
|
+
background-color: #e8e8e8; color: #212121;
|
|
35
|
+
border: 1px solid #bbbbbb; padding: 6px 12px;
|
|
36
|
+
}
|
|
37
|
+
QPushButton:hover { background-color: #d0d0d0; }
|
|
38
|
+
QFrame { background-color: #ffffff; color: #212121; }
|
|
39
|
+
QScrollArea { background-color: #f5f5f5; border: none; }
|
|
40
|
+
QToolBar { background-color: #eeeeee; border-bottom: 1px solid #cccccc; }
|
|
41
|
+
QLabel { color: #212121; }
|
|
42
|
+
QCheckBox { color: #212121; }
|
|
43
|
+
""")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_price(text):
|
|
47
|
+
text = text.strip().replace(",", ".")
|
|
48
|
+
if not text:
|
|
49
|
+
raise ValueError("empty price")
|
|
50
|
+
return float(text)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def db():
|
|
54
|
+
return pymysql.connect(
|
|
55
|
+
host="localhost",
|
|
56
|
+
port=3307,
|
|
57
|
+
user="root",
|
|
58
|
+
password="root",
|
|
59
|
+
database="restaurant_db",
|
|
60
|
+
cursorclass=pymysql.cursors.DictCursor
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DB:
|
|
65
|
+
def __init__(self):
|
|
66
|
+
self.conn = db()
|
|
67
|
+
self.ensure_schema()
|
|
68
|
+
|
|
69
|
+
def ensure_schema(self):
|
|
70
|
+
with self.conn.cursor() as c:
|
|
71
|
+
c.execute("SHOW COLUMNS FROM MenuItems LIKE 'photo'")
|
|
72
|
+
if not c.fetchone():
|
|
73
|
+
c.execute("ALTER TABLE MenuItems ADD COLUMN photo VARCHAR(255)")
|
|
74
|
+
c.execute("""
|
|
75
|
+
CREATE TABLE IF NOT EXISTS Orders (
|
|
76
|
+
order_id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
|
77
|
+
user_id INT NOT NULL,
|
|
78
|
+
total DECIMAL(10, 2) NOT NULL,
|
|
79
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
80
|
+
)
|
|
81
|
+
""")
|
|
82
|
+
c.execute("""
|
|
83
|
+
CREATE TABLE IF NOT EXISTS OrderItems (
|
|
84
|
+
order_item_id INT PRIMARY KEY NOT NULL AUTO_INCREMENT,
|
|
85
|
+
order_id INT NOT NULL,
|
|
86
|
+
item_id INT NOT NULL,
|
|
87
|
+
quantity INT NOT NULL,
|
|
88
|
+
price DECIMAL(10, 2) NOT NULL
|
|
89
|
+
)
|
|
90
|
+
""")
|
|
91
|
+
self.conn.commit()
|
|
92
|
+
|
|
93
|
+
def get(self, q, p=None):
|
|
94
|
+
with self.conn.cursor() as c:
|
|
95
|
+
c.execute(q, p or ())
|
|
96
|
+
return c.fetchall()
|
|
97
|
+
|
|
98
|
+
def one(self, q, p=None):
|
|
99
|
+
with self.conn.cursor() as c:
|
|
100
|
+
c.execute(q, p or ())
|
|
101
|
+
return c.fetchone()
|
|
102
|
+
|
|
103
|
+
def run(self, q, p=None):
|
|
104
|
+
with self.conn.cursor() as c:
|
|
105
|
+
c.execute(q, p or ())
|
|
106
|
+
self.conn.commit()
|
|
107
|
+
|
|
108
|
+
def save_order(self, user_id, cart_items):
|
|
109
|
+
total = sum(i['price'] * i['qty'] for i in cart_items)
|
|
110
|
+
try:
|
|
111
|
+
with self.conn.cursor() as c:
|
|
112
|
+
c.execute(
|
|
113
|
+
"INSERT INTO Orders (user_id, total) VALUES (%s, %s)",
|
|
114
|
+
(user_id, total)
|
|
115
|
+
)
|
|
116
|
+
c.execute("SELECT LAST_INSERT_ID() AS order_id")
|
|
117
|
+
order_id = c.fetchone()['order_id']
|
|
118
|
+
for item in cart_items:
|
|
119
|
+
c.execute(
|
|
120
|
+
"INSERT INTO OrderItems (order_id, item_id, quantity, price) VALUES (%s, %s, %s, %s)",
|
|
121
|
+
(order_id, item['item_id'], item['qty'], item['price'])
|
|
122
|
+
)
|
|
123
|
+
self.conn.commit()
|
|
124
|
+
return order_id, total
|
|
125
|
+
except Exception:
|
|
126
|
+
self.conn.rollback()
|
|
127
|
+
return None, None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def photo_label(path, w=260, h=180):
|
|
131
|
+
lab = QLabel()
|
|
132
|
+
lab.setFixedSize(w, h)
|
|
133
|
+
lab.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
134
|
+
lab.setScaledContents(True)
|
|
135
|
+
lab.setStyleSheet("border:1px solid gray; background:#f0f0f0")
|
|
136
|
+
if path and os.path.exists(path):
|
|
137
|
+
pix = QPixmap(path)
|
|
138
|
+
pix = pix.scaled(w, h, Qt.AspectRatioMode.KeepAspectRatio,
|
|
139
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
140
|
+
lab.setPixmap(pix)
|
|
141
|
+
else:
|
|
142
|
+
lab.setText("Нет фото")
|
|
143
|
+
return lab
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Login(QWidget):
|
|
147
|
+
def __init__(self, db):
|
|
148
|
+
super().__init__()
|
|
149
|
+
self.db = db
|
|
150
|
+
self.setWindowTitle("Вход")
|
|
151
|
+
self.resize(300, 200)
|
|
152
|
+
l = QVBoxLayout()
|
|
153
|
+
l.addWidget(QLabel("Логин:"))
|
|
154
|
+
self.u = QLineEdit()
|
|
155
|
+
l.addWidget(self.u)
|
|
156
|
+
l.addWidget(QLabel("Пароль:"))
|
|
157
|
+
self.p = QLineEdit()
|
|
158
|
+
self.p.setEchoMode(QLineEdit.EchoMode.Password)
|
|
159
|
+
l.addWidget(self.p)
|
|
160
|
+
b = QPushButton("Войти")
|
|
161
|
+
b.clicked.connect(self.login)
|
|
162
|
+
l.addWidget(b)
|
|
163
|
+
exit_b = QPushButton("Выход")
|
|
164
|
+
exit_b.clicked.connect(self.exit_app)
|
|
165
|
+
l.addWidget(exit_b)
|
|
166
|
+
self.setLayout(l)
|
|
167
|
+
|
|
168
|
+
def exit_app(self):
|
|
169
|
+
QApplication.instance().quit()
|
|
170
|
+
|
|
171
|
+
def login(self):
|
|
172
|
+
u = self.db.one(
|
|
173
|
+
"SELECT * FROM Users WHERE username=%s AND password_hash=%s",
|
|
174
|
+
(self.u.text(), self.p.text())
|
|
175
|
+
)
|
|
176
|
+
if u:
|
|
177
|
+
if u['role_id'] == 1:
|
|
178
|
+
self.admin = Admin(self.db, u, self)
|
|
179
|
+
self.admin.show()
|
|
180
|
+
elif u['role_id'] == 2:
|
|
181
|
+
self.client = Client(self.db, u, self)
|
|
182
|
+
self.client.show()
|
|
183
|
+
else:
|
|
184
|
+
QMessageBox.critical(self, "", "Неизвестная роль")
|
|
185
|
+
return
|
|
186
|
+
self.hide()
|
|
187
|
+
else:
|
|
188
|
+
QMessageBox.critical(self, "", "Неверно")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class Admin(QMainWindow):
|
|
192
|
+
def __init__(self, db, user, login_win):
|
|
193
|
+
super().__init__()
|
|
194
|
+
self.db = db
|
|
195
|
+
self.login_win = login_win
|
|
196
|
+
self._logging_out = False
|
|
197
|
+
self.sort = None
|
|
198
|
+
self.setWindowTitle(f"Админ: {user['username']}")
|
|
199
|
+
self.resize(900, 550)
|
|
200
|
+
|
|
201
|
+
if not os.path.exists('photos'):
|
|
202
|
+
os.makedirs('photos')
|
|
203
|
+
|
|
204
|
+
tb = self.addToolBar("")
|
|
205
|
+
for t, f in [
|
|
206
|
+
("Добавить", self.add),
|
|
207
|
+
("Цена ↑", lambda: self.set_sort("ASC")),
|
|
208
|
+
("Цена ↓", lambda: self.set_sort("DESC")),
|
|
209
|
+
("Выход", self.logout),
|
|
210
|
+
]:
|
|
211
|
+
b = QPushButton(t)
|
|
212
|
+
b.clicked.connect(f)
|
|
213
|
+
tb.addWidget(b)
|
|
214
|
+
|
|
215
|
+
self.scroll = QScrollArea()
|
|
216
|
+
self.scroll.setWidgetResizable(True)
|
|
217
|
+
self.w = QWidget()
|
|
218
|
+
self.layout = QGridLayout(self.w)
|
|
219
|
+
self.scroll.setWidget(self.w)
|
|
220
|
+
self.setCentralWidget(self.scroll)
|
|
221
|
+
self.load()
|
|
222
|
+
|
|
223
|
+
def set_sort(self, order):
|
|
224
|
+
self.sort = order
|
|
225
|
+
self.load()
|
|
226
|
+
|
|
227
|
+
def load(self):
|
|
228
|
+
for i in reversed(range(self.layout.count())):
|
|
229
|
+
self.layout.itemAt(i).widget().deleteLater()
|
|
230
|
+
q = "SELECT * FROM MenuItems"
|
|
231
|
+
if self.sort:
|
|
232
|
+
q += f" ORDER BY price {self.sort}"
|
|
233
|
+
items = self.db.get(q)
|
|
234
|
+
r, c = 0, 0
|
|
235
|
+
for x in items:
|
|
236
|
+
card = QFrame()
|
|
237
|
+
card.setFrameStyle(QFrame.Shape.Box)
|
|
238
|
+
card.setFixedSize(280, 380)
|
|
239
|
+
l = QVBoxLayout(card)
|
|
240
|
+
|
|
241
|
+
l.addWidget(photo_label(x.get('photo')))
|
|
242
|
+
l.addWidget(QLabel(f"<b>{x['name']}</b>"))
|
|
243
|
+
l.addWidget(QLabel(f"Цена: {x['price']} руб"))
|
|
244
|
+
status = "Доступен" if x['is_available'] else "Недоступен"
|
|
245
|
+
l.addWidget(QLabel(status))
|
|
246
|
+
|
|
247
|
+
b = QHBoxLayout()
|
|
248
|
+
e = QPushButton("Ред.")
|
|
249
|
+
e.clicked.connect(lambda ch, iid=x['item_id']: self.edit(iid))
|
|
250
|
+
d = QPushButton("Удал.")
|
|
251
|
+
d.clicked.connect(lambda ch, iid=x['item_id']: self.delete(iid))
|
|
252
|
+
b.addWidget(e)
|
|
253
|
+
b.addWidget(d)
|
|
254
|
+
l.addLayout(b)
|
|
255
|
+
|
|
256
|
+
self.layout.addWidget(card, r, c)
|
|
257
|
+
c += 1
|
|
258
|
+
if c >= 3:
|
|
259
|
+
c = 0
|
|
260
|
+
r += 1
|
|
261
|
+
|
|
262
|
+
def add(self):
|
|
263
|
+
self.dialog()
|
|
264
|
+
|
|
265
|
+
def edit(self, item_id):
|
|
266
|
+
self.dialog(item_id)
|
|
267
|
+
|
|
268
|
+
def delete(self, item_id):
|
|
269
|
+
if QMessageBox.question(self, "", "Удалить?") == QMessageBox.StandardButton.Yes:
|
|
270
|
+
row = self.db.one("SELECT photo FROM MenuItems WHERE item_id=%s", (item_id,))
|
|
271
|
+
if row and row.get('photo') and os.path.exists(row['photo']):
|
|
272
|
+
os.remove(row['photo'])
|
|
273
|
+
self.db.run("DELETE FROM MenuItems WHERE item_id=%s", (item_id,))
|
|
274
|
+
self.load()
|
|
275
|
+
|
|
276
|
+
def dialog(self, item_id=None):
|
|
277
|
+
d = QDialog(self)
|
|
278
|
+
d.setWindowTitle("Блюдо")
|
|
279
|
+
d.resize(400, 550)
|
|
280
|
+
l = QVBoxLayout()
|
|
281
|
+
|
|
282
|
+
n = QLineEdit()
|
|
283
|
+
p = QLineEdit()
|
|
284
|
+
a = QCheckBox("Доступен")
|
|
285
|
+
a.setChecked(True)
|
|
286
|
+
|
|
287
|
+
photo_path = None
|
|
288
|
+
photo_preview = photo_label(None, 380, 250)
|
|
289
|
+
photo_btn = QPushButton("Выбрать фото")
|
|
290
|
+
del_photo_btn = QPushButton("Удалить фото")
|
|
291
|
+
|
|
292
|
+
def choose():
|
|
293
|
+
nonlocal photo_path
|
|
294
|
+
f, _ = QFileDialog.getOpenFileName(
|
|
295
|
+
d, "Выбрать фото", "", "Images (*.png *.jpg *.jpeg *.bmp *.gif)"
|
|
296
|
+
)
|
|
297
|
+
if f:
|
|
298
|
+
photo_path = f
|
|
299
|
+
pix = QPixmap(f)
|
|
300
|
+
pix = pix.scaled(380, 250, Qt.AspectRatioMode.KeepAspectRatio,
|
|
301
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
302
|
+
photo_preview.setPixmap(pix)
|
|
303
|
+
|
|
304
|
+
def remove_photo():
|
|
305
|
+
nonlocal photo_path
|
|
306
|
+
photo_path = ""
|
|
307
|
+
photo_preview.clear()
|
|
308
|
+
photo_preview.setText("Нет фото")
|
|
309
|
+
|
|
310
|
+
photo_btn.clicked.connect(choose)
|
|
311
|
+
del_photo_btn.clicked.connect(remove_photo)
|
|
312
|
+
|
|
313
|
+
l.addWidget(QLabel("Название:"))
|
|
314
|
+
l.addWidget(n)
|
|
315
|
+
l.addWidget(QLabel("Цена:"))
|
|
316
|
+
l.addWidget(p)
|
|
317
|
+
l.addWidget(QLabel("Фото:"))
|
|
318
|
+
l.addWidget(photo_preview)
|
|
319
|
+
l.addWidget(photo_btn)
|
|
320
|
+
l.addWidget(del_photo_btn)
|
|
321
|
+
l.addWidget(a)
|
|
322
|
+
|
|
323
|
+
if item_id:
|
|
324
|
+
r = self.db.one("SELECT * FROM MenuItems WHERE item_id=%s", (item_id,))
|
|
325
|
+
if r:
|
|
326
|
+
n.setText(r['name'])
|
|
327
|
+
p.setText(str(r['price']))
|
|
328
|
+
a.setChecked(r['is_available'])
|
|
329
|
+
if r.get('photo') and os.path.exists(r['photo']):
|
|
330
|
+
photo_path = r['photo']
|
|
331
|
+
pix = QPixmap(r['photo'])
|
|
332
|
+
pix = pix.scaled(380, 250, Qt.AspectRatioMode.KeepAspectRatio,
|
|
333
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
334
|
+
photo_preview.setPixmap(pix)
|
|
335
|
+
|
|
336
|
+
def save_photo(target_id):
|
|
337
|
+
if not photo_path or photo_path == "":
|
|
338
|
+
return None
|
|
339
|
+
name_clean = n.text().strip().replace(' ', '_').replace('/', '_') or 'item'
|
|
340
|
+
ext = os.path.splitext(photo_path)[1] or '.jpg'
|
|
341
|
+
dest = f"photos/{name_clean}_{target_id}{ext}"
|
|
342
|
+
shutil.copy2(photo_path, dest)
|
|
343
|
+
return dest
|
|
344
|
+
|
|
345
|
+
def save():
|
|
346
|
+
try:
|
|
347
|
+
name = n.text().strip()
|
|
348
|
+
if not name:
|
|
349
|
+
QMessageBox.critical(d, "Ошибка", "Введите название")
|
|
350
|
+
return
|
|
351
|
+
price = parse_price(p.text())
|
|
352
|
+
|
|
353
|
+
if item_id:
|
|
354
|
+
if photo_path == "":
|
|
355
|
+
old = self.db.one("SELECT photo FROM MenuItems WHERE item_id=%s", (item_id,))
|
|
356
|
+
if old and old.get('photo') and os.path.exists(old['photo']):
|
|
357
|
+
os.remove(old['photo'])
|
|
358
|
+
new_photo = None
|
|
359
|
+
elif photo_path:
|
|
360
|
+
old = self.db.one("SELECT photo FROM MenuItems WHERE item_id=%s", (item_id,))
|
|
361
|
+
if old and old.get('photo') and old['photo'] != photo_path and os.path.exists(old['photo']):
|
|
362
|
+
os.remove(old['photo'])
|
|
363
|
+
new_photo = save_photo(item_id)
|
|
364
|
+
else:
|
|
365
|
+
old = self.db.one("SELECT photo FROM MenuItems WHERE item_id=%s", (item_id,))
|
|
366
|
+
new_photo = old.get('photo') if old else None
|
|
367
|
+
|
|
368
|
+
self.db.run(
|
|
369
|
+
"UPDATE MenuItems SET name=%s, price=%s, is_available=%s, photo=%s WHERE item_id=%s",
|
|
370
|
+
(name, price, a.isChecked(), new_photo, item_id)
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
self.db.run(
|
|
374
|
+
"INSERT INTO MenuItems (name, price, is_available, photo) VALUES (%s, %s, %s, %s)",
|
|
375
|
+
(name, price, a.isChecked(), None)
|
|
376
|
+
)
|
|
377
|
+
if photo_path and photo_path != "":
|
|
378
|
+
row = self.db.one("SELECT LAST_INSERT_ID() AS item_id")
|
|
379
|
+
new_photo = save_photo(row['item_id'])
|
|
380
|
+
self.db.run(
|
|
381
|
+
"UPDATE MenuItems SET photo=%s WHERE item_id=%s",
|
|
382
|
+
(new_photo, row['item_id'])
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
d.accept()
|
|
386
|
+
self.load()
|
|
387
|
+
except ValueError:
|
|
388
|
+
QMessageBox.critical(d, "Ошибка", "Проверьте цену (например: 450 или 450.50)")
|
|
389
|
+
except Exception as e:
|
|
390
|
+
QMessageBox.critical(d, "Ошибка", str(e))
|
|
391
|
+
|
|
392
|
+
l.addWidget(QPushButton("Сохранить", clicked=save))
|
|
393
|
+
d.setLayout(l)
|
|
394
|
+
d.exec()
|
|
395
|
+
|
|
396
|
+
def logout(self):
|
|
397
|
+
self.login_win.u.clear()
|
|
398
|
+
self.login_win.p.clear()
|
|
399
|
+
self.login_win.show()
|
|
400
|
+
self.login_win.raise_()
|
|
401
|
+
self._logging_out = True
|
|
402
|
+
self.close()
|
|
403
|
+
|
|
404
|
+
def closeEvent(self, event):
|
|
405
|
+
if self.login_win.isHidden() and not self._logging_out:
|
|
406
|
+
self.login_win.u.clear()
|
|
407
|
+
self.login_win.p.clear()
|
|
408
|
+
self.login_win.show()
|
|
409
|
+
self.login_win.raise_()
|
|
410
|
+
event.accept()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class Client(QMainWindow):
|
|
414
|
+
def __init__(self, db, user, login_win):
|
|
415
|
+
super().__init__()
|
|
416
|
+
self.db = db
|
|
417
|
+
self.login_win = login_win
|
|
418
|
+
self._logging_out = False
|
|
419
|
+
self.user = user
|
|
420
|
+
self.sort = None
|
|
421
|
+
self.cart = {}
|
|
422
|
+
self.setWindowTitle(f"Клиент: {user['username']}")
|
|
423
|
+
self.resize(900, 550)
|
|
424
|
+
|
|
425
|
+
w = QWidget()
|
|
426
|
+
self.setCentralWidget(w)
|
|
427
|
+
l = QVBoxLayout(w)
|
|
428
|
+
|
|
429
|
+
h = QHBoxLayout()
|
|
430
|
+
self.s = QLineEdit()
|
|
431
|
+
self.s.returnPressed.connect(self.search)
|
|
432
|
+
h.addWidget(QLabel("Поиск:"))
|
|
433
|
+
h.addWidget(self.s)
|
|
434
|
+
h.addWidget(QPushButton("Найти", clicked=self.search))
|
|
435
|
+
h.addWidget(QPushButton("Все", clicked=lambda: self.load()))
|
|
436
|
+
h.addWidget(QPushButton("Цена ↑", clicked=lambda: self.set_sort("ASC")))
|
|
437
|
+
h.addWidget(QPushButton("Цена ↓", clicked=lambda: self.set_sort("DESC")))
|
|
438
|
+
self.cart_btn = QPushButton("Корзина (0)")
|
|
439
|
+
self.cart_btn.clicked.connect(self.show_cart)
|
|
440
|
+
h.addWidget(self.cart_btn)
|
|
441
|
+
b = QPushButton("Выход")
|
|
442
|
+
b.clicked.connect(self.logout)
|
|
443
|
+
h.addWidget(b)
|
|
444
|
+
l.addLayout(h)
|
|
445
|
+
|
|
446
|
+
self.scroll = QScrollArea()
|
|
447
|
+
self.scroll.setWidgetResizable(True)
|
|
448
|
+
self.w2 = QWidget()
|
|
449
|
+
self.layout2 = QGridLayout(self.w2)
|
|
450
|
+
self.scroll.setWidget(self.w2)
|
|
451
|
+
l.addWidget(self.scroll)
|
|
452
|
+
self.load()
|
|
453
|
+
|
|
454
|
+
def cart_count(self):
|
|
455
|
+
return sum(v['qty'] for v in self.cart.values())
|
|
456
|
+
|
|
457
|
+
def cart_total(self):
|
|
458
|
+
return sum(v['price'] * v['qty'] for v in self.cart.values())
|
|
459
|
+
|
|
460
|
+
def update_cart_btn(self):
|
|
461
|
+
self.cart_btn.setText(f"Корзина ({self.cart_count()})")
|
|
462
|
+
|
|
463
|
+
def cart_inc(self, iid, qty_lbl, sum_lbl, total_lbl):
|
|
464
|
+
if iid not in self.cart:
|
|
465
|
+
return
|
|
466
|
+
self.cart[iid]['qty'] += 1
|
|
467
|
+
qty_lbl.setText(str(self.cart[iid]['qty']))
|
|
468
|
+
sum_lbl.setText(f"= {self.cart[iid]['price'] * self.cart[iid]['qty']:.0f} руб")
|
|
469
|
+
total_lbl.setText(f"Итого: {self.cart_total():.0f} руб")
|
|
470
|
+
self.update_cart_btn()
|
|
471
|
+
|
|
472
|
+
def cart_dec(self, iid, qty_lbl, sum_lbl, total_lbl):
|
|
473
|
+
if iid not in self.cart or self.cart[iid]['qty'] <= 1:
|
|
474
|
+
return
|
|
475
|
+
self.cart[iid]['qty'] -= 1
|
|
476
|
+
qty_lbl.setText(str(self.cart[iid]['qty']))
|
|
477
|
+
sum_lbl.setText(f"= {self.cart[iid]['price'] * self.cart[iid]['qty']:.0f} руб")
|
|
478
|
+
total_lbl.setText(f"Итого: {self.cart_total():.0f} руб")
|
|
479
|
+
self.update_cart_btn()
|
|
480
|
+
|
|
481
|
+
def cart_remove(self, iid, row, total_lbl, empty_lbl):
|
|
482
|
+
if iid in self.cart:
|
|
483
|
+
del self.cart[iid]
|
|
484
|
+
row.deleteLater()
|
|
485
|
+
total_lbl.setText(f"Итого: {self.cart_total():.0f} руб")
|
|
486
|
+
self.update_cart_btn()
|
|
487
|
+
if not self.cart:
|
|
488
|
+
empty_lbl.show()
|
|
489
|
+
|
|
490
|
+
def checkout(self, dialog):
|
|
491
|
+
order_id, total = self.db.save_order(self.user['user_id'], list(self.cart.values()))
|
|
492
|
+
if order_id:
|
|
493
|
+
self.cart.clear()
|
|
494
|
+
self.update_cart_btn()
|
|
495
|
+
dialog.accept()
|
|
496
|
+
QMessageBox.information(self, "", f"Заказ №{order_id} на {total:.0f} руб оформлен!")
|
|
497
|
+
else:
|
|
498
|
+
QMessageBox.critical(self, "", "Не удалось сохранить заказ в базу данных")
|
|
499
|
+
|
|
500
|
+
def logout(self):
|
|
501
|
+
self.login_win.u.clear()
|
|
502
|
+
self.login_win.p.clear()
|
|
503
|
+
self.login_win.show()
|
|
504
|
+
self.login_win.raise_()
|
|
505
|
+
self._logging_out = True
|
|
506
|
+
self.close()
|
|
507
|
+
|
|
508
|
+
def closeEvent(self, event):
|
|
509
|
+
if self.login_win.isHidden() and not self._logging_out:
|
|
510
|
+
self.login_win.u.clear()
|
|
511
|
+
self.login_win.p.clear()
|
|
512
|
+
self.login_win.show()
|
|
513
|
+
self.login_win.raise_()
|
|
514
|
+
event.accept()
|
|
515
|
+
|
|
516
|
+
def add_to_cart(self, item):
|
|
517
|
+
iid = item['item_id']
|
|
518
|
+
if iid in self.cart:
|
|
519
|
+
self.cart[iid]['qty'] += 1
|
|
520
|
+
else:
|
|
521
|
+
self.cart[iid] = {
|
|
522
|
+
'item_id': iid,
|
|
523
|
+
'name': item['name'],
|
|
524
|
+
'price': float(item['price']),
|
|
525
|
+
'qty': 1
|
|
526
|
+
}
|
|
527
|
+
self.update_cart_btn()
|
|
528
|
+
QMessageBox.information(self, "", f"«{item['name']}» добавлено в корзину")
|
|
529
|
+
|
|
530
|
+
def set_sort(self, order):
|
|
531
|
+
self.sort = order
|
|
532
|
+
self.load(self.s.text() or None)
|
|
533
|
+
|
|
534
|
+
def load(self, s=None):
|
|
535
|
+
for i in reversed(range(self.layout2.count())):
|
|
536
|
+
self.layout2.itemAt(i).widget().deleteLater()
|
|
537
|
+
order = f" ORDER BY price {self.sort}" if self.sort else ""
|
|
538
|
+
if s:
|
|
539
|
+
items = self.db.get(
|
|
540
|
+
f"SELECT * FROM MenuItems WHERE is_available=1 AND name LIKE %s{order}",
|
|
541
|
+
(f"%{s}%",)
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
items = self.db.get(f"SELECT * FROM MenuItems WHERE is_available=1{order}")
|
|
545
|
+
r, c = 0, 0
|
|
546
|
+
for x in items:
|
|
547
|
+
card = QFrame()
|
|
548
|
+
card.setFrameStyle(QFrame.Shape.Box)
|
|
549
|
+
card.setFixedSize(280, 390)
|
|
550
|
+
l2 = QVBoxLayout(card)
|
|
551
|
+
|
|
552
|
+
l2.addWidget(photo_label(x.get('photo')))
|
|
553
|
+
l2.addWidget(QLabel(f"<b>{x['name']}</b>"))
|
|
554
|
+
l2.addWidget(QLabel(f"Цена: {x['price']} руб"))
|
|
555
|
+
|
|
556
|
+
btns = QHBoxLayout()
|
|
557
|
+
btns.addWidget(QPushButton("Подробнее", clicked=lambda ch, iid=x['item_id']: self.detail(iid)))
|
|
558
|
+
btns.addWidget(QPushButton("В корзину", clicked=lambda ch, item=x: self.add_to_cart(item)))
|
|
559
|
+
l2.addLayout(btns)
|
|
560
|
+
|
|
561
|
+
self.layout2.addWidget(card, r, c)
|
|
562
|
+
c += 1
|
|
563
|
+
if c >= 3:
|
|
564
|
+
c = 0
|
|
565
|
+
r += 1
|
|
566
|
+
|
|
567
|
+
def search(self):
|
|
568
|
+
self.load(self.s.text())
|
|
569
|
+
|
|
570
|
+
def detail(self, item_id):
|
|
571
|
+
x = self.db.one("SELECT * FROM MenuItems WHERE item_id=%s", (item_id,))
|
|
572
|
+
if x:
|
|
573
|
+
d = QDialog(self)
|
|
574
|
+
d.setWindowTitle(x['name'])
|
|
575
|
+
d.resize(450, 520)
|
|
576
|
+
l = QVBoxLayout()
|
|
577
|
+
if x.get('photo') and os.path.exists(x['photo']):
|
|
578
|
+
lab = QLabel()
|
|
579
|
+
lab.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
580
|
+
lab.setScaledContents(True)
|
|
581
|
+
pix = QPixmap(x['photo'])
|
|
582
|
+
pix = pix.scaled(400, 300, Qt.AspectRatioMode.KeepAspectRatio,
|
|
583
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
584
|
+
lab.setPixmap(pix)
|
|
585
|
+
l.addWidget(lab)
|
|
586
|
+
l.addWidget(QLabel(f"<b>{x['name']}</b>"))
|
|
587
|
+
l.addWidget(QLabel(f"Цена: {x['price']} руб"))
|
|
588
|
+
btns = QHBoxLayout()
|
|
589
|
+
btns.addWidget(QPushButton("В корзину", clicked=lambda: (self.add_to_cart(x), d.accept())))
|
|
590
|
+
btns.addWidget(QPushButton("Закрыть", clicked=d.accept))
|
|
591
|
+
l.addLayout(btns)
|
|
592
|
+
d.setLayout(l)
|
|
593
|
+
d.exec()
|
|
594
|
+
|
|
595
|
+
def show_cart(self):
|
|
596
|
+
d = QDialog(self)
|
|
597
|
+
d.setWindowTitle("Корзина")
|
|
598
|
+
d.resize(520, 480)
|
|
599
|
+
l = QVBoxLayout()
|
|
600
|
+
|
|
601
|
+
if not self.cart:
|
|
602
|
+
l.addWidget(QLabel("Корзина пуста"))
|
|
603
|
+
l.addWidget(QPushButton("Закрыть", clicked=d.accept))
|
|
604
|
+
d.setLayout(l)
|
|
605
|
+
d.exec()
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
scroll = QScrollArea()
|
|
609
|
+
scroll.setWidgetResizable(True)
|
|
610
|
+
box = QWidget()
|
|
611
|
+
box_l = QVBoxLayout(box)
|
|
612
|
+
|
|
613
|
+
total_lbl = QLabel(f"Итого: {self.cart_total():.0f} руб")
|
|
614
|
+
total_lbl.setStyleSheet("font-size: 14px; font-weight: bold;")
|
|
615
|
+
empty_lbl = QLabel("Корзина пуста")
|
|
616
|
+
empty_lbl.hide()
|
|
617
|
+
|
|
618
|
+
for iid, item in list(self.cart.items()):
|
|
619
|
+
row = QFrame()
|
|
620
|
+
row.setFrameStyle(QFrame.Shape.Box)
|
|
621
|
+
row_l = QHBoxLayout(row)
|
|
622
|
+
|
|
623
|
+
row_l.addWidget(QLabel(f"<b>{item['name']}</b>"))
|
|
624
|
+
row_l.addWidget(QLabel(f"{item['price']:.0f} руб"))
|
|
625
|
+
|
|
626
|
+
qty_lbl = QLabel(str(item['qty']))
|
|
627
|
+
qty_lbl.setFixedWidth(24)
|
|
628
|
+
qty_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
629
|
+
sum_lbl = QLabel(f"= {item['price'] * item['qty']:.0f} руб")
|
|
630
|
+
|
|
631
|
+
qty_l = QHBoxLayout()
|
|
632
|
+
qty_l.addWidget(QPushButton("-", clicked=lambda _, i=iid, q=qty_lbl, s=sum_lbl, t=total_lbl: self.cart_dec(i, q, s, t)))
|
|
633
|
+
qty_l.addWidget(qty_lbl)
|
|
634
|
+
qty_l.addWidget(QPushButton("+", clicked=lambda _, i=iid, q=qty_lbl, s=sum_lbl, t=total_lbl: self.cart_inc(i, q, s, t)))
|
|
635
|
+
row_l.addLayout(qty_l)
|
|
636
|
+
row_l.addWidget(sum_lbl)
|
|
637
|
+
row_l.addWidget(QPushButton("Убрать", clicked=lambda _, i=iid, r=row, t=total_lbl, e=empty_lbl: self.cart_remove(i, r, t, e)))
|
|
638
|
+
box_l.addWidget(row)
|
|
639
|
+
|
|
640
|
+
scroll.setWidget(box)
|
|
641
|
+
l.addWidget(scroll)
|
|
642
|
+
l.addWidget(total_lbl)
|
|
643
|
+
l.addWidget(empty_lbl)
|
|
644
|
+
|
|
645
|
+
btns = QHBoxLayout()
|
|
646
|
+
btns.addWidget(QPushButton("Очистить", clicked=lambda: (self.cart.clear(), self.update_cart_btn(), d.accept(), self.show_cart())))
|
|
647
|
+
btns.addWidget(QPushButton("Оформить", clicked=lambda: self.checkout(d)))
|
|
648
|
+
btns.addWidget(QPushButton("Закрыть", clicked=d.accept))
|
|
649
|
+
l.addLayout(btns)
|
|
650
|
+
|
|
651
|
+
d.setLayout(l)
|
|
652
|
+
d.exec()
|
|
653
|
+
|
|
654
|
+
def main():
|
|
655
|
+
app = QApplication(sys.argv)
|
|
656
|
+
app.setQuitOnLastWindowClosed(False)
|
|
657
|
+
apply_light_theme(app)
|
|
658
|
+
login = Login(DB())
|
|
659
|
+
login.show()
|
|
660
|
+
login.raise_()
|
|
661
|
+
sys.exit(app.exec())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qt-datastore
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Qt datastore and form utilities for desktop applications
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: pymysql
|
|
8
|
+
Requires-Dist: PyQt6
|
|
9
|
+
Requires-Dist: cryptography
|
|
10
|
+
|
|
11
|
+
# qt-datastore
|
|
12
|
+
|
|
13
|
+
Qt datastore utilities for desktop applications with MySQL backend.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
qtds-ui
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m qt_datastore
|
|
21
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
qt_datastore/__init__.py
|
|
4
|
+
qt_datastore/__main__.py
|
|
5
|
+
qt_datastore/app.py
|
|
6
|
+
qt_datastore.egg-info/PKG-INFO
|
|
7
|
+
qt_datastore.egg-info/SOURCES.txt
|
|
8
|
+
qt_datastore.egg-info/dependency_links.txt
|
|
9
|
+
qt_datastore.egg-info/entry_points.txt
|
|
10
|
+
qt_datastore.egg-info/requires.txt
|
|
11
|
+
qt_datastore.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
qt_datastore
|