Initial commit: рабочая версия Main_v3 (add + фильтр дублей)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
__pycache__/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.spec
|
||||||
+357
@@ -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("<Configure>", 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("<Enter>", lambda e: canvas.bind_all("<MouseWheel>", _on_mousewheel))
|
||||||
|
canvas.bind("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>"))
|
||||||
|
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()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
Reference in New Issue
Block a user