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()