Files
kadastr-17/services/output_generator.py
2026-03-12 01:50:57 +05:00

332 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Генерация итоговых файлов: объединённая форма и Таблица 4."""
import copy
import io
import re
import zipfile
from openpyxl import 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):
"""
Объединить все заполненные формы в один xlsx, сохраняя стили, merge-ячейки
и встроенные изображения.
Работает на уровне 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
)
else:
base_sheet = base_sheet.replace('</worksheet>',
new_mc + '</worksheet>')
else:
base_sheet = re.sub(r'<mergeCells[^>]*>.*?</mergeCells>', '',
base_sheet, flags=re.DOTALL)
# Новый 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>'
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())
def generate_table4(generated: list, output_path: str):
"""
Таблица 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 = "Таблица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)