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/__init__.py +1 -0
- sqlite_sh/__main__.py +4 -0
- sqlite_sh/algorithm_flowchart_gost.pdf +0 -0
- sqlite_sh/assets/1.jpg +0 -0
- sqlite_sh/assets/10.jpg +0 -0
- sqlite_sh/assets/2.jpg +0 -0
- sqlite_sh/assets/3.jpg +0 -0
- sqlite_sh/assets/4.jpg +0 -0
- sqlite_sh/assets/5.jpg +0 -0
- sqlite_sh/assets/6.jpg +0 -0
- sqlite_sh/assets/7.jpg +0 -0
- sqlite_sh/assets/8.jpg +0 -0
- sqlite_sh/assets/9.jpg +0 -0
- sqlite_sh/assets/Icon.JPG +0 -0
- sqlite_sh/assets/Icon.ico +0 -0
- sqlite_sh/assets/Icon.png +0 -0
- sqlite_sh/assets/__init__.py +0 -0
- sqlite_sh/assets/picture.png +0 -0
- sqlite_sh/bootstrap.py +249 -0
- sqlite_sh/config.py +23 -0
- sqlite_sh/data/__init__.py +0 -0
- sqlite_sh/data/shop.db +0 -0
- sqlite_sh/database.sql +87 -0
- sqlite_sh/db_uml_diagram.pdf +0 -0
- sqlite_sh/diagram.py +37 -0
- sqlite_sh/gui.py +649 -0
- sqlite_sh/storage.py +269 -0
- sqlite_sh-0.1.0.dist-info/METADATA +95 -0
- sqlite_sh-0.1.0.dist-info/RECORD +33 -0
- sqlite_sh-0.1.0.dist-info/WHEEL +5 -0
- sqlite_sh-0.1.0.dist-info/entry_points.txt +4 -0
- sqlite_sh-0.1.0.dist-info/licenses/LICENSE +21 -0
- sqlite_sh-0.1.0.dist-info/top_level.txt +1 -0
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())
|