This commit is contained in:
2026-03-12 01:50:57 +05:00
parent ec82b528a3
commit c33befe7c2
12 changed files with 1553 additions and 69 deletions

View File

@@ -1,6 +1,8 @@
"""Загрузка исходных данных: Таблица 1 (КИ), Таблица 2, индекс MIF-файлов."""
"""Загрузка исходных данных: Таблица 1 (КИ), Таблица 2, Таблица 3, индекс MIF-файлов."""
import zipfile
from pathlib import Path
from xml.etree import ElementTree as ET
from openpyxl import load_workbook
@@ -44,3 +46,85 @@ def build_mif_index(mif_dir: str) -> dict:
for p in Path(mif_dir).rglob("*.mif"):
index[p.stem] = str(p)
return index
def load_table3(path: str):
"""
Таблица 3 (для схем): возвращает два словаря (images, scales).
images: {zu_name → PNG-байты}
scales: {zu_name → строка масштаба, например 'Масштаб: 1:150'}
Структура xlsx: каждый блок = 3 строки.
Строка 1: ('вид он', None, None, 'зу', 'название файла', zu_name)
Строка 2: изображение якорено здесь
Строка 3: масштаб в столбце A (c1)
Изображения извлекаются прямо из zip-архива xlsx через drawing/rels.
"""
# --- Читаем имена ZU и масштабы по строкам (1-based) ---
wb = load_workbook(path, data_only=True)
ws = wb.active
zu_by_row = {} # 1-based строка заголовка → zu_name
scales = {} # zu_name → масштаб (строка из c1 третьей строки блока)
for i, row in enumerate(ws.iter_rows(values_only=True), start=1):
zu_name = row[5] if len(row) > 5 else None
if zu_name and str(zu_name).strip().startswith("выявл"):
zu_name = str(zu_name).strip()
zu_by_row[i] = zu_name
# масштаб — третья строка блока (header+2)
scale_val = ws.cell(i + 2, 1).value
if scale_val:
scales[zu_name] = str(scale_val).strip()
# --- Читаем привязку картинок из drawing XML ---
_R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
_XDR = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
_A = "http://schemas.openxmlformats.org/drawingml/2006/main"
ns = {"xdr": _XDR, "a": _A, "r": _R}
result = {}
with zipfile.ZipFile(path, "r") as z:
names = z.namelist()
# Найдём все drawing-файлы
drawing_files = [n for n in names if n.startswith("xl/drawings/drawing") and n.endswith(".xml")]
for draw_path in drawing_files:
rels_path = draw_path.replace("xl/drawings/", "xl/drawings/_rels/") + ".rels"
if rels_path not in names:
continue
rels_root = ET.fromstring(z.read(rels_path))
rid_to_media = {
rel.get("Id"): rel.get("Target").split("/")[-1]
for rel in rels_root
}
draw_root = ET.fromstring(z.read(draw_path))
for anchor in draw_root:
from_el = anchor.find(f"{{{_XDR}}}from")
if from_el is None:
continue
row_0b = int(from_el.find(f"{{{_XDR}}}row").text) # 0-based
img_row_1b = row_0b + 1 # 1-based
blip = anchor.find(f".//{{{_A}}}blip")
if blip is None:
continue
rid = blip.get(f"{{{_R}}}embed")
media_filename = rid_to_media.get(rid)
if not media_filename:
continue
media_path = f"xl/media/{media_filename}"
if media_path not in names:
continue
# Ищем ближайшую ZU-строку заголовка, которая <= img_row_1b
header_row = max(
(r for r in zu_by_row if r <= img_row_1b),
default=None,
)
if header_row is None:
continue
zu_name = zu_by_row[header_row]
result[zu_name] = z.read(media_path)
return result, scales

View File

@@ -2,8 +2,11 @@
import copy
import datetime
import io
from openpyxl import load_workbook
from openpyxl.drawing.image import Image as XlImage
from openpyxl.drawing.spreadsheet_drawing import OneCellAnchor, AnchorMarker
from openpyxl.styles import Font
from services.data_loader import ki_val
@@ -29,10 +32,14 @@ TMPL = dict(
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, # кол-во предзаполненных строк ОКС
ZU_SCHEME_START=42, # строка-заголовок раздела 6 (до смещения)
ZU_SCHEME_PRE=1, # r43 = изображение, r44 = масштаб, r45-r50 = легенда
LOC_OKS=54, # F54: местоположение ОКС (до смещения)
OBOSN_OKS=58, # F58: обоснование ОКС (до смещения)
OKS_COORD_START=62, # первая строка данных ОКС (до смещения)
OKS_COORD_PRE=1, # кол-во предзаполненных строк ОКС
OKS_SCHEME_START=63, # строка-заголовок раздела 9 «9. Схема ОКС» (до смещения)
OKS_SCHEME_PRE=1, # r64 = изображение, r65 = масштаб
)
# ──────────────────────────── Вспомогательные ───────────────────────────── #
@@ -69,6 +76,58 @@ def _safe_insert_rows(ws, idx: int, amount: int):
)
def _safe_delete_rows(ws, idx: int, amount: int):
"""
Удалить `amount` строк начиная с `idx`, корректно сдвинув merge-диапазоны.
Диапазоны, целиком попавшие в удаляемый участок, удаляются.
Диапазоны, пересекающие границу — обрезаются/сдвигаются.
"""
end_idx = idx + amount # первая строка ПОСЛЕ удаляемого блока
to_remove = []
to_readjust = []
for rng in list(ws.merged_cells.ranges):
mn, mc, mx, xc = rng.min_row, rng.min_col, rng.max_row, rng.max_col
if mx < idx:
# полностью до зоны удаления — ничего не трогаем
continue
to_remove.append(rng) # нужно снять, а затем, возможно, заново добавить
if mn >= end_idx:
# полностью после — просто сдвигаем вверх
to_readjust.append((mn - amount, mc, mx - amount, xc))
elif mn >= idx and mx < end_idx:
# полностью внутри удаляемого — удаляем без восстановления
pass
elif mn < idx and mx < end_idx:
# начинается до, заканчивается внутри — обрезаем конец
new_mx = idx - 1
if new_mx >= mn:
to_readjust.append((mn, mc, new_mx, xc))
elif mn < idx and mx >= end_idx:
# охватывает весь удаляемый блок — уменьшаем
to_readjust.append((mn, mc, mx - amount, xc))
else:
# начинается внутри, заканчивается после — обрезаем начало
new_mn = idx
to_readjust.append((new_mn, mc, mx - amount, xc))
for rng in to_remove:
ws.merged_cells.remove(rng)
ws.delete_rows(idx, amount)
for min_r, min_c, max_r, max_c in to_readjust:
if min_r <= max_r:
ws.merge_cells(
start_row=min_r, start_column=min_c,
end_row=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)
@@ -120,8 +179,66 @@ def _apply_font_all(ws):
# ─────────────────────────────── Публичная функция ──────────────────────── #
def _embed_image(ws, png_bytes: bytes, anchor_row: int, pre_rows: int) -> int:
"""
Вставить PNG ровно в ОДНУ ячейку (строку) начиная с A{anchor_row}.
Стратегия «одна ячейка»:
Все pre_rows строк шаблона, зарезервированных под схему, кроме первой,
удаляются (_safe_delete_rows), чтобы осталась единственная объединённая
строка A{anchor_row}:F{anchor_row}.
• Высота этой строки выставляется равной реальной высоте PNG (pt).
• TwoCellAnchor с editAs='twoCell' привязывает изображение точно к этой
одной строке — от её верхнего левого угла до нижнего правого.
Возвращает изменение числа строк (всегда -(pre_rows-1), т.е. отрицательное).
"""
from PIL import Image as PILImage
pil = PILImage.open(io.BytesIO(png_bytes))
px_w, px_h = pil.size
dpi = pil.info.get("dpi", (96, 96))
if not isinstance(dpi, (tuple, list)) or dpi[0] == 0:
dpi = (96, 96)
pt_w = px_w * 72.0 / dpi[0]
pt_h = px_h * 72.0 / dpi[1]
# Масштаб по ширине листа (~420 pt для 6 колонок)
MAX_PT_W = 420.0
if pt_w > MAX_PT_W:
scale = MAX_PT_W / pt_w
pt_w = MAX_PT_W
pt_h *= scale
# Удаляем все строки зоны схемы кроме первой (anchor_row).
# Строка anchor_row уже является частью объединённой ячейки в шаблоне;
# после удаления нижних строк она станет单独ной строкой высотой pt_h.
rows_to_delete = pre_rows - 1
if rows_to_delete > 0:
_safe_delete_rows(ws, anchor_row + 1, rows_to_delete)
# Задаём высоту единственной оставшейся строки = высоте изображения
ws.row_dimensions[anchor_row].height = pt_h
# OneCellAnchor: верхний-левый угол прибит к ячейке A{anchor_row},
# размер задан явно в EMU (1 pt = 12700 EMU).
# В отличие от TwoCellAnchor, LibreOffice не пересчитывает позицию
# через сумму высот строк → картинка не уплывает вверх при склейке форм.
img = XlImage(io.BytesIO(png_bytes))
cell_anchor = OneCellAnchor()
cell_anchor._from = AnchorMarker(col=0, colOff=0, row=anchor_row - 1, rowOff=0)
cell_anchor.ext.cx = int(pt_w * 12700) # pt → EMU
cell_anchor.ext.cy = int(pt_h * 12700)
img.anchor = cell_anchor
ws.add_image(img)
return -rows_to_delete # отрицательное смещение: строки были удалены
def fill_form(template_path: str, output_path: str,
ki: dict, t2_row: tuple, mif_index: dict):
ki: dict, t2_row: tuple, mif_index: dict,
zu_scheme: bytes = None, oks_scheme: bytes = None,
zu_scale: str = None, oks_scale: str = None):
"""Заполнить шаблон данными одной строки Таблицы 2 и сохранить файл."""
def col(i, default=None):
@@ -180,7 +297,16 @@ def fill_form(template_path: str, output_path: str,
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: ОКС (со смещением)
# Раздел 6 (схема ЗУ):
# r43+offset = изображение (ZU_SCHEME_PRE=1, строки не удаляются)
# r44+offset = масштаб (c1)
if zu_scheme:
scheme_anchor = T["ZU_SCHEME_START"] + zu_offset + 1
_embed_image(ws, zu_scheme, scheme_anchor, T["ZU_SCHEME_PRE"])
if zu_scale:
_sv(ws, scheme_anchor + 1, 1, zu_scale, F)
# Разделы 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)
@@ -188,8 +314,17 @@ def fill_form(template_path: str, output_path: str,
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)
oks_offset = 0
if oks_coord_data:
_insert_coord_rows(ws, oks_coord_data, T["OKS_COORD_START"] + ofs, T["OKS_COORD_PRE"])
oks_offset = _insert_coord_rows(ws, oks_coord_data, T["OKS_COORD_START"] + ofs, T["OKS_COORD_PRE"])
# Раздел 9: схема ОКС (из Таблицы3 по Table2 столбец K «Название файла ОКС»)
# r64+ofs+oks_offset = изображение, r65+ofs+oks_offset = масштаб
if oks_scheme:
oks_scheme_anchor = T["OKS_SCHEME_START"] + ofs + oks_offset + 1
_embed_image(ws, oks_scheme, oks_scheme_anchor, T["OKS_SCHEME_PRE"])
if oks_scale:
_sv(ws, oks_scheme_anchor + 1, 1, oks_scale, F)
_apply_font_all(ws)
wb.save(output_path)

View File

@@ -1,9 +1,11 @@
"""Генерация итоговых файлов: объединённая форма и Таблица 4."""
import copy
import io
import re
import zipfile
from openpyxl import load_workbook, Workbook
from openpyxl import Workbook
from openpyxl.styles import Font
from services.form_filler import BASE_FONT
@@ -19,62 +21,311 @@ def make_filename(kv: str, zu_file: str) -> str:
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 = "Все формы"
"""
Объединить все заполненные формы в один xlsx, сохраняя стили, merge-ячейки
и встроенные изображения.
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,
Работает на уровне ZIP-содержимого xlsx. Все формы создаются из одного
шаблона, поэтому styles.xml одинаков — сдвигать индексы стилей не нужно.
Строки хранятся inline (inlineStr), shared strings отсутствуют.
"""
if not form_paths:
return
IMG_TYPE = ("http://schemas.openxmlformats.org/officeDocument/"
"2006/relationships/image")
RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
# ── Вспомогательные функции ──────────────────────────────────────────── #
def _has_content(row_xml: str) -> bool:
"""Строка имеет содержимое, если хотя бы одна ячейка несёт <is> или <v>."""
return bool(re.search(r'<(?:is|v)\b', row_xml))
def _shift_cell_ref(ref: str, offset: int) -> str:
"""'A5' + 3 → 'A8'"""
col = re.match(r'[A-Z]+', ref).group()
rn = int(re.search(r'\d+', ref).group())
return f'{col}{rn + offset}'
def _shift_merge_ref(ref: str, offset: int) -> str:
"""'A1:F3' + 3 → 'A4:F6'"""
return ':'.join(_shift_cell_ref(p, offset) for p in ref.split(':'))
# ── Сбор данных из каждого файла ─────────────────────────────────────── #
all_row_xmls = [] # строки <row ...>...</row> со сдвинутыми номерами
all_merges = [] # строки вида 'A1:F3'
all_anchors = [] # строки XML якорей изображений
all_rels = [] # (new_rid, target) для drawing rels
new_media = {} # new_name → bytes
row_offset = 0
rid_counter = 1
for form_path in form_paths:
with zipfile.ZipFile(form_path, 'r') as zf:
names = set(zf.namelist())
# ── sheet1.xml: строки и merge-ячейки ── #
sheet_xml = zf.read('xl/worksheets/sheet1.xml').decode('utf-8')
sd_m = re.search(r'<sheetData[^>]*>(.*?)</sheetData>',
sheet_xml, re.DOTALL)
max_r = 0
last_content_r = 0 # последняя строка с реальным содержимым
form_rows = [] # строки текущей формы: (r_num_in_form, xml_str)
if sd_m:
for rm in re.finditer(
r'(<row\b[^>]*>)(.*?)(</row>)',
sd_m.group(1), re.DOTALL
):
tag, content, end_tag = rm.group(1), rm.group(2), rm.group(3)
r_num = int(re.search(r'\br="(\d+)"', tag).group(1))
max_r = max(max_r, r_num)
if _has_content(content):
last_content_r = r_num
if row_offset > 0:
# Сдвинуть r= в <row r="N">
tag = re.sub(
r'\br="(\d+)"',
lambda m: f'r="{int(m.group(1)) + row_offset}"',
tag, count=1
)
# Сдвинуть r= в ячейках <c r="A5">
def _sc(m, off=row_offset):
col = re.match(r'[A-Z]+', m.group(1)).group()
rn = int(re.search(r'\d+', m.group(1)).group())
return f'r="{col}{rn + off}"'
content = re.sub(r'\br="([A-Z]+\d+)"', _sc, content)
form_rows.append((r_num, tag + content + end_tag))
# Добавляем только строки до last_content_r включительно,
# отбрасывая пустые хвостовые строки (styled-only rows из шаблона)
for r_in_form, xml_str in form_rows:
if r_in_form <= last_content_r:
all_row_xmls.append(xml_str)
# Объединения ячеек (только в пределах last_content_r)
mc_m = re.search(r'<mergeCells[^>]*>(.*?)</mergeCells>',
sheet_xml, re.DOTALL)
if mc_m:
for ref_m in re.finditer(r'ref="([^"]+)"', mc_m.group(1)):
# Парсим max_row диапазона
parts = ref_m.group(1).split(':')
mc_max_row = max(int(re.search(r'\d+', p).group()) for p in parts)
if mc_max_row <= last_content_r:
all_merges.append(_shift_merge_ref(ref_m.group(1), row_offset))
# ── drawing1.xml: якоря и медиафайлы ── #
drawing_name = 'xl/drawings/drawing1.xml'
rels_name = 'xl/drawings/_rels/drawing1.xml.rels'
if drawing_name in names:
drawing_xml = zf.read(drawing_name).decode('utf-8')
rels_xml = (zf.read(rels_name).decode('utf-8')
if rels_name in names else '')
# Маппинг старый rId → новый rId
local_rid_map = {}
for rel_m in re.finditer(r'<Relationship\b([^>]+)/>', rels_xml):
attrs = rel_m.group(1)
id_m = re.search(r'\bId="([^"]+)"', attrs)
tgt_m = re.search(r'\bTarget="([^"]+)"', attrs)
if not (id_m and tgt_m):
continue
old_rid = id_m.group(1)
target = tgt_m.group(1) # '/xl/media/imageN.png' или '../media/imageN.png'
media_zip = 'xl/media/' + target.split('/')[-1]
if media_zip in names:
new_name = f'image{rid_counter}.png'
new_media[new_name] = zf.read(media_zip)
new_rid = f'rId{rid_counter}'
all_rels.append((new_rid, f'../media/{new_name}'))
local_rid_map[old_rid] = new_rid
rid_counter += 1
# Якоря: сдвигаем строки и перемаппируем rId
for anc_m in re.finditer(
r'(<(?:xdr:)?(?:twoCellAnchor|oneCellAnchor|absoluteAnchor)\b[^>]*>)'
r'(.*?)'
r'(</(?:xdr:)?(?:twoCellAnchor|oneCellAnchor|absoluteAnchor)>)',
drawing_xml, re.DOTALL
):
anc = anc_m.group(1) + anc_m.group(2) + anc_m.group(3)
if row_offset > 0:
anc = re.sub(
r'(<(?:xdr:)?row>)(\d+)(</(?:xdr:)?row>)',
lambda m, off=row_offset:
f'{m.group(1)}{int(m.group(2)) + off}{m.group(3)}',
anc
)
for old_rid, new_rid in local_rid_map.items():
anc = anc.replace(f'embed="{old_rid}"',
f'embed="{new_rid}"')
all_anchors.append(anc)
row_offset += last_content_r
# ── Сборка итогового xlsx на основе первого файла ────────────────────── #
with zipfile.ZipFile(form_paths[0], 'r') as src_zf:
base_names = set(src_zf.namelist())
# Новый sheet1.xml — заменяем sheetData и mergeCells
base_sheet = src_zf.read('xl/worksheets/sheet1.xml').decode('utf-8')
new_sheet_data = '<sheetData>' + ''.join(all_row_xmls) + '</sheetData>'
base_sheet = re.sub(
r'<sheetData[^>]*>.*?</sheetData>',
new_sheet_data, base_sheet, flags=re.DOTALL
)
if all_merges:
new_mc = (
f'<mergeCells count="{len(all_merges)}">' +
''.join(f'<mergeCell ref="{r}"/>' for r in all_merges) +
'</mergeCells>'
)
if re.search(r'<mergeCells', base_sheet):
base_sheet = re.sub(
r'<mergeCells[^>]*>.*?</mergeCells>',
new_mc, base_sheet, flags=re.DOTALL
)
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
else:
base_sheet = base_sheet.replace('</worksheet>',
new_mc + '</worksheet>')
else:
base_sheet = re.sub(r'<mergeCells[^>]*>.*?</mergeCells>', '',
base_sheet, flags=re.DOTALL)
current_row += src_ws.max_row
# Новый drawing1.xml и его rels (если есть изображения)
new_drawing = None
new_drawing_rels = None
if all_anchors:
base_drw = (src_zf.read('xl/drawings/drawing1.xml').decode('utf-8')
if 'xl/drawings/drawing1.xml' in base_names else '')
root_m = re.match(r'(<wsDr\b[^>]*>)', base_drw)
root_open = (root_m.group(1) if root_m else
'<wsDr xmlns:xdr="http://schemas.openxmlformats.org/'
'drawingml/2006/spreadsheetDrawing" '
'xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" '
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/'
'2006/relationships" '
'xmlns="http://schemas.openxmlformats.org/drawingml/'
'2006/spreadsheetDrawing">')
new_drawing = root_open + ''.join(all_anchors) + '</wsDr>'
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
new_drawing_rels = (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
f'<Relationships xmlns="{RELS_NS}">' +
''.join(
f'<Relationship Id="{rid}" Type="{IMG_TYPE}" Target="{tgt}"/>'
for rid, tgt in all_rels
) +
'</Relationships>'
)
# Записываем итоговый zip
out_buf = io.BytesIO()
with zipfile.ZipFile(out_buf, 'w', zipfile.ZIP_DEFLATED) as out_zf:
for name in sorted(base_names):
if name == 'xl/worksheets/sheet1.xml':
out_zf.writestr(name, base_sheet.encode('utf-8'))
elif name == 'xl/drawings/drawing1.xml' and new_drawing:
out_zf.writestr(name, new_drawing.encode('utf-8'))
elif name == 'xl/drawings/_rels/drawing1.xml.rels' and new_drawing_rels:
out_zf.writestr(name, new_drawing_rels.encode('utf-8'))
elif re.match(r'xl/media/', name):
pass # заменяем новыми ниже
else:
out_zf.writestr(name, src_zf.read(name))
# Новые медиафайлы
for mname, mbytes in new_media.items():
out_zf.writestr(f'xl/media/{mname}', mbytes)
with open(combined_path, 'wb') as f:
f.write(out_buf.getvalue())
combined_wb.save(combined_path)
def generate_table4(generated: list, output_path: str):
"""Перечень заполненных форм (Таблица 4)."""
"""
Таблица 4: Сведения о выявленных фактах незаконного использования земель.
Структура согласно шаблону «Шаблон Таблицы4.xlsx»:
B1 — заголовок таблицы
R2 — заголовки 11 столбцов (AK)
R3/4 — уточнение статуса в J3/J4
R5 — нумерация столбцов 111
R6 — строка ИТОГО + формула COUNTA в G6
R7+ — данные (B=74, C=Челябинская область, D=УРФО,
E=кад.квартал, G=нет, H=местоположение ЗУ)
"""
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)
ws.title = "Таблица4"
F = BASE_FONT
Fb = Font(name="Times New Roman", size=11, bold=True)
TITLE = (
"Сведения о выявленных в рамках ККР федерального значения фактах "
"незаконного использования земель."
)
HEADERS = [
"№ п/п",
"Код субъекта Российской Федерации",
"Наименование субъекта Российской Федерации",
"Федеральный округ",
"Номер кадастрового квартала",
"Муниципальное образование",
("Кадастровый номер объекта недвижимости, "
"в отношении которого направлена"),
"Адрес объекта недвижимости",
("Дата поступления информации в Росреестр "
"(отдел государственного земельного надзора)"),
"Статус",
"Примечание",
]
# R1: заголовок в B1
ws.cell(1, 2, TITLE).font = Fb
# R2: заголовки столбцов AK
for i, h in enumerate(HEADERS, 1):
ws.cell(2, i, h).font = Fb
# R3/R4: уточнение статуса
ws.cell(3, 10, "(В работе /").font = copy.copy(F)
ws.cell(4, 10, "Нарушение устранено)").font = copy.copy(F)
# R5: нумерация столбцов 111
for i in range(1, 12):
ws.cell(5, i, i).font = copy.copy(F)
# R6: ИТОГО + формула подсчёта
ws.cell(6, 1, "ИТОГО").font = Fb
ws.cell(6, 7, "=COUNTA(G7:G1048576)").font = copy.copy(F)
# R7+: строки данных
for idx, rec in enumerate(generated, 1):
row = idx + 6 # строки 7, 8, 9, ...
ws.cell(row, 1, idx).font = copy.copy(F) # A: №
ws.cell(row, 2, "74").font = copy.copy(F) # B: Код субъекта
ws.cell(row, 3, "Челябинская область").font = copy.copy(F) # C: Наименование
ws.cell(row, 4, "УРФО").font = copy.copy(F) # D: Округ
ws.cell(row, 5, rec.get("kv", "")).font = copy.copy(F) # E: Кад.квартал
# F: Муниципальное образование — не задано в ТЗ, оставляем пустым
ws.cell(row, 7, "нет").font = copy.copy(F) # G: Кад.номер ОН
ws.cell(row, 8, rec.get("loc_zu", "")).font = copy.copy(F) # H: Адрес ОН
# I: Дата поступления, J: Статус, K: Примечание — не задано в ТЗ
wb.save(output_path)