SFM/app/pipeline/mapping.py
2026-07-09 10:16:44 +03:00

209 lines
7.1 KiB
Python

from __future__ import annotations
import json
import re
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class MappingRule:
column_name: str
candidate_keys: tuple[str, ...]
source: str = "any"
MAPPING_DATA_FILE = Path(__file__).resolve().with_name("mapping_table.json")
TARGET_REPORT_COLUMNS_COUNT = 204
_MANUAL_COLUMN_NAME_FIXES: dict[int, str] = {
1: "Имя XML файла",
178: "Место государственной регистрации ЕИО/Бенефициара (структура полностью)",
183: "Номер",
}
_INVALID_TAGS = {"", "-", "Источник", "путь", "подразумеваются", "тэга", "нашла"}
def _load_mapping_data() -> list[dict[str, str | int]]:
raw = json.loads(MAPPING_DATA_FILE.read_text(encoding="utf-8"))
if not isinstance(raw, list):
return []
result: list[dict[str, str | int]] = []
for item in raw:
if not isinstance(item, dict):
continue
idx = int(item.get("index", 0))
if not 1 <= idx <= TARGET_REPORT_COLUMNS_COUNT:
continue
column_name = str(item.get("column_name", "")).strip()
xml_tag = str(item.get("xml_tag", "")).strip()
xml_path = str(item.get("xml_path", "")).strip()
if idx in _MANUAL_COLUMN_NAME_FIXES:
column_name = _MANUAL_COLUMN_NAME_FIXES[idx]
column_name = _cleanup_column_name(column_name)
result.append(
{
"index": idx,
"column_name": column_name,
"xml_tag": xml_tag,
"xml_path": xml_path,
}
)
return sorted(result, key=lambda row: int(row["index"]))
def _cleanup_column_name(value: str) -> str:
cleaned = re.sub(r"\s+", " ", value).strip()
cleaned = re.sub(r"\s+не\s+нашла.*$", "", cleaned, flags=re.IGNORECASE)
return cleaned.strip(" -")
def _build_fixed_columns(mapping_data: list[dict[str, str | int]]) -> tuple[str, ...]:
columns: list[str] = []
for row in mapping_data:
column = str(row.get("column_name", "")).strip()
columns.append(column or f"Колонка {row['index']}")
if len(columns) < TARGET_REPORT_COLUMNS_COUNT:
for index in range(len(columns) + 1, TARGET_REPORT_COLUMNS_COUNT + 1):
columns.append(f"Колонка {index}")
return tuple(columns[:TARGET_REPORT_COLUMNS_COUNT])
def _build_rules(mapping_data: list[dict[str, str | int]]) -> tuple[MappingRule, ...]:
rules: list[MappingRule] = []
participant_columns = {
"Признак резидента участника",
"Тип участника",
"ИНН участника",
"Наименование участника",
"Счет участника",
}
for row in mapping_data:
column = str(row.get("column_name", "")).strip()
tag = str(row.get("xml_tag", "")).strip()
path = str(row.get("xml_path", "")).strip()
if not column or tag in _INVALID_TAGS:
continue
candidates = list(_extract_tag_candidate_keys(tag))
if column.startswith("ИНН"):
candidates = ["ИННФЛИП", "ИННФЛ", "ИННЮЛ", "ИНН", *candidates]
if column == "Тип участника":
candidates = ["Тип", "ТипУчастника", *candidates]
if path:
for path_tag in _extract_path_candidate_keys(path):
if path_tag not in candidates:
candidates.append(path_tag)
rules.append(
MappingRule(
column_name=column,
candidate_keys=tuple(candidates),
source=(
"participant"
if column in participant_columns or column.startswith("ИНН")
else "any"
),
)
)
return tuple(rules)
def _extract_tag_candidate_keys(tag: str) -> tuple[str, ...]:
candidates: list[str] = []
for raw_tag in re.split(r"[|,]", tag):
cleaned_tag = raw_tag.strip()
if not cleaned_tag or cleaned_tag in _INVALID_TAGS:
continue
if cleaned_tag not in candidates:
candidates.append(cleaned_tag)
return tuple(candidates)
def _extract_path_candidate_keys(path: str) -> tuple[str, ...]:
candidates: list[str] = []
for raw_path in re.findall(r"/[^\s|]+", path):
parts = [part for part in raw_path.strip("/").split("/") if part]
if not parts:
continue
dotted_path = ".".join(parts)
if dotted_path and dotted_path not in candidates:
candidates.append(dotted_path)
leaf_tag = parts[-1]
if leaf_tag and leaf_tag not in _INVALID_TAGS and leaf_tag not in candidates:
candidates.append(leaf_tag)
return tuple(candidates)
_MAPPING_DATA = _load_mapping_data()
FIXED_REPORT_COLUMNS: tuple[str, ...] = _build_fixed_columns(_MAPPING_DATA)
REPORT_MAPPING_RULES: tuple[MappingRule, ...] = _build_rules(_MAPPING_DATA)
def build_fixed_row(
*,
file_name: str,
record_id: str,
operation_index: int,
operation_fields: dict[str, str],
participant_fields: dict[str, str],
) -> dict[str, str]:
row: dict[str, str] = {
"Имя XML файла": file_name,
"Идентификатор записи": record_id,
"Уникальный номер операции": str(operation_index),
}
merged = _merge_fields(operation_fields, participant_fields)
merged_suffix_index = _build_suffix_index(merged)
participant_suffix_index = _build_suffix_index(participant_fields)
for rule in REPORT_MAPPING_RULES:
if row.get(rule.column_name):
continue
if rule.source == "participant":
source_payload = participant_fields
suffix_index = participant_suffix_index
else:
source_payload = merged
suffix_index = merged_suffix_index
row[rule.column_name] = _pick_value(
source_payload, suffix_index, rule.candidate_keys
)
for column in FIXED_REPORT_COLUMNS:
row.setdefault(column, "")
return row
def _merge_fields(
operation_fields: dict[str, str],
participant_fields: dict[str, str],
) -> dict[str, str]:
merged: dict[str, str] = {}
merged.update(operation_fields)
merged.update(participant_fields)
return merged
def _build_suffix_index(payload: dict[str, str]) -> dict[str, str]:
index: dict[str, str] = {}
for key, value in payload.items():
if not value:
continue
short = key.rsplit(".", maxsplit=1)[-1]
index.setdefault(short, value)
return index
def _pick_value(
payload: dict[str, str],
suffix_index: dict[str, str],
candidates: tuple[str, ...],
) -> str:
for key in candidates:
if key in payload and payload[key]:
return payload[key]
short = key.rsplit(".", maxsplit=1)[-1]
if short in payload and payload[short]:
return payload[short]
if short in suffix_index:
return suffix_index[short]
return ""