"""Заполнение одной формы 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)