Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5027a69f56 | ||
|
|
6be4e229d2 | ||
|
|
ca5fc684ae | ||
|
|
0aa3007c5e |
@@ -1,3 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
@@ -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'],
|
|
||||||
)
|
|
||||||
+161
-516
@@ -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:
|
||||||
|
# Используем указанный 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:
|
try:
|
||||||
folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base, f))]
|
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:
|
except Exception as e:
|
||||||
messagebox.showerror("Ошибка", f"Не удалось загрузить пользователей:\n{e}")
|
messagebox.showerror("Error", f"Could not load local users: {str(e)}")
|
||||||
return
|
|
||||||
|
|
||||||
self._load_items(self.user_vars, folders)
|
def load_remote_users(self, remote_path):
|
||||||
self._render_checklist(self.user_inner, self.user_vars, self.user_search_var.get(), self.on_user_click)
|
"""Загружаем список пользователей с удалённого ПК."""
|
||||||
|
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):
|
||||||
self.active_user = None
|
"""Обработчик двойного клика для выбора пользователя и загрузки баз из файла ibases.v8i."""
|
||||||
self.conn_vars.clear()
|
selected_index = self.folder_listbox.curselection()
|
||||||
self._render_checklist(self.conn_inner, self.conn_vars, "")
|
if selected_index:
|
||||||
self.conn_label.config(text="Подключённые базы: (выберите пользователя)")
|
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
|
||||||
|
|
||||||
|
with open(ibases_path, "r", encoding="utf-8") as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
|
||||||
|
# Ищем строки с названиями баз данных (начинаются с [)
|
||||||
|
databases = [line.strip()[1:-1] for line in lines if line.startswith("[")]
|
||||||
|
|
||||||
|
if not databases:
|
||||||
|
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
|
return
|
||||||
if not bases:
|
|
||||||
messagebox.showerror("Ошибка", "Не отмечена ни одна база!")
|
|
||||||
return
|
|
||||||
if not server:
|
|
||||||
messagebox.showerror("Ошибка", "Не указан SQL-сервер!")
|
|
||||||
return
|
|
||||||
|
|
||||||
use_zip = self.zip_enabled.get()
|
|
||||||
added = skipped = zadded = zskipped = 0
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
for user in users:
|
|
||||||
try:
|
|
||||||
a, s = self._add_entries_to_file(self.get_ibases_path(user), bases, server)
|
|
||||||
added += a
|
|
||||||
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:
|
|
||||||
try:
|
|
||||||
a, s = self._add_entries_to_file(zp, bases, server)
|
|
||||||
zadded += a
|
|
||||||
zskipped += s
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(f"{user} (ZIP): {e}")
|
|
||||||
|
|
||||||
# Обновим панель подключённых баз, если активный пользователь среди затронутых
|
|
||||||
if self.active_user in users:
|
|
||||||
self.load_connected_bases(self.active_user)
|
|
||||||
|
|
||||||
summary = (
|
|
||||||
f"Пользователей: {len(users)}, баз отмечено: {len(bases)}\n"
|
|
||||||
f"Локально — добавлено: {added}, пропущено (уже есть): {skipped}"
|
|
||||||
)
|
|
||||||
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:
|
try:
|
||||||
removed_local = self._delete_entries_from_file(self.get_ibases_path(user), bases)
|
if not self.ip_address:
|
||||||
except Exception as e:
|
base_path = f"C:\\Users\\{self.selected_user}\\AppData\\Roaming\\1C\\1CEStart"
|
||||||
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"
|
||||||
removed_zip = self._delete_entries_from_file(zp, bases)
|
|
||||||
except Exception as e:
|
|
||||||
errors.append(f"{user} (ZIP): {e}")
|
|
||||||
|
|
||||||
self.load_connected_bases(user)
|
ibases_path = os.path.join(base_path, "ibases.v8i")
|
||||||
|
|
||||||
summary = f"Пользователь: {user}\nУдалено локально: {removed_local}"
|
if not os.path.exists(ibases_path):
|
||||||
if use_zip:
|
with open(ibases_path, "w", encoding="utf-8") as file:
|
||||||
summary += f"\nУдалено в ZIP-профиле: {removed_zip}"
|
file.write(f"[{self.base_name}]\n")
|
||||||
self._report(summary, errors)
|
else:
|
||||||
|
with open(ibases_path, "a", encoding="utf-8") as file:
|
||||||
|
file.write(f"[{self.base_name}]\n")
|
||||||
|
|
||||||
def _report(self, summary, errors):
|
messagebox.showinfo("Success", f"Database {self.base_name} added to ibases.v8i")
|
||||||
if errors:
|
|
||||||
messagebox.showwarning("Готово с ошибками", summary + "\n\nОшибки:\n" + "\n".join(errors))
|
# Перезагружаем список баз данных после добавления
|
||||||
else:
|
self.load_ibases()
|
||||||
messagebox.showinfo("Готово", summary)
|
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Error", f"Could not add database to ibases.v8i: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 264 KiB |
Reference in New Issue
Block a user