|
|
@@ -0,0 +1,536 @@
|
|
|
+import psycopg2
|
|
|
+import tkinter as tk
|
|
|
+from tkinter import ttk, messagebox, simpledialog
|
|
|
+from datetime import datetime
|
|
|
+import csv
|
|
|
+
|
|
|
+# ========== НАСТРОЙКИ БД ==========
|
|
|
+DB_CONFIG = {
|
|
|
+ "dbname": "dbwork",
|
|
|
+ "user": "zhelezkov_fv",
|
|
|
+ "password": "0&6&I#u#s#B",
|
|
|
+ "host": "edu-pg.itiscaf.ru",
|
|
|
+ "port": 5432
|
|
|
+}
|
|
|
+
|
|
|
+def get_conn():
|
|
|
+ return psycopg2.connect(**DB_CONFIG)
|
|
|
+
|
|
|
+# ========== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ СПРАВОЧНИКОВ ==========
|
|
|
+def get_categories():
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("SELECT id, name FROM Category ORDER BY name")
|
|
|
+ return cur.fetchall()
|
|
|
+
|
|
|
+def get_equipment_list():
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("SELECT id, name FROM Equipment ORDER BY name")
|
|
|
+ return cur.fetchall()
|
|
|
+
|
|
|
+def get_employees():
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("SELECT id, full_name FROM Employee ORDER BY full_name")
|
|
|
+ return cur.fetchall()
|
|
|
+
|
|
|
+def get_suppliers():
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("SELECT id, address FROM Supplier ORDER BY address")
|
|
|
+ return cur.fetchall()
|
|
|
+
|
|
|
+def get_warehouses():
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("SELECT id, address FROM Warehouse ORDER BY address")
|
|
|
+ return cur.fetchall()
|
|
|
+
|
|
|
+# ========== ОБНОВЛЕНИЕ ТАБЛИЦ ==========
|
|
|
+def refresh_stock(tree):
|
|
|
+ for row in tree.get_children():
|
|
|
+ tree.delete(row)
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("""
|
|
|
+ SELECT id, name, manufacturer, category, current_quantity
|
|
|
+ FROM EquipmentStock
|
|
|
+ ORDER BY name
|
|
|
+ """)
|
|
|
+ for row in cur.fetchall():
|
|
|
+ tree.insert("", tk.END, values=row)
|
|
|
+
|
|
|
+def refresh_purchases(tree):
|
|
|
+ for row in tree.get_children():
|
|
|
+ tree.delete(row)
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("""
|
|
|
+ SELECT id, equipment, supplier, warehouse, quantity,
|
|
|
+ price_per_unit, total_cost, purchase_date, notes
|
|
|
+ FROM PurchaseDetails
|
|
|
+ ORDER BY purchase_date DESC
|
|
|
+ """)
|
|
|
+ for row in cur.fetchall():
|
|
|
+ tree.insert("", tk.END, values=row)
|
|
|
+
|
|
|
+def refresh_writeoffs(tree):
|
|
|
+ for row in tree.get_children():
|
|
|
+ tree.delete(row)
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("""
|
|
|
+ SELECT id, equipment, employee, quantity, reason, write_off_date
|
|
|
+ FROM WriteOffDetails
|
|
|
+ ORDER BY write_off_date DESC
|
|
|
+ """)
|
|
|
+ for row in cur.fetchall():
|
|
|
+ tree.insert("", tk.END, values=row)
|
|
|
+
|
|
|
+# ========== ПОИСК ==========
|
|
|
+def search_stock(tree, search_entry):
|
|
|
+ keyword = search_entry.get().strip()
|
|
|
+ for row in tree.get_children():
|
|
|
+ tree.delete(row)
|
|
|
+ if not keyword:
|
|
|
+ refresh_stock(tree)
|
|
|
+ return
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("""
|
|
|
+ SELECT e.id, e.name, e.manufacturer, c.name AS category,
|
|
|
+ COALESCE(SUM(p.quantity), 0) - COALESCE(SUM(w.quantity), 0) AS current_quantity
|
|
|
+ FROM Equipment e
|
|
|
+ LEFT JOIN Category c ON e.category_id = c.id
|
|
|
+ LEFT JOIN Purchase p ON e.id = p.equipment_id
|
|
|
+ LEFT JOIN WriteOff w ON e.id = w.equipment_id
|
|
|
+ WHERE e.name ILIKE %s OR e.manufacturer ILIKE %s
|
|
|
+ GROUP BY e.id, c.name
|
|
|
+ ORDER BY e.name
|
|
|
+ """, (f'{keyword}%', f'{keyword}%')) # изменено: только % в конце
|
|
|
+ for row in cur.fetchall():
|
|
|
+ tree.insert("", tk.END, values=row)
|
|
|
+
|
|
|
+# ========== CRUD ДЛЯ ЗАКУПОК ==========
|
|
|
+def add_purchase(parent, refresh_callback):
|
|
|
+ win = tk.Toplevel(parent)
|
|
|
+ win.title("Новая закупка")
|
|
|
+ win.geometry("500x400")
|
|
|
+ win.grab_set()
|
|
|
+
|
|
|
+ # Поля
|
|
|
+ tk.Label(win, text="Оборудование:").grid(row=0, column=0, sticky="e", padx=5, pady=5)
|
|
|
+ equip_var = tk.StringVar()
|
|
|
+ equip_combo = ttk.Combobox(win, textvariable=equip_var, values=[f"{e[0]} - {e[1]}" for e in get_equipment_list()], width=40)
|
|
|
+ equip_combo.grid(row=0, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Поставщик:").grid(row=1, column=0, sticky="e")
|
|
|
+ supp_var = tk.StringVar()
|
|
|
+ supp_combo = ttk.Combobox(win, textvariable=supp_var, values=[f"{s[0]} - {s[1]}" for s in get_suppliers()], width=40)
|
|
|
+ supp_combo.grid(row=1, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Склад:").grid(row=2, column=0, sticky="e")
|
|
|
+ wh_var = tk.StringVar()
|
|
|
+ wh_combo = ttk.Combobox(win, textvariable=wh_var, values=[f"{w[0]} - {w[1]}" for w in get_warehouses()], width=40)
|
|
|
+ wh_combo.grid(row=2, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Количество:").grid(row=3, column=0, sticky="e")
|
|
|
+ qty_entry = tk.Entry(win)
|
|
|
+ qty_entry.grid(row=3, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Цена за ед. (руб):").grid(row=4, column=0, sticky="e")
|
|
|
+ price_entry = tk.Entry(win)
|
|
|
+ price_entry.grid(row=4, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Дата закупки (ГГГГ-ММ-ДД):").grid(row=5, column=0, sticky="e")
|
|
|
+ date_entry = tk.Entry(win)
|
|
|
+ date_entry.insert(0, datetime.now().strftime("%Y-%m-%d"))
|
|
|
+ date_entry.grid(row=5, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Примечание:").grid(row=6, column=0, sticky="ne")
|
|
|
+ notes_text = tk.Text(win, height=4, width=30)
|
|
|
+ notes_text.grid(row=6, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ def save():
|
|
|
+ if not equip_var.get() or not supp_var.get() or not wh_var.get():
|
|
|
+ messagebox.showerror("Ошибка", "Заполните все обязательные поля")
|
|
|
+ return
|
|
|
+ try:
|
|
|
+ eq_id = int(equip_var.get().split(" - ")[0])
|
|
|
+ sup_id = int(supp_var.get().split(" - ")[0])
|
|
|
+ wh_id = int(wh_var.get().split(" - ")[0])
|
|
|
+ qty = int(qty_entry.get())
|
|
|
+ price = float(price_entry.get())
|
|
|
+ date = date_entry.get()
|
|
|
+ datetime.strptime(date, "%Y-%m-%d")
|
|
|
+ notes = notes_text.get("1.0", tk.END).strip()
|
|
|
+ except Exception:
|
|
|
+ messagebox.showerror("Ошибка", "Проверьте введённые данные (количество – целое, цена – число, дата в формате ГГГГ-ММ-ДД)")
|
|
|
+ return
|
|
|
+ try:
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("""
|
|
|
+ INSERT INTO Purchase (equipment_id, supplier_id, warehouse_id, quantity, price_per_unit, purchase_date, notes)
|
|
|
+ VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
|
+ """, (eq_id, sup_id, wh_id, qty, price, date, notes))
|
|
|
+ conn.commit()
|
|
|
+ messagebox.showinfo("Успех", "Закупка добавлена")
|
|
|
+ win.destroy()
|
|
|
+ refresh_callback()
|
|
|
+ except Exception as e:
|
|
|
+ messagebox.showerror("Ошибка БД", str(e))
|
|
|
+
|
|
|
+ tk.Button(win, text="Сохранить", command=save, bg="lightgreen").grid(row=7, column=0, columnspan=2, pady=15)
|
|
|
+
|
|
|
+def delete_purchase(tree, refresh_callback):
|
|
|
+ selected = tree.selection()
|
|
|
+ if not selected:
|
|
|
+ messagebox.showwarning("Выбор", "Выберите запись для удаления")
|
|
|
+ return
|
|
|
+ item = tree.item(selected[0])
|
|
|
+ purchase_id = item['values'][0]
|
|
|
+ if messagebox.askyesno("Удаление", f"Удалить закупку ID {purchase_id}?"):
|
|
|
+ try:
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("DELETE FROM Purchase WHERE id = %s", (purchase_id,))
|
|
|
+ conn.commit()
|
|
|
+ refresh_callback()
|
|
|
+ except Exception as e:
|
|
|
+ messagebox.showerror("Ошибка", str(e))
|
|
|
+
|
|
|
+def edit_purchase(tree, refresh_callback):
|
|
|
+ selected = tree.selection()
|
|
|
+ if not selected:
|
|
|
+ messagebox.showwarning("Выбор", "Выберите закупку для редактирования")
|
|
|
+ return
|
|
|
+ item = tree.item(selected[0])
|
|
|
+ purchase_id = item['values'][0]
|
|
|
+ # Получаем текущие данные
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("SELECT equipment_id, supplier_id, warehouse_id, quantity, price_per_unit, purchase_date, notes FROM Purchase WHERE id = %s", (purchase_id,))
|
|
|
+ data = cur.fetchone()
|
|
|
+ if not data:
|
|
|
+ return
|
|
|
+ win = tk.Toplevel(tree)
|
|
|
+ win.title("Редактирование закупки")
|
|
|
+ win.geometry("500x400")
|
|
|
+ win.grab_set()
|
|
|
+
|
|
|
+ # Заполняем поля (аналогично add, но с предзаполнением)
|
|
|
+ # ... (для краткости покажу принцип, полный код аналогичен add)
|
|
|
+ # Для экономии места, в рабочей версии нужно реализовать, но здесь ограничение символов.
|
|
|
+ # Я дам полный код в конце ответа отдельным блоком.
|
|
|
+ messagebox.showinfo("Уведомление", "Функция редактирования аналогична добавлению. Полный код предоставлен в итоговом приложении.")
|
|
|
+
|
|
|
+# ========== CRUD ДЛЯ СПИСАНИЙ ==========
|
|
|
+def add_writeoff(parent, refresh_callback):
|
|
|
+ win = tk.Toplevel(parent)
|
|
|
+ win.title("Новое списание")
|
|
|
+ win.geometry("450x350")
|
|
|
+ win.grab_set()
|
|
|
+
|
|
|
+ tk.Label(win, text="Оборудование:").grid(row=0, column=0, sticky="e")
|
|
|
+ equip_var = tk.StringVar()
|
|
|
+ equip_combo = ttk.Combobox(win, textvariable=equip_var, values=[f"{e[0]} - {e[1]}" for e in get_equipment_list()], width=40)
|
|
|
+ equip_combo.grid(row=0, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Сотрудник:").grid(row=1, column=0, sticky="e")
|
|
|
+ emp_var = tk.StringVar()
|
|
|
+ emp_combo = ttk.Combobox(win, textvariable=emp_var, values=[f"{e[0]} - {e[1]}" for e in get_employees()], width=40)
|
|
|
+ emp_combo.grid(row=1, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Количество:").grid(row=2, column=0, sticky="e")
|
|
|
+ qty_entry = tk.Entry(win)
|
|
|
+ qty_entry.grid(row=2, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Причина списания:").grid(row=3, column=0, sticky="ne")
|
|
|
+ reason_text = tk.Text(win, height=3, width=30)
|
|
|
+ reason_text.grid(row=3, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Дата списания (ГГГГ-ММ-ДД):").grid(row=4, column=0, sticky="e")
|
|
|
+ date_entry = tk.Entry(win)
|
|
|
+ date_entry.insert(0, datetime.now().strftime("%Y-%m-%d"))
|
|
|
+ date_entry.grid(row=4, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ def save():
|
|
|
+ if not equip_var.get() or not emp_var.get():
|
|
|
+ messagebox.showerror("Ошибка", "Заполните оборудование и сотрудника")
|
|
|
+ return
|
|
|
+ try:
|
|
|
+ eq_id = int(equip_var.get().split(" - ")[0])
|
|
|
+ emp_id = int(emp_var.get().split(" - ")[0])
|
|
|
+ qty = int(qty_entry.get())
|
|
|
+ reason = reason_text.get("1.0", tk.END).strip()
|
|
|
+ date = date_entry.get()
|
|
|
+ datetime.strptime(date, "%Y-%m-%d")
|
|
|
+ except Exception:
|
|
|
+ messagebox.showerror("Ошибка", "Неверные данные")
|
|
|
+ return
|
|
|
+ try:
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("""
|
|
|
+ INSERT INTO WriteOff (equipment_id, employee_id, quantity, reason, write_off_date)
|
|
|
+ VALUES (%s, %s, %s, %s, %s)
|
|
|
+ """, (eq_id, emp_id, qty, reason, date))
|
|
|
+ conn.commit()
|
|
|
+ messagebox.showinfo("Успех", "Списание добавлено")
|
|
|
+ win.destroy()
|
|
|
+ refresh_callback()
|
|
|
+ except Exception as e:
|
|
|
+ messagebox.showerror("Ошибка", str(e))
|
|
|
+
|
|
|
+ tk.Button(win, text="Сохранить", command=save, bg="lightcoral").grid(row=5, column=0, columnspan=2, pady=15)
|
|
|
+
|
|
|
+def delete_writeoff(tree, refresh_callback):
|
|
|
+ selected = tree.selection()
|
|
|
+ if not selected:
|
|
|
+ messagebox.showwarning("Выбор", "Выберите списание для удаления")
|
|
|
+ return
|
|
|
+ item = tree.item(selected[0])
|
|
|
+ wo_id = item['values'][0]
|
|
|
+ if messagebox.askyesno("Удаление", f"Удалить списание ID {wo_id}?"):
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("DELETE FROM WriteOff WHERE id = %s", (wo_id,))
|
|
|
+ conn.commit()
|
|
|
+ refresh_callback()
|
|
|
+
|
|
|
+# ========== ЭКСПОРТ ==========
|
|
|
+def export_to_csv(tree, filename_prefix):
|
|
|
+ data = []
|
|
|
+ for child in tree.get_children():
|
|
|
+ vals = tree.item(child)['values']
|
|
|
+ data.append(vals)
|
|
|
+ if not data:
|
|
|
+ messagebox.showwarning("Нет данных", "Нечего экспортировать")
|
|
|
+ return
|
|
|
+ filename = f"{filename_prefix}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
|
|
+ with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
|
|
|
+ writer = csv.writer(f)
|
|
|
+ cols = [tree.heading(c)['text'] for c in tree['columns']]
|
|
|
+ writer.writerow(cols)
|
|
|
+ writer.writerows(data)
|
|
|
+ messagebox.showinfo("Экспорт", f"Сохранён файл {filename}")
|
|
|
+
|
|
|
+# ========== ГЛАВНОЕ ОКНО (ВКЛАДКИ) ==========
|
|
|
+class App:
|
|
|
+ def __init__(self):
|
|
|
+ self.root = tk.Tk()
|
|
|
+ self.root.title("Учёт оборудования (партионный)")
|
|
|
+ self.root.geometry("1300x650")
|
|
|
+ self.root.configure(bg="#f0f0f0")
|
|
|
+
|
|
|
+ style = ttk.Style()
|
|
|
+ style.theme_use("clam")
|
|
|
+ style.configure("TNotebook.Tab", font=("Arial", 10, "bold"), padding=[10, 5])
|
|
|
+ style.configure("TButton", font=("Arial", 9), padding=4)
|
|
|
+ style.configure("Treeview.Heading", font=("Arial", 10, "bold"))
|
|
|
+
|
|
|
+ self.notebook = ttk.Notebook(self.root)
|
|
|
+ self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
+
|
|
|
+ # Вкладка "Остатки оборудования"
|
|
|
+ self.stock_frame = ttk.Frame(self.notebook)
|
|
|
+ self.notebook.add(self.stock_frame, text="📦 Остатки оборудования")
|
|
|
+ self.create_stock_tab()
|
|
|
+
|
|
|
+ # Вкладка "Закупки"
|
|
|
+ self.purchase_frame = ttk.Frame(self.notebook)
|
|
|
+ self.notebook.add(self.purchase_frame, text="📥 Закупки")
|
|
|
+ self.create_purchase_tab()
|
|
|
+
|
|
|
+ # Вкладка "Списания"
|
|
|
+ self.writeoff_frame = ttk.Frame(self.notebook)
|
|
|
+ self.notebook.add(self.writeoff_frame, text="📤 Списания")
|
|
|
+ self.create_writeoff_tab()
|
|
|
+
|
|
|
+ self.root.mainloop()
|
|
|
+
|
|
|
+ def create_stock_tab(self):
|
|
|
+ # Поиск
|
|
|
+ search_frame = ttk.Frame(self.stock_frame)
|
|
|
+ search_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
|
+ ttk.Label(search_frame, text="Поиск:").pack(side=tk.LEFT, padx=5)
|
|
|
+ self.stock_search = ttk.Entry(search_frame, width=30)
|
|
|
+ self.stock_search.pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(search_frame, text="🔍 Найти", command=lambda: search_stock(self.stock_tree, self.stock_search)).pack(side=tk.LEFT, padx=2)
|
|
|
+ ttk.Button(search_frame, text="🔄 Сброс", command=lambda: refresh_stock(self.stock_tree)).pack(side=tk.LEFT, padx=2)
|
|
|
+
|
|
|
+ columns = ("ID", "Наименование", "Производитель", "Категория", "Остаток (шт)")
|
|
|
+ self.stock_tree = ttk.Treeview(self.stock_frame, columns=columns, show="headings")
|
|
|
+ for col in columns:
|
|
|
+ self.stock_tree.heading(col, text=col)
|
|
|
+ self.stock_tree.column(col, width=150)
|
|
|
+ self.stock_tree.pack(fill=tk.BOTH, expand=True)
|
|
|
+
|
|
|
+ btn_frame = ttk.Frame(self.stock_frame)
|
|
|
+ btn_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
|
+ ttk.Button(btn_frame, text="🔄 Обновить", command=lambda: refresh_stock(self.stock_tree)).pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(btn_frame, text="📎 Экспорт CSV", command=lambda: export_to_csv(self.stock_tree, "stock")).pack(side=tk.LEFT, padx=5)
|
|
|
+
|
|
|
+ refresh_stock(self.stock_tree)
|
|
|
+
|
|
|
+ def create_purchase_tab(self):
|
|
|
+ # Панель поиска (можно по оборудованию или поставщику)
|
|
|
+ search_frame = ttk.Frame(self.purchase_frame)
|
|
|
+ search_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
|
+ ttk.Label(search_frame, text="Поиск (оборудование/поставщик):").pack(side=tk.LEFT, padx=5)
|
|
|
+ self.purchase_search = ttk.Entry(search_frame, width=30)
|
|
|
+ self.purchase_search.pack(side=tk.LEFT, padx=5)
|
|
|
+ def search_purchases():
|
|
|
+ kw = self.purchase_search.get().strip()
|
|
|
+ for item in self.purchase_tree.get_children():
|
|
|
+ self.purchase_tree.delete(item)
|
|
|
+ if not kw:
|
|
|
+ refresh_purchases(self.purchase_tree)
|
|
|
+ return
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("""
|
|
|
+ SELECT id, equipment, supplier, warehouse, quantity, price_per_unit, total_cost, purchase_date, notes
|
|
|
+ FROM PurchaseDetails
|
|
|
+ WHERE equipment ILIKE %s OR supplier ILIKE %s
|
|
|
+ ORDER BY purchase_date DESC
|
|
|
+ """, (f'{kw}%', f'{kw}%')) # поиск с начала
|
|
|
+ for row in cur.fetchall():
|
|
|
+ self.purchase_tree.insert("", tk.END, values=row)
|
|
|
+ ttk.Button(search_frame, text="🔍 Найти", command=search_purchases).pack(side=tk.LEFT, padx=2)
|
|
|
+ ttk.Button(search_frame, text="🔄 Сброс", command=lambda: refresh_purchases(self.purchase_tree)).pack(side=tk.LEFT, padx=2)
|
|
|
+
|
|
|
+ columns = ("ID", "Оборудование", "Поставщик", "Склад", "Кол-во", "Цена за ед.", "Итого", "Дата", "Примечания")
|
|
|
+ self.purchase_tree = ttk.Treeview(self.purchase_frame, columns=columns, show="headings")
|
|
|
+ for col in columns:
|
|
|
+ self.purchase_tree.heading(col, text=col)
|
|
|
+ self.purchase_tree.column(col, width=100)
|
|
|
+ self.purchase_tree.pack(fill=tk.BOTH, expand=True)
|
|
|
+
|
|
|
+ btn_frame = ttk.Frame(self.purchase_frame)
|
|
|
+ btn_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
|
+ ttk.Button(btn_frame, text="➕ Добавить закупку", command=lambda: add_purchase(self.root, lambda: refresh_purchases(self.purchase_tree))).pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(btn_frame, text="✏️ Редактировать", command=lambda: edit_purchase(self.purchase_tree, lambda: refresh_purchases(self.purchase_tree))).pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(btn_frame, text="❌ Удалить", command=lambda: delete_purchase(self.purchase_tree, lambda: refresh_purchases(self.purchase_tree))).pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(btn_frame, text="🔄 Обновить", command=lambda: refresh_purchases(self.purchase_tree)).pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(btn_frame, text="📎 Экспорт CSV", command=lambda: export_to_csv(self.purchase_tree, "purchases")).pack(side=tk.LEFT, padx=5)
|
|
|
+
|
|
|
+ refresh_purchases(self.purchase_tree)
|
|
|
+
|
|
|
+ def create_writeoff_tab(self):
|
|
|
+ # Поиск по оборудованию/сотруднику
|
|
|
+ search_frame = ttk.Frame(self.writeoff_frame)
|
|
|
+ search_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
|
+ ttk.Label(search_frame, text="Поиск (оборудование/сотрудник):").pack(side=tk.LEFT, padx=5)
|
|
|
+ self.writeoff_search = ttk.Entry(search_frame, width=30)
|
|
|
+ self.writeoff_search.pack(side=tk.LEFT, padx=5)
|
|
|
+ def search_writeoffs():
|
|
|
+ kw = self.writeoff_search.get().strip()
|
|
|
+ for item in self.writeoff_tree.get_children():
|
|
|
+ self.writeoff_tree.delete(item)
|
|
|
+ if not kw:
|
|
|
+ refresh_writeoffs(self.writeoff_tree)
|
|
|
+ return
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("""
|
|
|
+ SELECT id, equipment, employee, quantity, reason, write_off_date
|
|
|
+ FROM WriteOffDetails
|
|
|
+ WHERE equipment ILIKE %s OR employee ILIKE %s
|
|
|
+ ORDER BY write_off_date DESC
|
|
|
+ """, (f'{kw}%', f'{kw}%'))
|
|
|
+ for row in cur.fetchall():
|
|
|
+ self.writeoff_tree.insert("", tk.END, values=row)
|
|
|
+ ttk.Button(search_frame, text="🔍 Найти", command=search_writeoffs).pack(side=tk.LEFT, padx=2)
|
|
|
+ ttk.Button(search_frame, text="🔄 Сброс", command=lambda: refresh_writeoffs(self.writeoff_tree)).pack(side=tk.LEFT, padx=2)
|
|
|
+
|
|
|
+ columns = ("ID", "Оборудование", "Сотрудник", "Кол-во", "Причина", "Дата списания")
|
|
|
+ self.writeoff_tree = ttk.Treeview(self.writeoff_frame, columns=columns, show="headings")
|
|
|
+ for col in columns:
|
|
|
+ self.writeoff_tree.heading(col, text=col)
|
|
|
+ self.writeoff_tree.column(col, width=140)
|
|
|
+ self.writeoff_tree.pack(fill=tk.BOTH, expand=True)
|
|
|
+
|
|
|
+ btn_frame = ttk.Frame(self.writeoff_frame)
|
|
|
+ btn_frame.pack(fill=tk.X, padx=5, pady=5)
|
|
|
+ ttk.Button(btn_frame, text="➕ Добавить списание", command=lambda: add_writeoff(self.root, lambda: refresh_writeoffs(self.writeoff_tree))).pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(btn_frame, text="❌ Удалить", command=lambda: delete_writeoff(self.writeoff_tree, lambda: refresh_writeoffs(self.writeoff_tree))).pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(btn_frame, text="🔄 Обновить", command=lambda: refresh_writeoffs(self.writeoff_tree)).pack(side=tk.LEFT, padx=5)
|
|
|
+ ttk.Button(btn_frame, text="📎 Экспорт CSV", command=lambda: export_to_csv(self.writeoff_tree, "writeoffs")).pack(side=tk.LEFT, padx=5)
|
|
|
+
|
|
|
+ refresh_writeoffs(self.writeoff_tree)
|
|
|
+
|
|
|
+# Функция редактирования закупки (дополнение к классу, но вынесем отдельно)
|
|
|
+def edit_purchase(tree, refresh_callback):
|
|
|
+ selected = tree.selection()
|
|
|
+ if not selected:
|
|
|
+ messagebox.showwarning("Выбор", "Выберите закупку")
|
|
|
+ return
|
|
|
+ item = tree.item(selected[0])
|
|
|
+ purchase_id = item['values'][0]
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("SELECT equipment_id, supplier_id, warehouse_id, quantity, price_per_unit, purchase_date, notes FROM Purchase WHERE id = %s", (purchase_id,))
|
|
|
+ data = cur.fetchone()
|
|
|
+ if not data:
|
|
|
+ return
|
|
|
+ eq_id, sup_id, wh_id, qty, price, date, notes = data
|
|
|
+ win = tk.Toplevel()
|
|
|
+ win.title("Редактирование закупки")
|
|
|
+ win.geometry("500x400")
|
|
|
+ win.grab_set()
|
|
|
+
|
|
|
+ # Получаем названия для предзаполнения
|
|
|
+ eq_name = next((f"{e[0]} - {e[1]}" for e in get_equipment_list() if e[0] == eq_id), "")
|
|
|
+ sup_name = next((f"{s[0]} - {s[1]}" for s in get_suppliers() if s[0] == sup_id), "")
|
|
|
+ wh_name = next((f"{w[0]} - {w[1]}" for w in get_warehouses() if w[0] == wh_id), "")
|
|
|
+
|
|
|
+ tk.Label(win, text="Оборудование:").grid(row=0, column=0, sticky="e")
|
|
|
+ equip_var = tk.StringVar(value=eq_name)
|
|
|
+ equip_combo = ttk.Combobox(win, textvariable=equip_var, values=[f"{e[0]} - {e[1]}" for e in get_equipment_list()], width=40)
|
|
|
+ equip_combo.grid(row=0, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Поставщик:").grid(row=1, column=0, sticky="e")
|
|
|
+ supp_var = tk.StringVar(value=sup_name)
|
|
|
+ supp_combo = ttk.Combobox(win, textvariable=supp_var, values=[f"{s[0]} - {s[1]}" for s in get_suppliers()], width=40)
|
|
|
+ supp_combo.grid(row=1, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Склад:").grid(row=2, column=0, sticky="e")
|
|
|
+ wh_var = tk.StringVar(value=wh_name)
|
|
|
+ wh_combo = ttk.Combobox(win, textvariable=wh_var, values=[f"{w[0]} - {w[1]}" for w in get_warehouses()], width=40)
|
|
|
+ wh_combo.grid(row=2, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Количество:").grid(row=3, column=0, sticky="e")
|
|
|
+ qty_entry = tk.Entry(win)
|
|
|
+ qty_entry.insert(0, str(qty))
|
|
|
+ qty_entry.grid(row=3, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Цена за ед.:").grid(row=4, column=0, sticky="e")
|
|
|
+ price_entry = tk.Entry(win)
|
|
|
+ price_entry.insert(0, str(price))
|
|
|
+ price_entry.grid(row=4, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Дата (ГГГГ-ММ-ДД):").grid(row=5, column=0, sticky="e")
|
|
|
+ date_entry = tk.Entry(win)
|
|
|
+ date_entry.insert(0, date.strftime("%Y-%m-%d") if isinstance(date, datetime) else date)
|
|
|
+ date_entry.grid(row=5, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ tk.Label(win, text="Примечание:").grid(row=6, column=0, sticky="ne")
|
|
|
+ notes_text = tk.Text(win, height=4, width=30)
|
|
|
+ notes_text.insert("1.0", notes or "")
|
|
|
+ notes_text.grid(row=6, column=1, padx=5, pady=5)
|
|
|
+
|
|
|
+ def save_edit():
|
|
|
+ try:
|
|
|
+ eq_id_new = int(equip_var.get().split(" - ")[0])
|
|
|
+ sup_id_new = int(supp_var.get().split(" - ")[0])
|
|
|
+ wh_id_new = int(wh_var.get().split(" - ")[0])
|
|
|
+ new_qty = int(qty_entry.get())
|
|
|
+ new_price = float(price_entry.get())
|
|
|
+ new_date = date_entry.get()
|
|
|
+ datetime.strptime(new_date, "%Y-%m-%d")
|
|
|
+ new_notes = notes_text.get("1.0", tk.END).strip()
|
|
|
+ except Exception:
|
|
|
+ messagebox.showerror("Ошибка", "Проверьте данные")
|
|
|
+ return
|
|
|
+ with get_conn() as conn, conn.cursor() as cur:
|
|
|
+ cur.execute("""
|
|
|
+ UPDATE Purchase
|
|
|
+ SET equipment_id=%s, supplier_id=%s, warehouse_id=%s, quantity=%s,
|
|
|
+ price_per_unit=%s, purchase_date=%s, notes=%s
|
|
|
+ WHERE id=%s
|
|
|
+ """, (eq_id_new, sup_id_new, wh_id_new, new_qty, new_price, new_date, new_notes, purchase_id))
|
|
|
+ conn.commit()
|
|
|
+ win.destroy()
|
|
|
+ refresh_callback()
|
|
|
+ messagebox.showinfo("Успех", "Закупка обновлена")
|
|
|
+
|
|
|
+ tk.Button(win, text="Сохранить", command=save_edit, bg="lightblue").grid(row=7, column=0, columnspan=2, pady=15)
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ App()
|