test
This commit is contained in:
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
output
|
||||||
|
data
|
||||||
|
./data/
|
||||||
|
./output/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
*.log
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.DS_Store
|
||||||
|
*.idea/
|
||||||
|
*.vscode/
|
||||||
|
*.ipynb_checkpoints/
|
||||||
|
./.venv/
|
||||||
|
.venv/
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.14
|
||||||
284
main.py
Normal file
284
main.py
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
"""
|
||||||
|
Программа 17: Заполнение формы по выявленным-1
|
||||||
|
Графический интерфейс (tkinter) + запуск обработки.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog, messagebox, ttk
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from services.data_loader import load_ki, load_table2, build_mif_index
|
||||||
|
from services.form_filler import fill_form
|
||||||
|
from services.output_generator import make_filename, combine_forms, generate_table4
|
||||||
|
|
||||||
|
# ─────────────────────────────── Цвета / стиль ──────────────────────────── #
|
||||||
|
|
||||||
|
BG = "#1C3557" # тёмно-синий фон
|
||||||
|
BG_FRAME = "#243F6B" # фон панелей
|
||||||
|
FG = "#FFFFFF" # белый текст
|
||||||
|
FG_DIM = "#A8C4E0" # приглушённый текст
|
||||||
|
BTN_RUN = "#2E7D32" # зелёная кнопка «Запустить»
|
||||||
|
BTN_HELP = "#1565C0" # синяя кнопка «Справка»
|
||||||
|
FONT = ("Arial", 11)
|
||||||
|
FONT_H = ("Arial", 13, "bold")
|
||||||
|
FONT_LOG = ("Courier New", 9)
|
||||||
|
|
||||||
|
# ─────────────────────────────── Основная логика ────────────────────────── #
|
||||||
|
|
||||||
|
def run_processing(table1_path: str, table2_path: str, mif_dir: str,
|
||||||
|
template_path: str, output_dir: str,
|
||||||
|
log_callback, progress_callback, done_callback):
|
||||||
|
"""Выполняется в отдельном потоке."""
|
||||||
|
try:
|
||||||
|
out = Path(output_dir)
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
log_callback("[1/4] Загрузка исходных данных...")
|
||||||
|
progress_callback(10)
|
||||||
|
ki = load_ki(table1_path)
|
||||||
|
t2_rows = load_table2(table2_path)
|
||||||
|
mif_idx = build_mif_index(mif_dir)
|
||||||
|
log_callback(f" КИ: {next(iter(ki.values()), '?')}")
|
||||||
|
log_callback(f" Таблица 2: {len(t2_rows)} строк")
|
||||||
|
log_callback(f" MIF-файлов: {len(mif_idx)}")
|
||||||
|
|
||||||
|
log_callback("\n[2/4] Генерация форм...")
|
||||||
|
generated, errors = [], []
|
||||||
|
total = sum(1 for r in t2_rows if r[0])
|
||||||
|
|
||||||
|
for i, t2_row in enumerate(t2_rows, 1):
|
||||||
|
zu_file = t2_row[0] if t2_row[0] else ""
|
||||||
|
kv = t2_row[1] if t2_row[1] else ""
|
||||||
|
if not zu_file:
|
||||||
|
continue
|
||||||
|
filename = make_filename(str(kv), str(zu_file))
|
||||||
|
out_path = out / filename
|
||||||
|
log_callback(f" [{len(generated)+len(errors)+1}/{total}] {filename} ...", end="")
|
||||||
|
try:
|
||||||
|
fill_form(template_path, str(out_path), ki, t2_row, mif_idx)
|
||||||
|
generated.append({"name": filename, "path": str(out_path),
|
||||||
|
"kv": str(kv), "zu_file": str(zu_file)})
|
||||||
|
log_callback(" OK")
|
||||||
|
except Exception as exc:
|
||||||
|
log_callback(f" ОШИБКА: {exc}")
|
||||||
|
errors.append((filename, str(exc)))
|
||||||
|
progress_callback(10 + int(70 * (len(generated) + len(errors)) / max(total, 1)))
|
||||||
|
|
||||||
|
log_callback(f"\n Создано: {len(generated)}, ошибок: {len(errors)}")
|
||||||
|
|
||||||
|
if generated:
|
||||||
|
log_callback("\n[3/4] Создание объединённого файла...")
|
||||||
|
progress_callback(82)
|
||||||
|
combined = out / "ВСЕ_ФОРМЫ.xlsx"
|
||||||
|
try:
|
||||||
|
combine_forms([r["path"] for r in generated], str(combined))
|
||||||
|
log_callback(f" → {combined}")
|
||||||
|
except Exception as exc:
|
||||||
|
log_callback(f" ОШИБКА объединения: {exc}")
|
||||||
|
|
||||||
|
log_callback("\n[4/4] Создание Таблицы 4...")
|
||||||
|
progress_callback(95)
|
||||||
|
t4 = out / "Таблица4_Перечень_форм.xlsx"
|
||||||
|
generate_table4(generated, str(t4))
|
||||||
|
log_callback(f" → {t4}")
|
||||||
|
|
||||||
|
progress_callback(100)
|
||||||
|
done_callback(generated, errors, str(out))
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
log_callback(f"\nКРИТИЧЕСКАЯ ОШИБКА:\n{traceback.format_exc()}")
|
||||||
|
done_callback([], [(str(exc), str(exc))], output_dir)
|
||||||
|
|
||||||
|
# ─────────────────────────────── GUI ────────────────────────────────────── #
|
||||||
|
|
||||||
|
class App(tk.Tk):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.title("Программа 17 — Выявленные-1")
|
||||||
|
self.geometry("700x600")
|
||||||
|
self.minsize(600, 550)
|
||||||
|
self.configure(bg=BG)
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
# ── Построение интерфейса ────────────────────────────────────────────── #
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
# Заголовок
|
||||||
|
tk.Label(self, text="Программа 17: Заполнение формы по Выявленным-1",
|
||||||
|
font=FONT_H, bg=BG, fg=FG).pack(pady=(14, 8))
|
||||||
|
|
||||||
|
# Карточка с полями ввода
|
||||||
|
card = tk.Frame(self, bg=BG_FRAME, bd=0)
|
||||||
|
card.pack(fill=tk.X, padx=18, pady=4)
|
||||||
|
|
||||||
|
self.var_t1 = self._add_field(card, "Таблица 1 (КИ):", "xlsx", 0)
|
||||||
|
self.var_t2 = self._add_field(card, "Таблица 2 (данные):", "xlsx", 1)
|
||||||
|
self.var_mif = self._add_field(card, "Папка MIF:", "dir", 2)
|
||||||
|
self.var_template = self._add_field(card, "Шаблон XLSX:", "xlsx", 3)
|
||||||
|
self.var_output = self._add_field(card, "Папка вывода:", "dir", 4)
|
||||||
|
|
||||||
|
# Дефолты
|
||||||
|
base = Path(__file__).parent
|
||||||
|
data = base / "data" / "Исх.данные"
|
||||||
|
self.var_t1.set(str(data / "Таблица1 КИ-Неустроева.xlsx"))
|
||||||
|
self.var_t2.set(str(data / "Таблица 2 Перечень выявленных исх.данные.xlsx"))
|
||||||
|
self.var_mif.set(str(data / "1. 74 34 0400007 ГК вымпел-73"))
|
||||||
|
self.var_template.set(str(base / "data" / "ШАБЛОН ВЫЯВЛЕННЫЕ-1.xlsx"))
|
||||||
|
self.var_output.set(str(base / "output"))
|
||||||
|
|
||||||
|
# Прогресс-бар
|
||||||
|
pf = tk.Frame(self, bg=BG)
|
||||||
|
pf.pack(fill=tk.X, padx=18, pady=(10, 2))
|
||||||
|
self.progress = ttk.Progressbar(pf, length=600, mode="determinate")
|
||||||
|
self.progress.pack(fill=tk.X)
|
||||||
|
self.progress_label = tk.Label(pf, text="Готов к работе",
|
||||||
|
font=("Arial", 9), bg=BG, fg=FG_DIM)
|
||||||
|
self.progress_label.pack(anchor="w")
|
||||||
|
|
||||||
|
# Лог
|
||||||
|
lf = tk.Frame(self, bg=BG)
|
||||||
|
lf.pack(fill=tk.BOTH, expand=True, padx=18, pady=(4, 4))
|
||||||
|
tk.Label(lf, text="Журнал:", font=("Arial", 9), bg=BG, fg=FG_DIM).pack(anchor="w")
|
||||||
|
self.log_text = tk.Text(lf, font=FONT_LOG, bg="#0D1F33", fg="#C8E6FF",
|
||||||
|
height=10, state=tk.DISABLED, wrap=tk.WORD)
|
||||||
|
sb = tk.Scrollbar(lf, command=self.log_text.yview)
|
||||||
|
self.log_text.configure(yscrollcommand=sb.set)
|
||||||
|
sb.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Кнопки
|
||||||
|
bf = tk.Frame(self, bg=BG)
|
||||||
|
bf.pack(fill=tk.X, padx=18, pady=(6, 12))
|
||||||
|
tk.Button(bf, text="Справка", command=self._show_help,
|
||||||
|
font=FONT, bg=BTN_HELP, fg=FG,
|
||||||
|
padx=14, pady=8, relief=tk.FLAT).pack(side=tk.LEFT)
|
||||||
|
self.btn_run = tk.Button(bf, text="▶ Запустить",
|
||||||
|
command=self._start, font=FONT_H,
|
||||||
|
bg=BTN_RUN, fg=FG, padx=20, pady=8, relief=tk.FLAT)
|
||||||
|
self.btn_run.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
def _add_field(self, parent, label: str, kind: str, row_idx: int):
|
||||||
|
"""Добавить строку: метка + поле + кнопка «...»."""
|
||||||
|
var = tk.StringVar()
|
||||||
|
tk.Label(parent, text=label, font=FONT, bg=BG_FRAME, fg=FG,
|
||||||
|
anchor="w").grid(row=row_idx * 2, column=0, columnspan=3,
|
||||||
|
sticky="w", padx=12, pady=(8, 0))
|
||||||
|
entry = tk.Entry(parent, textvariable=var, font=FONT, width=55)
|
||||||
|
entry.grid(row=row_idx * 2 + 1, column=0, padx=(12, 4), pady=(0, 2), sticky="ew")
|
||||||
|
btn = tk.Button(parent, text="…", font=FONT, width=3,
|
||||||
|
command=lambda k=kind, v=var: self._browse(k, v))
|
||||||
|
btn.grid(row=row_idx * 2 + 1, column=1, padx=(0, 12), pady=(0, 2))
|
||||||
|
parent.columnconfigure(0, weight=1)
|
||||||
|
return var
|
||||||
|
|
||||||
|
def _browse(self, kind: str, var: tk.StringVar):
|
||||||
|
if kind == "dir":
|
||||||
|
path = filedialog.askdirectory(title="Выберите папку")
|
||||||
|
else:
|
||||||
|
path = filedialog.askopenfilename(
|
||||||
|
title="Выберите файл",
|
||||||
|
filetypes=[("Excel файлы", "*.xlsx *.xls"), ("Все файлы", "*.*")]
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
var.set(path)
|
||||||
|
|
||||||
|
# ── Запуск ──────────────────────────────────────────────────────────── #
|
||||||
|
|
||||||
|
def _start(self):
|
||||||
|
t1 = self.var_t1.get().strip()
|
||||||
|
t2 = self.var_t2.get().strip()
|
||||||
|
mif = self.var_mif.get().strip()
|
||||||
|
tmpl = self.var_template.get().strip()
|
||||||
|
out = self.var_output.get().strip()
|
||||||
|
|
||||||
|
missing = [name for name, v in [("Таблица 1", t1), ("Таблица 2", t2),
|
||||||
|
("Папка MIF", mif), ("Шаблон", tmpl),
|
||||||
|
("Папка вывода", out)] if not v]
|
||||||
|
if missing:
|
||||||
|
messagebox.showerror("Ошибка", "Не заполнены поля:\n• " + "\n• ".join(missing))
|
||||||
|
return
|
||||||
|
|
||||||
|
self._log_clear()
|
||||||
|
self._set_progress(0, "Запуск...")
|
||||||
|
self.btn_run.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=run_processing,
|
||||||
|
args=(t1, t2, mif, tmpl, out,
|
||||||
|
self._log_append,
|
||||||
|
lambda v: self.after(0, self._set_progress, v, None),
|
||||||
|
self._on_done),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
# ── Обратные вызовы из потока ────────────────────────────────────────── #
|
||||||
|
|
||||||
|
def _log_append(self, text: str, end: str = "\n"):
|
||||||
|
def _do():
|
||||||
|
self.log_text.config(state=tk.NORMAL)
|
||||||
|
self.log_text.insert(tk.END, text + end)
|
||||||
|
self.log_text.see(tk.END)
|
||||||
|
self.log_text.config(state=tk.DISABLED)
|
||||||
|
self.after(0, _do)
|
||||||
|
|
||||||
|
def _log_clear(self):
|
||||||
|
self.log_text.config(state=tk.NORMAL)
|
||||||
|
self.log_text.delete("1.0", tk.END)
|
||||||
|
self.log_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def _set_progress(self, value: int, label_text=None):
|
||||||
|
self.progress["value"] = value
|
||||||
|
if label_text is not None:
|
||||||
|
self.progress_label.config(text=label_text)
|
||||||
|
elif value == 100:
|
||||||
|
self.progress_label.config(text="Готово!")
|
||||||
|
|
||||||
|
def _on_done(self, generated: list, errors: list, out_dir: str):
|
||||||
|
def _do():
|
||||||
|
self.btn_run.config(state=tk.NORMAL)
|
||||||
|
if errors and not generated:
|
||||||
|
messagebox.showerror("Ошибка", f"Обработка завершилась с ошибками:\n{errors[0][1]}")
|
||||||
|
return
|
||||||
|
msg = (
|
||||||
|
f"Обработка завершена!\n\n"
|
||||||
|
f"Создано форм: {len(generated)}\n"
|
||||||
|
f"Ошибок: {len(errors)}\n\n"
|
||||||
|
f"Результаты в папке:\n{out_dir}"
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
msg += f"\n\nФайлы с ошибками ({len(errors)}):\n" + \
|
||||||
|
"\n".join(f"• {n}" for n, _ in errors[:10])
|
||||||
|
messagebox.showinfo("Готово", msg)
|
||||||
|
self.after(0, _do)
|
||||||
|
|
||||||
|
# ── Справка ─────────────────────────────────────────────────────────── #
|
||||||
|
|
||||||
|
def _show_help(self):
|
||||||
|
messagebox.showinfo("Справка", """\
|
||||||
|
Программа 17: Заполнение формы по Выявленным-1
|
||||||
|
|
||||||
|
Поля ввода:
|
||||||
|
• Таблица 1 — файл xlsx с данными кадастрового инженера
|
||||||
|
• Таблица 2 — файл xlsx со списком выявленных объектов
|
||||||
|
• Папка MIF — папка с MIF-файлами контуров (*.mif)
|
||||||
|
• Шаблон — файл шаблона XLSX
|
||||||
|
• Папка вывода — куда сохранять результаты
|
||||||
|
|
||||||
|
Результаты:
|
||||||
|
• Отдельный xlsx для каждой строки Таблицы 2
|
||||||
|
• ВСЕ_ФОРМЫ.xlsx — все формы на одном листе
|
||||||
|
• Таблица4_Перечень_форм.xlsx — перечень файлов
|
||||||
|
|
||||||
|
Шрифт всех ячеек: Times New Roman 11 pt.
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────────────────────────── Точка входа ────────────────────────────── #
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = App()
|
||||||
|
app.mainloop()
|
||||||
9
pyproject.toml
Normal file
9
pyproject.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[project]
|
||||||
|
name = "kadastr-17"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = [
|
||||||
|
"openpyxl>=3.1",
|
||||||
|
]
|
||||||
229
read_mif_file.py
Normal file
229
read_mif_file.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Any, Optional, Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MifFile:
|
||||||
|
name: str
|
||||||
|
path: str
|
||||||
|
headers: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
features: List[Dict[str, Any]] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
def _unquote(s: str) -> str:
|
||||||
|
s = s.strip()
|
||||||
|
if s.startswith('"') and s.endswith('"'):
|
||||||
|
return s[1:-1]
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def parse_mif(file_path: str) -> Union[MifFile, List[MifFile]]:
|
||||||
|
"""
|
||||||
|
Примитивный парсер MIF-файла. Обрабатывает заголовки (Version, Charset, Delimiter,
|
||||||
|
CoordSys, Columns) и простые объекты в блоке Data: Region, Text, Center/Centre, Pen, Brush.
|
||||||
|
|
||||||
|
Не претендует на полную спецификацию MapInfo MIF, но удобен для быстрого извлечения структур.
|
||||||
|
"""
|
||||||
|
p = Path(file_path)
|
||||||
|
if not p.exists():
|
||||||
|
raise FileNotFoundError(file_path)
|
||||||
|
|
||||||
|
# If a directory is passed, parse all .mif files inside and return a list
|
||||||
|
if p.is_dir():
|
||||||
|
return load_mif_files(str(p))
|
||||||
|
|
||||||
|
mif = MifFile(name=p.name, path=str(p.resolve()))
|
||||||
|
lines = [ln.rstrip('\n') for ln in p.read_text(encoding='utf-8', errors='replace').splitlines()]
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
n = len(lines)
|
||||||
|
# parse header until "Data"
|
||||||
|
while i < n:
|
||||||
|
line = lines[i].strip()
|
||||||
|
if not line:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
up = line.upper()
|
||||||
|
if up.startswith('VERSION'):
|
||||||
|
mif.headers['version'] = line.split(None, 1)[1].strip() if len(line.split()) > 1 else None
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if up.startswith('CHARSET'):
|
||||||
|
# Charset "WindowsCyrillic"
|
||||||
|
rest = line.split(None, 1)[1] if len(line.split()) > 1 else ''
|
||||||
|
mif.headers['charset'] = _unquote(rest)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if up.startswith('DELIMITER'):
|
||||||
|
rest = line.split(None, 1)[1] if len(line.split()) > 1 else ''
|
||||||
|
mif.headers['delimiter'] = _unquote(rest)
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if up.startswith('COORDSYS'):
|
||||||
|
mif.headers['coordsys'] = line[len('CoordSys'):].strip()
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if up.startswith('COLUMNS'):
|
||||||
|
# next N lines are column definitions
|
||||||
|
parts = line.split()
|
||||||
|
cnt = int(parts[1]) if len(parts) > 1 else 0
|
||||||
|
i += 1
|
||||||
|
cols: List[str] = []
|
||||||
|
for _ in range(cnt):
|
||||||
|
if i >= n:
|
||||||
|
break
|
||||||
|
cols.append(lines[i].strip())
|
||||||
|
i += 1
|
||||||
|
mif.headers['columns'] = cols
|
||||||
|
continue
|
||||||
|
if up == 'DATA':
|
||||||
|
i += 1
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# parse data features
|
||||||
|
while i < n:
|
||||||
|
raw = lines[i].lstrip()
|
||||||
|
if not raw:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
parts = raw.split(None, 1)
|
||||||
|
typ = parts[0]
|
||||||
|
if typ.upper() == 'REGION':
|
||||||
|
feat: Dict[str, Any] = {'type': 'Region', 'props': {}}
|
||||||
|
# Region <N_polygons> — количество контуров (полигонов) в объекте
|
||||||
|
try:
|
||||||
|
n_polygons = int(parts[1]) if len(parts) > 1 else 1
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
n_polygons = 1
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
all_points: List[Tuple[float, float]] = []
|
||||||
|
contours: List[List[Tuple[float, float]]] = []
|
||||||
|
|
||||||
|
for _poly in range(n_polygons):
|
||||||
|
# Первая строка каждого контура — количество точек
|
||||||
|
if i >= n:
|
||||||
|
break
|
||||||
|
cnt_line = lines[i].strip()
|
||||||
|
try:
|
||||||
|
pts_cnt = int(cnt_line)
|
||||||
|
i += 1
|
||||||
|
except ValueError:
|
||||||
|
pts_cnt = 0
|
||||||
|
|
||||||
|
poly_pts: List[Tuple[float, float]] = []
|
||||||
|
for _ in range(pts_cnt):
|
||||||
|
if i >= n:
|
||||||
|
break
|
||||||
|
xy = lines[i].strip().split()
|
||||||
|
if len(xy) >= 2:
|
||||||
|
try:
|
||||||
|
poly_pts.append((float(xy[0]), float(xy[1])))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if poly_pts:
|
||||||
|
contours.append(poly_pts)
|
||||||
|
all_points.extend(poly_pts)
|
||||||
|
|
||||||
|
# Если один контур — points == список точек (как было раньше)
|
||||||
|
# Если несколько — points == точки всех контуров подряд, contours — отдельно
|
||||||
|
feat['props']['points'] = all_points
|
||||||
|
feat['props']['contours'] = contours # List[List[(x,y)]]
|
||||||
|
|
||||||
|
# Читаем атрибуты (Pen, Brush, Center) до начала следующего объекта
|
||||||
|
while i < n:
|
||||||
|
t = lines[i].lstrip()
|
||||||
|
if not t:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
up2 = t.upper()
|
||||||
|
if up2.startswith('PEN'):
|
||||||
|
feat['props']['pen'] = t[len('Pen'):].strip()
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if up2.startswith('BRUSH'):
|
||||||
|
feat['props']['brush'] = t[len('Brush'):].strip()
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if up2.startswith('CENTER') or up2.startswith('CENTRE'):
|
||||||
|
rest = t.split(None, 1)[1] if len(t.split()) > 1 else ''
|
||||||
|
coords = rest.split()
|
||||||
|
try:
|
||||||
|
cx = float(coords[0]); cy = float(coords[1])
|
||||||
|
feat['props']['center'] = (cx, cy)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if up2.split()[0] in ('REGION', 'TEXT', 'POINT', 'PLINE', 'LINE'):
|
||||||
|
break
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
mif.features.append(feat)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if typ.upper() == 'TEXT':
|
||||||
|
feat = {'type': 'Text', 'props': {}}
|
||||||
|
# next line: quoted string
|
||||||
|
i += 1
|
||||||
|
if i < n:
|
||||||
|
txt_line = lines[i].strip()
|
||||||
|
feat['props']['text'] = _unquote(txt_line)
|
||||||
|
i += 1
|
||||||
|
# next line: bounding coordinates (often 4 numbers)
|
||||||
|
if i < n:
|
||||||
|
coords = lines[i].strip().split()
|
||||||
|
try:
|
||||||
|
nums = [float(x) for x in coords]
|
||||||
|
feat['props']['bbox'] = nums
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
i += 1
|
||||||
|
# optional Font line
|
||||||
|
if i < n and lines[i].lstrip().upper().startswith('FONT'):
|
||||||
|
feat['props']['font'] = lines[i].lstrip()[len('Font'):].strip()
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
mif.features.append(feat)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# simple POINT parser
|
||||||
|
if typ.upper() == 'POINT':
|
||||||
|
feat = {'type': 'Point', 'props': {}}
|
||||||
|
rest = parts[1] if len(parts) > 1 else ''
|
||||||
|
coords = rest.split()
|
||||||
|
try:
|
||||||
|
feat['props']['xy'] = (float(coords[0]), float(coords[1]))
|
||||||
|
except Exception:
|
||||||
|
feat['props']['xy'] = None
|
||||||
|
i += 1
|
||||||
|
mif.features.append(feat)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# unknown / unsupported object: skip one line
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return mif
|
||||||
|
|
||||||
|
|
||||||
|
def load_mif_files(dir_path: str) -> List[MifFile]:
|
||||||
|
"""Найти все .mif файлы в директории (рекурсивно) и распарсить их в объекты MifFile."""
|
||||||
|
root = Path(dir_path)
|
||||||
|
if not root.exists():
|
||||||
|
raise FileNotFoundError(dir_path)
|
||||||
|
|
||||||
|
mif_files: List[MifFile] = []
|
||||||
|
for p in root.rglob('*.mif'):
|
||||||
|
try:
|
||||||
|
mif_files.append(parse_mif(str(p)))
|
||||||
|
except Exception:
|
||||||
|
# при ошибке парсинга — пропустить файл
|
||||||
|
continue
|
||||||
|
return mif_files
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["MifFile", "parse_mif", "load_mif_files"]
|
||||||
49
read_one_file_xlsx.py
Normal file
49
read_one_file_xlsx.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
def read_xlsx(file_path: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Прочитать .xlsx файл и вернуть простой объект со всеми данными.
|
||||||
|
|
||||||
|
Возвращаемая структура:
|
||||||
|
{
|
||||||
|
'name': <file name>,
|
||||||
|
'path': <abs path>,
|
||||||
|
'sheets': [
|
||||||
|
{'name': <sheet name>, 'rows': [[cell_value, ...], ...]},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Читает документ сверху-вниз, сохраняя значения ячеек в их питоновских типах.
|
||||||
|
"""
|
||||||
|
p = Path(file_path)
|
||||||
|
if not p.exists():
|
||||||
|
raise FileNotFoundError(file_path)
|
||||||
|
|
||||||
|
# try openpyxl first
|
||||||
|
try:
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError("openpyxl is required to read .xlsx files") from e
|
||||||
|
|
||||||
|
wb = load_workbook(filename=str(p), data_only=True)
|
||||||
|
out = {"name": p.name, "path": str(p.resolve()), "sheets": []}
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
rows: List[List[Any]] = []
|
||||||
|
for row in sheet.iter_rows(values_only=True):
|
||||||
|
rows.append(list(row))
|
||||||
|
out["sheets"].append({"name": sheet.title, "rows": rows})
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys, json
|
||||||
|
|
||||||
|
target = sys.argv[1] if len(sys.argv) > 1 else "data/Исх.данные/Таблица1 КИ-Неустроева.xlsx"
|
||||||
|
try:
|
||||||
|
data = read_xlsx(target)
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2, default=str))
|
||||||
|
except Exception as exc:
|
||||||
|
print('Error:', exc)
|
||||||
32
read_path.py
Normal file
32
read_path.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def list_files_in_dir(dir_path: str, recursive: bool = False) -> List[str]:
|
||||||
|
"""
|
||||||
|
Возвращает список файлов в директории с их полными путями.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dir_path: путь к директории (строка или Path-подобный).
|
||||||
|
recursive: если True, включает файлы во всех поддиректориях.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список путей (строк) к файлам.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: если директория не найдена.
|
||||||
|
"""
|
||||||
|
p = Path(dir_path)
|
||||||
|
if not p.exists():
|
||||||
|
raise FileNotFoundError(f"Directory not found: {dir_path}")
|
||||||
|
|
||||||
|
if recursive:
|
||||||
|
files = [str(f.resolve()) for f in p.rglob("*") if f.is_file()]
|
||||||
|
else:
|
||||||
|
files = [str(f.resolve()) for f in p.iterdir() if f.is_file()]
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["list_files_in_dir"]
|
||||||
|
|
||||||
1
services/__init__.py
Normal file
1
services/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# services package
|
||||||
46
services/data_loader.py
Normal file
46
services/data_loader.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""Загрузка исходных данных: Таблица 1 (КИ), Таблица 2, индекс MIF-файлов."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
|
||||||
|
|
||||||
|
def load_ki(path: str) -> dict:
|
||||||
|
"""Таблица 1: {название_поля → значение}."""
|
||||||
|
wb = load_workbook(path, data_only=True)
|
||||||
|
ws = wb.active
|
||||||
|
ki = {}
|
||||||
|
for row in ws.iter_rows(values_only=True):
|
||||||
|
if row[0] is not None and row[1] is not None:
|
||||||
|
ki[str(row[0]).strip()] = str(row[1]).strip()
|
||||||
|
return ki
|
||||||
|
|
||||||
|
|
||||||
|
def ki_val(ki: dict, keyword: str) -> str:
|
||||||
|
"""Найти значение в словаре КИ по подстроке ключа (без учёта регистра)."""
|
||||||
|
for k, v in ki.items():
|
||||||
|
if keyword.lower() in k.lower():
|
||||||
|
return v
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def load_table2(path: str) -> list:
|
||||||
|
"""Таблица 2: список строк (кортежи). Заголовок и пустые строки пропускаются."""
|
||||||
|
wb = load_workbook(path, data_only=True)
|
||||||
|
ws = wb.active
|
||||||
|
rows = []
|
||||||
|
for i, row in enumerate(ws.iter_rows(values_only=True)):
|
||||||
|
if i == 0:
|
||||||
|
continue
|
||||||
|
if all(v is None for v in row[:5]):
|
||||||
|
continue
|
||||||
|
rows.append(row)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def build_mif_index(mif_dir: str) -> dict:
|
||||||
|
"""Индекс MIF-файлов: stem → полный путь."""
|
||||||
|
index = {}
|
||||||
|
for p in Path(mif_dir).rglob("*.mif"):
|
||||||
|
index[p.stem] = str(p)
|
||||||
|
return index
|
||||||
195
services/form_filler.py
Normal file
195
services/form_filler.py
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
"""Заполнение одной формы XLSX по шаблону."""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
from openpyxl.styles import Font
|
||||||
|
|
||||||
|
from services.data_loader import ki_val
|
||||||
|
from services.mif_processor import get_contours, build_coord_rows
|
||||||
|
|
||||||
|
# ─────────────────────────────── Константы ──────────────────────────────── #
|
||||||
|
|
||||||
|
BASE_FONT = Font(name="Times New Roman", size=11)
|
||||||
|
|
||||||
|
# Позиции строк шаблона (1-based, до вставки доп. строк координат)
|
||||||
|
TMPL = dict(
|
||||||
|
KV=2, # A2: кадастровый квартал
|
||||||
|
DATE=5, # F5: дата подготовки
|
||||||
|
FIO=11, # F11: ФИО КИ
|
||||||
|
SNILS=12, # F12: СНИЛС
|
||||||
|
PHONE=13, # F13: телефон
|
||||||
|
EMAIL=15, # A15: email + адрес
|
||||||
|
SRO=17, # A17: наименование СРО
|
||||||
|
REGNR=19, # A19: рег. номер
|
||||||
|
SOGL=24, # A24: соглашение
|
||||||
|
LOC_ZU=29, # F29: местоположение ЗУ
|
||||||
|
AREA_ZU=31, # F31: площадь ЗУ
|
||||||
|
OBOSN_ZU=33, # F33: обоснование ЗУ
|
||||||
|
ZU_COORD_START=37, # первая строка данных координат ЗУ
|
||||||
|
ZU_COORD_PRE=5, # кол-во предзаполненных строк (37-41)
|
||||||
|
LOC_OKS=54, # F54: местоположение ОКС (до смещения)
|
||||||
|
OBOSN_OKS=58, # F58: обоснование ОКС (до смещения)
|
||||||
|
OKS_COORD_START=62, # первая строка данных ОКС (до смещения)
|
||||||
|
OKS_COORD_PRE=1, # кол-во предзаполненных строк ОКС
|
||||||
|
)
|
||||||
|
|
||||||
|
# ──────────────────────────── Вспомогательные ───────────────────────────── #
|
||||||
|
|
||||||
|
def _sv(ws, row: int, col: int, value, font=None):
|
||||||
|
cell = ws.cell(row=row, column=col)
|
||||||
|
cell.value = value
|
||||||
|
if font:
|
||||||
|
cell.font = copy.copy(font)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_insert_rows(ws, idx: int, amount: int):
|
||||||
|
"""
|
||||||
|
Вставить строки с корректным сдвигом слиянных ячеек.
|
||||||
|
openpyxl's ws.insert_rows() имеет известный баг: не сдвигает все merge-диапазоны.
|
||||||
|
Решение: вручную снимаем/пересчитываем merge до вызова insert_rows.
|
||||||
|
"""
|
||||||
|
# Собираем все диапазоны, которые пересекаются с точкой вставки или ниже неё
|
||||||
|
to_readjust = []
|
||||||
|
for rng in list(ws.merged_cells.ranges):
|
||||||
|
if rng.max_row >= idx:
|
||||||
|
to_readjust.append((rng.min_row, rng.min_col, rng.max_row, rng.max_col))
|
||||||
|
ws.merged_cells.remove(rng)
|
||||||
|
|
||||||
|
ws.insert_rows(idx, amount)
|
||||||
|
|
||||||
|
# Восстанавливаем диапазоны с поправкой
|
||||||
|
for min_r, min_c, max_r, max_c in to_readjust:
|
||||||
|
new_min_r = min_r + amount if min_r >= idx else min_r
|
||||||
|
new_max_r = max_r + amount if max_r >= idx else max_r
|
||||||
|
ws.merge_cells(
|
||||||
|
start_row=new_min_r, start_column=min_c,
|
||||||
|
end_row=new_max_r, end_column=max_c,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_row_style(ws, src_row: int, dst_row: int):
|
||||||
|
for col in range(1, ws.max_column + 1):
|
||||||
|
src = ws.cell(row=src_row, column=col)
|
||||||
|
dst = ws.cell(row=dst_row, column=col)
|
||||||
|
if src.has_style:
|
||||||
|
dst.font = copy.copy(src.font)
|
||||||
|
dst.border = copy.copy(src.border)
|
||||||
|
dst.fill = copy.copy(src.fill)
|
||||||
|
dst.number_format = src.number_format
|
||||||
|
dst.alignment = copy.copy(src.alignment)
|
||||||
|
|
||||||
|
|
||||||
|
def _insert_coord_rows(ws, coord_data: list, start_row: int, pre_allocated: int) -> int:
|
||||||
|
"""Вставить строки координат. Возвращает кол-во добавленных строк (смещение)."""
|
||||||
|
needed = len(coord_data)
|
||||||
|
extra = max(0, needed - pre_allocated)
|
||||||
|
if extra > 0:
|
||||||
|
_safe_insert_rows(ws, start_row + pre_allocated, extra)
|
||||||
|
for i in range(extra):
|
||||||
|
_copy_row_style(ws, start_row, start_row + pre_allocated + i)
|
||||||
|
|
||||||
|
F = BASE_FONT
|
||||||
|
for i, entry in enumerate(coord_data):
|
||||||
|
row_idx = start_row + i
|
||||||
|
if entry is None:
|
||||||
|
continue
|
||||||
|
num, x_str, y_str, method, mt = entry
|
||||||
|
_sv(ws, row_idx, 1, num, F)
|
||||||
|
_sv(ws, row_idx, 3, x_str, F)
|
||||||
|
_sv(ws, row_idx, 4, y_str, F)
|
||||||
|
_sv(ws, row_idx, 5, method, F)
|
||||||
|
_sv(ws, row_idx, 6, mt, F)
|
||||||
|
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_font_all(ws):
|
||||||
|
"""Применить Times New Roman 11 ко всем заполненным ячейкам листа."""
|
||||||
|
for row in ws.iter_rows():
|
||||||
|
for cell in row:
|
||||||
|
if cell.value is not None:
|
||||||
|
old = cell.font
|
||||||
|
cell.font = Font(
|
||||||
|
name="Times New Roman", size=11,
|
||||||
|
bold=old.bold if old else False,
|
||||||
|
italic=old.italic if old else False,
|
||||||
|
underline=old.underline if old else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─────────────────────────────── Публичная функция ──────────────────────── #
|
||||||
|
|
||||||
|
def fill_form(template_path: str, output_path: str,
|
||||||
|
ki: dict, t2_row: tuple, mif_index: dict):
|
||||||
|
"""Заполнить шаблон данными одной строки Таблицы 2 и сохранить файл."""
|
||||||
|
|
||||||
|
def col(i, default=None):
|
||||||
|
return t2_row[i] if len(t2_row) > i and t2_row[i] is not None else default
|
||||||
|
|
||||||
|
zu_file = col(0, "")
|
||||||
|
kv = col(1, "")
|
||||||
|
date_prep = col(2)
|
||||||
|
sogl = col(3, "")
|
||||||
|
loc_zu = col(4, "")
|
||||||
|
area_zu = col(5, "")
|
||||||
|
err_zu = col(6, "")
|
||||||
|
obosn_zu = col(7, "")
|
||||||
|
method_zu = col(8, "")
|
||||||
|
mt_zu = col(9, "")
|
||||||
|
oks_file = col(10, "")
|
||||||
|
loc_oks = col(11, "")
|
||||||
|
obosn_oks = col(12, "")
|
||||||
|
method_oks = col(13, "")
|
||||||
|
mt_oks = col(14, "")
|
||||||
|
|
||||||
|
wb = load_workbook(template_path)
|
||||||
|
ws = wb.active
|
||||||
|
F = BASE_FONT
|
||||||
|
T = TMPL
|
||||||
|
|
||||||
|
# Шапка
|
||||||
|
_sv(ws, T["KV"], 1, str(kv), F)
|
||||||
|
# Дата: конвертируем datetime в строку dd.mm.yyyy
|
||||||
|
if isinstance(date_prep, datetime.datetime):
|
||||||
|
date_str = date_prep.strftime("%d.%m.%Y")
|
||||||
|
else:
|
||||||
|
date_str = str(date_prep) if date_prep else ""
|
||||||
|
_sv(ws, T["DATE"], 6, date_str, F)
|
||||||
|
|
||||||
|
# Раздел 2: КИ
|
||||||
|
_sv(ws, T["FIO"], 6, ki_val(ki, "ФИО"), F)
|
||||||
|
_sv(ws, T["SNILS"], 6, ki_val(ki, "Страховой"), F)
|
||||||
|
_sv(ws, T["PHONE"], 6, ki_val(ki, "Контактный"), F)
|
||||||
|
_sv(ws, T["EMAIL"], 1, ki_val(ki, "Адрес электронной почты"), F)
|
||||||
|
_sv(ws, T["SRO"], 1, ki_val(ki, "Наименование саморегулируемой"), F)
|
||||||
|
_sv(ws, T["REGNR"], 1, ki_val(ki, "Уникальный"), F)
|
||||||
|
|
||||||
|
# Раздел 3: Основания
|
||||||
|
_sv(ws, T["SOGL"], 1, sogl, F)
|
||||||
|
|
||||||
|
# Раздел 4: ЗУ
|
||||||
|
_sv(ws, T["LOC_ZU"], 6, loc_zu, F)
|
||||||
|
area_str = f"{area_zu}±{err_zu}" if (area_zu or err_zu) else ""
|
||||||
|
_sv(ws, T["AREA_ZU"], 6, area_str, F)
|
||||||
|
_sv(ws, T["OBOSN_ZU"], 6, obosn_zu, F)
|
||||||
|
|
||||||
|
# Раздел 5: координаты ЗУ
|
||||||
|
zu_path = mif_index.get(str(zu_file).strip())
|
||||||
|
zu_contours = get_contours(zu_path) if zu_path else []
|
||||||
|
zu_coord_data = build_coord_rows(zu_contours, str(method_zu), mt_zu)
|
||||||
|
zu_offset = _insert_coord_rows(ws, zu_coord_data, T["ZU_COORD_START"], T["ZU_COORD_PRE"])
|
||||||
|
|
||||||
|
# Разделы 7-9: ОКС (со смещением)
|
||||||
|
ofs = zu_offset
|
||||||
|
_sv(ws, T["LOC_OKS"] + ofs, 6, loc_oks or "-", F)
|
||||||
|
_sv(ws, T["OBOSN_OKS"] + ofs, 6, obosn_oks or "-", F)
|
||||||
|
|
||||||
|
oks_path = mif_index.get(str(oks_file).strip()) if oks_file else None
|
||||||
|
oks_contours = get_contours(oks_path) if oks_path else []
|
||||||
|
oks_coord_data = build_coord_rows(oks_contours, str(method_oks or ""), mt_oks)
|
||||||
|
if oks_coord_data:
|
||||||
|
_insert_coord_rows(ws, oks_coord_data, T["OKS_COORD_START"] + ofs, T["OKS_COORD_PRE"])
|
||||||
|
|
||||||
|
_apply_font_all(ws)
|
||||||
|
wb.save(output_path)
|
||||||
59
services/mif_processor.py
Normal file
59
services/mif_processor.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""Извлечение контуров из MIF-файлов и формирование строк координат."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# read_mif_file лежит в корне проекта — добавляем его в путь поиска
|
||||||
|
_PROJECT_ROOT = str(Path(__file__).parent.parent)
|
||||||
|
if _PROJECT_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, _PROJECT_ROOT)
|
||||||
|
|
||||||
|
from read_mif_file import parse_mif # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def get_contours(mif_path: str) -> list:
|
||||||
|
"""
|
||||||
|
Извлечь список контуров (Region) из MIF-файла.
|
||||||
|
Возвращает: [[(x, y), ...], ...] — список контуров.
|
||||||
|
Region может содержать несколько полигонов (contours).
|
||||||
|
"""
|
||||||
|
result = parse_mif(mif_path)
|
||||||
|
if isinstance(result, list):
|
||||||
|
return []
|
||||||
|
all_contours = []
|
||||||
|
for feat in result.features:
|
||||||
|
if feat["type"] == "Region":
|
||||||
|
# contours — список полигонов (исправленный парсер)
|
||||||
|
for poly in feat["props"].get("contours", []):
|
||||||
|
if poly:
|
||||||
|
all_contours.append(list(poly))
|
||||||
|
return all_contours
|
||||||
|
|
||||||
|
|
||||||
|
def build_coord_rows(contours: list, method: str, mt) -> list:
|
||||||
|
"""
|
||||||
|
Сформировать список строк для раздела координат.
|
||||||
|
|
||||||
|
Каждый элемент: (номер, X_str, Y_str, method, mt)
|
||||||
|
или None — пустой разделитель между контурами.
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
- Первая координата MIF → X (колонка C), вторая → Y (D).
|
||||||
|
- Все точки нумеруются подряд по всем контурам.
|
||||||
|
- Последняя (замыкающая) точка контура получает номер первой точки этого контура.
|
||||||
|
"""
|
||||||
|
rows = []
|
||||||
|
global_num = 1
|
||||||
|
for ci, contour in enumerate(contours):
|
||||||
|
if ci > 0:
|
||||||
|
rows.append(None)
|
||||||
|
if not contour:
|
||||||
|
continue
|
||||||
|
first_num = global_num
|
||||||
|
for mx, my in contour[:-1]:
|
||||||
|
rows.append((global_num, f"{mx:.2f}", f"{my:.2f}", method, mt))
|
||||||
|
global_num += 1
|
||||||
|
if len(contour) >= 2:
|
||||||
|
mx, my = contour[-1]
|
||||||
|
rows.append((first_num, f"{mx:.2f}", f"{my:.2f}", method, mt))
|
||||||
|
return rows
|
||||||
80
services/output_generator.py
Normal file
80
services/output_generator.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"""Генерация итоговых файлов: объединённая форма и Таблица 4."""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import re
|
||||||
|
|
||||||
|
from openpyxl import load_workbook, Workbook
|
||||||
|
from openpyxl.styles import Font
|
||||||
|
|
||||||
|
from services.form_filler import BASE_FONT
|
||||||
|
|
||||||
|
|
||||||
|
def make_filename(kv: str, zu_file: str) -> str:
|
||||||
|
"""
|
||||||
|
Формат: {кв_через_подчёркивание}_{zu_file}.xlsx
|
||||||
|
Пример: 74_34_0400007_выявл1_{вымпел_100}.xlsx
|
||||||
|
"""
|
||||||
|
kv_clean = str(kv).replace(":", "_")
|
||||||
|
safe = re.sub(r'[<>"/\\|?*]', "_", str(zu_file))
|
||||||
|
return f"{kv_clean}_{safe}.xlsx"
|
||||||
|
|
||||||
|
|
||||||
|
def combine_forms(form_paths: list, combined_path: str):
|
||||||
|
"""Объединить все формы в один лист."""
|
||||||
|
combined_wb = Workbook()
|
||||||
|
combined_ws = combined_wb.active
|
||||||
|
combined_ws.title = "Все формы"
|
||||||
|
|
||||||
|
current_row = 1
|
||||||
|
for path in form_paths:
|
||||||
|
src_wb = load_workbook(path, data_only=True)
|
||||||
|
src_ws = src_wb.active
|
||||||
|
last_cell = None
|
||||||
|
for src_row in src_ws.iter_rows():
|
||||||
|
for cell in src_row:
|
||||||
|
dst = combined_ws.cell(
|
||||||
|
row=current_row + cell.row - 1,
|
||||||
|
column=cell.column,
|
||||||
|
)
|
||||||
|
dst.value = cell.value
|
||||||
|
if cell.has_style:
|
||||||
|
dst.font = copy.copy(cell.font)
|
||||||
|
dst.border = copy.copy(cell.border)
|
||||||
|
dst.fill = copy.copy(cell.fill)
|
||||||
|
dst.number_format = cell.number_format
|
||||||
|
dst.alignment = copy.copy(cell.alignment)
|
||||||
|
last_cell = cell
|
||||||
|
if last_cell:
|
||||||
|
src_row_dim = src_ws.row_dimensions[last_cell.row]
|
||||||
|
combined_ws.row_dimensions[current_row + last_cell.row - 1].height = src_row_dim.height
|
||||||
|
|
||||||
|
current_row += src_ws.max_row
|
||||||
|
|
||||||
|
if form_paths:
|
||||||
|
src_wb = load_workbook(form_paths[0])
|
||||||
|
src_ws = src_wb.active
|
||||||
|
for col_letter, col_dim in src_ws.column_dimensions.items():
|
||||||
|
combined_ws.column_dimensions[col_letter].width = col_dim.width
|
||||||
|
|
||||||
|
combined_wb.save(combined_path)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_table4(generated: list, output_path: str):
|
||||||
|
"""Перечень заполненных форм (Таблица 4)."""
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Перечень форм"
|
||||||
|
F = BASE_FONT
|
||||||
|
headers = ["№ п/п", "Кадастровый квартал", "Название файла ЗУ", "Имя файла", "Путь"]
|
||||||
|
for col_idx, h in enumerate(headers, 1):
|
||||||
|
ws.cell(row=1, column=col_idx, value=h).font = Font(
|
||||||
|
name="Times New Roman", size=11, bold=True
|
||||||
|
)
|
||||||
|
for i, rec in enumerate(generated, 1):
|
||||||
|
row = i + 1
|
||||||
|
ws.cell(row=row, column=1, value=i).font = copy.copy(F)
|
||||||
|
ws.cell(row=row, column=2, value=rec["kv"]).font = copy.copy(F)
|
||||||
|
ws.cell(row=row, column=3, value=rec["zu_file"]).font = copy.copy(F)
|
||||||
|
ws.cell(row=row, column=4, value=rec["name"]).font = copy.copy(F)
|
||||||
|
ws.cell(row=row, column=5, value=rec["path"]).font = copy.copy(F)
|
||||||
|
wb.save(output_path)
|
||||||
35
uv.lock
generated
Normal file
35
uv.lock
generated
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "et-xmlfile"
|
||||||
|
version = "2.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kadastr-17"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "openpyxl" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "openpyxl", specifier = ">=3.1" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openpyxl"
|
||||||
|
version = "3.1.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "et-xmlfile" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user