Compare commits

..
4 Commits
Author SHA1 Message Date
saturn 5027a69f56 Добавлен список подключенных баз 2024-10-22 12:27:36 +03:00
saturn 6be4e229d2 test 2024-10-22 12:12:50 +03:00
saturn ca5fc684ae test 2024-10-22 12:12:19 +03:00
saturn 0aa3007c5e Initial commit 2024-10-22 11:27:51 +03:00
5 changed files with 163 additions and 558 deletions
-3
View File
@@ -1,3 +0,0 @@
__pycache__/
build/
dist/
-39
View File
@@ -1,39 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['Main_v3.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='1C_Base_Adder',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['download.ico'],
)
+159 -514
View File
@@ -1,581 +1,226 @@
import os import os
import re
import tkinter as tk import tkinter as tk
from tkinter import messagebox, filedialog from tkinter import messagebox
import pyodbc import pyodbc
import uuid import uuid
import sys
_BANNER = ("""
....:::::::::
::::::::::-----::::::
::::------------=--------::..
:::--=--============---==+===-:::.
::---========++=++++======+++++==--:.:
:--==++++++++++++++++++========---==-::.
--==++++++++++++++++++++=====---:::-==--::
:-==+**++++++++++++++++++====------::-==-::
-==+**++++++++++++++++++++=====--------=-::
-=++*++++++++++++++++++++++=========-----::
-=+++++++++++++++++++++++++===========----:
:=++++++++++++++++++++++++++==========---::
-=++++++++++++++++++===+++++===-----====--
-=++++++++++++++++++++==+++++=====-----==-
-++++++++++++++++++++++++++++++++====-----
==+++++++*#####*******++++++++++++++++=--=
+===++++++****##%%%%##*****++++**###%##*+==-
+*++=++++****#%##%@%%%%###***++*#%%%%%###*===-
+*++=++++**####*#%@%#%%%##**++*#%%%@@%+*%%*+=-
*++=+++++**+****####%%##*++==+#%%#%%#+++*+==-
+*+++++++++++****###****++=====*####*++=====-
+*=+++++++++++++++++++++++=======+**++=-----
+*=+++++++++++++++++***++++==-=====+++==----
++=+++++++++++++++*****+++==---===========--
+=+++++++++++++*****+++++===---++======--=-
==++++++++++**********+++++==--+++++=====-
-==+++++++++*********##*******==++++++===-
-==++++++++*****#**##*####**+====++++++==
#%%%%%===+++++++++***##*****####**++++++==+++=-
*+*#%%%#*+====++++++++**#%%%##****##********+====-
++++++*##%@%#+++**====+++++++*##############***###*+====-
++******###%%@@@@@%####*+=+==+++++**************+++++=+*++===-
#***+**#*###%@@@@@@@@@@@@@%###*+=+==++++*****++*********++==++++=-=+=+=-
#*#%%%###%%@@@@@@@@@@@@@@@@%###*++++++++**+************+++========*****++==-
%%%######%%####%@@@@@@@@@@@@@@%%**+++++++++++++++***+++=========*###******+++++==
%%@@@@%%%%%%%%%#%%@@@@@@@@@@@@@@%#**+*+**++++++++++++++++=====+%###########***++++*
%@@@@@@@@@%%@@%%%%%%%@@@@@@@@@@@@@@###*****++++++*+++++++===+**#%####%###%####***+***
%%%@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@%####**++++***++++*#####*%##%####%#%%%%##*******
%%%%@%@@@@@@@@@@@@@@@@@@@%@@@@@@@@@@@@@@@@@@@%%%%%%##%%%@%%####*%%#%####%#%%%%%#########
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%##%#*#%#%####%##%%%%%%###%#%##
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%#%%###%######%##%#%%%%%%%%%%%##
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%####%%#####%##%%%%%%%%%%%%%###
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%####%%#%%##%#%%%%%%%%%%@%@%###
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%#####%%%#%%%%#%%%%%%%@@@@@@@%%##
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%%#%%%%##%%#%%%%%%%%%%%%@@@@@@@%%#**
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%@@@%####%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@@@@@@%%%#**
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%%%@@@@%%%%%%%#%%%%%%%##%%%%%%%%%%%%%%%%%@@@@@@%%%%##**
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%%@@@@%%@@@@@@@%%%%%%##%%%%%%%###%%%%%%%%%%%%%%%%%@@%%%%%%%%##*+
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%%@@@@@@@@%%%%@@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%###%%*#
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%@@@@@@%@@@%@@@@@@@%%%@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%
""")
# В оконном режиме (сборка без консоли) sys.stdout может быть None — не роняем старт
if getattr(sys, "stdout", None):
try:
print(_BANNER)
except Exception:
pass
class RemoteFolderSelector(tk.Tk): class RemoteFolderSelector(tk.Tk):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.title("Добавление баз 1С пользователям") self.title("Remote Folder and SQL Database Selector")
self.geometry("760x780") self.geometry("600x400")
self.minsize(700, 640)
# --- Состояние ---
self.ip_address = None # None/"" => локальный ПК, иначе IP удалённого ПК
self.active_user = None # пользователь, чьи базы показаны в панели "Подключённые базы"
self.user_vars = {} # имя пользователя -> BooleanVar (галочка)
self.db_vars = {} # имя базы из SQL -> BooleanVar (галочка)
self.conn_vars = {} # имя подключённой базы активного пользователя -> BooleanVar
# Компоновка окна: две колонки сверху, широкая кнопка "Добавить" снизу
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
# Создадим фреймы для лучшей компоновки элементов
self.frame_left = tk.Frame(self) self.frame_left = tk.Frame(self)
self.frame_left.grid(row=0, column=0, padx=10, pady=10, sticky="nsew") self.frame_left.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
self.frame_right = tk.Frame(self) self.frame_right = tk.Frame(self)
self.frame_right.grid(row=0, column=1, padx=10, pady=10, sticky="nsew") self.frame_right.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
self._build_left_panel() # Сделаем окна масштабируемыми
self._build_right_panel() self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
# Большая кнопка добавления снизу, на всю ширину окна # Переменные для выбора пользователя и базы данных
self.add_button = tk.Button( self.selected_user = None
self, text="Добавить отмеченные базы отмеченным пользователям", self.base_name = None
font=("Segoe UI", 11, "bold"), command=self.add_database_to_ibases self.server_name = None
) self.ip_address = None
self.add_button.grid(row=1, column=0, columnspan=2, padx=10, pady=(0, 12), sticky="ew")
# По умолчанию грузим пользователей локального ПК # Поле ввода IP-адреса
self.load_users() 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")
# ------------------------------------------------------------------ UI self.ip_entry = tk.Entry(self.frame_left, width=30)
self.ip_entry.grid(row=1, column=0, padx=5, pady=5, sticky="w")
def _build_left_panel(self): self.connect_button = tk.Button(self.frame_left, text="Load Users", command=self.load_users)
f = self.frame_left self.connect_button.grid(row=2, column=0, padx=5, pady=5)
f.grid_columnconfigure(0, weight=1)
tk.Label(f, text="IP-адрес (необязательно):").grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w")
self.ip_entry = tk.Entry(f)
self.ip_entry.grid(row=1, column=0, padx=5, pady=(0, 5), sticky="ew")
tk.Button(f, text="Загрузить пользователей", command=self.load_users)\
.grid(row=2, column=0, padx=5, pady=5, sticky="ew")
# Поиск по пользователям
tk.Label(f, text="Поиск пользователей:").grid(row=3, column=0, padx=5, pady=(5, 0), sticky="w")
self.user_search_var = tk.StringVar()
self.user_search_var.trace_add("write", lambda *a: self._render_checklist(
self.user_inner, self.user_vars, self.user_search_var.get(), self.on_user_click))
tk.Entry(f, textvariable=self.user_search_var)\
.grid(row=4, column=0, padx=5, pady=(0, 5), sticky="ew")
# Отметить все / снять все (с учётом фильтра)
bar = tk.Frame(f)
bar.grid(row=5, column=0, padx=5, pady=(0, 3), sticky="w")
tk.Button(bar, text="Отметить все",
command=lambda: self._set_all(self.user_vars, self.user_search_var.get(), True))\
.pack(side="left", padx=(0, 4))
tk.Button(bar, text="Снять все",
command=lambda: self._set_all(self.user_vars, self.user_search_var.get(), False))\
.pack(side="left")
# Список пользователей # Список пользователей
self.user_container, self.user_inner = self._create_scrollable_checklist(f, height=150) self.folder_listbox = tk.Listbox(self.frame_left, width=30, height=10)
self.user_container.grid(row=6, column=0, padx=5, pady=5, sticky="nsew") self.folder_listbox.grid(row=3, column=0, padx=5, pady=5)
f.grid_rowconfigure(6, weight=1) self.folder_listbox.bind("<Double-Button-1>", self.select_user)
# Панель подключённых баз активного пользователя # Поля для ввода данных SQL
self.conn_label = tk.Label(f, text="Подключённые базы: (выберите пользователя)") self.server_label = tk.Label(self.frame_right, text="SQL Server:")
self.conn_label.grid(row=7, column=0, padx=5, pady=(8, 0), sticky="w") self.server_label.grid(row=0, column=0, padx=5, pady=5, sticky="w")
bar2 = tk.Frame(f) self.server_entry = tk.Entry(self.frame_right, width=30)
bar2.grid(row=8, column=0, padx=5, pady=(0, 3), sticky="w") self.server_entry.grid(row=1, column=0, padx=5, pady=5, sticky="w")
tk.Button(bar2, text="Отметить все",
command=lambda: self._set_all(self.conn_vars, "", True)).pack(side="left", padx=(0, 4))
tk.Button(bar2, text="Снять все",
command=lambda: self._set_all(self.conn_vars, "", False)).pack(side="left")
self.conn_container, self.conn_inner = self._create_scrollable_checklist(f, height=150)
self.conn_container.grid(row=9, column=0, padx=5, pady=5, sticky="nsew")
f.grid_rowconfigure(9, weight=1)
tk.Button(f, text="Удалить отмеченные базы у пользователя", command=self.delete_connected_bases)\
.grid(row=10, column=0, padx=5, pady=5, sticky="ew")
# Опция дублирования в ZIP (roaming) профиль
self.zip_enabled = tk.BooleanVar(value=False)
tk.Checkbutton(f, text="Дублировать изменения в ZIP-профиль пользователя",
variable=self.zip_enabled, command=self._toggle_zip)\
.grid(row=11, column=0, padx=5, pady=(8, 0), sticky="w")
zf = tk.Frame(f)
zf.grid(row=12, column=0, padx=5, pady=(0, 5), sticky="ew")
zf.grid_columnconfigure(0, weight=1)
self.zip_path_var = tk.StringVar(value=r"Z:\ZIP_User_Profiles")
self.zip_path_entry = tk.Entry(zf, textvariable=self.zip_path_var, state="disabled")
self.zip_path_entry.grid(row=0, column=0, sticky="ew", padx=(0, 4))
self.zip_browse_btn = tk.Button(zf, text="Обзор...", state="disabled", command=self._browse_zip)
self.zip_browse_btn.grid(row=0, column=1)
def _build_right_panel(self):
f = self.frame_right
f.grid_columnconfigure(0, weight=1)
tk.Label(f, text="SQL-сервер:").grid(row=0, column=0, padx=5, pady=(5, 0), sticky="w")
self.server_entry = tk.Entry(f)
self.server_entry.grid(row=1, column=0, padx=5, pady=(0, 5), sticky="ew")
self.server_entry.insert(0, "SERVERSQL") self.server_entry.insert(0, "SERVERSQL")
tk.Label(f, text="Логин SQL:").grid(row=2, column=0, padx=5, pady=(5, 0), sticky="w") self.login_label = tk.Label(self.frame_right, text="SQL Login:")
self.login_entry = tk.Entry(f) self.login_label.grid(row=2, column=0, padx=5, pady=5, sticky="w")
self.login_entry.grid(row=3, column=0, padx=5, pady=(0, 5), sticky="ew")
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.login_entry.insert(0, "sa")
tk.Label(f, text="Пароль SQL:").grid(row=4, column=0, padx=5, pady=(5, 0), sticky="w") self.password_label = tk.Label(self.frame_right, text="SQL Password:")
self.password_entry = tk.Entry(f, show="*") self.password_label.grid(row=4, column=0, padx=5, pady=5, sticky="w")
self.password_entry.grid(row=5, column=0, padx=5, pady=(0, 5), sticky="ew")
tk.Button(f, text="Загрузить базы", command=self.load_databases)\ self.password_entry = tk.Entry(self.frame_right, show="*", width=30)
.grid(row=6, column=0, padx=5, pady=5, sticky="ew") 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)
tk.Label(f, text="Поиск баз:").grid(row=7, column=0, padx=5, pady=(5, 0), sticky="w") self.sql_button.grid(row=6, column=0, padx=5, pady=5)
self.db_search_var = tk.StringVar()
self.db_search_var.trace_add("write", lambda *a: self._render_checklist(
self.db_inner, self.db_vars, self.db_search_var.get()))
tk.Entry(f, textvariable=self.db_search_var)\
.grid(row=8, column=0, padx=5, pady=(0, 5), sticky="ew")
bar = tk.Frame(f) # Список баз данных
bar.grid(row=9, column=0, padx=5, pady=(0, 3), sticky="w") self.db_listbox = tk.Listbox(self.frame_right, width=30, height=10)
tk.Button(bar, text="Отметить все", self.db_listbox.grid(row=7, column=0, padx=5, pady=5)
command=lambda: self._set_all(self.db_vars, self.db_search_var.get(), True))\ self.db_listbox.bind("<Double-Button-1>", self.select_database)
.pack(side="left", padx=(0, 4))
tk.Button(bar, text="Снять все",
command=lambda: self._set_all(self.db_vars, self.db_search_var.get(), False))\
.pack(side="left")
self.db_container, self.db_inner = self._create_scrollable_checklist(f, height=380) # Кнопка добавления базы
self.db_container.grid(row=10, column=0, padx=5, pady=5, sticky="nsew") self.add_button = tk.Button(self.frame_left, text="Add to ibases.v8i", command=self.add_database_to_ibases)
f.grid_rowconfigure(10, weight=1) self.add_button.grid(row=4, column=0, padx=5, pady=5)
def _create_scrollable_checklist(self, parent, width=220, height=180): # Список баз из файла ibases.v8i
"""Прокручиваемая область для чекбоксов. Возвращает (container, inner).""" self.ibases_listbox = tk.Listbox(self.frame_left, width=30, height=10)
container = tk.Frame(parent, borderwidth=1, relief="sunken") self.ibases_listbox.grid(row=5, column=0, padx=5, pady=5)
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") self.load_users()
canvas.bind("<Enter>", lambda e: canvas.bind_all("<MouseWheel>", _on_mousewheel))
canvas.bind("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>"))
return container, inner
def _load_items(self, vars_dict, items):
"""Создаём свежий набор BooleanVar по списку items (галочки сбрасываются)."""
vars_dict.clear()
for item in items:
vars_dict[item] = tk.BooleanVar(value=False)
def _render_checklist(self, inner, vars_dict, filter_text="", on_click=None):
"""Рисуем чекбоксы, показывая только элементы, подходящие под фильтр.
Переменные (галочки) сохраняются между перерисовками — состояние не теряется."""
for child in inner.winfo_children():
child.destroy()
ft = (filter_text or "").lower()
for name, var in vars_dict.items():
if ft and ft not in name.lower():
continue
cmd = (lambda n=name: on_click(n)) if on_click else None
tk.Checkbutton(inner, text=name, variable=var, anchor="w", command=cmd)\
.pack(fill="x", anchor="w")
# После перерисовки (например, при вводе в поиск) возвращаем прокрутку наверх,
# иначе список остаётся проскроллен вниз и результат поиска не виден.
inner.master.yview_moveto(0)
def _set_all(self, vars_dict, filter_text, value):
"""Отмечаем/снимаем все чекбоксы, попадающие под текущий фильтр."""
ft = (filter_text or "").lower()
for name, var in vars_dict.items():
if ft and ft not in name.lower():
continue
var.set(value)
def _get_checked(self, vars_dict):
return [name for name, var in vars_dict.items() if var.get()]
def _toggle_zip(self):
state = "normal" if self.zip_enabled.get() else "disabled"
self.zip_path_entry.config(state=state)
self.zip_browse_btn.config(state=state)
def _browse_zip(self):
path = filedialog.askdirectory(title="Каталог с ZIP-профилями пользователей")
if path:
self.zip_path_var.set(os.path.normpath(path))
# -------------------------------------------------------------- Загрузка
def load_users(self): def load_users(self):
"""Загружаем список пользователей с локального ПК или с удалённого по IP.""" """Загружаем пользователей с указанного IP-адреса или локального ПК."""
self.ip_address = self.ip_entry.get().strip() self.ip_address = self.ip_entry.get()
if not self.ip_address:
base = r"C:\Users" if not self.ip_address: # Если IP не указан, используем локальный ПК
local_path = r"C:\Users"
self.load_local_users(local_path)
else: else:
base = f"\\\\{self.ip_address}\\C$\\Users"
try: try:
folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base, f))] # Используем указанный IP для подключения к удалённому ПК
remote_path = f"\\\\{self.ip_address}\\C$\\Users"
self.load_remote_users(remote_path)
except Exception as e: except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить пользователей:\n{e}") messagebox.showerror("Error", f"Could not load users: {str(e)}")
def load_local_users(self, path):
"""Загружаем список пользователей с локального ПК."""
try:
folders = os.listdir(path)
self.folder_listbox.delete(0, tk.END)
for folder in folders:
if os.path.isdir(os.path.join(path, folder)):
self.folder_listbox.insert(tk.END, folder)
except Exception as e:
messagebox.showerror("Error", f"Could not load local users: {str(e)}")
def load_remote_users(self, remote_path):
"""Загружаем список пользователей с удалённого ПК."""
try:
folders = os.listdir(remote_path)
self.folder_listbox.delete(0, tk.END)
for folder in folders:
if os.path.isdir(os.path.join(remote_path, folder)):
self.folder_listbox.insert(tk.END, folder)
except Exception as e:
messagebox.showerror("Error", f"Could not load remote users: {str(e)}")
def select_user(self, event):
"""Обработчик двойного клика для выбора пользователя и загрузки баз из файла ibases.v8i."""
selected_index = self.folder_listbox.curselection()
if selected_index:
self.selected_user = self.folder_listbox.get(selected_index)
messagebox.showinfo("User Selected", f"Selected User: {self.selected_user}")
self.load_ibases()
def load_ibases(self):
"""Загружаем базы данных из файла ibases.v8i выбранного пользователя."""
try:
# Определяем путь к файлу ibases.v8i
if not self.ip_address:
base_path = f"C:\\Users\\{self.selected_user}\\AppData\\Roaming\\1C\\1CEStart"
else:
base_path = f"\\\\{self.ip_address}\\C$\\Users\\{self.selected_user}\\AppData\\Roaming\\1C\\1CEStart"
ibases_path = os.path.join(base_path, "ibases.v8i")
if not os.path.exists(ibases_path):
messagebox.showinfo("Info", f"No ibases.v8i file found for user: {self.selected_user}")
return return
self._load_items(self.user_vars, folders) with open(ibases_path, "r", encoding="utf-8") as file:
self._render_checklist(self.user_inner, self.user_vars, self.user_search_var.get(), self.on_user_click) lines = file.readlines()
# Сбрасываем панель подключённых баз — она относилась к прежнему источнику # Ищем строки с названиями баз данных (начинаются с [)
self.active_user = None databases = [line.strip()[1:-1] for line in lines if line.startswith("[")]
self.conn_vars.clear()
self._render_checklist(self.conn_inner, self.conn_vars, "") if not databases:
self.conn_label.config(text="Подключённые базы: (выберите пользователя)") messagebox.showinfo("Info", "No databases found in ibases.v8i")
return
# Сортируем базы в алфавитном порядке и выводим их в Listbox
databases.sort()
self.ibases_listbox.delete(0, tk.END)
for db in databases:
self.ibases_listbox.insert(tk.END, db)
except Exception as e:
messagebox.showerror("Error", f"Could not load databases from ibases.v8i: {str(e)}")
def load_databases(self): def load_databases(self):
"""Подключаемся к SQL-серверу и загружаем список баз.""" """Подключаемся к SQL серверу и загружаем список баз данных."""
server = self.server_entry.get().strip() server = self.server_entry.get()
login = self.login_entry.get() login = self.login_entry.get()
password = self.password_entry.get() password = self.password_entry.get()
try: try:
connection_string = f"DRIVER={{SQL Server}};SERVER={server};UID={login};PWD={password};" connection_string = f"DRIVER={{SQL Server}};SERVER={server};UID={login};PWD={password};"
connection = pyodbc.connect(connection_string) connection = pyodbc.connect(connection_string)
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute("SELECT name FROM sys.databases") cursor.execute("SELECT name FROM sys.databases")
databases = cursor.fetchall() databases = cursor.fetchall()
cursor.close()
self.db_listbox.delete(0, tk.END)
for db in databases:
self.db_listbox.insert(tk.END, db[0])
connection.close() connection.close()
except Exception as e: except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось подключиться к SQL-серверу:\n{e}") messagebox.showerror("Error", f"Could not load databases: {str(e)}")
return
# Служебные базы SQL Server скрываем — это не базы 1С def select_database(self, event):
system_dbs = {"master", "tempdb", "model", "msdb"} """Обработчик двойного клика для выбора базы данных."""
names = [db[0] for db in databases if db[0].lower() not in system_dbs] selected_index = self.db_listbox.curselection()
self._load_items(self.db_vars, names) if selected_index:
self._render_checklist(self.db_inner, self.db_vars, self.db_search_var.get()) self.base_name = self.db_listbox.get(selected_index)
messagebox.showinfo("Database Selected", f"Selected Database: {self.base_name}")
def on_user_click(self, user):
"""Клик по пользователю — делаем его активным и показываем его подключённые базы."""
self.load_connected_bases(user)
def load_connected_bases(self, user):
"""Читаем ibases.v8i пользователя и показываем список его баз в панели."""
self.active_user = user
self.ip_address = self.ip_entry.get().strip()
path = self.get_ibases_path(user)
text = self._read_ibases(path)
_, sections = self._parse_ibases(text)
names = [name for name, _ in sections]
self._load_items(self.conn_vars, names)
self._render_checklist(self.conn_inner, self.conn_vars, "")
suffix = "" if names else " (баз нет)"
self.conn_label.config(text=f"Подключённые базы: {user}{suffix}")
# --------------------------------------------------------------- Пути
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 get_zip_ibases_path(self, user):
"""Путь к ibases.v8i внутри ZIP-профиля пользователя.
Папку профиля ищем автопоиском: <user> либо <user>.* (например <user>.V6).
Возвращаем None, если корень недоступен или профиль не найден."""
root = self.zip_path_var.get().strip()
if not root or not os.path.isdir(root):
return None
u = user.lower()
candidates = []
try:
for name in os.listdir(root):
if not os.path.isdir(os.path.join(root, name)):
continue
low = name.lower()
if low == u or low.startswith(u + "."):
candidates.append(name)
except Exception:
return None
if not candidates:
return None
# Приоритет: точное <user>.V6, затем любой суффикс .*, затем совпадение без суффикса
def score(n):
low = n.lower()
if low == u + ".v6":
return 0
if "." in low:
return 1
return 2
candidates.sort(key=score)
chosen = candidates[0]
return os.path.join(root, chosen, "AppData", "Roaming", "1C", "1CEStart", "ibases.v8i")
# ------------------------------------------------------- Чтение/запись v8i
def _read_ibases(self, path):
"""Читаем ibases.v8i, устойчиво к кодировке (UTF-8 с BOM / без / cp1251)."""
if not os.path.exists(path):
return ""
for enc in ("utf-8-sig", "cp1251"):
try:
with open(path, "r", encoding=enc) as f:
return f.read()
except UnicodeDecodeError:
continue
with open(path, "r", encoding="utf-8", errors="ignore") as f:
return f.read()
def _parse_ibases(self, text):
"""Разбираем содержимое на (преамбула, [(имя_базы, блок_текста), ...]).
Секция начинается со строки вида [ИмяБазы]."""
header_re = re.compile(r"^\s*\[(.+?)\]\s*$")
preamble = []
sections = [] # список [имя, [строки]]
current = None
for line in text.splitlines(keepends=True):
m = header_re.match(line)
if m:
current = [m.group(1), [line]]
sections.append(current)
elif current is None:
preamble.append(line)
else:
current[1].append(line)
return "".join(preamble), [(name, "".join(ls)) for name, ls in sections]
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_entries_to_file(self, ibases_path, bases, server):
"""Добавляем базы в один файл ibases.v8i (с фильтром дублей).
Возвращаем (добавлено, пропущено)."""
base_dir = os.path.dirname(ibases_path)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
existing = self._read_ibases(ibases_path)
entries = []
added = skipped = 0
for base in bases:
connect_line = f'Connect=Srvr="{server}";Ref="{base}";'
if connect_line in existing or any(connect_line in e for e in entries):
skipped += 1
continue
entries.append(self.build_base_entry(base, server))
added += 1
if entries:
with open(ibases_path, "a", encoding="utf-8") as f:
f.write("".join(entries))
return added, skipped
def _delete_entries_from_file(self, ibases_path, base_names):
"""Удаляем указанные базы из одного файла ibases.v8i. Возвращаем число удалённых."""
if not os.path.exists(ibases_path):
return 0
text = self._read_ibases(ibases_path)
preamble, sections = self._parse_ibases(text)
target = {b.lower() for b in base_names}
kept = [blk for (name, blk) in sections if name.lower() not in target]
removed = len(sections) - len(kept)
if removed:
with open(ibases_path, "w", encoding="utf-8") as f:
f.write(preamble + "".join(kept))
return removed
# ----------------------------------------------------------- Действия
def add_database_to_ibases(self): def add_database_to_ibases(self):
"""Добавляем отмеченные базы всем отмеченным пользователям (+ ZIP по галочке).""" """Добавляем базу данных в файл ibases.v8i."""
self.ip_address = self.ip_entry.get().strip() if not self.base_name:
users = self._get_checked(self.user_vars) messagebox.showerror("Error", "No database selected.")
bases = self._get_checked(self.db_vars)
server = self.server_entry.get().strip()
if not users:
messagebox.showerror("Ошибка", "Не отмечен ни один пользователь!")
return
if not bases:
messagebox.showerror("Ошибка", "Не отмечена ни одна база!")
return
if not server:
messagebox.showerror("Ошибка", "Не указан SQL-сервер!")
return return
use_zip = self.zip_enabled.get()
added = skipped = zadded = zskipped = 0
errors = []
for user in users:
try: try:
a, s = self._add_entries_to_file(self.get_ibases_path(user), bases, server) if not self.ip_address:
added += a base_path = f"C:\\Users\\{self.selected_user}\\AppData\\Roaming\\1C\\1CEStart"
skipped += s
except Exception as e:
errors.append(f"{user}: {e}")
if use_zip:
zp = self.get_zip_ibases_path(user)
if zp is None:
errors.append(f"{user}: ZIP-профиль не найден")
else: else:
try: base_path = f"\\\\{self.ip_address}\\C$\\Users\\{self.selected_user}\\AppData\\Roaming\\1C\\1CEStart"
a, s = self._add_entries_to_file(zp, bases, server)
zadded += a
zskipped += s
except Exception as e:
errors.append(f"{user} (ZIP): {e}")
# Обновим панель подключённых баз, если активный пользователь среди затронутых ibases_path = os.path.join(base_path, "ibases.v8i")
if self.active_user in users:
self.load_connected_bases(self.active_user)
summary = ( if not os.path.exists(ibases_path):
f"Пользователей: {len(users)}, баз отмечено: {len(bases)}\n" with open(ibases_path, "w", encoding="utf-8") as file:
f"Локально — добавлено: {added}, пропущено (уже есть): {skipped}" file.write(f"[{self.base_name}]\n")
)
if use_zip:
summary += f"\nZIP-профили — добавлено: {zadded}, пропущено: {zskipped}"
self._report(summary, errors)
def delete_connected_bases(self):
"""Удаляем отмеченные подключённые базы у активного пользователя (+ ZIP по галочке)."""
user = self.active_user
if not user:
messagebox.showerror("Ошибка", "Сначала выберите пользователя слева.")
return
bases = self._get_checked(self.conn_vars)
if not bases:
messagebox.showerror("Ошибка", "Не отмечена ни одна база для удаления!")
return
if not messagebox.askyesno(
"Подтверждение",
f"Удалить {len(bases)} баз(ы) у пользователя «{user}»?\n\n" + "\n".join(bases)
):
return
self.ip_address = self.ip_entry.get().strip()
use_zip = self.zip_enabled.get()
removed_local = removed_zip = 0
errors = []
try:
removed_local = self._delete_entries_from_file(self.get_ibases_path(user), bases)
except Exception as e:
errors.append(f"{user}: {e}")
if use_zip:
zp = self.get_zip_ibases_path(user)
if zp is None:
errors.append(f"{user}: ZIP-профиль не найден")
else: else:
try: with open(ibases_path, "a", encoding="utf-8") as file:
removed_zip = self._delete_entries_from_file(zp, bases) file.write(f"[{self.base_name}]\n")
messagebox.showinfo("Success", f"Database {self.base_name} added to ibases.v8i")
# Перезагружаем список баз данных после добавления
self.load_ibases()
except Exception as e: except Exception as e:
errors.append(f"{user} (ZIP): {e}") messagebox.showerror("Error", f"Could not add database to ibases.v8i: {str(e)}")
self.load_connected_bases(user)
summary = f"Пользователь: {user}\nУдалено локально: {removed_local}"
if use_zip:
summary += f"\nУдалено в ZIP-профиле: {removed_zip}"
self._report(summary, errors)
def _report(self, summary, errors):
if errors:
messagebox.showwarning("Готово с ошибками", summary + "\n\nОшибки:\n" + "\n".join(errors))
else:
messagebox.showinfo("Готово", summary)
if __name__ == "__main__": if __name__ == "__main__":
+2
View File
@@ -0,0 +1,2 @@
# 1C_Bases
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB