This commit is contained in:
2026-03-10 20:21:31 +05:00
commit ec82b528a3
14 changed files with 1041 additions and 0 deletions

1
services/__init__.py Normal file
View File

@@ -0,0 +1 @@
# services package

46
services/data_loader.py Normal file
View 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
View 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
View 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

View 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)