testR1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 столбцов (A–K)
|
||||
R3/4 — уточнение статуса в J3/J4
|
||||
R5 — нумерация столбцов 1–11
|
||||
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: заголовки столбцов A–K
|
||||
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: нумерация столбцов 1–11
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user