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.
@@ -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
+ # qt-datastore
2
+
3
+ Qt datastore utilities for desktop applications with MySQL backend.
4
+
5
+ ```bash
6
+ qtds-ui
7
+ ```
8
+
9
+ ```bash
10
+ python -m qt_datastore
11
+ ```
@@ -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,4 @@
1
+ from qt_datastore.app import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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,2 @@
1
+ [console_scripts]
2
+ qtds-ui = qt_datastore.app:main
@@ -0,0 +1,3 @@
1
+ pymysql
2
+ PyQt6
3
+ cryptography
@@ -0,0 +1 @@
1
+ qt_datastore
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+