commit fdfd79346d3bbe651ff68a803814ea1b7cac5059 Author: Satur@it-depot.ru Date: Fri Jul 3 11:50:56 2026 +0300 Initial commit: рабочая версия Main_v3 (add + фильтр дублей) Co-Authored-By: Claude Opus 4.8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..147c64c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +build/ +dist/ +*.spec diff --git a/Main_v3.py b/Main_v3.py new file mode 100644 index 0000000..c3b24eb --- /dev/null +++ b/Main_v3.py @@ -0,0 +1,357 @@ +import os +import tkinter as tk +from tkinter import messagebox +import pyodbc +import uuid +import sys + +_BANNER = (""" + + + ....::::::::: + ::::::::::-----:::::: + ::::------------=--------::.. + :::--=--============---==+===-:::. + ::---========++=++++======+++++==--:.: + :--==++++++++++++++++++========---==-::. + --==++++++++++++++++++++=====---:::-==--:: + :-==+**++++++++++++++++++====------::-==-:: + -==+**++++++++++++++++++++=====--------=-:: + -=++*++++++++++++++++++++++=========-----:: + -=+++++++++++++++++++++++++===========----: + :=++++++++++++++++++++++++++==========---:: + -=++++++++++++++++++===+++++===-----====-- + -=++++++++++++++++++++==+++++=====-----==- + -++++++++++++++++++++++++++++++++====----- + ==+++++++*#####*******++++++++++++++++=--= + +===++++++****##%%%%##*****++++**###%##*+==- + +*++=++++****#%##%@%%%%###***++*#%%%%%###*===- + +*++=++++**####*#%@%#%%%##**++*#%%%@@%+*%%*+=- + *++=+++++**+****####%%##*++==+#%%#%%#+++*+==- + +*+++++++++++****###****++=====*####*++=====- + +*=+++++++++++++++++++++++=======+**++=----- + +*=+++++++++++++++++***++++==-=====+++==---- + ++=+++++++++++++++*****+++==---===========-- + +=+++++++++++++*****+++++===---++======--=- + ==++++++++++**********+++++==--+++++=====- + -==+++++++++*********##*******==++++++===- + -==++++++++*****#**##*####**+====++++++== + #%%%%%===+++++++++***##*****####**++++++==+++=- + *+*#%%%#*+====++++++++**#%%%##****##********+====- + ++++++*##%@%#+++**====+++++++*##############***###*+====- + ++******###%%@@@@@%####*+=+==+++++**************+++++=+*++===- +#***+**#*###%@@@@@@@@@@@@@%###*+=+==++++*****++*********++==++++=-=+=+=- +#*#%%%###%%@@@@@@@@@@@@@@@@%###*++++++++**+************+++========*****++==- +%%%######%%####%@@@@@@@@@@@@@@%%**+++++++++++++++***+++=========*###******+++++== +%%@@@@%%%%%%%%%#%%@@@@@@@@@@@@@@%#**+*+**++++++++++++++++=====+%###########***++++* +%@@@@@@@@@%%@@%%%%%%%@@@@@@@@@@@@@@###*****++++++*+++++++===+**#%####%###%####***+*** +%%%@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@%####**++++***++++*#####*%##%####%#%%%%##******* +%%%%@%@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@%%%%%%##%%%@%%####*%%#%####%#%%%%%######### +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%##%#*#%#%####%##%%%%%%###%#%## +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%#%%###%######%##%#%%%%%%%%%%%## +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%####%%#####%##%%%%%%%%%%%%%### +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%####%%#%%##%#%%%%%%%%%%@%@%### +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%#####%%%#%%%%#%%%%%%%@@@@@@@%%## +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%#%%%%##%%#%%%%%%%%%%%%@@@@@@@%%#** +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%@@@%####%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@%%%#** +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%@@@@%%%%%%%#%%%%%%%##%%%%%%%%%%%%%%%%%@@@@@@%%%%##** +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%@@@@%%@@@@@@@%%%%%%##%%%%%%%###%%%%%%%%%%%%%%%%%@@%%%%%%%%##*+ +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%@@@@@@@@%%%%@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%###%%*# +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%@@@@@@%@@@%@@@@@@@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%% +""") + +# В оконном режиме (сборка без консоли) sys.stdout может быть None — не роняем старт +if getattr(sys, "stdout", None): + try: + print(_BANNER) + except Exception: + pass + + +class RemoteFolderSelector(tk.Tk): + def __init__(self): + super().__init__() + self.title("Remote Folder and SQL Database Selector") + self.geometry("620x560") + + # Создадим фреймы для лучшей компоновки элементов + self.frame_left = tk.Frame(self) + self.frame_left.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") + + self.frame_right = tk.Frame(self) + self.frame_right.grid(row=0, column=1, padx=10, pady=10, sticky="nsew") + + # Сделаем окна масштабируемыми + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(0, weight=1) + + # Текущий режим подключения (локально либо по IP) + self.ip_address = None + + # Поле ввода IP-адреса + self.ip_label = tk.Label(self.frame_left, text="Enter IP (optional):") + self.ip_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") + + self.ip_entry = tk.Entry(self.frame_left, width=30) + self.ip_entry.grid(row=1, column=0, padx=5, pady=5, sticky="w") + + self.connect_button = tk.Button(self.frame_left, text="Load Users", command=self.load_users) + self.connect_button.grid(row=2, column=0, padx=5, pady=5) + + # Панель "отметить все / снять все" для пользователей + self.user_toolbar = tk.Frame(self.frame_left) + self.user_toolbar.grid(row=3, column=0, padx=5, pady=(5, 0), sticky="w") + tk.Button(self.user_toolbar, text="Отметить все", + command=lambda: self._set_all(self.user_vars, True)).pack(side="left", padx=(0, 4)) + tk.Button(self.user_toolbar, text="Снять все", + command=lambda: self._set_all(self.user_vars, False)).pack(side="left") + + # Список пользователей с чекбоксами + self.user_vars = {} + self.user_container, self.user_inner = self._create_scrollable_checklist(self.frame_left) + self.user_container.grid(row=4, column=0, padx=5, pady=5, sticky="nsew") + self.frame_left.grid_rowconfigure(4, weight=1) + self.frame_left.grid_columnconfigure(0, weight=1) + + # Поля для ввода данных SQL + self.server_label = tk.Label(self.frame_right, text="SQL Server:") + self.server_label.grid(row=0, column=0, padx=5, pady=5, sticky="w") + + self.server_entry = tk.Entry(self.frame_right, width=30) + self.server_entry.grid(row=1, column=0, padx=5, pady=5, sticky="w") + self.server_entry.insert(0, "SERVERSQL") + + self.login_label = tk.Label(self.frame_right, text="SQL Login:") + self.login_label.grid(row=2, column=0, padx=5, pady=5, sticky="w") + + self.login_entry = tk.Entry(self.frame_right, width=30) + self.login_entry.grid(row=3, column=0, padx=5, pady=5, sticky="w") + self.login_entry.insert(0, "sa") + + self.password_label = tk.Label(self.frame_right, text="SQL Password:") + self.password_label.grid(row=4, column=0, padx=5, pady=5, sticky="w") + + self.password_entry = tk.Entry(self.frame_right, show="*", width=30) + self.password_entry.grid(row=5, column=0, padx=5, pady=5, sticky="w") + + self.sql_button = tk.Button(self.frame_right, text="Load Databases", command=self.load_databases) + self.sql_button.grid(row=6, column=0, padx=5, pady=5) + + # Панель "отметить все / снять все" для баз + self.db_toolbar = tk.Frame(self.frame_right) + self.db_toolbar.grid(row=7, column=0, padx=5, pady=(5, 0), sticky="w") + tk.Button(self.db_toolbar, text="Отметить все", + command=lambda: self._set_all(self.db_vars, True)).pack(side="left", padx=(0, 4)) + tk.Button(self.db_toolbar, text="Снять все", + command=lambda: self._set_all(self.db_vars, False)).pack(side="left") + + # Список баз данных с чекбоксами + self.db_vars = {} + self.db_container, self.db_inner = self._create_scrollable_checklist(self.frame_right) + self.db_container.grid(row=8, column=0, padx=5, pady=5, sticky="nsew") + self.frame_right.grid_rowconfigure(8, weight=1) + self.frame_right.grid_columnconfigure(0, weight=1) + + # Кнопка добавления базы + self.add_button = tk.Button(self.frame_left, text="Добавить отмеченные базы", command=self.add_database_to_ibases) + self.add_button.grid(row=5, column=0, padx=5, pady=5) + + # Загрузка пользователей с локального компьютера по умолчанию + self.load_users() + + def _create_scrollable_checklist(self, parent, width=200, height=180): + """Создаём прокручиваемую область для чекбоксов. Возвращаем (container, inner).""" + container = tk.Frame(parent, borderwidth=1, relief="sunken") + canvas = tk.Canvas(container, width=width, height=height, highlightthickness=0) + scrollbar = tk.Scrollbar(container, orient="vertical", command=canvas.yview) + inner = tk.Frame(canvas) + inner.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=inner, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # Прокрутка колесом мыши, когда курсор над областью + def _on_mousewheel(event): + canvas.yview_scroll(int(-event.delta / 120), "units") + + canvas.bind("", lambda e: canvas.bind_all("", _on_mousewheel)) + canvas.bind("", lambda e: canvas.unbind_all("")) + return container, inner + + def _populate_checklist(self, inner, vars_dict, items): + """Пересоздаём чекбоксы в области inner по списку items.""" + for child in inner.winfo_children(): + child.destroy() + vars_dict.clear() + for item in items: + var = tk.BooleanVar(value=False) + vars_dict[item] = var + tk.Checkbutton(inner, text=item, variable=var, anchor="w").pack(fill="x", anchor="w") + + def _set_all(self, vars_dict, value): + """Отмечаем (True) либо снимаем (False) все чекбоксы.""" + for var in vars_dict.values(): + var.set(value) + + def _get_checked(self, vars_dict): + """Возвращаем список отмеченных элементов.""" + return [name for name, var in vars_dict.items() if var.get()] + + def load_users(self): + """Загружаем пользователей с указанного IP-адреса или локального ПК.""" + self.ip_address = self.ip_entry.get() + + if not self.ip_address: # Если IP не указан, используем локальный ПК + local_path = r"C:\Users" + self.load_local_users(local_path) + else: + try: + # Используем указанный IP для подключения к удалённому ПК + remote_path = f"\\\\{self.ip_address}\\C$\\Users" + self.load_remote_users(remote_path) + except Exception as e: + messagebox.showerror("Error", f"Could not load users: {str(e)}") + + def load_local_users(self, path): + """Загружаем список пользователей с локального ПК.""" + try: + folders = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))] + self._populate_checklist(self.user_inner, self.user_vars, folders) + except Exception as e: + messagebox.showerror("Error", f"Could not load local users: {str(e)}") + + def load_remote_users(self, remote_path): + """Загружаем список пользователей с удалённого ПК.""" + try: + folders = [f for f in os.listdir(remote_path) if os.path.isdir(os.path.join(remote_path, f))] + self._populate_checklist(self.user_inner, self.user_vars, folders) + except Exception as e: + messagebox.showerror("Error", f"Could not load remote users: {str(e)}") + + def load_databases(self): + """Подключаемся к SQL серверу и загружаем список баз данных.""" + server = self.server_entry.get() + login = self.login_entry.get() + password = self.password_entry.get() + + try: + connection_string = f"DRIVER={{SQL Server}};SERVER={server};UID={login};PWD={password};" + connection = pyodbc.connect(connection_string) + cursor = connection.cursor() + + cursor.execute("SELECT name FROM sys.databases") + databases = cursor.fetchall() + + # Скрываем служебные базы SQL Server, они не являются базами 1С + system_dbs = {"master", "tempdb", "model", "msdb"} + names = [db[0] for db in databases if db[0].lower() not in system_dbs] + self._populate_checklist(self.db_inner, self.db_vars, names) + + cursor.close() + connection.close() + except Exception as e: + messagebox.showerror("Error", f"Could not connect to SQL server: {str(e)}") + + def get_ibases_path(self, user): + """Возвращаем путь к ibases.v8i выбранного пользователя (локально либо по IP).""" + if not self.ip_address: + base_path = f"C:\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart" + else: + base_path = f"\\\\{self.ip_address}\\C$\\Users\\{user}\\AppData\\Roaming\\1C\\1CEStart" + return os.path.join(base_path, "ibases.v8i") + + def build_base_entry(self, base, server): + """Формируем запись одной базы для ibases.v8i.""" + return ( + f"\n[{base}]\n" + f'Connect=Srvr="{server}";Ref="{base}";\n' + f"ID={uuid.uuid4()}\n" + "OrderInList=255\n" + "Folder=/\n" + "OrderInTree=16640\n" + "External=0\n" + "ClientConnectionSpeed=Normal\n" + "App=Auto\n" + "WA=1\n" + "Version=8.3\n" + "DefaultApp=ThickClient\n" + "DisableLocalSpeechToText=0\n" + ) + + def add_database_to_ibases(self): + """Добавляем все выбранные базы всем выбранным пользователям.""" + # Актуализируем режим (локально / по IP) на момент добавления + self.ip_address = self.ip_entry.get().strip() + + users = self._get_checked(self.user_vars) + bases = self._get_checked(self.db_vars) + server = self.server_entry.get().strip() + + if not users: + messagebox.showerror("Error", "Не отмечен ни один пользователь!") + return + if not bases: + messagebox.showerror("Error", "Не отмечена ни одна база!") + return + if not server: + messagebox.showerror("Error", "Не указан SQL-сервер!") + return + + added = 0 + skipped = 0 + errors = [] + + for user in users: + try: + ibases_path = self.get_ibases_path(user) + base_dir = os.path.dirname(ibases_path) + + # Создаём каталог, если его нет + if not os.path.exists(base_dir): + os.makedirs(base_dir) + + # Читаем текущее содержимое файла для проверки дублей + existing = "" + if os.path.exists(ibases_path): + with open(ibases_path, "r", encoding="utf-8") as f: + existing = f.read() + + entries_to_add = [] + for base in bases: + # Дубль определяем по строке подключения (сервер + имя базы) + connect_line = f'Connect=Srvr="{server}";Ref="{base}";' + already_present = connect_line in existing or any( + connect_line in e for e in entries_to_add + ) + if already_present: + skipped += 1 + continue + entries_to_add.append(self.build_base_entry(base, server)) + added += 1 + + # Дописываем новые записи одним заходом + if entries_to_add: + with open(ibases_path, "a", encoding="utf-8") as f: + for entry in entries_to_add: + f.write(entry) + except Exception as e: + errors.append(f"{user}: {e}") + + # Итоговый отчёт + summary = ( + f"Пользователей: {len(users)}, баз: {len(bases)}\n" + f"Добавлено записей: {added}\n" + f"Пропущено (уже есть): {skipped}" + ) + if errors: + summary += "\n\nОшибки:\n" + "\n".join(errors) + messagebox.showwarning("Готово с ошибками", summary) + else: + messagebox.showinfo("Готово", summary) + + +if __name__ == "__main__": + app = RemoteFolderSelector() + app.mainloop() diff --git a/photo_2026-07-03_11-04-21-Photoroom.ico b/photo_2026-07-03_11-04-21-Photoroom.ico new file mode 100644 index 0000000..8c7a4e1 Binary files /dev/null and b/photo_2026-07-03_11-04-21-Photoroom.ico differ