sqlite-sh 0.1.0__py3-none-any.whl

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.
sqlite_sh/gui.py ADDED
@@ -0,0 +1,649 @@
1
+ import shutil
2
+ import sys
3
+ import tkinter as tk
4
+ from pathlib import Path
5
+ from tkinter import filedialog, messagebox, ttk
6
+
7
+ from . import storage as store
8
+ from .config import ASSETS_DIR, DB_FILE
9
+
10
+
11
+ ROLE_ADMIN = "Администратор"
12
+ ROLE_MANAGER = "Менеджер"
13
+ ROLE_CLIENT = "Авторизированный клиент"
14
+
15
+
16
+ class RomanShoes(tk.Tk):
17
+ def __init__(self):
18
+ super().__init__()
19
+ self.title("Обувь - учет товаров")
20
+ self.geometry("1240x740")
21
+ self.minsize(1040, 620)
22
+ self.configure(bg="#FFFFFF")
23
+ self.user = None
24
+ self.product_editor = None
25
+ self.order_editor = None
26
+ self.logo_image = self.load_png("Icon.png", max_width=64, max_height=64)
27
+ self.placeholder_image = self.load_png("picture.png", max_width=54, max_height=42)
28
+ self.setup_style()
29
+ self.show_login()
30
+
31
+ def setup_style(self):
32
+ self.option_add("*Font", ("Times New Roman", 11))
33
+ style = ttk.Style(self)
34
+ try:
35
+ style.theme_use("clam")
36
+ except tk.TclError:
37
+ pass
38
+ style.configure("Accent.TButton", background="#00FA9A")
39
+ style.configure("Header.TFrame", background="#7FFF00")
40
+ style.configure("Header.TLabel", background="#7FFF00", font=("Times New Roman", 18, "bold"))
41
+ style.configure("SubHeader.TLabel", background="#7FFF00", font=("Times New Roman", 11))
42
+ style.configure("Side.TFrame", background="#F7FFF0")
43
+ style.configure("Workspace.TFrame", background="#FFFFFF")
44
+ style.configure("Treeview", rowheight=50, font=("Times New Roman", 10))
45
+ style.configure("Treeview.Heading", font=("Times New Roman", 10, "bold"))
46
+
47
+ def load_png(self, filename, max_width=None, max_height=None):
48
+ path = ASSETS_DIR / filename
49
+ if not path.exists():
50
+ return None
51
+ try:
52
+ image = tk.PhotoImage(file=str(path))
53
+ if max_width and max_height:
54
+ scale = max(
55
+ 1,
56
+ (image.width() + max_width - 1) // max_width,
57
+ (image.height() + max_height - 1) // max_height,
58
+ )
59
+ if scale > 1:
60
+ image = image.subsample(scale, scale)
61
+ return image
62
+ except tk.TclError:
63
+ return None
64
+
65
+ def clear(self):
66
+ for child in self.winfo_children():
67
+ child.destroy()
68
+
69
+ def header(self, title):
70
+ frame = ttk.Frame(self, style="Header.TFrame", padding=(14, 8))
71
+ frame.pack(fill=tk.X)
72
+ if self.logo_image:
73
+ ttk.Label(frame, image=self.logo_image, background="#7FFF00").pack(side=tk.LEFT, padx=(0, 12))
74
+ text_box = ttk.Frame(frame, style="Header.TFrame")
75
+ text_box.pack(side=tk.LEFT, fill=tk.X, expand=True)
76
+ ttk.Label(text_box, text=title, style="Header.TLabel").pack(anchor=tk.W)
77
+ right_box = ttk.Frame(frame, style="Header.TFrame")
78
+ right_box.pack(side=tk.RIGHT, anchor=tk.NE)
79
+ if self.user:
80
+ ttk.Label(
81
+ right_box,
82
+ text=f"{self.user['role']}: {self.user['full_name']}",
83
+ style="SubHeader.TLabel",
84
+ ).pack(anchor=tk.E)
85
+
86
+ def show_login(self):
87
+ self.user = None
88
+ self.clear()
89
+ self.header("Авторизация пользователя")
90
+ body = ttk.Frame(self, padding=24, style="Workspace.TFrame")
91
+ body.pack(expand=True)
92
+ if self.logo_image:
93
+ ttk.Label(body, image=self.logo_image).grid(row=0, column=0, columnspan=2, pady=(0, 12))
94
+ ttk.Label(body, text="ООО Обувь", font=("Times New Roman", 16, "bold")).grid(
95
+ row=1, column=0, columnspan=2, pady=(0, 14)
96
+ )
97
+ ttk.Label(body, text="Логин").grid(row=2, column=0, sticky="w", pady=5)
98
+ login = ttk.Entry(body, width=35)
99
+ login.grid(row=2, column=1, pady=5)
100
+ ttk.Label(body, text="Пароль").grid(row=3, column=0, sticky="w", pady=5)
101
+ password = ttk.Entry(body, width=35, show="*")
102
+ password.grid(row=3, column=1, pady=5)
103
+
104
+ def do_login():
105
+ user = store.authenticate(login.get().strip(), password.get().strip())
106
+ if not user:
107
+ messagebox.showerror("Ошибка входа", "Неверный логин или пароль. Проверьте данные и повторите вход.")
108
+ return
109
+ self.user = dict(user)
110
+ self.show_products()
111
+
112
+ ttk.Button(body, text="Войти", style="Accent.TButton", command=do_login).grid(
113
+ row=4, column=0, columnspan=2, sticky="ew", pady=10
114
+ )
115
+ ttk.Button(body, text="Войти как гость", command=lambda: self.login_guest()).grid(
116
+ row=5, column=0, columnspan=2, sticky="ew"
117
+ )
118
+ login.focus_set()
119
+
120
+ def login_guest(self):
121
+ self.user = {"role": "Гость", "full_name": "Гость"}
122
+ self.show_products()
123
+
124
+ def can_filter(self):
125
+ return self.user and self.user["role"] in {ROLE_MANAGER, ROLE_ADMIN}
126
+
127
+ def is_admin(self):
128
+ return self.user and self.user["role"] == ROLE_ADMIN
129
+
130
+ def can_orders(self):
131
+ return self.user and self.user["role"] in {ROLE_MANAGER, ROLE_ADMIN}
132
+
133
+ def show_products(self):
134
+ self.clear()
135
+ self.header("Каталог обуви")
136
+ workspace = ttk.Frame(self, style="Workspace.TFrame")
137
+ workspace.pack(fill=tk.BOTH, expand=True)
138
+ side = tk.Frame(workspace, bg="#F7FFF0", width=245, padx=12, pady=12)
139
+ side.pack(side=tk.LEFT, fill=tk.Y)
140
+ side.pack_propagate(False)
141
+ table_frame = ttk.Frame(workspace, padding=(8, 8, 8, 8))
142
+ table_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
143
+
144
+ tk.Label(side, text="Действия", bg="#F7FFF0", font=("Times New Roman", 14, "bold")).pack(anchor=tk.W, pady=(0, 8))
145
+ ttk.Button(side, text="Выйти", command=self.show_login).pack(fill=tk.X, pady=3)
146
+ if self.can_orders():
147
+ ttk.Button(side, text="Открыть заказы", command=self.show_orders).pack(fill=tk.X, pady=3)
148
+ if self.is_admin():
149
+ ttk.Button(side, text="Добавить товар", style="Accent.TButton", command=lambda: self.open_product_editor()).pack(
150
+ fill=tk.X, pady=(12, 3)
151
+ )
152
+
153
+ self.search_var = tk.StringVar()
154
+ self.supplier_var = tk.StringVar(value="Все поставщики")
155
+ self.sort_var = tk.StringVar(value="Без сортировки")
156
+ if self.can_filter():
157
+ tk.Label(side, text="Отбор каталога", bg="#F7FFF0", font=("Times New Roman", 14, "bold")).pack(
158
+ anchor=tk.W, pady=(18, 8)
159
+ )
160
+ tk.Label(side, text="Найти товар", bg="#F7FFF0").pack(anchor=tk.W)
161
+ search = ttk.Entry(side, textvariable=self.search_var, width=25)
162
+ search.pack(fill=tk.X, pady=(2, 8))
163
+ tk.Label(side, text="Поставщик", bg="#F7FFF0").pack(anchor=tk.W)
164
+ supplier = ttk.Combobox(
165
+ side,
166
+ textvariable=self.supplier_var,
167
+ values=store.suppliers_for_filter(),
168
+ state="readonly",
169
+ )
170
+ supplier.pack(fill=tk.X, pady=(2, 8))
171
+ tk.Label(side, text="Сортировка", bg="#F7FFF0").pack(anchor=tk.W)
172
+ sort = ttk.Combobox(
173
+ side,
174
+ textvariable=self.sort_var,
175
+ values=["Без сортировки", "Остаток по возрастанию", "Остаток по убыванию"],
176
+ state="readonly",
177
+ )
178
+ sort.pack(fill=tk.X, pady=(2, 8))
179
+ self.search_var.trace_add("write", lambda *_: self.refresh_products())
180
+ self.supplier_var.trace_add("write", lambda *_: self.refresh_products())
181
+ self.sort_var.trace_add("write", lambda *_: self.refresh_products())
182
+
183
+ columns = (
184
+ "id",
185
+ "article",
186
+ "name",
187
+ "category",
188
+ "description",
189
+ "manufacturer",
190
+ "supplier",
191
+ "price",
192
+ "unit",
193
+ "stock",
194
+ "discount",
195
+ )
196
+ self.product_images = []
197
+ self.products_tree = ttk.Treeview(table_frame, columns=columns, show="tree headings", selectmode="browse")
198
+ self.products_tree.heading("#0", text="Фото")
199
+ self.products_tree.column("#0", width=58, anchor=tk.CENTER, stretch=False)
200
+ headings = {
201
+ "id": "ID",
202
+ "article": "Артикул",
203
+ "name": "Наименование",
204
+ "category": "Категория",
205
+ "description": "Описание",
206
+ "manufacturer": "Производитель",
207
+ "supplier": "Поставщик",
208
+ "price": "Цена",
209
+ "unit": "Ед.",
210
+ "stock": "Склад",
211
+ "discount": "Скидка",
212
+ }
213
+ widths = {
214
+ "id": 44,
215
+ "article": 82,
216
+ "name": 105,
217
+ "category": 105,
218
+ "description": 220,
219
+ "manufacturer": 115,
220
+ "supplier": 105,
221
+ "price": 95,
222
+ "unit": 42,
223
+ "stock": 58,
224
+ "discount": 62,
225
+ }
226
+ for col in columns:
227
+ self.products_tree.heading(col, text=headings[col])
228
+ self.products_tree.column(col, width=widths[col], anchor=tk.W, stretch=(col == "description"))
229
+ self.products_tree.tag_configure("big_discount", background="#2E8B57")
230
+ self.products_tree.tag_configure("no_stock", background="#ADD8E6")
231
+ y_scroll = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.products_tree.yview)
232
+ x_scroll = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=self.products_tree.xview)
233
+ self.products_tree.configure(yscrollcommand=y_scroll.set, xscrollcommand=x_scroll.set)
234
+ self.products_tree.grid(row=0, column=0, sticky="nsew")
235
+ y_scroll.grid(row=0, column=1, sticky="ns")
236
+ x_scroll.grid(row=1, column=0, sticky="ew")
237
+ table_frame.rowconfigure(0, weight=1)
238
+ table_frame.columnconfigure(0, weight=1)
239
+ if self.is_admin():
240
+ self.products_tree.bind("<Double-1>", self.edit_selected_product)
241
+ ttk.Button(side, text="Удалить выбранный товар", command=self.delete_selected_product).pack(fill=tk.X, pady=(3, 0))
242
+ self.refresh_products()
243
+
244
+ def refresh_products(self):
245
+ if not hasattr(self, "products_tree"):
246
+ return
247
+ for item in self.products_tree.get_children():
248
+ self.products_tree.delete(item)
249
+ self.product_images.clear()
250
+ search = self.search_var.get() if self.can_filter() else ""
251
+ supplier = self.supplier_var.get() if self.can_filter() else ""
252
+ sort_stock = self.sort_var.get() if self.can_filter() else ""
253
+ for product in store.list_products(search, supplier, sort_stock):
254
+ old_price = float(product["price"])
255
+ discount = int(product["discount"])
256
+ final_price = old_price * (100 - discount) / 100
257
+ price_text = f"{old_price:.2f}"
258
+ if discount > 0:
259
+ price_text = f"{old_price:.2f} -> {final_price:.2f}"
260
+ tags = []
261
+ if product["stock"] <= 0:
262
+ tags.append("no_stock")
263
+ elif discount > 15:
264
+ tags.append("big_discount")
265
+ image = self.placeholder_image or ""
266
+ self.products_tree.insert(
267
+ "",
268
+ tk.END,
269
+ image=image,
270
+ values=(
271
+ product["id"],
272
+ product["article"],
273
+ product["name"],
274
+ product["category"],
275
+ product["description"],
276
+ product["manufacturer"],
277
+ product["supplier"],
278
+ price_text,
279
+ product["unit"],
280
+ product["stock"],
281
+ f"{discount}%",
282
+ ),
283
+ tags=tags,
284
+ )
285
+ if image:
286
+ self.product_images.append(image)
287
+
288
+ def selected_tree_id(self, tree):
289
+ selected = tree.selection()
290
+ if not selected:
291
+ return None
292
+ return int(tree.item(selected[0], "values")[0])
293
+
294
+ def edit_selected_product(self, _event=None):
295
+ product_id = self.selected_tree_id(self.products_tree)
296
+ if product_id:
297
+ self.open_product_editor(product_id)
298
+
299
+ def delete_selected_product(self):
300
+ product_id = self.selected_tree_id(self.products_tree)
301
+ if not product_id:
302
+ messagebox.showwarning("Удаление товара", "Выберите товар для удаления.")
303
+ return
304
+ if store.product_in_orders(product_id):
305
+ messagebox.showerror("Удаление товара", "Товар присутствует в заказе, удалить его нельзя.")
306
+ return
307
+ if messagebox.askyesno("Удаление товара", "Удалить выбранный товар?"):
308
+ store.delete_product(product_id)
309
+ self.refresh_products()
310
+
311
+ def open_product_editor(self, product_id=None):
312
+ if self.product_editor and self.product_editor.winfo_exists():
313
+ self.product_editor.focus()
314
+ return
315
+ self.product_editor = ProductEditor(self, product_id)
316
+
317
+ def show_orders(self):
318
+ self.clear()
319
+ self.header("Заказы")
320
+ workspace = ttk.Frame(self, style="Workspace.TFrame")
321
+ workspace.pack(fill=tk.BOTH, expand=True)
322
+ side = tk.Frame(workspace, bg="#F7FFF0", width=230, padx=12, pady=12)
323
+ side.pack(side=tk.LEFT, fill=tk.Y)
324
+ side.pack_propagate(False)
325
+ tk.Label(side, text="Раздел заказов", bg="#F7FFF0", font=("Times New Roman", 14, "bold")).pack(
326
+ anchor=tk.W, pady=(0, 8)
327
+ )
328
+ ttk.Button(side, text="Назад к каталогу", command=self.show_products).pack(fill=tk.X, pady=3)
329
+ ttk.Button(side, text="Выйти", command=self.show_login).pack(fill=tk.X, pady=3)
330
+ if self.is_admin():
331
+ ttk.Button(side, text="Добавить заказ", style="Accent.TButton", command=lambda: self.open_order_editor()).pack(
332
+ fill=tk.X, pady=(12, 3)
333
+ )
334
+ columns = ("id", "items", "status", "pickup", "order_date", "delivery_date", "client", "code")
335
+ table_frame = ttk.Frame(workspace, padding=(8, 8, 8, 8))
336
+ table_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
337
+ self.orders_tree = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
338
+ headings = {
339
+ "id": "Номер",
340
+ "items": "Артикулы",
341
+ "status": "Статус",
342
+ "pickup": "Пункт выдачи",
343
+ "order_date": "Дата заказа",
344
+ "delivery_date": "Дата выдачи",
345
+ "client": "Клиент",
346
+ "code": "Код",
347
+ }
348
+ widths = {"id": 70, "items": 260, "pickup": 300}
349
+ for col in columns:
350
+ self.orders_tree.heading(col, text=headings[col])
351
+ self.orders_tree.column(col, width=widths.get(col, 120), anchor=tk.W)
352
+ y_scroll = ttk.Scrollbar(table_frame, orient=tk.VERTICAL, command=self.orders_tree.yview)
353
+ x_scroll = ttk.Scrollbar(table_frame, orient=tk.HORIZONTAL, command=self.orders_tree.xview)
354
+ self.orders_tree.configure(yscrollcommand=y_scroll.set, xscrollcommand=x_scroll.set)
355
+ self.orders_tree.grid(row=0, column=0, sticky="nsew")
356
+ y_scroll.grid(row=0, column=1, sticky="ns")
357
+ x_scroll.grid(row=1, column=0, sticky="ew")
358
+ table_frame.rowconfigure(0, weight=1)
359
+ table_frame.columnconfigure(0, weight=1)
360
+ if self.is_admin():
361
+ self.orders_tree.bind("<Double-1>", self.edit_selected_order)
362
+ ttk.Button(side, text="Удалить выбранный заказ", command=self.delete_selected_order).pack(fill=tk.X, pady=(3, 0))
363
+ self.refresh_orders()
364
+
365
+ def refresh_orders(self):
366
+ if not hasattr(self, "orders_tree"):
367
+ return
368
+ for item in self.orders_tree.get_children():
369
+ self.orders_tree.delete(item)
370
+ for order in store.list_orders():
371
+ self.orders_tree.insert(
372
+ "",
373
+ tk.END,
374
+ values=(
375
+ order["id"],
376
+ order["items"] or "",
377
+ order["status"],
378
+ order["pickup_point"] or "",
379
+ order["order_date"] or "",
380
+ order["delivery_date"] or "",
381
+ order["client_name"] or "",
382
+ order["receive_code"] or "",
383
+ ),
384
+ )
385
+
386
+ def open_order_editor(self, order_id=None):
387
+ if self.order_editor and self.order_editor.winfo_exists():
388
+ self.order_editor.focus()
389
+ return
390
+ self.order_editor = OrderEditor(self, order_id)
391
+
392
+ def edit_selected_order(self, _event=None):
393
+ order_id = self.selected_tree_id(self.orders_tree)
394
+ if order_id:
395
+ self.open_order_editor(order_id)
396
+
397
+ def delete_selected_order(self):
398
+ order_id = self.selected_tree_id(self.orders_tree)
399
+ if not order_id:
400
+ messagebox.showwarning("Удаление заказа", "Выберите заказ для удаления.")
401
+ return
402
+ if messagebox.askyesno("Удаление заказа", "Удалить выбранный заказ?"):
403
+ store.delete_order(order_id)
404
+ self.refresh_orders()
405
+
406
+
407
+ class ProductEditor(tk.Toplevel):
408
+ def __init__(self, app, product_id=None):
409
+ super().__init__(app)
410
+ self.app = app
411
+ self.product_id = product_id
412
+ self.title("Редактирование товара" if product_id else "Добавление товара")
413
+ self.geometry("620x560")
414
+ self.resizable(False, False)
415
+ self.image_path_var = tk.StringVar()
416
+ self.vars = {}
417
+ self.build()
418
+ if product_id:
419
+ self.load_product()
420
+ self.protocol("WM_DELETE_WINDOW", self.destroy)
421
+
422
+ def build(self):
423
+ frame = ttk.Frame(self, padding=12)
424
+ frame.pack(fill=tk.BOTH, expand=True)
425
+ fields = [
426
+ ("article", "Артикул"),
427
+ ("name", "Наименование"),
428
+ ("category", "Категория"),
429
+ ("description", "Описание"),
430
+ ("manufacturer", "Производитель"),
431
+ ("supplier", "Поставщик"),
432
+ ("price", "Цена"),
433
+ ("unit", "Единица измерения"),
434
+ ("stock", "Количество на складе"),
435
+ ("discount", "Действующая скидка"),
436
+ ]
437
+ combo_sources = {
438
+ "category": "categories",
439
+ "manufacturer": "manufacturers",
440
+ "supplier": "suppliers",
441
+ "unit": "units",
442
+ }
443
+ row = 0
444
+ if self.product_id:
445
+ ttk.Label(frame, text="ID").grid(row=row, column=0, sticky="w", pady=4)
446
+ ttk.Label(frame, text=str(self.product_id)).grid(row=row, column=1, sticky="ew", pady=4)
447
+ row += 1
448
+ for key, label in fields:
449
+ ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", pady=4)
450
+ var = tk.StringVar()
451
+ if key in combo_sources:
452
+ widget = ttk.Combobox(frame, textvariable=var, values=store.options(combo_sources[key]), width=45)
453
+ elif key == "description":
454
+ widget = ttk.Entry(frame, textvariable=var, width=48)
455
+ else:
456
+ widget = ttk.Entry(frame, textvariable=var, width=48)
457
+ widget.grid(row=row, column=1, sticky="ew", pady=4)
458
+ self.vars[key] = var
459
+ row += 1
460
+ ttk.Label(frame, text="Фото").grid(row=row, column=0, sticky="w", pady=4)
461
+ ttk.Entry(frame, textvariable=self.image_path_var, width=38, state="readonly").grid(row=row, column=1, sticky="w")
462
+ ttk.Button(frame, text="Выбрать файл", command=self.choose_image).grid(row=row, column=1, sticky="e")
463
+ row += 1
464
+ ttk.Button(frame, text="Сохранить", style="Accent.TButton", command=self.save).grid(
465
+ row=row, column=0, columnspan=2, sticky="ew", pady=12
466
+ )
467
+
468
+ def load_product(self):
469
+ product = store.get_product(self.product_id)
470
+ mapping = {
471
+ "article": product["article"],
472
+ "name": product["name"],
473
+ "category": product["category"],
474
+ "description": product["description"],
475
+ "manufacturer": product["manufacturer"],
476
+ "supplier": product["supplier"],
477
+ "price": str(product["price"]),
478
+ "unit": product["unit"],
479
+ "stock": str(product["stock"]),
480
+ "discount": str(product["discount"]),
481
+ }
482
+ for key, value in mapping.items():
483
+ self.vars[key].set(value or "")
484
+ self.image_path_var.set(product["image_path"] or "")
485
+
486
+ def choose_image(self):
487
+ filename = filedialog.askopenfilename(
488
+ parent=self,
489
+ title="Выберите изображение",
490
+ filetypes=[("Images", "*.png *.gif *.jpg *.jpeg"), ("All files", "*.*")],
491
+ )
492
+ if not filename:
493
+ return
494
+ source = Path(filename)
495
+ ASSETS_DIR.mkdir(exist_ok=True)
496
+ target = ASSETS_DIR / source.name
497
+ if source.resolve() != target.resolve():
498
+ shutil.copy2(source, target)
499
+ self.image_path_var.set(target.name)
500
+
501
+ def save(self):
502
+ try:
503
+ price = float(self.vars["price"].get().replace(",", "."))
504
+ stock = int(float(self.vars["stock"].get()))
505
+ discount = int(float(self.vars["discount"].get()))
506
+ if price < 0 or stock < 0 or discount < 0:
507
+ raise ValueError
508
+ required = ["article", "name", "category", "manufacturer", "supplier", "unit"]
509
+ for key in required:
510
+ if not self.vars[key].get().strip():
511
+ raise ValueError
512
+ except ValueError:
513
+ messagebox.showerror(
514
+ "Ошибка сохранения",
515
+ "Проверьте обязательные поля. Цена, количество и скидка должны быть неотрицательными числами.",
516
+ parent=self,
517
+ )
518
+ return
519
+ try:
520
+ store.save_product(
521
+ {
522
+ "article": self.vars["article"].get().strip(),
523
+ "name": self.vars["name"].get().strip(),
524
+ "category": self.vars["category"].get().strip(),
525
+ "description": self.vars["description"].get().strip(),
526
+ "manufacturer": self.vars["manufacturer"].get().strip(),
527
+ "supplier": self.vars["supplier"].get().strip(),
528
+ "price": price,
529
+ "unit": self.vars["unit"].get().strip(),
530
+ "stock": stock,
531
+ "discount": discount,
532
+ "image_path": self.image_path_var.get().strip() or None,
533
+ },
534
+ self.product_id,
535
+ )
536
+ except Exception as exc:
537
+ messagebox.showerror("Ошибка сохранения", str(exc), parent=self)
538
+ return
539
+ self.app.refresh_products()
540
+ self.destroy()
541
+
542
+
543
+ class OrderEditor(tk.Toplevel):
544
+ def __init__(self, app, order_id=None):
545
+ super().__init__(app)
546
+ self.app = app
547
+ self.order_id = order_id
548
+ self.title("Редактирование заказа" if order_id else "Добавление заказа")
549
+ self.geometry("660x430")
550
+ self.resizable(False, False)
551
+ self.vars = {}
552
+ self.build()
553
+ if order_id:
554
+ self.load_order()
555
+
556
+ def build(self):
557
+ frame = ttk.Frame(self, padding=12)
558
+ frame.pack(fill=tk.BOTH, expand=True)
559
+ fields = [
560
+ ("items", "Артикулы и количество (А112Т4, 2, F635R4, 1)"),
561
+ ("status", "Статус заказа"),
562
+ ("pickup_point", "Адрес пункта выдачи"),
563
+ ("order_date", "Дата заказа YYYY-MM-DD"),
564
+ ("delivery_date", "Дата выдачи YYYY-MM-DD"),
565
+ ("client_name", "ФИО клиента"),
566
+ ("receive_code", "Код получения"),
567
+ ]
568
+ for row, (key, label) in enumerate(fields):
569
+ ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", pady=5)
570
+ var = tk.StringVar()
571
+ if key == "status":
572
+ widget = ttk.Combobox(frame, textvariable=var, values=["Новый", "Завершен", "Отменен"], width=48)
573
+ elif key == "pickup_point":
574
+ widget = ttk.Combobox(frame, textvariable=var, values=store.options("pickup_points"), width=48)
575
+ else:
576
+ widget = ttk.Entry(frame, textvariable=var, width=51)
577
+ widget.grid(row=row, column=1, sticky="ew", pady=5)
578
+ self.vars[key] = var
579
+ ttk.Button(frame, text="Сохранить", style="Accent.TButton", command=self.save).grid(
580
+ row=len(fields), column=0, columnspan=2, sticky="ew", pady=12
581
+ )
582
+
583
+ def load_order(self):
584
+ order, items = store.get_order(self.order_id)
585
+ self.vars["items"].set(", ".join(f"{item['article']}, {item['quantity']}" for item in items))
586
+ self.vars["status"].set(order["status"] or "")
587
+ self.vars["pickup_point"].set(order["pickup_point"] or "")
588
+ self.vars["order_date"].set(order["order_date"] or "")
589
+ self.vars["delivery_date"].set(order["delivery_date"] or "")
590
+ self.vars["client_name"].set(order["client_name"] or "")
591
+ self.vars["receive_code"].set(order["receive_code"] or "")
592
+
593
+ def parse_items(self):
594
+ parts = [part.strip() for part in self.vars["items"].get().split(",") if part.strip()]
595
+ if len(parts) % 2:
596
+ raise ValueError("Артикулы должны идти парами: артикул, количество.")
597
+ items = []
598
+ for index in range(0, len(parts), 2):
599
+ quantity = int(float(parts[index + 1]))
600
+ if quantity <= 0:
601
+ raise ValueError("Количество товара в заказе должно быть больше нуля.")
602
+ items.append((parts[index], quantity))
603
+ if not items:
604
+ raise ValueError("Добавьте хотя бы один товар в заказ.")
605
+ return items
606
+
607
+ def save(self):
608
+ try:
609
+ items = self.parse_items()
610
+ if not self.vars["status"].get().strip() or not self.vars["pickup_point"].get().strip():
611
+ raise ValueError("Заполните статус и пункт выдачи.")
612
+ store.save_order(
613
+ {
614
+ "status": self.vars["status"].get().strip(),
615
+ "pickup_point": self.vars["pickup_point"].get().strip(),
616
+ "order_date": self.vars["order_date"].get().strip() or None,
617
+ "delivery_date": self.vars["delivery_date"].get().strip() or None,
618
+ "client_name": self.vars["client_name"].get().strip(),
619
+ "receive_code": self.vars["receive_code"].get().strip(),
620
+ },
621
+ items,
622
+ self.order_id,
623
+ )
624
+ except Exception as exc:
625
+ messagebox.showerror("Ошибка сохранения заказа", str(exc), parent=self)
626
+ return
627
+ self.app.refresh_orders()
628
+ self.destroy()
629
+
630
+
631
+ def main():
632
+ try:
633
+ store.require_database()
634
+ except FileNotFoundError as exc:
635
+ messagebox.showerror("База данных не найдена", f"{exc}\n\nЗапустите python bootstrap_db.py")
636
+ return 1
637
+ app = RomanShoes()
638
+ icon = ASSETS_DIR / "Icon.ico"
639
+ if icon.exists():
640
+ try:
641
+ app.iconbitmap(str(icon))
642
+ except tk.TclError:
643
+ pass
644
+ app.mainloop()
645
+ return 0
646
+
647
+
648
+ if __name__ == "__main__":
649
+ sys.exit(main())