programm.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. import psycopg2
  2. import tkinter as tk
  3. from tkinter import ttk, messagebox, simpledialog
  4. from datetime import datetime
  5. import csv
  6. # ========== НАСТРОЙКИ БД ==========
  7. DB_CONFIG = {
  8. "dbname": "dbwork",
  9. "user": "zhelezkov_fv",
  10. "password": "0&6&I#u#s#B",
  11. "host": "edu-pg.itiscaf.ru",
  12. "port": 5432
  13. }
  14. def get_conn():
  15. return psycopg2.connect(**DB_CONFIG)
  16. # ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ СПРАВОЧНИКОВ ==========
  17. def get_categories():
  18. with get_conn() as conn, conn.cursor() as cur:
  19. cur.execute("SELECT id, name FROM Category ORDER BY name")
  20. return cur.fetchall()
  21. def get_equipment_list():
  22. with get_conn() as conn, conn.cursor() as cur:
  23. cur.execute("SELECT id, name FROM Equipment ORDER BY name")
  24. return cur.fetchall()
  25. def get_employees():
  26. with get_conn() as conn, conn.cursor() as cur:
  27. cur.execute("SELECT id, full_name FROM Employee ORDER BY full_name")
  28. return cur.fetchall()
  29. def get_suppliers():
  30. with get_conn() as conn, conn.cursor() as cur:
  31. cur.execute("SELECT id, address FROM Supplier ORDER BY address")
  32. return cur.fetchall()
  33. def get_warehouses():
  34. with get_conn() as conn, conn.cursor() as cur:
  35. cur.execute("SELECT id, address FROM Warehouse ORDER BY address")
  36. return cur.fetchall()
  37. # ========== ОБНОВЛЕНИЕ ТАБЛИЦ ==========
  38. def refresh_stock(tree):
  39. for row in tree.get_children():
  40. tree.delete(row)
  41. with get_conn() as conn, conn.cursor() as cur:
  42. cur.execute("""
  43. SELECT id, name, manufacturer, category, current_quantity
  44. FROM EquipmentStock
  45. ORDER BY name
  46. """)
  47. for row in cur.fetchall():
  48. tree.insert("", tk.END, values=row)
  49. def refresh_purchases(tree):
  50. for row in tree.get_children():
  51. tree.delete(row)
  52. with get_conn() as conn, conn.cursor() as cur:
  53. cur.execute("""
  54. SELECT id, equipment, supplier, warehouse, quantity,
  55. price_per_unit, total_cost, purchase_date, notes
  56. FROM PurchaseDetails
  57. ORDER BY purchase_date DESC
  58. """)
  59. for row in cur.fetchall():
  60. tree.insert("", tk.END, values=row)
  61. def refresh_writeoffs(tree):
  62. for row in tree.get_children():
  63. tree.delete(row)
  64. with get_conn() as conn, conn.cursor() as cur:
  65. cur.execute("""
  66. SELECT id, equipment, employee, quantity, reason, write_off_date
  67. FROM WriteOffDetails
  68. ORDER BY write_off_date DESC
  69. """)
  70. for row in cur.fetchall():
  71. tree.insert("", tk.END, values=row)
  72. # ========== ПОИСК ==========
  73. def search_stock(tree, search_entry):
  74. keyword = search_entry.get().strip()
  75. for row in tree.get_children():
  76. tree.delete(row)
  77. if not keyword:
  78. refresh_stock(tree)
  79. return
  80. with get_conn() as conn, conn.cursor() as cur:
  81. cur.execute("""
  82. SELECT e.id, e.name, e.manufacturer, c.name AS category,
  83. COALESCE(SUM(p.quantity), 0) - COALESCE(SUM(w.quantity), 0) AS current_quantity
  84. FROM Equipment e
  85. LEFT JOIN Category c ON e.category_id = c.id
  86. LEFT JOIN Purchase p ON e.id = p.equipment_id
  87. LEFT JOIN WriteOff w ON e.id = w.equipment_id
  88. WHERE e.name ILIKE %s OR e.manufacturer ILIKE %s
  89. GROUP BY e.id, c.name
  90. ORDER BY e.name
  91. """, (f'{keyword}%', f'{keyword}%')) # изменено: только % в конце
  92. for row in cur.fetchall():
  93. tree.insert("", tk.END, values=row)
  94. # ========== CRUD ДЛЯ ЗАКУПОК ==========
  95. def add_purchase(parent, refresh_callback):
  96. win = tk.Toplevel(parent)
  97. win.title("Новая закупка")
  98. win.geometry("500x400")
  99. win.grab_set()
  100. # Поля
  101. tk.Label(win, text="Оборудование:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
  102. equip_var = tk.StringVar()
  103. equip_combo = ttk.Combobox(win, textvariable=equip_var, values=[f"{e[0]} - {e[1]}" for e in get_equipment_list()], width=40)
  104. equip_combo.grid(row=0, column=1, padx=5, pady=5)
  105. tk.Label(win, text="Поставщик:").grid(row=1, column=0, sticky="e")
  106. supp_var = tk.StringVar()
  107. supp_combo = ttk.Combobox(win, textvariable=supp_var, values=[f"{s[0]} - {s[1]}" for s in get_suppliers()], width=40)
  108. supp_combo.grid(row=1, column=1, padx=5, pady=5)
  109. tk.Label(win, text="Склад:").grid(row=2, column=0, sticky="e")
  110. wh_var = tk.StringVar()
  111. wh_combo = ttk.Combobox(win, textvariable=wh_var, values=[f"{w[0]} - {w[1]}" for w in get_warehouses()], width=40)
  112. wh_combo.grid(row=2, column=1, padx=5, pady=5)
  113. tk.Label(win, text="Количество:").grid(row=3, column=0, sticky="e")
  114. qty_entry = tk.Entry(win)
  115. qty_entry.grid(row=3, column=1, padx=5, pady=5)
  116. tk.Label(win, text="Цена за ед. (руб):").grid(row=4, column=0, sticky="e")
  117. price_entry = tk.Entry(win)
  118. price_entry.grid(row=4, column=1, padx=5, pady=5)
  119. tk.Label(win, text="Дата закупки (ГГГГ-ММ-ДД):").grid(row=5, column=0, sticky="e")
  120. date_entry = tk.Entry(win)
  121. date_entry.insert(0, datetime.now().strftime("%Y-%m-%d"))
  122. date_entry.grid(row=5, column=1, padx=5, pady=5)
  123. tk.Label(win, text="Примечание:").grid(row=6, column=0, sticky="ne")
  124. notes_text = tk.Text(win, height=4, width=30)
  125. notes_text.grid(row=6, column=1, padx=5, pady=5)
  126. def save():
  127. if not equip_var.get() or not supp_var.get() or not wh_var.get():
  128. messagebox.showerror("Ошибка", "Заполните все обязательные поля")
  129. return
  130. try:
  131. eq_id = int(equip_var.get().split(" - ")[0])
  132. sup_id = int(supp_var.get().split(" - ")[0])
  133. wh_id = int(wh_var.get().split(" - ")[0])
  134. qty = int(qty_entry.get())
  135. price = float(price_entry.get())
  136. date = date_entry.get()
  137. datetime.strptime(date, "%Y-%m-%d")
  138. notes = notes_text.get("1.0", tk.END).strip()
  139. except Exception:
  140. messagebox.showerror("Ошибка", "Проверьте введённые данные (количество – целое, цена – число, дата в формате ГГГГ-ММ-ДД)")
  141. return
  142. try:
  143. with get_conn() as conn, conn.cursor() as cur:
  144. cur.execute("""
  145. INSERT INTO Purchase (equipment_id, supplier_id, warehouse_id, quantity, price_per_unit, purchase_date, notes)
  146. VALUES (%s, %s, %s, %s, %s, %s, %s)
  147. """, (eq_id, sup_id, wh_id, qty, price, date, notes))
  148. conn.commit()
  149. messagebox.showinfo("Успех", "Закупка добавлена")
  150. win.destroy()
  151. refresh_callback()
  152. except Exception as e:
  153. messagebox.showerror("Ошибка БД", str(e))
  154. tk.Button(win, text="Сохранить", command=save, bg="lightgreen").grid(row=7, column=0, columnspan=2, pady=15)
  155. def delete_purchase(tree, refresh_callback):
  156. selected = tree.selection()
  157. if not selected:
  158. messagebox.showwarning("Выбор", "Выберите запись для удаления")
  159. return
  160. item = tree.item(selected[0])
  161. purchase_id = item['values'][0]
  162. if messagebox.askyesno("Удаление", f"Удалить закупку ID {purchase_id}?"):
  163. try:
  164. with get_conn() as conn, conn.cursor() as cur:
  165. cur.execute("DELETE FROM Purchase WHERE id = %s", (purchase_id,))
  166. conn.commit()
  167. refresh_callback()
  168. except Exception as e:
  169. messagebox.showerror("Ошибка", str(e))
  170. def edit_purchase(tree, refresh_callback):
  171. selected = tree.selection()
  172. if not selected:
  173. messagebox.showwarning("Выбор", "Выберите закупку для редактирования")
  174. return
  175. item = tree.item(selected[0])
  176. purchase_id = item['values'][0]
  177. # Получаем текущие данные
  178. with get_conn() as conn, conn.cursor() as cur:
  179. cur.execute("SELECT equipment_id, supplier_id, warehouse_id, quantity, price_per_unit, purchase_date, notes FROM Purchase WHERE id = %s", (purchase_id,))
  180. data = cur.fetchone()
  181. if not data:
  182. return
  183. win = tk.Toplevel(tree)
  184. win.title("Редактирование закупки")
  185. win.geometry("500x400")
  186. win.grab_set()
  187. # Заполняем поля (аналогично add, но с предзаполнением)
  188. # ... (для краткости покажу принцип, полный код аналогичен add)
  189. # Для экономии места, в рабочей версии нужно реализовать, но здесь ограничение символов.
  190. # Я дам полный код в конце ответа отдельным блоком.
  191. messagebox.showinfo("Уведомление", "Функция редактирования аналогична добавлению. Полный код предоставлен в итоговом приложении.")
  192. # ========== CRUD ДЛЯ СПИСАНИЙ ==========
  193. def add_writeoff(parent, refresh_callback):
  194. win = tk.Toplevel(parent)
  195. win.title("Новое списание")
  196. win.geometry("450x350")
  197. win.grab_set()
  198. tk.Label(win, text="Оборудование:").grid(row=0, column=0, sticky="e")
  199. equip_var = tk.StringVar()
  200. equip_combo = ttk.Combobox(win, textvariable=equip_var, values=[f"{e[0]} - {e[1]}" for e in get_equipment_list()], width=40)
  201. equip_combo.grid(row=0, column=1, padx=5, pady=5)
  202. tk.Label(win, text="Сотрудник:").grid(row=1, column=0, sticky="e")
  203. emp_var = tk.StringVar()
  204. emp_combo = ttk.Combobox(win, textvariable=emp_var, values=[f"{e[0]} - {e[1]}" for e in get_employees()], width=40)
  205. emp_combo.grid(row=1, column=1, padx=5, pady=5)
  206. tk.Label(win, text="Количество:").grid(row=2, column=0, sticky="e")
  207. qty_entry = tk.Entry(win)
  208. qty_entry.grid(row=2, column=1, padx=5, pady=5)
  209. tk.Label(win, text="Причина списания:").grid(row=3, column=0, sticky="ne")
  210. reason_text = tk.Text(win, height=3, width=30)
  211. reason_text.grid(row=3, column=1, padx=5, pady=5)
  212. tk.Label(win, text="Дата списания (ГГГГ-ММ-ДД):").grid(row=4, column=0, sticky="e")
  213. date_entry = tk.Entry(win)
  214. date_entry.insert(0, datetime.now().strftime("%Y-%m-%d"))
  215. date_entry.grid(row=4, column=1, padx=5, pady=5)
  216. def save():
  217. if not equip_var.get() or not emp_var.get():
  218. messagebox.showerror("Ошибка", "Заполните оборудование и сотрудника")
  219. return
  220. try:
  221. eq_id = int(equip_var.get().split(" - ")[0])
  222. emp_id = int(emp_var.get().split(" - ")[0])
  223. qty = int(qty_entry.get())
  224. reason = reason_text.get("1.0", tk.END).strip()
  225. date = date_entry.get()
  226. datetime.strptime(date, "%Y-%m-%d")
  227. except Exception:
  228. messagebox.showerror("Ошибка", "Неверные данные")
  229. return
  230. try:
  231. with get_conn() as conn, conn.cursor() as cur:
  232. cur.execute("""
  233. INSERT INTO WriteOff (equipment_id, employee_id, quantity, reason, write_off_date)
  234. VALUES (%s, %s, %s, %s, %s)
  235. """, (eq_id, emp_id, qty, reason, date))
  236. conn.commit()
  237. messagebox.showinfo("Успех", "Списание добавлено")
  238. win.destroy()
  239. refresh_callback()
  240. except Exception as e:
  241. messagebox.showerror("Ошибка", str(e))
  242. tk.Button(win, text="Сохранить", command=save, bg="lightcoral").grid(row=5, column=0, columnspan=2, pady=15)
  243. def delete_writeoff(tree, refresh_callback):
  244. selected = tree.selection()
  245. if not selected:
  246. messagebox.showwarning("Выбор", "Выберите списание для удаления")
  247. return
  248. item = tree.item(selected[0])
  249. wo_id = item['values'][0]
  250. if messagebox.askyesno("Удаление", f"Удалить списание ID {wo_id}?"):
  251. with get_conn() as conn, conn.cursor() as cur:
  252. cur.execute("DELETE FROM WriteOff WHERE id = %s", (wo_id,))
  253. conn.commit()
  254. refresh_callback()
  255. # ========== ЭКСПОРТ ==========
  256. def export_to_csv(tree, filename_prefix):
  257. data = []
  258. for child in tree.get_children():
  259. vals = tree.item(child)['values']
  260. data.append(vals)
  261. if not data:
  262. messagebox.showwarning("Нет данных", "Нечего экспортировать")
  263. return
  264. filename = f"{filename_prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
  265. with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
  266. writer = csv.writer(f)
  267. cols = [tree.heading(c)['text'] for c in tree['columns']]
  268. writer.writerow(cols)
  269. writer.writerows(data)
  270. messagebox.showinfo("Экспорт", f"Сохранён файл {filename}")
  271. # ========== ГЛАВНОЕ ОКНО (ВКЛАДКИ) ==========
  272. class App:
  273. def __init__(self):
  274. self.root = tk.Tk()
  275. self.root.title("Учёт оборудования (партионный)")
  276. self.root.geometry("1300x650")
  277. self.root.configure(bg="#f0f0f0")
  278. style = ttk.Style()
  279. style.theme_use("clam")
  280. style.configure("TNotebook.Tab", font=("Arial", 10, "bold"), padding=[10, 5])
  281. style.configure("TButton", font=("Arial", 9), padding=4)
  282. style.configure("Treeview.Heading", font=("Arial", 10, "bold"))
  283. self.notebook = ttk.Notebook(self.root)
  284. self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
  285. # Вкладка "Остатки оборудования"
  286. self.stock_frame = ttk.Frame(self.notebook)
  287. self.notebook.add(self.stock_frame, text="📦 Остатки оборудования")
  288. self.create_stock_tab()
  289. # Вкладка "Закупки"
  290. self.purchase_frame = ttk.Frame(self.notebook)
  291. self.notebook.add(self.purchase_frame, text="📥 Закупки")
  292. self.create_purchase_tab()
  293. # Вкладка "Списания"
  294. self.writeoff_frame = ttk.Frame(self.notebook)
  295. self.notebook.add(self.writeoff_frame, text="📤 Списания")
  296. self.create_writeoff_tab()
  297. self.root.mainloop()
  298. def create_stock_tab(self):
  299. # Поиск
  300. search_frame = ttk.Frame(self.stock_frame)
  301. search_frame.pack(fill=tk.X, padx=5, pady=5)
  302. ttk.Label(search_frame, text="Поиск:").pack(side=tk.LEFT, padx=5)
  303. self.stock_search = ttk.Entry(search_frame, width=30)
  304. self.stock_search.pack(side=tk.LEFT, padx=5)
  305. ttk.Button(search_frame, text="🔍 Найти", command=lambda: search_stock(self.stock_tree, self.stock_search)).pack(side=tk.LEFT, padx=2)
  306. ttk.Button(search_frame, text="🔄 Сброс", command=lambda: refresh_stock(self.stock_tree)).pack(side=tk.LEFT, padx=2)
  307. columns = ("ID", "Наименование", "Производитель", "Категория", "Остаток (шт)")
  308. self.stock_tree = ttk.Treeview(self.stock_frame, columns=columns, show="headings")
  309. for col in columns:
  310. self.stock_tree.heading(col, text=col)
  311. self.stock_tree.column(col, width=150)
  312. self.stock_tree.pack(fill=tk.BOTH, expand=True)
  313. btn_frame = ttk.Frame(self.stock_frame)
  314. btn_frame.pack(fill=tk.X, padx=5, pady=5)
  315. ttk.Button(btn_frame, text="🔄 Обновить", command=lambda: refresh_stock(self.stock_tree)).pack(side=tk.LEFT, padx=5)
  316. ttk.Button(btn_frame, text="📎 Экспорт CSV", command=lambda: export_to_csv(self.stock_tree, "stock")).pack(side=tk.LEFT, padx=5)
  317. refresh_stock(self.stock_tree)
  318. def create_purchase_tab(self):
  319. # Панель поиска (можно по оборудованию или поставщику)
  320. search_frame = ttk.Frame(self.purchase_frame)
  321. search_frame.pack(fill=tk.X, padx=5, pady=5)
  322. ttk.Label(search_frame, text="Поиск (оборудование/поставщик):").pack(side=tk.LEFT, padx=5)
  323. self.purchase_search = ttk.Entry(search_frame, width=30)
  324. self.purchase_search.pack(side=tk.LEFT, padx=5)
  325. def search_purchases():
  326. kw = self.purchase_search.get().strip()
  327. for item in self.purchase_tree.get_children():
  328. self.purchase_tree.delete(item)
  329. if not kw:
  330. refresh_purchases(self.purchase_tree)
  331. return
  332. with get_conn() as conn, conn.cursor() as cur:
  333. cur.execute("""
  334. SELECT id, equipment, supplier, warehouse, quantity, price_per_unit, total_cost, purchase_date, notes
  335. FROM PurchaseDetails
  336. WHERE equipment ILIKE %s OR supplier ILIKE %s
  337. ORDER BY purchase_date DESC
  338. """, (f'{kw}%', f'{kw}%')) # поиск с начала
  339. for row in cur.fetchall():
  340. self.purchase_tree.insert("", tk.END, values=row)
  341. ttk.Button(search_frame, text="🔍 Найти", command=search_purchases).pack(side=tk.LEFT, padx=2)
  342. ttk.Button(search_frame, text="🔄 Сброс", command=lambda: refresh_purchases(self.purchase_tree)).pack(side=tk.LEFT, padx=2)
  343. columns = ("ID", "Оборудование", "Поставщик", "Склад", "Кол-во", "Цена за ед.", "Итого", "Дата", "Примечания")
  344. self.purchase_tree = ttk.Treeview(self.purchase_frame, columns=columns, show="headings")
  345. for col in columns:
  346. self.purchase_tree.heading(col, text=col)
  347. self.purchase_tree.column(col, width=100)
  348. self.purchase_tree.pack(fill=tk.BOTH, expand=True)
  349. btn_frame = ttk.Frame(self.purchase_frame)
  350. btn_frame.pack(fill=tk.X, padx=5, pady=5)
  351. ttk.Button(btn_frame, text="➕ Добавить закупку", command=lambda: add_purchase(self.root, lambda: refresh_purchases(self.purchase_tree))).pack(side=tk.LEFT, padx=5)
  352. ttk.Button(btn_frame, text="✏️ Редактировать", command=lambda: edit_purchase(self.purchase_tree, lambda: refresh_purchases(self.purchase_tree))).pack(side=tk.LEFT, padx=5)
  353. ttk.Button(btn_frame, text="❌ Удалить", command=lambda: delete_purchase(self.purchase_tree, lambda: refresh_purchases(self.purchase_tree))).pack(side=tk.LEFT, padx=5)
  354. ttk.Button(btn_frame, text="🔄 Обновить", command=lambda: refresh_purchases(self.purchase_tree)).pack(side=tk.LEFT, padx=5)
  355. ttk.Button(btn_frame, text="📎 Экспорт CSV", command=lambda: export_to_csv(self.purchase_tree, "purchases")).pack(side=tk.LEFT, padx=5)
  356. refresh_purchases(self.purchase_tree)
  357. def create_writeoff_tab(self):
  358. # Поиск по оборудованию/сотруднику
  359. search_frame = ttk.Frame(self.writeoff_frame)
  360. search_frame.pack(fill=tk.X, padx=5, pady=5)
  361. ttk.Label(search_frame, text="Поиск (оборудование/сотрудник):").pack(side=tk.LEFT, padx=5)
  362. self.writeoff_search = ttk.Entry(search_frame, width=30)
  363. self.writeoff_search.pack(side=tk.LEFT, padx=5)
  364. def search_writeoffs():
  365. kw = self.writeoff_search.get().strip()
  366. for item in self.writeoff_tree.get_children():
  367. self.writeoff_tree.delete(item)
  368. if not kw:
  369. refresh_writeoffs(self.writeoff_tree)
  370. return
  371. with get_conn() as conn, conn.cursor() as cur:
  372. cur.execute("""
  373. SELECT id, equipment, employee, quantity, reason, write_off_date
  374. FROM WriteOffDetails
  375. WHERE equipment ILIKE %s OR employee ILIKE %s
  376. ORDER BY write_off_date DESC
  377. """, (f'{kw}%', f'{kw}%'))
  378. for row in cur.fetchall():
  379. self.writeoff_tree.insert("", tk.END, values=row)
  380. ttk.Button(search_frame, text="🔍 Найти", command=search_writeoffs).pack(side=tk.LEFT, padx=2)
  381. ttk.Button(search_frame, text="🔄 Сброс", command=lambda: refresh_writeoffs(self.writeoff_tree)).pack(side=tk.LEFT, padx=2)
  382. columns = ("ID", "Оборудование", "Сотрудник", "Кол-во", "Причина", "Дата списания")
  383. self.writeoff_tree = ttk.Treeview(self.writeoff_frame, columns=columns, show="headings")
  384. for col in columns:
  385. self.writeoff_tree.heading(col, text=col)
  386. self.writeoff_tree.column(col, width=140)
  387. self.writeoff_tree.pack(fill=tk.BOTH, expand=True)
  388. btn_frame = ttk.Frame(self.writeoff_frame)
  389. btn_frame.pack(fill=tk.X, padx=5, pady=5)
  390. ttk.Button(btn_frame, text="➕ Добавить списание", command=lambda: add_writeoff(self.root, lambda: refresh_writeoffs(self.writeoff_tree))).pack(side=tk.LEFT, padx=5)
  391. ttk.Button(btn_frame, text="❌ Удалить", command=lambda: delete_writeoff(self.writeoff_tree, lambda: refresh_writeoffs(self.writeoff_tree))).pack(side=tk.LEFT, padx=5)
  392. ttk.Button(btn_frame, text="🔄 Обновить", command=lambda: refresh_writeoffs(self.writeoff_tree)).pack(side=tk.LEFT, padx=5)
  393. ttk.Button(btn_frame, text="📎 Экспорт CSV", command=lambda: export_to_csv(self.writeoff_tree, "writeoffs")).pack(side=tk.LEFT, padx=5)
  394. refresh_writeoffs(self.writeoff_tree)
  395. # Функция редактирования закупки (дополнение к классу, но вынесем отдельно)
  396. def edit_purchase(tree, refresh_callback):
  397. selected = tree.selection()
  398. if not selected:
  399. messagebox.showwarning("Выбор", "Выберите закупку")
  400. return
  401. item = tree.item(selected[0])
  402. purchase_id = item['values'][0]
  403. with get_conn() as conn, conn.cursor() as cur:
  404. cur.execute("SELECT equipment_id, supplier_id, warehouse_id, quantity, price_per_unit, purchase_date, notes FROM Purchase WHERE id = %s", (purchase_id,))
  405. data = cur.fetchone()
  406. if not data:
  407. return
  408. eq_id, sup_id, wh_id, qty, price, date, notes = data
  409. win = tk.Toplevel()
  410. win.title("Редактирование закупки")
  411. win.geometry("500x400")
  412. win.grab_set()
  413. # Получаем названия для предзаполнения
  414. eq_name = next((f"{e[0]} - {e[1]}" for e in get_equipment_list() if e[0] == eq_id), "")
  415. sup_name = next((f"{s[0]} - {s[1]}" for s in get_suppliers() if s[0] == sup_id), "")
  416. wh_name = next((f"{w[0]} - {w[1]}" for w in get_warehouses() if w[0] == wh_id), "")
  417. tk.Label(win, text="Оборудование:").grid(row=0, column=0, sticky="e")
  418. equip_var = tk.StringVar(value=eq_name)
  419. equip_combo = ttk.Combobox(win, textvariable=equip_var, values=[f"{e[0]} - {e[1]}" for e in get_equipment_list()], width=40)
  420. equip_combo.grid(row=0, column=1, padx=5, pady=5)
  421. tk.Label(win, text="Поставщик:").grid(row=1, column=0, sticky="e")
  422. supp_var = tk.StringVar(value=sup_name)
  423. supp_combo = ttk.Combobox(win, textvariable=supp_var, values=[f"{s[0]} - {s[1]}" for s in get_suppliers()], width=40)
  424. supp_combo.grid(row=1, column=1, padx=5, pady=5)
  425. tk.Label(win, text="Склад:").grid(row=2, column=0, sticky="e")
  426. wh_var = tk.StringVar(value=wh_name)
  427. wh_combo = ttk.Combobox(win, textvariable=wh_var, values=[f"{w[0]} - {w[1]}" for w in get_warehouses()], width=40)
  428. wh_combo.grid(row=2, column=1, padx=5, pady=5)
  429. tk.Label(win, text="Количество:").grid(row=3, column=0, sticky="e")
  430. qty_entry = tk.Entry(win)
  431. qty_entry.insert(0, str(qty))
  432. qty_entry.grid(row=3, column=1, padx=5, pady=5)
  433. tk.Label(win, text="Цена за ед.:").grid(row=4, column=0, sticky="e")
  434. price_entry = tk.Entry(win)
  435. price_entry.insert(0, str(price))
  436. price_entry.grid(row=4, column=1, padx=5, pady=5)
  437. tk.Label(win, text="Дата (ГГГГ-ММ-ДД):").grid(row=5, column=0, sticky="e")
  438. date_entry = tk.Entry(win)
  439. date_entry.insert(0, date.strftime("%Y-%m-%d") if isinstance(date, datetime) else date)
  440. date_entry.grid(row=5, column=1, padx=5, pady=5)
  441. tk.Label(win, text="Примечание:").grid(row=6, column=0, sticky="ne")
  442. notes_text = tk.Text(win, height=4, width=30)
  443. notes_text.insert("1.0", notes or "")
  444. notes_text.grid(row=6, column=1, padx=5, pady=5)
  445. def save_edit():
  446. try:
  447. eq_id_new = int(equip_var.get().split(" - ")[0])
  448. sup_id_new = int(supp_var.get().split(" - ")[0])
  449. wh_id_new = int(wh_var.get().split(" - ")[0])
  450. new_qty = int(qty_entry.get())
  451. new_price = float(price_entry.get())
  452. new_date = date_entry.get()
  453. datetime.strptime(new_date, "%Y-%m-%d")
  454. new_notes = notes_text.get("1.0", tk.END).strip()
  455. except Exception:
  456. messagebox.showerror("Ошибка", "Проверьте данные")
  457. return
  458. with get_conn() as conn, conn.cursor() as cur:
  459. cur.execute("""
  460. UPDATE Purchase
  461. SET equipment_id=%s, supplier_id=%s, warehouse_id=%s, quantity=%s,
  462. price_per_unit=%s, purchase_date=%s, notes=%s
  463. WHERE id=%s
  464. """, (eq_id_new, sup_id_new, wh_id_new, new_qty, new_price, new_date, new_notes, purchase_id))
  465. conn.commit()
  466. win.destroy()
  467. refresh_callback()
  468. messagebox.showinfo("Успех", "Закупка обновлена")
  469. tk.Button(win, text="Сохранить", command=save_edit, bg="lightblue").grid(row=7, column=0, columnspan=2, pady=15)
  470. if __name__ == "__main__":
  471. App()