USER
какой код лучше:
import os
import json
import traceback
from datetime import datetime
import subprocess
import pandas as pd
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL
from docx.oxml.ns import qn, nsdecls
from docx.oxml import OxmlElement, parse_xml
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
try:
import win32com.client
WIN32COM_AVAILABLE = True
except ImportError:
WIN32COM_AVAILABLE = False
WORKDIR = os.getcwd()
TEMPLATES_DIR = os.path.join(WORKDIR, "templates")
CONFIG_DIR = os.path.join(WORKDIR, "config")
PROFILES_DIR = os.path.join(CONFIG_DIR, "profiles")
for d in (TEMPLATES_DIR, CONFIG_DIR, PROFILES_DIR):
os.makedirs(d, exist_ok=True)
SETTINGS_FILE = os.path.join(CONFIG_DIR, "settings.json")
PLACEHOLDERS_FILE = os.path.join(CONFIG_DIR, "placeholders.json")
MANDATORY_COLS = ['Объект', 'Свойство', 'Значение']
class Logger:
def __init__(self):
self.history = []
def log(self, msg):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
record = f"[{ts}] {msg}"
self.history.append(record)
print(record)
if hasattr(app_ref, "txt_log") and app_ref.txt_log:
app_ref.txt_log.config(state='normal')
app_ref.txt_log.insert("end", record + "\n")
app_ref.txt_log.see("end")
app_ref.txt_log.config(state='disabled')
def get_text(self):
return "\n".join(self.history)
def save_to_file(self, path):
try:
with open(path, 'w', encoding='utf-8') as f:
f.write(self.get_text())
self.log(f"Лог сохранён: {path}")
except Exception as e:
self.log(f"Ошибка сохранения лога: {e}")
logger = Logger()
app_ref = None # Ссылка на приложение, для логирования в UI
class Settings:
def __init__(self):
self.data = {
"table_style": {
"header_bold": True,
"alignment": "center",
"font_name": "Calibri",
"font_size": 11,
"border_style": "тонкая",
"section_header_font_name": "Calibri",
"section_header_font_size": 12,
"section_header_bold": True,
"section_header_align": "center",
"section_header_bg_color": "FFFF00"
},
"export": {
"format": "docx",
"pdf_password": "",
"compress_images": True,
"template": None
}
}
self.load()
def load(self):
if os.path.exists(SETTINGS_FILE):
try:
with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
d = json.load(f)
self.data.update(d)
exp = self.data.get('export', {})
if 'format' not in exp:
exp['format'] = 'docx'
if 'pdf_password' not in exp:
exp['pdf_password'] = ''
if 'compress_images' not in exp:
exp['compress_images'] = True
self.data['export'] = exp
ts = self.data.get('table_style', {})
defaults = {
"border_style": "тонкая",
"section_header_font_name": "Calibri",
"section_header_font_size": 12,
"section_header_bold": True,
"section_header_align": "center",
"section_header_bg_color": "FFFF00"
}
for k, v in defaults.items():
if k not in ts:
ts[k] = v
self.data['table_style'] = ts
logger.log("Настройки загружены")
except Exception as e:
logger.log(f"Ошибка загрузки настроек: {e}")
def save(self):
try:
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(self.data, f, ensure_ascii=False, indent=4)
logger.log("Настройки сохранены")
except Exception as e:
logger.log(f"Ошибка сохранения настроек: {e}")
def save_profile(self, filename):
try:
with open(os.path.join(PROFILES_DIR, filename), 'w', encoding='utf-8') as f:
json.dump(self.data, f, ensure_ascii=False, indent=4)
logger.log(f"Профиль сохранён: {filename}")
return True
except Exception as e:
logger.log(f"Ошибка сохранения профиля: {e}")
return False
def load_profile(self, filename):
try:
with open(os.path.join(PROFILES_DIR, filename), 'r', encoding='utf-8') as f:
d = json.load(f)
self.data.update(d)
ts = self.data.get('table_style', {})
defaults = {
"border_style": "тонкая",
"section_header_font_name": "Calibri",
"section_header_font_size": 12,
"section_header_bold": True,
"section_header_align": "center",
"section_header_bg_color": "FFFF00"
}
for k, v in defaults.items():
if k not in ts:
ts[k] = v
self.data['table_style'] = ts
logger.log(f"Профиль загружен: {filename}")
return True
except Exception as e:
logger.log(f"Ошибка загрузки профиля: {e}")
return False
settings = Settings()
class Placeholders:
def __init__(self):
self.placeholders = {}
self.load()
def load(self):
if os.path.isfile(PLACEHOLDERS_FILE):
try:
with open(PLACEHOLDERS_FILE, 'r', encoding='utf-8') as f:
self.placeholders = json.load(f)
logger.log("Плейсхолдеры загружены")
except Exception as e:
logger.log(f"Ошибка загрузки плейсхолдеров: {e}")
def save(self):
try:
with open(PLACEHOLDERS_FILE, 'w', encoding='utf-8') as f:
json.dump(self.placeholders, f, ensure_ascii=False, indent=4)
logger.log("Плейсхолдеры сохранены")
except Exception as e:
logger.log(f"Ошибка сохранения плейсхолдеров: {e}")
placeholders = Placeholders()
def extract_article_full(obj):
if not isinstance(obj, str) or not obj.strip():
return ""
low = obj.lower()
idx = low.find("idis")
if idx >= 0:
return obj[idx + 4:].strip()
words = obj.strip().split()
if len(words) <= 2:
return obj.strip()
return " ".join(words[-3:]).strip()
def extract_product_type(obj):
if not isinstance(obj, str) or not obj.strip():
return ""
low = obj.lower()
idx = low.find("idis")
if idx >= 0:
part = obj[:idx].strip()
if part:
return part
return "Неизвестно"
words = obj.strip().split()
if len(words) <= 3:
return " ".join(words)
return " ".join(words[:3])
def extract_param(prop):
if not isinstance(prop, str):
return ""
return prop.split("_")[-1].strip()
def load_and_transform_excel_grouped(path):
logger.log(f"Загрузка файла: {path}")
df = pd.read_excel(path, engine="openpyxl")
missing = [c for c in MANDATORY_COLS if c not in df.columns]
if missing:
raise ValueError(f"Отсутствуют колонки: {missing}")
df["Тип товара"] = df["Объект"].apply(extract_product_type)
df["Артикул"] = df["Объект"].apply(extract_article_full)
df["Параметр"] = df["Свойство"].apply(extract_param)
df["Значение"] = df["Значение"].astype(str)
df = df[(df["Артикул"] != "") & (df["Параметр"] != "")].reset_index(drop=True)
grouped = {}
for art, gdf in df.groupby("Артикул"):
obj_full = gdf["Объект"].iloc[0]
prod_type = gdf["Тип товара"].iloc[0]
sections = []
current_header = None
current_params = []
for _, row in gdf.iterrows():
if row["Значение"].strip().lower() == "<раздел>":
if current_header is not None or current_params:
sections.append({"header": current_header, "params": current_params})
current_header = row["Параметр"]
current_params = []
else:
current_params.append({"parameter": row["Параметр"], "value": row["Значение"]})
if current_header is not None or current_params:
sections.append({"header": current_header, "params": current_params})
if not sections:
sections = [
{
"header": None,
"params": [{"parameter": r["Параметр"], "value": r["Значение"]} for _, r in gdf.iterrows()],
}
]
grouped[art] = {"object_full": obj_full, "product_type": prod_type, "sections": sections}
logger.log(f"Данные сгруппированы по артикулам: {len(grouped)}")
return grouped
def inline_replace_in_paragraph(paragraph, placeholder, replacement):
runs = paragraph.runs
full_text = "".join(run.text for run in runs)
if placeholder not in full_text:
return
new_text = full_text.replace(placeholder, replacement)
for run in runs:
run.text = ""
if runs:
runs[0].text = new_text
def docx_replace_placeholder(doc, key, value):
ph = "{{" + key + "}}"
for p in doc.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
for tbl in doc.tables:
for row in tbl.rows:
for cell in row.cells:
for p in cell.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
replace_placeholder_in_header_footer(doc, key, value)
def replace_placeholder_in_header_footer(doc, key, value):
ph = "{{" + key + "}}"
for section in doc.sections:
header = section.header
for p in header.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
for tbl in header.tables:
for row in tbl.rows:
for cell in row.cells:
for p in cell.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
footer = section.footer
for p in footer.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
for tbl in footer.tables:
for row in tbl.rows:
for cell in row.cells:
for p in cell.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
def set_table_border(table, border_style):
if border_style == 'нет':
border_xml = """
<w:tblBorders %s>
<w:top w:val="nil"/>
<w:left w:val="nil"/>
<w:bottom w:val="nil"/>
<w:right w:val="nil"/>
<w:insideH w:val="nil"/>
<w:insideV w:val="nil"/>
</w:tblBorders>
""" % nsdecls('w')
elif border_style == 'тонкая':
border_xml = """
<w:tblBorders %s>
<w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/>
</w:tblBorders>
""" % nsdecls('w')
elif border_style == 'толстая':
border_xml = """
<w:tblBorders %s>
<w:top w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:left w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:bottom w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:right w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:insideH w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:insideV w:val="single" w:sz="12" w:space="0" w:color="000000"/>
</w:tblBorders>
""" % nsdecls('w')
else:
border_xml = """
<w:tblBorders %s>
<w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/>
</w:tblBorders>
""" % nsdecls('w')
tblPr = table._element.tblPr
for child in tblPr.findall("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tblBorders"):
tblPr.remove(child)
borders = parse_xml(border_xml)
tblPr.append(borders)
def set_cell_background(cell, hex_color):
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:fill'), hex_color)
tcPr.append(shd)
class App(tk.Tk):
def __init__(self):
super().__init__()
global app_ref
app_ref = self
self.title("Автоматизация техдокументации")
self.geometry("1200x720")
self.minsize(1000, 600)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.grouped_data = {}
self.filtered_df = pd.DataFrame()
self.filtered_df_backup = pd.DataFrame()
self.basket_df = pd.DataFrame(columns=["Объект", "Тип товара", "Артикул"])
# Храним параметры и их возможные значения для выбора фильтров (множество значений для каждого параметра)
self.all_params = [] # Список всех параметров
self.param_values = {} # { param_name: set(values) }
self.user_placeholders = placeholders.placeholders.copy()
self.templates = []
self.current_template = None
self.selected_profile = None
self.filter_params_selected = {} # {param_name: value} для нескольких параметров
self.create_widgets()
self.create_menu()
self.bind_shortcuts()
self.load_templates()
self.load_profiles()
self.load_placeholders_to_tree()
self.load_settings_to_gui()
self.create_placeholders_from_loaded_products()
logger.log("Приложение запущено")
def create_placeholders_from_loaded_products(self):
if not self.grouped_data:
logger.log("Данные товаров отсутствуют - автоматическое создание плейсхолдеров пропущено")
return
unique_params = set()
for info in self.grouped_data.values():
for section in info['sections']:
for param in section['params']:
unique_params.add(param['parameter'])
updated = False
for param in unique_params:
if param not in self.user_placeholders:
self.user_placeholders[param] = "{{" + param + "}}"
updated = True
if updated:
placeholders.placeholders = self.user_placeholders.copy()
placeholders.save()
self.refresh_placeholders_view()
logger.log(f"Автоматически создано плейсхолдеров из параметров: {len(unique_params)}")
else:
logger.log("Автоматическое создание плейсхолдеров: никаких новых плейсхолдеров не добавлено")
def create_menu(self):
menubar = tk.Menu(self)
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="Загрузить Excel", command=self.load_excel_dialog)
file_menu.add_separator()
file_menu.add_command(label="Сохранить лог...", command=self.export_log)
file_menu.add_separator()
file_menu.add_command(label="Выход", command=self.quit)
menubar.add_cascade(label="Файл", menu=file_menu)
basket_menu = tk.Menu(menubar, tearoff=0)
basket_menu.add_command(label="Удалить выделенные", command=self.remove_selected_from_basket)
basket_menu.add_command(label="Очистить корзину", command=self.clear_basket)
menubar.add_cascade(label="Корзина", menu=basket_menu)
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label="О программе", command=self.show_about)
menubar.add_cascade(label="Помощь", menu=help_menu)
self.config(menu=menubar)
def bind_shortcuts(self):
pass
def show_about(self):
messagebox.showinfo("О программе", "Автоматизация техдокументации\nВерсия 1.0")
def create_widgets(self):
self.nb = ttk.Notebook(self)
self.nb.grid(row=0, column=0, sticky="nsew")
# --- Вкладка данных ---
tab_data = ttk.Frame(self.nb)
self.nb.add(tab_data, text="Данные")
tab_data.grid_rowconfigure(1, weight=1)
tab_data.grid_columnconfigure(0, weight=1)
tab_data.grid_columnconfigure(1, weight=0)
filter_frame = ttk.Frame(tab_data)
filter_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
filter_frame.grid_columnconfigure(7, weight=1)
ttk.Label(filter_frame, text="Поиск по артикулу:").grid(row=0, column=0, sticky="w")
self.var_quick_search = tk.StringVar()
ent_quick_search = ttk.Entry(filter_frame, textvariable=self.var_quick_search)
ent_quick_search.grid(row=0, column=1, sticky="ew", padx=3)
filter_frame.grid_columnconfigure(1, weight=2)
ent_quick_search.bind("<KeyRelease>", self.quick_search)
ttk.Label(filter_frame, text="Фильтры параметров:").grid(row=0, column=2, sticky="w", padx=10)
self.btn_add_param_filter = ttk.Button(filter_frame, text="+ Фильтр", command=self.add_param_filter_row)
self.btn_add_param_filter.grid(row=0, column=7, sticky="e")
self.param_filters_container = ttk.Frame(filter_frame)
self.param_filters_container.grid(row=1, column=0, columnspan=8, sticky="ew", pady=5)
self.param_filters_container.grid_columnconfigure(0, weight=1)
self.param_filter_rows = []
tbl_frame = ttk.Frame(tab_data)
tbl_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
tbl_frame.grid_rowconfigure(0, weight=1)
tbl_frame.grid_columnconfigure(0, weight=1)
self.tree_data = ttk.Treeview(tbl_frame, show="headings", selectmode="extended")
self.tree_data.grid(row=0, column=0, sticky="nsew")
self.tree_data.bind("<Double-1>", self.add_selected_to_basket)
self.tree_data.bind("<Button-1>", self.show_item_params)
self.tree_data.bind("<Button-3>", self.context_menu_data)
scr_y = ttk.Scrollbar(tbl_frame, orient="vertical", command=self.tree_data.yview)
scr_y.grid(row=0, column=1, sticky="ns")
scr_x = ttk.Scrollbar(tbl_frame, orient="horizontal", command=self.tree_data.xview)
scr_x.grid(row=1, column=0, sticky="ew")
self.tree_data.configure(yscrollcommand=scr_y.set, xscrollcommand=scr_x.set)
right_frame = ttk.Frame(tab_data, relief="groove", borderwidth=2)
right_frame.grid(row=1, column=1, sticky="nsew", padx=5, pady=5)
tab_data.grid_columnconfigure(1, weight=0)
right_frame.grid_rowconfigure(1, weight=1)
right_frame.grid_columnconfigure(0, weight=1)
ttk.Label(right_frame, text="Параметры выбранного товара:").grid(row=0, column=0, sticky="w", padx=5, pady=(0, 5))
self.tree_params = ttk.Treeview(right_frame, show="headings", columns=["Параметр", "Значение"], height=20)
self.tree_params.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
self.tree_params.heading("Параметр", text="Параметр")
self.tree_params.heading("Значение", text="Значение")
self.tree_params.column("Параметр", width=150, anchor="w")
self.tree_params.column("Значение", width=150, anchor="w")
sb_params_y = ttk.Scrollbar(right_frame, orient="vertical", command=self.tree_params.yview)
sb_params_y.grid(row=1, column=1, sticky="ns")
self.tree_params.configure(yscrollcommand=sb_params_y.set)
basket_frame = ttk.LabelFrame(self, text="Корзина товаров")
basket_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
self.grid_rowconfigure(1, weight=1)
basket_frame.grid_rowconfigure(0, weight=1)
basket_frame.grid_columnconfigure(0, weight=1)
self.tree_basket = ttk.Treeview(basket_frame, show="headings", selectmode="extended", height=6)
self.tree_basket.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
self.tree_basket.bind("<Delete>", lambda e: self.remove_selected_from_basket())
self.tree_basket.bind("<Button-3>", self.context_menu_basket)
sb_basket_y = ttk.Scrollbar(basket_frame, orient="vertical", command=self.tree_basket.yview)
sb_basket_y.grid(row=0, column=1, sticky="ns", pady=5)
sb_basket_x = ttk.Scrollbar(basket_frame, orient="horizontal", command=self.tree_basket.xview)
sb_basket_x.grid(row=1, column=0, sticky="ew", padx=5)
self.tree_basket.configure(yscrollcommand=sb_basket_y.set, xscrollcommand=sb_basket_x.set)
ctrl_basket = ttk.Frame(basket_frame)
ctrl_basket.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
ctrl_basket.grid_columnconfigure(3, weight=1)
ttk.Button(ctrl_basket, text="Удалить выделенные", command=self.remove_selected_from_basket).grid(row=0, column=0, padx=5)
ttk.Button(ctrl_basket, text="Очистить корзину", command=self.clear_basket).grid(row=0, column=1, padx=5)
ttk.Button(ctrl_basket, text="Генерировать документ", command=self.generate_document).grid(row=0, column=2, padx=5)
tab_set_gen = ttk.Frame(self.nb)
self.nb.add(tab_set_gen, text="Настройки и Генерация")
tab_set_gen.grid_columnconfigure(0, weight=1)
tab_set_gen.grid_rowconfigure(6, weight=1) # строка списка плейсхолдеров - тянется по высоте
prof_frame = ttk.LabelFrame(tab_set_gen, text="Профили настроек")
prof_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
prof_frame.grid_columnconfigure(1, weight=1)
self.combo_profiles = ttk.Combobox(prof_frame, state='readonly')
self.combo_profiles.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
self.combo_profiles.bind("<<ComboboxSelected>>", self.on_profile_change)
btn_load_profile = ttk.Button(prof_frame, text="Загрузить профиль", command=self.load_profile)
btn_load_profile.grid(row=0, column=1, padx=5)
btn_save_profile = ttk.Button(prof_frame, text="Сохранить профиль", command=self.save_profile)
btn_save_profile.grid(row=0, column=2, padx=5)
btn_refresh_profiles = ttk.Button(prof_frame, text="Обновить список", command=self.load_profiles)
btn_refresh_profiles.grid(row=0, column=3, padx=5)
ttk.Label(tab_set_gen, text="Выбор шаблона:").grid(row=1, column=0, sticky='w', padx=10, pady=(0, 5))
self.combo_templates = ttk.Combobox(tab_set_gen, state='readonly')
self.combo_templates.grid(row=2, column=0, sticky="ew", padx=10)
self.combo_templates.bind("<<ComboboxSelected>>", self.on_template_change)
btn_preview_template = ttk.Button(tab_set_gen, text="Предпросмотр шаблона", command=self.preview_template)
btn_preview_template.grid(row=3, column=0, sticky='w', padx=10, pady=5)
ttk.Label(tab_set_gen, text="Плейсхолдеры:").grid(row=4, column=0, sticky='w', padx=10, pady=(5, 3))
# Контейнер для поиска + списка с прокруткой по вертикали
ph_container = ttk.Frame(tab_set_gen)
ph_container.grid(row=5, column=0, sticky="nsew", padx=10)
ph_container.grid_columnconfigure(0, weight=1)
ph_container.grid_rowconfigure(1, weight=1)
search_frame = ttk.Frame(ph_container)
search_frame.grid(row=0, column=0, sticky="ew")
search_frame.columnconfigure(1, weight=1)
ttk.Label(search_frame, text="Поиск:").grid(row=0, column=0, sticky="w")
self.var_placeholder_search = tk.StringVar()
ent_ph_search = ttk.Entry(search_frame, textvariable=self.var_placeholder_search)
ent_ph_search.grid(row=0, column=1, sticky="ew")
ent_ph_search.bind("<KeyRelease>", self.on_placeholder_search)
tree_frame = ttk.Frame(ph_container)
tree_frame.grid(row=1, column=0, sticky="nsew", pady=(5, 0))
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
self.tree_placeholders = ttk.Treeview(tree_frame, columns=("name", "value"), show='headings')
self.tree_placeholders.grid(row=0, column=0, sticky="nsew")
self.tree_placeholders.heading("name", text="Имя")
self.tree_placeholders.heading("value", text="Значение")
self.tree_placeholders.column("name", width=200, anchor="w")
self.tree_placeholders.column("value", width=400, anchor="w")
scrollbar_ph = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree_placeholders.yview)
scrollbar_ph.grid(row=0, column=1, sticky="ns")
self.tree_placeholders.configure(yscrollcommand=scrollbar_ph.set)
ph_btn_frame = ttk.Frame(tab_set_gen)
ph_btn_frame.grid(row=7, column=0, sticky="ew", padx=10, pady=5)
for i, (text, cmd) in enumerate([
("Добавить", self.add_placeholder),
("Редактировать", self.edit_placeholder),
("Удалить", self.delete_placeholder),
("Импорт из JSON", self.import_placeholders),
("Экспорт в JSON", self.export_placeholders),
]):
ttk.Button(ph_btn_frame, text=text, command=cmd).grid(row=0, column=i, padx=5)
# Параметры таблицы
style_label = ttk.Label(tab_set_gen, text="Параметры таблицы:")
style_label.grid(row=8, column=0, sticky='w', padx=10, pady=(10, 3))
style_frm = ttk.Frame(tab_set_gen)
style_frm.grid(row=9, column=0, sticky="ew", padx=10, pady=5)
style_frm.columnconfigure(10, weight=1)
self.var_header_bold = tk.BooleanVar(value=settings.data['table_style']['header_bold'])
ttk.Checkbutton(style_frm, text="Жирный заголовок", variable=self.var_header_bold).grid(row=0, column=0, padx=5, pady=2)
ttk.Label(style_frm, text="Выравнивание:").grid(row=0, column=1, padx=5, pady=2, sticky='w')
self.combo_align = ttk.Combobox(style_frm, values=['left', 'center', 'right'], state='readonly', width=10)
self.combo_align.set(settings.data['table_style']['alignment'])
self.combo_align.grid(row=0, column=2, padx=5, pady=2)
ttk.Label(style_frm, text="Шрифт:").grid(row=0, column=3, padx=5, pady=2, sticky='w')
self.entry_font_name = ttk.Entry(style_frm, width=15)
self.entry_font_name.insert(0, settings.data['table_style']['font_name'])
self.entry_font_name.grid(row=0, column=4, padx=5, pady=2)
ttk.Label(style_frm, text="Размер:").grid(row=0, column=5, padx=5, pady=2, sticky='w')
self.spin_font_size = ttk.Spinbox(style_frm, from_=6, to=48, width=5)
self.spin_font_size.delete(0, 'end')
self.spin_font_size.insert(0, settings.data['table_style']['font_size'])
self.spin_font_size.grid(row=0, column=6, padx=5, pady=2)
ttk.Label(style_frm, text="Стиль рамки:").grid(row=0, column=7, padx=5, pady=2, sticky='w')
self.combo_border_style = ttk.Combobox(style_frm, values=['нет', 'тонкая', 'толстая'], state='readonly', width=10)
self.combo_border_style.set(settings.data['table_style'].get('border_style', 'тонкая'))
self.combo_border_style.grid(row=0, column=8, padx=5, pady=2)
ttk.Label(style_frm, text="Шрифт заголовка раздела:").grid(row=1, column=0, sticky='w', padx=5, pady=3)
self.entry_sec_header_font_name = ttk.Entry(style_frm, width=15)
self.entry_sec_header_font_name.grid(row=1, column=1, padx=5)
self.entry_sec_header_font_name.insert(0, settings.data['table_style'].get("section_header_font_name", "Calibri"))
ttk.Label(style_frm, text="Размер шрифта:").grid(row=1, column=2, sticky='w', padx=5, pady=3)
self.spin_sec_header_font_size = ttk.Spinbox(style_frm, from_=6, to=48, width=5)
self.spin_sec_header_font_size.grid(row=1, column=3, padx=5)
self.spin_sec_header_font_size.delete(0, "end")
self.spin_sec_header_font_size.insert(0, settings.data['table_style'].get("section_header_font_size", 12))
self.var_sec_header_bold = tk.BooleanVar(value=settings.data['table_style'].get("section_header_bold", True))
ttk.Checkbutton(style_frm, text="Жирный заголовок раздела", variable=self.var_sec_header_bold).grid(row=1, column=4, padx=5)
ttk.Label(style_frm, text="Выравнивание заголовка:").grid(row=1, column=5, sticky='w', padx=5, pady=3)
self.combo_sec_header_align = ttk.Combobox(style_frm, values=['left', 'center', 'right'], state='readonly', width=10)
self.combo_sec_header_align.grid(row=1, column=6, padx=5)
self.combo_sec_header_align.set(settings.data['table_style'].get("section_header_align", "center"))
ttk.Label(style_frm, text="Цвет фона заголовка (HEX):").grid(row=1, column=7, sticky='w', padx=5, pady=3)
self.entry_sec_header_bg_color = ttk.Entry(style_frm, width=12)
self.entry_sec_header_bg_color.grid(row=1, column=8, padx=5)
self.entry_sec_header_bg_color.insert(0, settings.data['table_style'].get("section_header_bg_color", "FFFF00"))
export_label = ttk.Label(tab_set_gen, text="Настройки экспорта:")
export_label.grid(row=10, column=0, sticky='w', padx=10, pady=(10, 3))
export_frm = ttk.Frame(tab_set_gen)
export_frm.grid(row=11, column=0, sticky="ew", padx=10, pady=5)
export_frm.grid_columnconfigure(8, weight=1)
ttk.Label(export_frm, text="Формат файла:").grid(row=0, column=0, sticky="w", padx=5)
self.combo_export_format = ttk.Combobox(export_frm, values=['docx', 'pdf'], state='readonly', width=10)
self.combo_export_format.set(settings.data['export']['format'])
self.combo_export_format.grid(row=0, column=1, sticky="w", padx=5)
self.var_compress_images = tk.BooleanVar(value=settings.data['export']['compress_images'])
ttk.Checkbutton(export_frm, text="Сжимать изображения", variable=self.var_compress_images).grid(row=0, column=2, sticky="w", padx=10)
ttk.Label(export_frm, text="Пароль для PDF:").grid(row=0, column=3, sticky="w", padx=5)
self.entry_pdf_password = ttk.Entry(export_frm, width=20, show='*')
self.entry_pdf_password.insert(0, settings.data['export']['pdf_password'])
self.entry_pdf_password.grid(row=0, column=4, sticky="w", padx=5)
ttk.Button(tab_set_gen, text="Сохранить настройки в профиль", command=self.save_profile).grid(row=12, column=0, sticky="e", padx=10, pady=10)
def on_placeholder_search(self, event=None):
query = self.var_placeholder_search.get().strip().lower()
filtered = {}
for k, v in self.user_placeholders.items():
if query in k.lower():
filtered[k] = v
self.refresh_placeholders_view(filtered)
def refresh_placeholders_view(self, placeholders_dict=None):
if placeholders_dict is None:
placeholders_dict = self.user_placeholders
self.tree_placeholders.delete(*self.tree_placeholders.get_children())
for k in sorted(placeholders_dict.keys(), key=lambda s: s.lower()):
self.tree_placeholders.insert("", "end", values=(k, placeholders_dict[k]))
def add_placeholder(self):
name = simpledialog.askstring("Добавить плейсхолдер", "Имя плейсхолдера:", parent=self)
if not name:
return
if name in self.user_placeholders:
messagebox.showerror("Ошибка", "Такой плейсхолдер уже существует")
return
value = simpledialog.askstring("Добавить плейсхолдер", "Значение плейсхолдера:", parent=self)
if value is None:
return
self.user_placeholders[name] = value
placeholders.placeholders = self.user_placeholders.copy()
placeholders.save()
self.refresh_placeholders_view()
logger.log(f"Добавлен плейсхолдер: {name} = {value}")
def edit_placeholder(self):
sel = self.tree_placeholders.selection()
if not sel:
messagebox.showinfo("Редактировать", "Выделите плейсхолдер")
return
iid = sel[0]
name, old_value = self.tree_placeholders.item(iid, "values")
new_value = simpledialog.askstring("Редактировать плейсхолдер", f"Значение плейсхолдера '{name}':", initialvalue=old_value, parent=self)
if new_value is None:
return
self.user_placeholders[name] = new_value
placeholders.placeholders = self.user_placeholders.copy()
placeholders.save()
self.refresh_placeholders_view()
logger.log(f"Изменён плейсхолдер: {name} = {new_value}")
def delete_placeholder(self):
sel = self.tree_placeholders.selection()
if not sel:
messagebox.showinfo("Удалить", "Выделите плейсхолдер")
return
iid = sel[0]
name = self.tree_placeholders.item(iid, "values")[0]
if messagebox.askyesno("Удалить плейсхолдер", f"Удалить плейсхолдер '{name}'?"):
self.user_placeholders.pop(name, None)
placeholders.placeholders = self.user_placeholders.copy()
placeholders.save()
self.refresh_placeholders_view()
logger.log(f"Удалён плейсхолдер: {name}")
def import_placeholders(self):
path = filedialog.askopenfilename(title="Импорт плейсхолдеров из JSON", filetypes=[("JSON", "*.json")])
if not path:
return
try:
with open(path, 'r', encoding='utf-8') as f:
d = json.load(f)
if not isinstance(d, dict):
raise ValueError("Файл должен содержать словарь плейсхолдеров")
self.user_placeholders.update(d)
placeholders.placeholders = self.user_placeholders.copy()
placeholders.save()
self.refresh_placeholders_view()
messagebox.showinfo("Импорт", f"Импортировано {len(d)} плейсхолдеров")
logger.log(f"Импортировано плейсхолдеров: {len(d)}")
except Exception as e:
messagebox.showerror("Ошибка импорта", str(e))
logger.log(f"Ошибка импорта плейсхолдеров: {e}")
def export_placeholders(self):
path = filedialog.asksaveasfilename(title="Экспорт плейсхолдеров в JSON", defaultextension=".json",
filetypes=[("JSON", "*.json")])
if not path:
return
try:
with open(path, 'w', encoding='utf-8') as f:
json.dump(self.user_placeholders, f, ensure_ascii=False, indent=4)
messagebox.showinfo("Экспорт", f"Плейсхолдеры экспортированы: {path}")
logger.log(f"Плейсхолдеры экспортированы: {path}")
except Exception as e:
messagebox.showerror("Ошибка экспорта", str(e))
logger.log(f"Ошибка экспорта плейсхолдеров: {e}")
def export_log(self):
path = filedialog.asksaveasfilename(title="Сохранить лог", defaultextension=".txt",
filetypes=[("Text files", "*.txt")])
if not path:
return
logger.save_to_file(path)
messagebox.showinfo("Лог сохранён", f"Лог сохранён в файл:\n{path}")
def save_gui_to_settings(self):
d = settings.data
exp = d.setdefault("export", {})
exp["format"] = self.combo_export_format.get() if hasattr(self, "combo_export_format") else "docx"
exp["compress_images"] = self.var_compress_images.get() if hasattr(self, "var_compress_images") else True
exp["pdf_password"] = self.entry_pdf_password.get() if hasattr(self, "entry_pdf_password") else ""
exp["template"] = self.combo_templates.get() if hasattr(self, "combo_templates") else None
ts = d.setdefault("table_style", {})
ts["header_bold"] = self.var_header_bold.get() if hasattr(self, "var_header_bold") else True
ts["alignment"] = self.combo_align.get() if hasattr(self, "combo_align") else "center"
ts["font_name"] = self.entry_font_name.get() if hasattr(self, "entry_font_name") else "Calibri"
try:
ts["font_size"] = int(self.spin_font_size.get()) if hasattr(self, "spin_font_size") else 11
except:
ts["font_size"] = 11
ts["border_style"] = self.combo_border_style.get() if hasattr(self, "combo_border_style") else "тонкая"
ts["section_header_font_name"] = self.entry_sec_header_font_name.get() if hasattr(self, "entry_sec_header_font_name") else "Calibri"
try:
ts["section_header_font_size"] = int(self.spin_sec_header_font_size.get()) if hasattr(self, "spin_sec_header_font_size") else 12
except:
ts["section_header_font_size"] = 12
ts["section_header_bold"] = self.var_sec_header_bold.get() if hasattr(self, "var_sec_header_bold") else True
ts["section_header_align"] = self.combo_sec_header_align.get() if hasattr(self, "combo_sec_header_align") else "center"
ts["section_header_bg_color"] = self.entry_sec_header_bg_color.get() if hasattr(self, "entry_sec_header_bg_color") else "FFFF00"
settings.save()
def load_settings_to_gui(self):
d = settings.data
ex = d.get("export", {})
ts = d.get("table_style", {})
if hasattr(self, "combo_export_format"):
self.combo_export_format.set(ex.get("format", "docx"))
if hasattr(self, "var_compress_images"):
self.var_compress_images.set(ex.get("compress_images", True))
if hasattr(self, "entry_pdf_password"):
self.entry_pdf_password.delete(0, "end")
self.entry_pdf_password.insert(0, ex.get("pdf_password", ""))
if hasattr(self, "var_header_bold"):
self.var_header_bold.set(ts.get("header_bold", True))
if hasattr(self, "combo_align"):
self.combo_align.set(ts.get("alignment", "center"))
if hasattr(self, "entry_font_name"):
self.entry_font_name.delete(0, "end")
self.entry_font_name.insert(0, ts.get("font_name", "Calibri"))
if hasattr(self, "spin_font_size"):
self.spin_font_size.delete(0, "end")
self.spin_font_size.insert(0, ts.get("font_size", 11))
if hasattr(self, "combo_border_style"):
self.combo_border_style.set(ts.get("border_style", "тонкая"))
if hasattr(self, "entry_sec_header_font_name"):
self.entry_sec_header_font_name.delete(0, "end")
self.entry_sec_header_font_name.insert(0, ts.get("section_header_font_name", "Calibri"))
if hasattr(self, "spin_sec_header_font_size"):
self.spin_sec_header_font_size.delete(0, "end")
self.spin_sec_header_font_size.insert(0, ts.get("section_header_font_size", 12))
if hasattr(self, "var_sec_header_bold"):
self.var_sec_header_bold.set(ts.get("section_header_bold", True))
if hasattr(self, "combo_sec_header_align"):
self.combo_sec_header_align.set(ts.get("section_header_align", "center"))
if hasattr(self, "entry_sec_header_bg_color"):
self.entry_sec_header_bg_color.delete(0, "end")
self.entry_sec_header_bg_color.insert(0, ts.get("section_header_bg_color", "FFFF00"))
if hasattr(self, "combo_templates"):
self.load_templates()
tmpl = ex.get("template")
if self.templates:
if tmpl in self.templates:
self.combo_templates.set(tmpl)
self.current_template = tmpl
else:
self.combo_templates.set(self.templates[0])
self.current_template = self.templates[0]
def load_templates(self):
self.templates = sorted([f for f in os.listdir(TEMPLATES_DIR) if f.lower().endswith(".docx")])
if hasattr(self, "combo_templates"):
self.combo_templates["values"] = self.templates
if self.templates:
if self.current_template not in self.templates:
self.combo_templates.set(self.templates[0])
self.current_template = self.templates[0]
def generate_document(self, path=None, show_messages=True):
if self.basket_df.empty:
if show_messages:
messagebox.showwarning("Генерация", "Корзина пуста")
return
if not self.current_template:
if show_messages:
messagebox.showwarning("Генерация", "Шаблон не выбран")
return
self.save_gui_to_settings()
save_dir = None
if path is None:
save_dir = filedialog.askdirectory(title="Выберите папку для сохранения документов")
if not save_dir:
return
else:
save_dir = os.path.dirname(path)
try:
template_path = os.path.join(TEMPLATES_DIR, self.current_template)
phs_global = self.user_placeholders.copy()
unique_arts = list(dict.fromkeys(self.basket_df["Артикул"]))
for art in unique_arts:
doc = Document(template_path)
if art in self.grouped_data:
info = self.grouped_data[art]
art_info = {
"Объект": info["object_full"],
"Тип товара": info["product_type"],
"Артикул": art,
}
art_sections = info["sections"]
else:
art_info = {"Объект": "", "Тип товара": "", "Артикул": art}
art_sections = []
phs = dict(phs_global)
phs.update(art_info)
for section in art_sections:
for param in section["params"]:
pname = param["parameter"]
pvalue = param["value"]
if pname in phs and phs[pname] == "{{" + pname + "}}":
phs[pname] = pvalue
for k, v in phs.items():
docx_replace_placeholder(doc, k, v)
placeholder_paragraph = None
for p in doc.paragraphs:
if "{{Таблица}}" in p.text:
placeholder_paragraph = p
break
if placeholder_paragraph is None:
logger.log(f"Плейсхолдер {{Таблица}} не найден в шаблоне для артикула {art}")
if show_messages:
messagebox.showwarning("Генерация",
f"Шаблон не содержит плейсхолдера {{Таблица}} для артикула {art}")
filename = f"{datetime.now().strftime('%Y-%m-%d')}-паспорт IDIS-{art}.docx"
file_path = os.path.join(save_dir, filename)
doc.save(file_path)
continue
style = settings.data.get("table_style", {})
border_style = style.get("border_style", "тонкая")
previous_element = placeholder_paragraph._element
sh_font_name = style.get("section_header_font_name", "Calibri")
sh_font_size = style.get("section_header_font_size", 12)
sh_bold = style.get("section_header_bold", True)
sh_align = style.get("section_header_align", "center")
sh_bg_color = style.get("section_header_bg_color", "FFFF00")
for section in art_sections:
tbl = doc.add_table(rows=1, cols=2)
set_table_border(tbl, border_style)
row = tbl.rows[0]
cell = row.cells[0]
cell.merge(row.cells[1])
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
paragraph = cell.paragraphs[0]
paragraph.alignment = {
"left": WD_ALIGN_PARAGRAPH.LEFT,
"center": WD_ALIGN_PARAGRAPH.CENTER,
"right": WD_ALIGN_PARAGRAPH.RIGHT,
}.get(sh_align, WD_ALIGN_PARAGRAPH.CENTER)
for r in paragraph.runs:
r.text = ''
run = paragraph.add_run(section.get("header", ""))
run.font.name = sh_font_name
run.font.size = Pt(sh_font_size)
run.bold = sh_bold
set_cell_background(cell, sh_bg_color)
for param in section["params"]:
row_cells = tbl.add_row().cells
row_cells[0].text = str(param["parameter"])
row_cells[1].text = str(param["value"])
for i in (0, 1):
p_cell = row_cells[i].paragraphs[0]
p_cell.style = None
for r in p_cell.runs:
r.font.name = style.get("font_name", "Calibri")
r.font.size = Pt(style.get("font_size", 11))
r.bold = False
p_cell.alignment = {
"left": WD_ALIGN_PARAGRAPH.LEFT,
"right": WD_ALIGN_PARAGRAPH.RIGHT,
"center": WD_ALIGN_PARAGRAPH.CENTER,
}.get(style.get("alignment", "center"), WD_ALIGN_PARAGRAPH.CENTER)
previous_element.addnext(tbl._element)
previous_element = tbl._element
p = placeholder_paragraph._element
p.getparent().remove(p)
filename = f"{datetime.now().strftime('%Y-%m-%d')}-паспорт IDIS-{art}.docx"
file_path = os.path.join(save_dir, filename)
doc.save(file_path)
logger.log(f"Создан файл для артикула {art}: {file_path}")
if settings.data.get("export", {}).get("format") == "pdf":
pdf_path = os.path.splitext(file_path)[0] + ".pdf"
if WIN32COM_AVAILABLE:
convert_docx_to_pdf(file_path, pdf_path)
logger.log(f"PDF создан для артикула {art}: {pdf_path}")
else:
logger.log("Для создания PDF необходим MS Word и pywin32")
if show_messages:
messagebox.showinfo("Успех", f"Обработано товаров: {len(unique_arts)}\nФайлы сохранены в:\n{save_dir}")
except Exception as e:
traceback.print_exc()
logger.log(f"Ошибка генерации документа: {e}")
if show_messages:
messagebox.showerror("Ошибка генерации", str(e))
def load_excel_dialog(self):
path = filedialog.askopenfilename(title="Выберите Excel файл", filetypes=[("Excel Files", "*.xlsx *.xls")])
if not path:
return
try:
self.grouped_data = load_and_transform_excel_grouped(path)
param_dict = {}
for info in self.grouped_data.values():
for section in info['sections']:
for param in section['params']:
pname, pval = param['parameter'], param['value']
if pname not in param_dict:
param_dict[pname] = set()
param_dict[pname].add(pval)
self.all_params = sorted(param_dict.keys())
self.param_values = dict((k, sorted(v)) for k, v in param_dict.items())
for row in self.param_filter_rows:
row['param']['values'] = self.all_params
p = row['param'].get()
if p and p in self.param_values:
row['value']['values'] = [''] + self.param_values[p]
else:
row['value']['values'] = []
row['value'].set('')
rows = []
for art, info in self.grouped_data.items():
obj = info["object_full"]
ptype = info["product_type"]
rows.append({"Объект": obj, "Тип товара": ptype, "Артикул": art})
self.filtered_df = pd.DataFrame(rows)
self.filtered_df_backup = self.filtered_df.copy()
self.apply_all_filters()
self.basket_df = pd.DataFrame(columns=["Объект", "Тип товара", "Артикул"])
self.update_basket_table()
self.create_placeholders_from_loaded_products()
messagebox.showinfo("Успех", f"Данные загружены: {len(self.filtered_df_backup)} записей")
logger.log(f"Файл загружен: {path}, записей: {len(self.filtered_df_backup)}")
except Exception as e:
traceback.print_exc()
messagebox.showerror("Ошибка загрузки", str(e))
logger.log(f"Ошибка загрузки: {e}")
def update_data_table(self):
self.tree_data.delete(*self.tree_data.get_children())
if self.filtered_df.empty:
self.tree_data["columns"] = []
return
self.tree_data["columns"] = list(self.filtered_df.columns)
for col in self.filtered_df.columns:
self.tree_data.heading(col, text=col)
self.tree_data.column(col, width=160, anchor="w")
for _, row in self.filtered_df.iterrows():
self.tree_data.insert("", "end", values=list(row))
def quick_search(self, event=None):
self.apply_all_filters()
def show_item_params(self, event=None):
sel = self.tree_data.selection()
if not sel:
return
iid = sel[0]
item = self.tree_data.item(iid)
art = item["values"][2]
if art in self.grouped_data:
if self.grouped_data[art]["sections"]:
params = self.grouped_data[art]["sections"][0]["params"]
else:
params = []
self.tree_params.delete(*self.tree_params.get_children())
for param in params:
self.tree_params.insert("", "end", values=(param["parameter"], param["value"]))
def add_selected_to_basket(self, event=None):
sel = self.tree_data.selection()
if not sel:
messagebox.showinfo("Добавление", "Выделите записи для добавления в корзину")
return
rows = [self.tree_data.item(iid)["values"] for iid in sel]
df_add = pd.DataFrame(rows, columns=["Объект", "Тип товара", "Артикул"])
self.basket_df = pd.concat([self.basket_df, df_add], ignore_index=True)
self.basket_df.drop_duplicates(inplace=True)
self.basket_df.reset_index(drop=True, inplace=True)
self.update_basket_table()
logger.log(f"В корзину добавлено: {len(rows)} записей")
def update_basket_table(self):
self.tree_basket.delete(*self.tree_basket.get_children())
if self.basket_df.empty:
self.tree_basket["columns"] = []
return
self.tree_basket["columns"] = list(self.basket_df.columns)
for col in self.basket_df.columns:
self.tree_basket.heading(col, text=col)
self.tree_basket.column(col, width=160, anchor="w")
for _, row in self.basket_df.iterrows():
self.tree_basket.insert("", "end", values=list(row))
def remove_selected_from_basket(self):
sel = self.tree_basket.selection()
if not sel:
messagebox.showinfo("Удаление", "Выделите записи для удаления из корзины")
return
idxs = [self.tree_basket.index(iid) for iid in sel]
self.basket_df.drop(self.basket_df.index[idxs], inplace=True)
self.basket_df.reset_index(drop=True, inplace=True)
self.update_basket_table()
logger.log(f"Удалено из корзины: {len(idxs)} записей")
def clear_basket(self):
if messagebox.askyesno("Очистка корзины", "Очистить корзину?"):
self.basket_df = pd.DataFrame(columns=["Объект", "Тип товара", "Артикул"])
self.update_basket_table()
logger.log("Корзина очищена")
def context_menu_data(self, event):
iid = self.tree_data.identify_row(event.y)
if not iid:
return
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="Добавить в корзину", command=self.add_selected_to_basket)
menu.post(event.x_root, event.y_root)
def context_menu_basket(self, event):
iid = self.tree_basket.identify_row(event.y)
if not iid:
return
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="Удалить из корзины", command=self.remove_selected_from_basket)
menu.post(event.x_root, event.y_root)
def preview_template(self):
if not self.current_template:
messagebox.showwarning("Предпросмотр", "Шаблон не выбран")
return
path = os.path.join(TEMPLATES_DIR, self.current_template)
try:
if os.name == 'nt':
os.startfile(path)
else:
try:
subprocess.Popen(["open", path])
except:
subprocess.Popen(["xdg-open", path])
except Exception as e:
messagebox.showerror("Ошибка", f"Ошибка предпросмотра шаблона:\n{e}")
def save_profile(self):
try:
self.save_gui_to_settings()
if not self.selected_profile:
prof_name = simpledialog.askstring("Сохранить профиль", "Имя профиля (без расширения):", parent=self)
if not prof_name:
messagebox.showinfo("Отмена", "Сохранение отменено")
return
fname = prof_name.strip() + ".json"
else:
fname = self.selected_profile
if settings.save_profile(fname):
self.selected_profile = fname
self.load_profiles()
self.combo_profiles.set(fname)
messagebox.showinfo("Успех", f"Профиль {fname} сохранён")
except Exception as e:
messagebox.showerror("Ошибка", f"Ошибка сохранения профиля:\n{e}")
def load_profile(self):
prof = self.combo_profiles.get()
if not prof:
return
if settings.load_profile(prof):
self.selected_profile = prof
self.load_settings_to_gui()
messagebox.showinfo("Успех", f"Профиль {prof} загружен")
def load_profiles(self):
if not os.path.exists(PROFILES_DIR):
os.makedirs(PROFILES_DIR)
files = sorted([f for f in os.listdir(PROFILES_DIR) if f.endswith('.json')])
self.combo_profiles['values'] = files
if files and not self.selected_profile:
self.combo_profiles.set(files[0])
self.selected_profile = files[0]
def on_profile_change(self, event=None):
selected = self.combo_profiles.get()
if selected:
self.selected_profile = selected
if settings.load_profile(selected):
self.load_settings_to_gui()
messagebox.showinfo("Профиль", f"Профиль '{selected}' загружен")
else:
messagebox.showerror("Ошибка", f"Ошибка загрузки профиля: {selected}")
def on_template_change(self, event=None):
selected = self.combo_templates.get()
if selected:
self.current_template = selected
logger.log(f"Выбран шаблон: {selected}")
def load_placeholders_to_tree(self):
self.user_placeholders = placeholders.placeholders.copy()
self.refresh_placeholders_view()
def add_param_filter_row(self):
row_frame = ttk.Frame(self.param_filters_container)
row_frame.pack(fill="x", pady=2)
param_combo = ttk.Combobox(row_frame, state="readonly", width=25)
param_combo['values'] = self.all_params
param_combo.pack(side='left', padx=2)
param_combo.bind("<<ComboboxSelected>>", lambda e, c=param_combo, rf=row_frame: self.on_param_combo_change(c, rf))
value_combo = ttk.Combobox(row_frame, state="readonly", width=25)
value_combo.pack(side='left', padx=2)
value_combo.bind("<<ComboboxSelected>>", lambda e, c=param_combo, v=value_combo: self.on_param_value_change(c, v))
del_btn = ttk.Button(row_frame, text="✖", width=3,
command=lambda rf=row_frame: self.remove_param_filter_row(rf))
del_btn.pack(side='left', padx=2)
self.param_filter_rows.append({'frame': row_frame, 'param': param_combo, 'value': value_combo, 'del_btn': del_btn})
def remove_param_filter_row(self, row_frame):
self.param_filter_rows = [r for r in self.param_filter_rows if r['frame'] != row_frame]
row_frame.destroy()
self.apply_all_filters()
def on_param_combo_change(self, param_combo, row_frame):
param = param_combo.get()
value_combo = None
for row in self.param_filter_rows:
if row['frame'] == row_frame:
value_combo = row['value']
break
if value_combo:
values = []
if param and param in self.param_values:
values = sorted(self.param_values[param])
value_combo['values'] = [''] + values
value_combo.set('')
self.apply_all_filters()
def on_param_value_change(self, param_combo, value_combo):
self.apply_all_filters()
def collect_param_filters(self):
"""
Считывает все выбранные фильтры параметров из интерфейса,
возвращает dict параметр: множество значений для фильтра; фильтры без значения игнорируются
"""
res = {}
for row in self.param_filter_rows:
p = row['param'].get()
v = row['value'].get()
if p and v:
res.setdefault(p, set()).add(v)
return res
def apply_all_filters(self):
"""
Применяет все фильтры к исходным данным:
- быстрый поиск по артикулу
- фильтрация по параметрам (каждый параметр — набор возможных значений)
"""
if self.filtered_df_backup.empty:
return
df = self.filtered_df_backup.copy()
# Фильтр по артикулу (ввод в строку поиска)
q = self.var_quick_search.get().lower().strip()
if q:
df = df[df['Артикул'].str.lower().str.contains(q, na=False)]
param_filters = self.collect_param_filters()
if param_filters:
filtered_arts = []
for art in df['Артикул']:
info = self.grouped_data.get(art)
if not info:
continue
all_match = True
for p, vals in param_filters.items():
match = False
for section in info['sections']:
for param_dict in section['params']:
if param_dict['parameter'] == p and param_dict['value'] in vals:
match = True
break
if match:
break
if not match:
all_match = False
break
if all_match:
filtered_arts.append(art)
df = df[df['Артикул'].isin(filtered_arts)]
self.filtered_df = df
self.update_data_table()
def convert_docx_to_pdf(docx_path, pdf_path):
word = win32com.client.Dispatch("Word.Application")
word.Visible = False
doc = word.Documents.Open(docx_path)
doc.SaveAs(pdf_path, FileFormat=17)
doc.Close()
word.Quit()
def main():
app = App()
app.mainloop()
if __name__ == '__main__':
main()
или
import os
import json
import traceback
from datetime import datetime
import subprocess
import threading
import pandas as pd
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_ALIGN_VERTICAL
from docx.oxml.ns import qn, nsdecls
from docx.oxml import OxmlElement, parse_xml
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
try:
import win32com.client
WIN32COM_AVAILABLE = True
except ImportError:
WIN32COM_AVAILABLE = False
WORKDIR = os.getcwd()
TEMPLATES_DIR = os.path.join(WORKDIR, "templates")
CONFIG_DIR = os.path.join(WORKDIR, "config")
PROFILES_DIR = os.path.join(CONFIG_DIR, "profiles")
for d in (TEMPLATES_DIR, CONFIG_DIR, PROFILES_DIR):
os.makedirs(d, exist_ok=True)
SETTINGS_FILE = os.path.join(CONFIG_DIR, "settings.json")
PLACEHOLDERS_FILE = os.path.join(CONFIG_DIR, "placeholders.json")
MANDATORY_COLS = ['Объект', 'Свойство', 'Значение']
class Logger:
def __init__(self, ui_log_callback=None):
self.history = []
self.ui_log_callback = ui_log_callback
def log(self, msg):
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
record = f"[{ts}] {msg}"
self.history.append(record)
print(record)
if self.ui_log_callback:
self.ui_log_callback(record)
def get_text(self):
return "\n".join(self.history)
def save_to_file(self, path):
try:
with open(path, 'w', encoding='utf-8') as f:
f.write(self.get_text())
self.log(f"Лог сохранён: {path}")
except Exception as e:
self.log(f"Ошибка сохранения лога: {e}")
class Settings:
def __init__(self, logger=None):
self.logger = logger
self.data = {
"table_style": {
"header_bold": True,
"alignment": "center",
"font_name": "Calibri",
"font_size": 11,
"border_style": "тонкая",
"section_header_font_name": "Calibri",
"section_header_font_size": 12,
"section_header_bold": True,
"section_header_align": "center",
"section_header_bg_color": "FFFF00"
},
"export": {
"format": "docx",
"pdf_password": "",
"compress_images": True,
"template": None
}
}
self.load()
def _apply_defaults(self):
exp = self.data.setdefault('export', {})
exp.setdefault('format', 'docx')
exp.setdefault('pdf_password', '')
exp.setdefault('compress_images', True)
ts = self.data.setdefault('table_style', {})
defaults = {
"border_style": "тонкая",
"section_header_font_name": "Calibri",
"section_header_font_size": 12,
"section_header_bold": True,
"section_header_align": "center",
"section_header_bg_color": "FFFF00"
}
for k, v in defaults.items():
ts.setdefault(k, v)
def load(self):
if os.path.exists(SETTINGS_FILE):
try:
with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
d = json.load(f)
self.data.update(d)
self._apply_defaults()
if self.logger:
self.logger.log("Настройки загружены")
except Exception as e:
if self.logger:
self.logger.log(f"Ошибка загрузки настроек: {e}")
def save(self):
try:
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
json.dump(self.data, f, ensure_ascii=False, indent=4)
if self.logger:
self.logger.log("Настройки сохранены")
except Exception as e:
if self.logger:
self.logger.log(f"Ошибка сохранения настроек: {e}")
def save_profile(self, filename):
try:
with open(os.path.join(PROFILES_DIR, filename), 'w', encoding='utf-8') as f:
json.dump(self.data, f, ensure_ascii=False, indent=4)
if self.logger:
self.logger.log(f"Профиль сохранён: {filename}")
return True
except Exception as e:
if self.logger:
self.logger.log(f"Ошибка сохранения профиля: {e}")
return False
def load_profile(self, filename):
try:
with open(os.path.join(PROFILES_DIR, filename), 'r', encoding='utf-8') as f:
d = json.load(f)
self.data.update(d)
self._apply_defaults()
if self.logger:
self.logger.log(f"Профиль загружен: {filename}")
return True
except Exception as e:
if self.logger:
self.logger.log(f"Ошибка загрузки профиля: {e}")
return False
class Placeholders:
def __init__(self, logger=None):
self.placeholders = {}
self.logger = logger
self.load()
def load(self):
if os.path.isfile(PLACEHOLDERS_FILE):
try:
with open(PLACEHOLDERS_FILE, 'r', encoding='utf-8') as f:
self.placeholders = json.load(f)
if self.logger:
self.logger.log("Плейсхолдеры загружены")
except Exception as e:
if self.logger:
self.logger.log(f"Ошибка загрузки плейсхолдеров: {e}")
def save(self):
try:
with open(PLACEHOLDERS_FILE, 'w', encoding='utf-8') as f:
json.dump(self.placeholders, f, ensure_ascii=False, indent=4)
if self.logger:
self.logger.log("Плейсхолдеры сохранены")
except Exception as e:
if self.logger:
self.logger.log(f"Ошибка сохранения плейсхолдеров: {e}")
def split_by_idis(obj):
if not isinstance(obj, str) or not obj.strip():
return "", ""
low = obj.lower()
idx = low.find("idis")
if idx >= 0:
return obj[:idx].strip(), obj[idx + 4:].strip()
return obj, ""
def extract_article_full(obj):
_, article = split_by_idis(obj)
if article:
return article
words = obj.strip().split()
if len(words) <= 2:
return obj.strip()
return " ".join(words[-3:]).strip()
def extract_product_type(obj):
product_type, _ = split_by_idis(obj)
if product_type:
return product_type
words = obj.strip().split()
if len(words) <= 3:
return " ".join(words)
return " ".join(words[:3])
def extract_param(prop):
if not isinstance(prop, str):
return ""
return prop.split("_")[-1].strip()
def load_and_transform_excel_grouped(path):
df = pd.read_excel(path, engine="openpyxl")
missing = [c for c in MANDATORY_COLS if c not in df.columns]
if missing:
raise ValueError(f"Отсутствуют колонки: {missing}")
df["Тип товара"] = df["Объект"].apply(extract_product_type)
df["Артикул"] = df["Объект"].apply(extract_article_full)
df["Параметр"] = df["Свойство"].apply(extract_param)
df["Значение"] = df["Значение"].astype(str)
df = df[(df["Артикул"] != "") & (df["Параметр"] != "")].reset_index(drop=True)
grouped = {}
for art, gdf in df.groupby("Артикул"):
obj_full = gdf["Объект"].iloc[0]
prod_type = gdf["Тип товара"].iloc[0]
sections = []
current_header = None
current_params = []
for _, row in gdf.iterrows():
if row["Значение"].strip().lower() == "<раздел>":
if current_header is not None or current_params:
sections.append({"header": current_header, "params": current_params})
current_header = row["Параметр"]
current_params = []
else:
current_params.append({"parameter": row["Параметр"], "value": row["Значение"]})
if current_header is not None or current_params:
sections.append({"header": current_header, "params": current_params})
if not sections:
sections = [
{
"header": None,
"params": [{"parameter": r["Параметр"], "value": r["Значение"]} for _, r in gdf.iterrows()],
}
]
grouped[art] = {"object_full": obj_full, "product_type": prod_type, "sections": sections}
return grouped
def inline_replace_in_paragraph(paragraph, placeholder, replacement):
runs = paragraph.runs
full_text = "".join(run.text for run in runs)
if placeholder not in full_text:
return
new_text = full_text.replace(placeholder, replacement)
for run in runs:
run.text = ""
if runs:
runs[0].text = new_text
def docx_replace_placeholder(doc, key, value):
ph = "{{" + key + "}}"
for p in doc.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
for tbl in doc.tables:
for row in tbl.rows:
for cell in row.cells:
for p in cell.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
replace_placeholder_in_header_footer(doc, key, value)
def replace_placeholder_in_header_footer(doc, key, value):
ph = "{{" + key + "}}"
for section in doc.sections:
header = section.header
for p in header.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
for tbl in header.tables:
for row in tbl.rows:
for cell in row.cells:
for p in cell.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
footer = section.footer
for p in footer.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
for tbl in footer.tables:
for row in tbl.rows:
for cell in row.cells:
for p in cell.paragraphs:
if ph in p.text:
inline_replace_in_paragraph(p, ph, str(value))
def set_table_border(table, border_style):
borders_templates = {
'нет': """
<w:tblBorders %s>
<w:top w:val="nil"/>
<w:left w:val="nil"/>
<w:bottom w:val="nil"/>
<w:right w:val="nil"/>
<w:insideH w:val="nil"/>
<w:insideV w:val="nil"/>
</w:tblBorders>
""",
'тонкая': """
<w:tblBorders %s>
<w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/>
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/>
</w:tblBorders>
""",
'толстая': """
<w:tblBorders %s>
<w:top w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:left w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:bottom w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:right w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:insideH w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:insideV w:val="single" w:sz="12" w:space="0" w:color="000000"/>
</w:tblBorders>
""",
}
border_xml = borders_templates.get(border_style.lower(), borders_templates['тонкая']) % nsdecls('w')
tblPr = table._element.tblPr
for child in tblPr.findall("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tblBorders"):
tblPr.remove(child)
borders = parse_xml(border_xml)
tblPr.append(borders)
def set_cell_background(cell, hex_color):
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:fill'), hex_color)
tcPr.append(shd)
def convert_docx_to_pdf(docx_path, pdf_path):
if not WIN32COM_AVAILABLE:
raise RuntimeError("Для создания PDF необходим MS Word и pywin32")
word = win32com.client.Dispatch("Word.Application")
word.Visible = False
doc = word.Documents.Open(docx_path)
doc.SaveAs(pdf_path, FileFormat=17)
doc.Close()
word.Quit()
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Автоматизация техдокументации")
self.geometry("1200x720")
self.minsize(1000, 600)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.logger = Logger(ui_log_callback=self.append_log_to_text_widget)
self.settings = Settings(logger=self.logger)
self.placeholders = Placeholders(logger=self.logger)
self.grouped_data = {}
self.filtered_df = pd.DataFrame()
self.filtered_df_backup = pd.DataFrame()
self.basket_df = pd.DataFrame(columns=["Объект", "Тип товара", "Артикул"])
self.all_params = []
self.param_values = {}
self.user_placeholders = self.placeholders.placeholders.copy()
self.templates = []
self.current_template = None
self.selected_profile = None
self.param_filter_rows = []
self.var_quick_search = tk.StringVar()
self.var_placeholder_search = tk.StringVar()
self.var_header_bold = tk.BooleanVar()
self.var_compress_images = tk.BooleanVar()
self.var_sec_header_bold = tk.BooleanVar()
self.create_widgets()
self.create_menu()
self.bind_shortcuts()
self.load_templates()
self.load_profiles()
self.load_placeholders_to_tree()
self.load_settings_to_gui()
self.create_placeholders_from_loaded_products()
self.logger.log("Приложение запущено")
def append_log_to_text_widget(self, record):
# Реализация вывода лога в UI (можно добавить виджет Text для логов)
pass
def load_placeholders_to_tree(self):
self.user_placeholders = self.placeholders.placeholders.copy()
self.refresh_placeholders_view()
def refresh_placeholders_view(self, placeholders_dict=None):
if placeholders_dict is None:
placeholders_dict = self.user_placeholders
self.tree_placeholders.delete(*self.tree_placeholders.get_children())
for k in sorted(placeholders_dict.keys(), key=lambda s: s.lower()):
self.tree_placeholders.insert("", "end", values=(k, placeholders_dict[k]))
def on_placeholder_search(self, event=None):
query = self.var_placeholder_search.get().strip().lower()
filtered = {}
for k, v in self.user_placeholders.items():
if query in k.lower():
filtered[k] = v
self.refresh_placeholders_view(filtered)
def add_placeholder(self):
name = simpledialog.askstring("Добавить плейсхолдер", "Имя плейсхолдера:", parent=self)
if not name:
return
if name in self.user_placeholders:
messagebox.showerror("Ошибка", "Такой плейсхолдер уже существует")
return
value = simpledialog.askstring("Добавить плейсхолдер", "Значение плейсхолдера:", parent=self)
if value is None:
return
self.user_placeholders[name] = value
self.placeholders.placeholders = self.user_placeholders.copy()
self.placeholders.save()
self.refresh_placeholders_view()
self.logger.log(f"Добавлен плейсхолдер: {name} = {value}")
def edit_placeholder(self):
sel = self.tree_placeholders.selection()
if not sel:
messagebox.showinfo("Редактировать", "Выделите плейсхолдер")
return
iid = sel[0]
name, old_value = self.tree_placeholders.item(iid, "values")
new_value = simpledialog.askstring("Редактировать плейсхолдер", f"Значение плейсхолдера '{name}':", initialvalue=old_value, parent=self)
if new_value is None:
return
self.user_placeholders[name] = new_value
self.placeholders.placeholders = self.user_placeholders.copy()
self.placeholders.save()
self.refresh_placeholders_view()
self.logger.log(f"Изменён плейсхолдер: {name} = {new_value}")
def delete_placeholder(self):
sel = self.tree_placeholders.selection()
if not sel:
messagebox.showinfo("Удалить", "Выделите плейсхолдер")
return
iid = sel[0]
name = self.tree_placeholders.item(iid, "values")[0]
if messagebox.askyesno("Удалить плейсхолдер", f"Удалить плейсхолдер '{name}'?"):
self.user_placeholders.pop(name, None)
self.placeholders.placeholders = self.user_placeholders.copy()
self.placeholders.save()
self.refresh_placeholders_view()
self.logger.log(f"Удалён плейсхолдер: {name}")
def import_placeholders(self):
path = filedialog.askopenfilename(title="Импорт плейсхолдеров из JSON", filetypes=[("JSON", "*.json")])
if not path:
return
try:
with open(path, 'r', encoding='utf-8') as f:
d = json.load(f)
if not isinstance(d, dict):
raise ValueError("Файл должен содержать словарь плейсхолдеров")
self.user_placeholders.update(d)
self.placeholders.placeholders = self.user_placeholders.copy()
self.placeholders.save()
self.refresh_placeholders_view()
messagebox.showinfo("Импорт", f"Импортировано {len(d)} плейсхолдеров")
self.logger.log(f"Импортировано плейсхолдеров: {len(d)}")
except Exception as e:
messagebox.showerror("Ошибка импорта", str(e))
self.logger.log(f"Ошибка импорта плейсхолдеров: {e}")
def export_placeholders(self):
path = filedialog.asksaveasfilename(title="Экспорт плейсхолдеров в JSON", defaultextension=".json",
filetypes=[("JSON", "*.json")])
if not path:
return
try:
with open(path, 'w', encoding='utf-8') as f:
json.dump(self.user_placeholders, f, ensure_ascii=False, indent=4)
messagebox.showinfo("Экспорт", f"Плейсхолдеры экспортированы: {path}")
self.logger.log(f"Плейсхолдеры экспортированы: {path}")
except Exception as e:
messagebox.showerror("Ошибка экспорта", str(e))
self.logger.log(f"Ошибка экспорта плейсхолдеров: {e}")
def save_profile(self):
try:
self.save_gui_to_settings()
if not self.selected_profile:
prof_name = simpledialog.askstring("Сохранить профиль", "Имя профиля (без расширения):", parent=self)
if not prof_name:
messagebox.showinfo("Отмена", "Сохранение отменено")
return
fname = prof_name.strip() + ".json"
else:
fname = self.selected_profile
if self.settings.save_profile(fname):
self.selected_profile = fname
self.load_profiles()
self.combo_profiles.set(fname)
messagebox.showinfo("Успех", f"Профиль {fname} сохранён")
except Exception as e:
messagebox.showerror("Ошибка", f"Ошибка сохранения профиля:\n{e}")
def load_profile(self):
prof = self.combo_profiles.get()
if not prof:
return
if self.settings.load_profile(prof):
self.selected_profile = prof
self.load_settings_to_gui()
messagebox.showinfo("Успех", f"Профиль {prof} загружен")
def load_profiles(self):
if not os.path.exists(PROFILES_DIR):
os.makedirs(PROFILES_DIR)
files = sorted([f for f in os.listdir(PROFILES_DIR) if f.endswith('.json')])
self.combo_profiles['values'] = files
if files and not self.selected_profile:
self.combo_profiles.set(files[0])
self.selected_profile = files[0]
def on_profile_change(self, event=None):
selected = self.combo_profiles.get()
if selected:
self.selected_profile = selected
if self.settings.load_profile(selected):
self.load_settings_to_gui()
messagebox.showinfo("Профиль", f"Профиль '{selected}' загружен")
else:
messagebox.showerror("Ошибка", f"Ошибка загрузки профиля: {selected}")
def load_templates(self):
self.templates = sorted([f for f in os.listdir(TEMPLATES_DIR) if f.lower().endswith(".docx")])
self.combo_templates["values"] = self.templates
if self.templates:
if not self.current_template or self.current_template not in self.templates:
self.current_template = self.templates[0]
self.combo_templates.set(self.current_template)
def on_template_change(self, event=None):
selected = self.combo_templates.get()
if selected:
self.current_template = selected
self.logger.log(f"Выбран шаблон: {selected}")
def preview_template(self):
if not self.current_template:
messagebox.showwarning("Предпросмотр", "Шаблон не выбран")
return
path = os.path.join(TEMPLATES_DIR, self.current_template)
try:
if os.name == 'nt':
os.startfile(path)
else:
try:
subprocess.Popen(["open", path])
except:
subprocess.Popen(["xdg-open", path])
except Exception as e:
messagebox.showerror("Ошибка", f"Ошибка предпросмотра шаблона:\n{e}")
def export_log(self):
path = filedialog.asksaveasfilename(title="Сохранить лог", defaultextension=".txt",
filetypes=[("Text files", "*.txt")])
if not path:
return
self.logger.save_to_file(path)
messagebox.showinfo("Лог сохранён", f"Лог сохранён в файл:\n{path}")
def add_param_filter_row(self):
row_frame = ttk.Frame(self.param_filters_container)
row_frame.pack(fill="x", pady=2)
param_combo = ttk.Combobox(row_frame, state="readonly", width=25, postcommand=lambda: param_combo.configure(values=self.all_params))
param_combo.pack(side='left', padx=2)
param_combo.bind("<<ComboboxSelected>>", lambda e, c=param_combo, rf=row_frame: self.on_param_combo_change(c, rf))
value_combo = ttk.Combobox(row_frame, state="readonly", width=25)
value_combo.pack(side='left', padx=2)
value_combo.bind("<<ComboboxSelected>>", lambda e, c=param_combo, v=value_combo: self.on_param_value_change(c, v))
del_btn = ttk.Button(row_frame, text="✖", width=3,
command=lambda rf=row_frame: self.remove_param_filter_row(rf))
del_btn.pack(side='left', padx=2)
self.param_filter_rows.append({'frame': row_frame, 'param': param_combo, 'value': value_combo, 'del_btn': del_btn})
def remove_param_filter_row(self, row_frame):
self.param_filter_rows = [r for r in self.param_filter_rows if r['frame'] != row_frame]
row_frame.destroy()
self.apply_all_filters()
def on_param_combo_change(self, param_combo, row_frame):
param = param_combo.get()
value_combo = None
for row in self.param_filter_rows:
if row['frame'] == row_frame:
value_combo = row['value']
break
if value_combo:
values = []
if param and param in self.param_values:
values = sorted(self.param_values[param])
value_combo['values'] = [''] + values
value_combo.set('')
self.apply_all_filters()
def on_param_value_change(self, param_combo, value_combo):
self.apply_all_filters()
def collect_param_filters(self):
res = {}
for row in self.param_filter_rows:
p = row['param'].get()
v = row['value'].get()
if p and v:
res.setdefault(p, set()).add(v)
return res
def quick_search(self, event=None):
self.apply_all_filters()
def apply_all_filters(self):
if self.filtered_df_backup.empty:
return
df = self.filtered_df_backup.copy()
q = self.var_quick_search.get().lower().strip()
if q:
df = df[df['Артикул'].str.lower().str.contains(q, na=False)]
param_filters = self.collect_param_filters()
if param_filters:
filtered_arts = []
for art in df['Артикул']:
info = self.grouped_data.get(art)
if not info:
continue
all_match = True
for p, vals in param_filters.items():
match = any(
param_dict['parameter'] == p and param_dict['value'] in vals
for section in info['sections']
for param_dict in section['params']
)
if not match:
all_match = False
break
if all_match:
filtered_arts.append(art)
df = df[df['Артикул'].isin(filtered_arts)]
self.filtered_df = df
self.update_data_table()
def update_data_table(self):
self.tree_data.delete(*self.tree_data.get_children())
if self.filtered_df.empty:
self.tree_data["columns"] = []
return
self.tree_data["columns"] = list(self.filtered_df.columns)
for col in self.filtered_df.columns:
self.tree_data.heading(col, text=col)
self.tree_data.column(col, width=160, anchor="w")
for _, row in self.filtered_df.iterrows():
self.tree_data.insert("", "end", values=list(row))
def show_item_params(self, event=None):
sel = self.tree_data.selection()
if not sel:
return
iid = sel[0]
item = self.tree_data.item(iid)
if not item or not item.get("values"):
return
art = item["values"][2]
if art in self.grouped_data:
if self.grouped_data[art]["sections"]:
params = self.grouped_data[art]["sections"][0]["params"]
else:
params = []
self.tree_params.delete(*self.tree_params.get_children())
for param in params:
self.tree_params.insert("", "end", values=(param["parameter"], param["value"]))
def add_selected_to_basket(self, event=None):
sel = self.tree_data.selection()
if not sel:
messagebox.showinfo("Добавление", "Выделите записи для добавления в корзину")
return
rows = [self.tree_data.item(iid)["values"] for iid in sel]
df_add = pd.DataFrame(rows, columns=["Объект", "Тип товара", "Артикул"])
self.basket_df = pd.concat([self.basket_df, df_add], ignore_index=True)
self.basket_df.drop_duplicates(inplace=True)
self.basket_df.reset_index(drop=True, inplace=True)
self.update_basket_table()
self.logger.log(f"В корзину добавлено: {len(rows)} записей")
def update_basket_table(self):
self.tree_basket.delete(*self.tree_basket.get_children())
if self.basket_df.empty:
self.tree_basket["columns"] = []
return
self.tree_basket["columns"] = list(self.basket_df.columns)
for col in self.basket_df.columns:
self.tree_basket.heading(col, text=col)
self.tree_basket.column(col, width=160, anchor="w")
for _, row in self.basket_df.iterrows():
self.tree_basket.insert("", "end", values=list(row))
def remove_selected_from_basket(self):
sel = self.tree_basket.selection()
if not sel:
messagebox.showinfo("Удаление", "Выделите записи для удаления из корзины")
return
idxs = [self.tree_basket.index(iid) for iid in sel]
self.basket_df.drop(self.basket_df.index[idxs], inplace=True)
self.basket_df.reset_index(drop=True, inplace=True)
self.update_basket_table()
self.logger.log(f"Удалено из корзины: {len(idxs)} записей")
def clear_basket(self):
if messagebox.askyesno("Очистка корзины", "Очистить корзину?"):
self.basket_df = pd.DataFrame(columns=["Объект", "Тип товара", "Артикул"])
self.update_basket_table()
self.logger.log("Корзина очищена")
def save_gui_to_settings(self):
d = self.settings.data
exp = d.setdefault("export", {})
exp["format"] = self.combo_export_format.get() if hasattr(self, "combo_export_format") else "docx"
exp["compress_images"] = self.var_compress_images.get() if hasattr(self, "var_compress_images") else True
exp["pdf_password"] = self.entry_pdf_password.get() if hasattr(self, "entry_pdf_password") else ""
exp["template"] = self.combo_templates.get() if hasattr(self, "combo_templates") else None
ts = d.setdefault("table_style", {})
ts["header_bold"] = self.var_header_bold.get() if hasattr(self, "var_header_bold") else True
ts["alignment"] = self.combo_align.get() if hasattr(self, "combo_align") else "center"
ts["font_name"] = self.entry_font_name.get() if hasattr(self, "entry_font_name") else "Calibri"
try:
ts["font_size"] = int(self.spin_font_size.get()) if hasattr(self, "spin_font_size") else 11
except:
ts["font_size"] = 11
ts["border_style"] = self.combo_border_style.get() if hasattr(self, "combo_border_style") else "тонкая"
ts["section_header_font_name"] = self.entry_sec_header_font_name.get() if hasattr(self, "entry_sec_header_font_name") else "Calibri"
try:
ts["section_header_font_size"] = int(self.spin_sec_header_font_size.get()) if hasattr(self, "spin_sec_header_font_size") else 12
except:
ts["section_header_font_size"] = 12
ts["section_header_bold"] = self.var_sec_header_bold.get() if hasattr(self, "var_sec_header_bold") else True
ts["section_header_align"] = self.combo_sec_header_align.get() if hasattr(self, "combo_sec_header_align") else "center"
ts["section_header_bg_color"] = self.entry_sec_header_bg_color.get() if hasattr(self, "entry_sec_header_bg_color") else "FFFF00"
self.settings.save()
def load_settings_to_gui(self):
d = self.settings.data
ex = d.get("export", {})
ts = d.get("table_style", {})
if hasattr(self, "combo_export_format"):
self.combo_export_format.set(ex.get("format", "docx"))
if hasattr(self, "var_compress_images"):
self.var_compress_images.set(ex.get("compress_images", True))
if hasattr(self, "entry_pdf_password"):
self.entry_pdf_password.delete(0, "end")
self.entry_pdf_password.insert(0, ex.get("pdf_password", ""))
if hasattr(self, "var_header_bold"):
self.var_header_bold.set(ts.get("header_bold", True))
if hasattr(self, "combo_align"):
self.combo_align.set(ts.get("alignment", "center"))
if hasattr(self, "entry_font_name"):
self.entry_font_name.delete(0, "end")
self.entry_font_name.insert(0, ts.get("font_name", "Calibri"))
if hasattr(self, "spin_font_size"):
self.spin_font_size.delete(0, "end")
self.spin_font_size.insert(0, ts.get("font_size", 11))
if hasattr(self, "combo_border_style"):
self.combo_border_style.set(ts.get("border_style", "тонкая"))
if hasattr(self, "entry_sec_header_font_name"):
self.entry_sec_header_font_name.delete(0, "end")
self.entry_sec_header_font_name.insert(0, ts.get("section_header_font_name", "Calibri"))
if hasattr(self, "spin_sec_header_font_size"):
self.spin_sec_header_font_size.delete(0, "end")
self.spin_sec_header_font_size.insert(0, ts.get("section_header_font_size", 12))
if hasattr(self, "var_sec_header_bold"):
self.var_sec_header_bold.set(ts.get("section_header_bold", True))
if hasattr(self, "combo_sec_header_align"):
self.combo_sec_header_align.set(ts.get("section_header_align", "center"))
if hasattr(self, "entry_sec_header_bg_color"):
self.entry_sec_header_bg_color.delete(0, "end")
self.entry_sec_header_bg_color.insert(0, ts.get("section_header_bg_color", "FFFF00"))
if hasattr(self, "combo_templates"):
self.load_templates()
tmpl = ex.get("template")
if self.templates:
if tmpl in self.templates:
self.combo_templates.set(tmpl)
self.current_template = tmpl
else:
self.combo_templates.set(self.templates[0])
self.current_template = self.templates[0]
def load_excel_dialog(self):
path = filedialog.askopenfilename(title="Выберите Excel файл", filetypes=[("Excel Files", "*.xlsx *.xls")])
if not path:
return
def task():
try:
self.logger.log(f"Загрузка файла: {path}")
self.grouped_data = load_and_transform_excel_grouped(path)
param_dict = {}
for info in self.grouped_data.values():
for section in info['sections']:
for param in section['params']:
pname, pval = param['parameter'], param['value']
if pname not in param_dict:
param_dict[pname] = set()
param_dict[pname].add(pval)
self.all_params = sorted(param_dict.keys())
self.param_values = dict((k, sorted(v)) for k, v in param_dict.items())
for row in self.param_filter_rows:
row['param']['values'] = self.all_params
p = row['param'].get()
if p and p in self.param_values:
row['value']['values'] = [''] + self.param_values[p]
else:
row['value']['values'] = []
row['value'].set('')
rows = []
for art, info in self.grouped_data.items():
obj = info["object_full"]
ptype = info["product_type"]
rows.append({"Объект": obj, "Тип товара": ptype, "Артикул": art})
self.filtered_df = pd.DataFrame(rows)
self.filtered_df_backup = self.filtered_df.copy()
self.apply_all_filters()
self.basket_df = pd.DataFrame(columns=["Объект", "Тип товара", "Артикул"])
self.after(0, self.update_basket_table)
self.create_placeholders_from_loaded_products()
self.after(0, lambda: messagebox.showinfo("Успех", f"Данные загружены: {len(self.filtered_df_backup)} записей"))
self.logger.log(f"Файл загружен: {path}, записей: {len(self.filtered_df_backup)}")
except Exception as e:
self.after(0, lambda: messagebox.showerror("Ошибка загрузки", str(e)))
self.logger.log(f"Ошибка загрузки: {e}")
traceback.print_exc()
threading.Thread(target=task, daemon=True).start()
def create_placeholders_from_loaded_products(self):
if not self.grouped_data:
self.logger.log("Данные товаров отсутствуют - автоматическое создание плейсхолдеров пропущено")
return
unique_params = set()
for info in self.grouped_data.values():
for section in info['sections']:
for param in section['params']:
unique_params.add(param['parameter'])
updated = False
for param in unique_params:
if param not in self.user_placeholders:
self.user_placeholders[param] = "{{" + param + "}}"
updated = True
if updated:
self.placeholders.placeholders = self.user_placeholders.copy()
self.placeholders.save()
self.refresh_placeholders_view()
self.logger.log(f"Автоматически создано плейсхолдеров из параметров: {len(unique_params)}")
else:
self.logger.log("Автоматическое создание плейсхолдеров: новых плейсхолдеров не добавлено")
def generate_document(self, path=None, show_messages=True):
if self.basket_df.empty:
if show_messages:
messagebox.showwarning("Генерация", "Корзина пуста")
return
if not self.current_template:
if show_messages:
messagebox.showwarning("Генерация", "Шаблон не выбран")
return
self.save_gui_to_settings()
if path is None:
save_dir = filedialog.askdirectory(title="Выберите папку для сохранения документов")
if not save_dir:
return
else:
save_dir = os.path.dirname(path)
def task():
try:
template_path = os.path.join(TEMPLATES_DIR, self.current_template)
phs_global = self.user_placeholders.copy()
unique_arts = list(dict.fromkeys(self.basket_df["Артикул"]))
for art in unique_arts:
doc = Document(template_path)
if art in self.grouped_data:
info = self.grouped_data[art]
art_info = {
"Объект": info["object_full"],
"Тип товара": info["product_type"],
"Артикул": art,
}
art_sections = info["sections"]
else:
art_info = {"Объект": "", "Тип товара": "", "Артикул": art}
art_sections = []
phs = dict(phs_global)
phs.update(art_info)
for section in art_sections:
for param in section["params"]:
pname = param["parameter"]
pvalue = param["value"]
if pname in phs and phs[pname] == "{{" + pname + "}}":
phs[pname] = pvalue
for k, v in phs.items():
docx_replace_placeholder(doc, k, v)
placeholder_paragraph = None
for p in doc.paragraphs:
if "{{Таблица}}" in p.text:
placeholder_paragraph = p
break
if not placeholder_paragraph:
self.logger.log(f"Плейсхолдер {{Таблица}} не найден в шаблоне для артикула {art}")
if show_messages:
self.after(0, lambda art=art: messagebox.showwarning(
"Генерация", f"Шаблон не содержит плейсхолдера {{Таблица}} для артикула {art}"))
filename = f"{datetime.now().strftime('%Y-%m-%d')}-паспорт IDIS-{art}.docx"
file_path = os.path.join(save_dir, filename)
doc.save(file_path)
continue
style = self.settings.data.get("table_style", {})
border_style = style.get("border_style", "тонкая")
previous_element = placeholder_paragraph._element
sh_font_name = style.get("section_header_font_name", "Calibri")
sh_font_size = style.get("section_header_font_size", 12)
sh_bold = style.get("section_header_bold", True)
sh_align = style.get("section_header_align", "center")
sh_bg_color = style.get("section_header_bg_color", "FFFF00")
for section in art_sections:
tbl = doc.add_table(rows=1, cols=2)
set_table_border(tbl, border_style)
row = tbl.rows[0]
cell = row.cells[0]
cell.merge(row.cells[1])
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
paragraph = cell.paragraphs[0]
paragraph.alignment = {
"left": WD_ALIGN_PARAGRAPH.LEFT,
"center": WD_ALIGN_PARAGRAPH.CENTER,
"right": WD_ALIGN_PARAGRAPH.RIGHT,
}.get(sh_align, WD_ALIGN_PARAGRAPH.CENTER)
for r in paragraph.runs:
r.text = ''
run = paragraph.add_run(section.get("header", ""))
run.font.name = sh_font_name
run.font.size = Pt(sh_font_size)
run.bold = sh_bold
set_cell_background(cell, sh_bg_color)
for param in section["params"]:
row_cells = tbl.add_row().cells
row_cells[0].text = str(param["parameter"])
row_cells[1].text = str(param["value"])
for i in (0, 1):
p_cell = row_cells[i].paragraphs[0]
p_cell.style = None
for r in p_cell.runs:
r.font.name = style.get("font_name", "Calibri")
r.font.size = Pt(style.get("font_size", 11))
r.bold = False
p_cell.alignment = {
"left": WD_ALIGN_PARAGRAPH.LEFT,
"right": WD_ALIGN_PARAGRAPH.RIGHT,
"center": WD_ALIGN_PARAGRAPH.CENTER,
}.get(style.get("alignment", "center"), WD_ALIGN_PARAGRAPH.CENTER)
previous_element.addnext(tbl._element)
previous_element = tbl._element
p = placeholder_paragraph._element
p.getparent().remove(p)
filename = f"{datetime.now().strftime('%Y-%m-%d')}-паспорт IDIS-{art}.docx"
file_path = os.path.join(save_dir, filename)
doc.save(file_path)
self.logger.log(f"Создан файл для артикула {art}: {file_path}")
if self.settings.data.get("export", {}).get("format") == "pdf":
pdf_path = os.path.splitext(file_path)[0] + ".pdf"
try:
convert_docx_to_pdf(file_path, pdf_path)
self.logger.log(f"PDF создан для артикула {art}: {pdf_path}")
except Exception as e:
self.logger.log(f"Ошибка при создании PDF для артикула {art}: {e}")
if show_messages:
self.after(0, lambda: messagebox.showinfo(
"Успех", f"Обработано товаров: {len(unique_arts)}\nФайлы сохранены в:\n{save_dir}"))
except Exception as e:
traceback.print_exc()
self.logger.log(f"Ошибка генерации документа: {e}")
if show_messages:
self.after(0, lambda: messagebox.showerror("Ошибка генерации", str(e)))
threading.Thread(target=task, daemon=True).start()
def context_menu_data(self, event):
iid = self.tree_data.identify_row(event.y)
if not iid:
return
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="Добавить в корзину", command=self.add_selected_to_basket)
menu.tk_popup(event.x_root, event.y_root)
menu.grab_release()
def context_menu_basket(self, event):
iid = self.tree_basket.identify_row(event.y)
if not iid:
return
menu = tk.Menu(self, tearoff=0)
menu.add_command(label="Удалить из корзины", command=self.remove_selected_from_basket)
menu.tk_popup(event.x_root, event.y_root)
menu.grab_release()
def create_menu(self):
menubar = tk.Menu(self)
file_menu = tk.Menu(menubar, tearoff=0)
file_menu.add_command(label="Загрузить Excel", command=self.load_excel_dialog)
file_menu.add_separator()
file_menu.add_command(label="Сохранить лог...", command=self.export_log)
file_menu.add_separator()
file_menu.add_command(label="Выход", command=self.quit)
menubar.add_cascade(label="Файл", menu=file_menu)
basket_menu = tk.Menu(menubar, tearoff=0)
basket_menu.add_command(label="Удалить выделенные", command=self.remove_selected_from_basket)
basket_menu.add_command(label="Очистить корзину", command=self.clear_basket)
menubar.add_cascade(label="Корзина", menu=basket_menu)
help_menu = tk.Menu(menubar, tearoff=0)
help_menu.add_command(label="О программе", command=self.show_about)
menubar.add_cascade(label="Помощь", menu=help_menu)
self.config(menu=menubar)
def bind_shortcuts(self):
pass # Можно добавить горячие клавиши
def create_widgets(self):
self.nb = ttk.Notebook(self)
self.nb.grid(row=0, column=0, sticky="nsew")
self.create_tab_data()
self.create_tab_settings_gen()
self.create_basket_frame()
def create_tab_data(self):
tab_data = ttk.Frame(self.nb)
self.nb.add(tab_data, text="Данные")
tab_data.grid_rowconfigure(1, weight=1)
tab_data.grid_columnconfigure(0, weight=1)
tab_data.grid_columnconfigure(1, weight=0)
filter_frame = ttk.Frame(tab_data)
filter_frame.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
filter_frame.grid_columnconfigure(7, weight=1)
ttk.Label(filter_frame, text="Поиск по артикулу:").grid(row=0, column=0, sticky="w")
ent_quick_search = ttk.Entry(filter_frame, textvariable=self.var_quick_search)
ent_quick_search.grid(row=0, column=1, sticky="ew", padx=3)
filter_frame.grid_columnconfigure(1, weight=2)
ent_quick_search.bind("<KeyRelease>", self.quick_search)
ttk.Label(filter_frame, text="Фильтры параметров:").grid(row=0, column=2, sticky="w", padx=10)
self.btn_add_param_filter = ttk.Button(filter_frame, text="+ Фильтр", command=self.add_param_filter_row)
self.btn_add_param_filter.grid(row=0, column=7, sticky="e")
self.param_filters_container = ttk.Frame(filter_frame)
self.param_filters_container.grid(row=1, column=0, columnspan=8, sticky="ew", pady=5)
self.param_filters_container.grid_columnconfigure(0, weight=1)
self.param_filter_rows = []
tbl_frame = ttk.Frame(tab_data)
tbl_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
tbl_frame.grid_rowconfigure(0, weight=1)
tbl_frame.grid_columnconfigure(0, weight=1)
self.tree_data = ttk.Treeview(tbl_frame, show="headings", selectmode="extended")
self.tree_data.grid(row=0, column=0, sticky="nsew")
self.tree_data.bind("<Double-1>", self.add_selected_to_basket)
self.tree_data.bind("<<TreeviewSelect>>", self.show_item_params)
self.tree_data.bind("<Button-3>", self.context_menu_data)
scr_y = ttk.Scrollbar(tbl_frame, orient="vertical", command=self.tree_data.yview)
scr_y.grid(row=0, column=1, sticky="ns")
scr_x = ttk.Scrollbar(tbl_frame, orient="horizontal", command=self.tree_data.xview)
scr_x.grid(row=1, column=0, sticky="ew")
self.tree_data.configure(yscrollcommand=scr_y.set, xscrollcommand=scr_x.set)
right_frame = ttk.Frame(tab_data, relief="groove", borderwidth=2)
right_frame.grid(row=1, column=1, sticky="nsew", padx=5, pady=5)
tab_data.grid_columnconfigure(1, weight=0)
right_frame.grid_rowconfigure(1, weight=1)
right_frame.grid_columnconfigure(0, weight=1)
ttk.Label(right_frame, text="Параметры выбранного товара:").grid(row=0, column=0, sticky="w", padx=5, pady=(0, 5))
self.tree_params = ttk.Treeview(right_frame, show="headings", columns=["Параметр", "Значение"], height=20)
self.tree_params.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
self.tree_params.heading("Параметр", text="Параметр")
self.tree_params.heading("Значение", text="Значение")
self.tree_params.column("Параметр", width=150, anchor="w")
self.tree_params.column("Значение", width=150, anchor="w")
sb_params_y = ttk.Scrollbar(right_frame, orient="vertical", command=self.tree_params.yview)
sb_params_y.grid(row=1, column=1, sticky="ns")
self.tree_params.configure(yscrollcommand=sb_params_y.set)
def create_basket_frame(self):
basket_frame = ttk.LabelFrame(self, text="Корзина товаров")
basket_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
self.grid_rowconfigure(1, weight=1)
basket_frame.grid_rowconfigure(0, weight=1)
basket_frame.grid_columnconfigure(0, weight=1)
self.tree_basket = ttk.Treeview(basket_frame, show="headings", selectmode="extended", height=6)
self.tree_basket.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
self.tree_basket.bind("<Delete>", lambda e: self.remove_selected_from_basket())
self.tree_basket.bind("<Button-3>", self.context_menu_basket)
sb_basket_y = ttk.Scrollbar(basket_frame, orient="vertical", command=self.tree_basket.yview)
sb_basket_y.grid(row=0, column=1, sticky="ns", pady=5)
sb_basket_x = ttk.Scrollbar(basket_frame, orient="horizontal", command=self.tree_basket.xview)
sb_basket_x.grid(row=1, column=0, sticky="ew", padx=5)
self.tree_basket.configure(yscrollcommand=sb_basket_y.set, xscrollcommand=sb_basket_x.set)
ctrl_basket = ttk.Frame(basket_frame)
ctrl_basket.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=5)
ctrl_basket.grid_columnconfigure(3, weight=1)
ttk.Button(ctrl_basket, text="Удалить выделенные", command=self.remove_selected_from_basket).grid(row=0, column=0, padx=5)
ttk.Button(ctrl_basket, text="Очистить корзину", command=self.clear_basket).grid(row=0, column=1, padx=5)
ttk.Button(ctrl_basket, text="Генерировать документ", command=self.generate_document).grid(row=0, column=2, padx=5)
def create_tab_settings_gen(self):
tab = ttk.Frame(self.nb)
self.nb.add(tab, text="Настройки и Генерация")
tab.grid_columnconfigure(0, weight=1)
tab.grid_rowconfigure(6, weight=1)
prof_frame = ttk.LabelFrame(tab, text="Профили настроек")
prof_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5)
prof_frame.grid_columnconfigure(1, weight=1)
self.combo_profiles = ttk.Combobox(prof_frame, state='readonly')
self.combo_profiles.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
self.combo_profiles.bind("<<ComboboxSelected>>", self.on_profile_change)
ttk.Button(prof_frame, text="Загрузить профиль", command=self.load_profile).grid(row=0, column=1, padx=5)
ttk.Button(prof_frame, text="Сохранить профиль", command=self.save_profile).grid(row=0, column=2, padx=5)
ttk.Button(prof_frame, text="Обновить список", command=self.load_profiles).grid(row=0, column=3, padx=5)
ttk.Label(tab, text="Выбор шаблона:").grid(row=1, column=0, sticky='w', padx=10, pady=(0, 5))
self.combo_templates = ttk.Combobox(tab, state='readonly')
self.combo_templates.grid(row=2, column=0, sticky="ew", padx=10)
self.combo_templates.bind("<<ComboboxSelected>>", self.on_template_change)
ttk.Button(tab, text="Предпросмотр шаблона", command=self.preview_template).grid(row=3, column=0, sticky='w', padx=10, pady=5)
ttk.Label(tab, text="Плейсхолдеры:").grid(row=4, column=0, sticky='w', padx=10, pady=(5, 3))
ph_container = ttk.Frame(tab)
ph_container.grid(row=5, column=0, sticky="nsew", padx=10)
ph_container.grid_columnconfigure(0, weight=1)
ph_container.grid_rowconfigure(1, weight=1)
search_frame = ttk.Frame(ph_container)
search_frame.grid(row=0, column=0, sticky="ew")
search_frame.columnconfigure(1, weight=1)
ttk.Label(search_frame, text="Поиск:").grid(row=0, column=0, sticky="w")
ent_ph_search = ttk.Entry(search_frame, textvariable=self.var_placeholder_search)
ent_ph_search.grid(row=0, column=1, sticky="ew")
ent_ph_search.bind("<KeyRelease>", self.on_placeholder_search)
tree_frame = ttk.Frame(ph_container)
tree_frame.grid(row=1, column=0, sticky="nsew", pady=(5, 0))
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
self.tree_placeholders = ttk.Treeview(tree_frame, columns=("name", "value"), show='headings')
self.tree_placeholders.grid(row=0, column=0, sticky="nsew")
self.tree_placeholders.heading("name", text="Имя")
self.tree_placeholders.heading("value", text="Значение")
self.tree_placeholders.column("name", width=200, anchor="w")
self.tree_placeholders.column("value", width=400, anchor="w")
scrollbar_ph = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree_placeholders.yview)
scrollbar_ph.grid(row=0, column=1, sticky="ns")
self.tree_placeholders.configure(yscrollcommand=scrollbar_ph.set)
ph_btn_frame = ttk.Frame(tab)
ph_btn_frame.grid(row=7, column=0, sticky="ew", padx=10, pady=5)
for i, (text, cmd) in enumerate([
("Добавить", self.add_placeholder),
("Редактировать", self.edit_placeholder),
("Удалить", self.delete_placeholder),
("Импорт из JSON", self.import_placeholders),
("Экспорт в JSON", self.export_placeholders),
]):
ttk.Button(ph_btn_frame, text=text, command=cmd).grid(row=0, column=i, padx=5)
style_label = ttk.Label(tab, text="Параметры таблицы:")
style_label.grid(row=8, column=0, sticky='w', padx=10, pady=(10, 3))
style_frm = ttk.Frame(tab)
style_frm.grid(row=9, column=0, sticky="ew", padx=10, pady=5)
style_frm.columnconfigure(10, weight=1)
self.var_header_bold.set(self.settings.data['table_style'].get('header_bold', True))
ttk.Checkbutton(style_frm, text="Жирный заголовок", variable=self.var_header_bold).grid(row=0, column=0, padx=5, pady=2)
ttk.Label(style_frm, text="Выравнивание:").grid(row=0, column=1, padx=5, pady=2, sticky='w')
self.combo_align = ttk.Combobox(style_frm, values=['left', 'center', 'right'], state='readonly', width=10)
self.combo_align.set(self.settings.data['table_style'].get('alignment', 'center'))
self.combo_align.grid(row=0, column=2, padx=5, pady=2)
ttk.Label(style_frm, text="Шрифт:").grid(row=0, column=3, padx=5, pady=2, sticky='w')
self.entry_font_name = ttk.Entry(style_frm, width=15)
self.entry_font_name.insert(0, self.settings.data['table_style'].get('font_name', 'Calibri'))
self.entry_font_name.grid(row=0, column=4, padx=5, pady=2)
ttk.Label(style_frm, text="Размер:").grid(row=0, column=5, padx=5, pady=2, sticky='w')
self.spin_font_size = ttk.Spinbox(style_frm, from_=6, to=48, width=5)
self.spin_font_size.delete(0, 'end')
self.spin_font_size.insert(0, self.settings.data['table_style'].get('font_size', 11))
self.spin_font_size.grid(row=0, column=6, padx=5, pady=2)
ttk.Label(style_frm, text="Стиль рамки:").grid(row=0, column=7, padx=5, pady=2, sticky='w')
self.combo_border_style = ttk.Combobox(style_frm, values=['нет', 'тонкая', 'толстая'], state='readonly', width=10)
self.combo_border_style.set(self.settings.data['table_style'].get('border_style', 'тонкая'))
self.combo_border_style.grid(row=0, column=8, padx=5, pady=2)
ttk.Label(style_frm, text="Шрифт заголовка раздела:").grid(row=1, column=0, sticky='w', padx=5, pady=3)
self.entry_sec_header_font_name = ttk.Entry(style_frm, width=15)
self.entry_sec_header_font_name.grid(row=1, column=1, padx=5)
self.entry_sec_header_font_name.insert(0, self.settings.data['table_style'].get("section_header_font_name", "Calibri"))
ttk.Label(style_frm, text="Размер шрифта:").grid(row=1, column=2, sticky='w', padx=5, pady=3)
self.spin_sec_header_font_size = ttk.Spinbox(style_frm, from_=6, to=48, width=5)
self.spin_sec_header_font_size.grid(row=1, column=3, padx=5)
self.spin_sec_header_font_size.delete(0, "end")
self.spin_sec_header_font_size.insert(0, self.settings.data['table_style'].get("section_header_font_size", 12))
self.var_sec_header_bold.set(self.settings.data['table_style'].get("section_header_bold", True))
ttk.Checkbutton(style_frm, text="Жирный заголовок раздела", variable=self.var_sec_header_bold).grid(row=1, column=4, padx=5)
ttk.Label(style_frm, text="Выравнивание заголовка:").grid(row=1, column=5, sticky='w', padx=5, pady=3)
self.combo_sec_header_align = ttk.Combobox(style_frm, values=['left', 'center', 'right'], state='readonly', width=10)
self.combo_sec_header_align.grid(row=1, column=6, padx=5)
self.combo_sec_header_align.set(self.settings.data['table_style'].get("section_header_align", "center"))
ttk.Label(style_frm, text="Цвет фона заголовка (HEX):").grid(row=1, column=7, sticky='w', padx=5, pady=3)
self.entry_sec_header_bg_color = ttk.Entry(style_frm, width=12)
self.entry_sec_header_bg_color.grid(row=1, column=8, padx=5)
self.entry_sec_header_bg_color.insert(0, self.settings.data['table_style'].get("section_header_bg_color", "FFFF00"))
export_label = ttk.Label(tab, text="Настройки экспорта:")
export_label.grid(row=10, column=0, sticky='w', padx=10, pady=(10, 3))
export_frm = ttk.Frame(tab)
export_frm.grid(row=11, column=0, sticky="ew", padx=10, pady=5)
export_frm.grid_columnconfigure(8, weight=1)
ttk.Label(export_frm, text="Формат файла:").grid(row=0, column=0, sticky="w", padx=5)
self.combo_export_format = ttk.Combobox(export_frm, values=['docx', 'pdf'], state='readonly', width=10)
self.combo_export_format.set(self.settings.data['export'].get('format', 'docx'))
self.combo_export_format.grid(row=0, column=1, sticky="w", padx=5)
self.var_compress_images.set(self.settings.data['export'].get('compress_images', True))
ttk.Checkbutton(export_frm, text="Сжимать изображения", variable=self.var_compress_images).grid(row=0, column=2, sticky="w", padx=10)
ttk.Label(export_frm, text="Пароль для PDF:").grid(row=0, column=3, sticky="w", padx=5)
self.entry_pdf_password = ttk.Entry(export_frm, width=20, show='*')
self.entry_pdf_password.insert(0, self.settings.data['export'].get('pdf_password', ''))
self.entry_pdf_password.grid(row=0, column=4, sticky="w", padx=5)
ttk.Button(tab, text="Сохранить настройки в профиль", command=self.save_profile).grid(row=12, column=0, sticky="e", padx=10, pady=10)
def show_about(self):
messagebox.showinfo("О программе", "Автоматизация техдокументации\nВерсия 1.0")
def main():
app = App()
app.mainloop()
if __name__ == '__main__':
main()