Загрузить файлы в «/»
This commit is contained in:
parent
687df175aa
commit
b0111c451e
|
|
@ -0,0 +1,88 @@
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"source": "Таблица основных типов ISO-контейнеров (внешние/внутренние размеры, дверной проём, масса, объём)",
|
||||||
|
"usage": "Сверка п.21 FCL (проходимость груза в дверной проём 20DV, 40DV, 40HC) и оценка payload/объёма."
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"20DV": {
|
||||||
|
"label": "20' Standard (Dry Cube) — соответствует 20DV в критериях",
|
||||||
|
"external_mm": { "length": 6058, "width": 2438, "height": 2591 },
|
||||||
|
"internal_mm": { "length": 5905, "width": 2350, "height": 2381 },
|
||||||
|
"door_opening_mm": { "width": 2336, "height": 2291 },
|
||||||
|
"max_gross_kg": 30480,
|
||||||
|
"tare_kg": 2370,
|
||||||
|
"payload_kg": 28110,
|
||||||
|
"volume_m3": 33.2
|
||||||
|
},
|
||||||
|
"20HC": {
|
||||||
|
"label": "20' High Cube",
|
||||||
|
"external_mm": { "length": 6058, "width": 2438, "height": 2896 },
|
||||||
|
"internal_mm": { "length": 5905, "width": 2350, "height": 2693 },
|
||||||
|
"door_opening_mm": { "width": 2340, "height": 2597 },
|
||||||
|
"max_gross_kg": 30480,
|
||||||
|
"tare_kg": 2340,
|
||||||
|
"payload_kg": 28140,
|
||||||
|
"volume_m3": 37.5
|
||||||
|
},
|
||||||
|
"20HC_PW": {
|
||||||
|
"label": "20' High Cube Pallet Wide",
|
||||||
|
"external_mm": { "length": 6058, "width": 2484, "height": 2896 },
|
||||||
|
"internal_mm": { "length": 5898, "width": 2426, "height": 2698 },
|
||||||
|
"door_opening_mm": { "width": 2374, "height": 2585 },
|
||||||
|
"max_gross_kg": 30480,
|
||||||
|
"tare_kg": 2580,
|
||||||
|
"payload_kg": 27900,
|
||||||
|
"volume_m3": 38.6
|
||||||
|
},
|
||||||
|
"40DV": {
|
||||||
|
"label": "40' Standard (Dry Van) — соответствует 40DV в критериях",
|
||||||
|
"external_mm": { "length": 12192, "width": 2438, "height": 2591 },
|
||||||
|
"internal_mm": { "length": 12039, "width": 2350, "height": 2372 },
|
||||||
|
"door_opening_mm": { "width": 2336, "height": 2291 },
|
||||||
|
"max_gross_kg": 30480,
|
||||||
|
"tare_kg": 4000,
|
||||||
|
"payload_kg": 26480,
|
||||||
|
"volume_m3": 67.8
|
||||||
|
},
|
||||||
|
"40HC": {
|
||||||
|
"label": "40' High Cube — соответствует 40HC в критериях",
|
||||||
|
"external_mm": { "length": 12192, "width": 2438, "height": 2896 },
|
||||||
|
"internal_mm": { "length": 12039, "width": 2350, "height": 2693 },
|
||||||
|
"door_opening_mm": { "width": 2340, "height": 2597 },
|
||||||
|
"max_gross_kg": 32500,
|
||||||
|
"tare_kg": 4200,
|
||||||
|
"payload_kg": 28300,
|
||||||
|
"volume_m3": 76.5
|
||||||
|
},
|
||||||
|
"40HC_PW": {
|
||||||
|
"label": "40' High Cube Pallet Wide",
|
||||||
|
"external_mm": { "length": 12192, "width": 2500, "height": 2896 },
|
||||||
|
"internal_mm": { "length": 12039, "width": 2432, "height": 2693 },
|
||||||
|
"door_opening_mm": { "width": 2432, "height": 2597 },
|
||||||
|
"max_gross_kg": 35000,
|
||||||
|
"tare_kg": 4400,
|
||||||
|
"payload_kg": 30600,
|
||||||
|
"volume_m3": 79.3
|
||||||
|
},
|
||||||
|
"45HC_PW_A": {
|
||||||
|
"label": "45' High Cube Pallet Wide (Type A)",
|
||||||
|
"external_mm": { "length": 13716, "width": 2500, "height": 2750 },
|
||||||
|
"internal_mm": { "length": 13513, "width": 2444, "height": 2549 },
|
||||||
|
"door_opening_mm": { "width": 2416, "height": 2439 },
|
||||||
|
"max_gross_kg": 34000,
|
||||||
|
"tare_kg": 4180,
|
||||||
|
"payload_kg": 29820,
|
||||||
|
"volume_m3": 85.1
|
||||||
|
},
|
||||||
|
"45HC_PW_B": {
|
||||||
|
"label": "45' High Cube Pallet Wide (Type B)",
|
||||||
|
"external_mm": { "length": 13716, "width": 2500, "height": 2896 },
|
||||||
|
"internal_mm": { "length": 13513, "width": 2444, "height": 2670 },
|
||||||
|
"door_opening_mm": { "width": 2416, "height": 2580 },
|
||||||
|
"max_gross_kg": 34000,
|
||||||
|
"tare_kg": 5080,
|
||||||
|
"payload_kg": 28920,
|
||||||
|
"volume_m3": 89.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Справочник ISO-контейнеров: промпт RAG и сверка п.21 FCL (дверной проём vs габариты груза)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_JSON_PATH = os.path.join(_DIR, "container_iso_reference.json")
|
||||||
|
|
||||||
|
_iso_cache: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_iso_reference() -> Dict[str, Any]:
|
||||||
|
global _iso_cache
|
||||||
|
if _iso_cache is not None:
|
||||||
|
return _iso_cache
|
||||||
|
try:
|
||||||
|
with open(_JSON_PATH, "r", encoding="utf-8") as f:
|
||||||
|
_iso_cache = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
_iso_cache = {"types": {}}
|
||||||
|
return _iso_cache
|
||||||
|
|
||||||
|
|
||||||
|
def iso_reference_prompt_block() -> str:
|
||||||
|
"""Компактный блок для system prompt LLM (сверка п.21 и выбора контейнера)."""
|
||||||
|
data = load_iso_reference()
|
||||||
|
types = data.get("types") or {}
|
||||||
|
if not types:
|
||||||
|
return ""
|
||||||
|
lines: List[str] = [
|
||||||
|
"СПРАВОЧНИК ISO-КОНТЕЙНЕРОВ (мм, кг, м³) — для сверки с п.21 FCL (дверной проём) и оценки объёма/полезной нагрузки. "
|
||||||
|
"Не выдумывай числа; при расчётах используй только эти значения.",
|
||||||
|
"Ключи 20DV / 40DV / 40HC соответствуют формулировкам в критериях (проём 20DV, 40DV, 40HC).",
|
||||||
|
]
|
||||||
|
for key in ("20DV", "20HC", "20HC_PW", "40DV", "40HC", "40HC_PW", "45HC_PW_A", "45HC_PW_B"):
|
||||||
|
t = types.get(key)
|
||||||
|
if not isinstance(t, dict):
|
||||||
|
continue
|
||||||
|
door = t.get("door_opening_mm") or {}
|
||||||
|
inn = t.get("internal_mm") or {}
|
||||||
|
dw = door.get("width")
|
||||||
|
dh = door.get("height")
|
||||||
|
il = inn.get("length")
|
||||||
|
iw = inn.get("width")
|
||||||
|
ih = inn.get("height")
|
||||||
|
vol = t.get("volume_m3")
|
||||||
|
pay = t.get("payload_kg")
|
||||||
|
lines.append(
|
||||||
|
f"- {key} ({t.get('label', '')}): дверь {dw}×{dh} мм; "
|
||||||
|
f"внутри {il}×{iw}×{ih} мм; объём ~{vol} м³; payload ~{pay} кг"
|
||||||
|
)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _edges_mm_from_dim(d: Dict[str, Any]) -> Optional[Tuple[float, float, float]]:
|
||||||
|
"""Длина/ширина/высота груза в мм (из см в shipment)."""
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _num(v: Any) -> Optional[float]:
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
if isinstance(v, (int, float)):
|
||||||
|
return float(v)
|
||||||
|
s = str(v).strip().replace(" ", "").replace(",", ".")
|
||||||
|
try:
|
||||||
|
return float(s)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
l = _num(d.get("length_cm"))
|
||||||
|
w = _num(d.get("width_cm"))
|
||||||
|
h = _num(d.get("height_cm"))
|
||||||
|
if l is None or w is None or h is None:
|
||||||
|
return None
|
||||||
|
return l * 10.0, w * 10.0, h * 10.0
|
||||||
|
|
||||||
|
|
||||||
|
def _box_fits_door(
|
||||||
|
edges_mm: Tuple[float, float, float],
|
||||||
|
door_w_mm: float,
|
||||||
|
door_h_mm: float,
|
||||||
|
) -> bool:
|
||||||
|
"""Два наименьших ребра груза должны укладываться в ширину/высоту проёма (сортировка)."""
|
||||||
|
s0, s1, s2 = sorted(edges_mm)
|
||||||
|
dw, dh = sorted((door_w_mm, door_h_mm))
|
||||||
|
return s0 <= dw + 0.5 and s1 <= dh + 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def _codes_from_container_type(ct: str) -> List[str]:
|
||||||
|
"""Из строки вида «2×40HC, 1×20DC» извлекает коды из справочника."""
|
||||||
|
if not isinstance(ct, str) or not ct.strip():
|
||||||
|
return ["20DV", "40DV", "40HC"]
|
||||||
|
t = ct.upper()
|
||||||
|
found: List[str] = []
|
||||||
|
|
||||||
|
def _add(size: str, kind: Optional[str]) -> None:
|
||||||
|
k = (kind or "DV").upper()
|
||||||
|
if size == "20":
|
||||||
|
code = "20" + ("HC" if k in ("HC", "HQ") else "DV")
|
||||||
|
elif size == "40":
|
||||||
|
code = "40" + ("HC" if k in ("HC", "HQ") else "DV")
|
||||||
|
elif size == "45":
|
||||||
|
code = "45HC_PW_B"
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if code not in found:
|
||||||
|
found.append(code)
|
||||||
|
|
||||||
|
for m in re.finditer(
|
||||||
|
r"(\d{1,3})\s*[x×х**]\s*(20|40|45)\s*(DC|DV|GP|HC|HQ|OT|FR|RF|RH)?",
|
||||||
|
t,
|
||||||
|
re.IGNORECASE,
|
||||||
|
):
|
||||||
|
_add(m.group(2), m.group(3))
|
||||||
|
for m in re.finditer(
|
||||||
|
r"\b(20|40|45)\s*(DC|DV|GP|HC|HQ|OT|FR|RF|RH)\b",
|
||||||
|
t,
|
||||||
|
re.IGNORECASE,
|
||||||
|
):
|
||||||
|
_add(m.group(1), m.group(2))
|
||||||
|
if not found:
|
||||||
|
return ["20DV", "40DV", "40HC"]
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def door_clearance_summary(shipment: Dict[str, Any]) -> str:
|
||||||
|
"""
|
||||||
|
Текст для п.21: сверка габаритов мест с дверным проёмом 20DV/40DV/40HC из справочника.
|
||||||
|
"""
|
||||||
|
data = load_iso_reference()
|
||||||
|
types = data.get("types") or {}
|
||||||
|
dims = shipment.get("dimensions")
|
||||||
|
if not isinstance(dims, list) or not dims:
|
||||||
|
return (
|
||||||
|
"Нет габаритов грузовых мест — проверка проходимости по справочнику дверных проёмов "
|
||||||
|
"(20DV 2336×2291 мм, 40DV 2336×2291 мм, 40HC 2340×2597 мм) невозможна."
|
||||||
|
)
|
||||||
|
|
||||||
|
ct_raw = (shipment.get("container_type") or "").strip()
|
||||||
|
codes = _codes_from_container_type(ct_raw)
|
||||||
|
parts: List[str] = []
|
||||||
|
for i, d in enumerate(dims, start=1):
|
||||||
|
edges = _edges_mm_from_dim(d)
|
||||||
|
if not edges:
|
||||||
|
parts.append(f"Место {i}: нет полных Д×Ш×В в см.")
|
||||||
|
continue
|
||||||
|
place_bits: List[str] = []
|
||||||
|
for code in codes:
|
||||||
|
spec = types.get(code)
|
||||||
|
if not isinstance(spec, dict):
|
||||||
|
continue
|
||||||
|
door = spec.get("door_opening_mm") or {}
|
||||||
|
dw = float(door.get("width") or 0)
|
||||||
|
dh = float(door.get("height") or 0)
|
||||||
|
if dw <= 0 or dh <= 0:
|
||||||
|
continue
|
||||||
|
ok = _box_fits_door(edges, dw, dh)
|
||||||
|
label = spec.get("label", code)
|
||||||
|
status = "проходит" if ok else "не проходит"
|
||||||
|
place_bits.append(f"{code} ({int(dw)}×{int(dh)} мм): {status}")
|
||||||
|
if place_bits:
|
||||||
|
parts.append(f"Место {i} (рёбра {int(min(edges))}×{int(sorted(edges)[1])}×{int(max(edges))} мм): " + "; ".join(place_bits))
|
||||||
|
else:
|
||||||
|
parts.append(f"Место {i}: нет данных дверного проёма в справочнике для кодов {', '.join(codes)}.")
|
||||||
|
|
||||||
|
note = (
|
||||||
|
" Оценка: два наименьших габарита сравниваются с шириной и высотой дверного проёма (ориентация коробки)."
|
||||||
|
)
|
||||||
|
prefix = (
|
||||||
|
f"Справочник дверных проёмов (п.21). Контейнеры в заявке: {ct_raw or 'не указаны — проверка по 20DV/40DV/40HC'}. "
|
||||||
|
)
|
||||||
|
return prefix + " ".join(parts) + note
|
||||||
|
|
||||||
|
|
@ -0,0 +1,801 @@
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"source_xlsx": "Запрос для ИИ Общий(3).xlsx",
|
||||||
|
"by_shipping_type": {
|
||||||
|
"Автомобильная перевозка (LTL)": {
|
||||||
|
"sheet": "Авто LTL интерфейс",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "6",
|
||||||
|
"label": "Количество грузовых мест и вес, объем"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "7",
|
||||||
|
"label": "Габариты грузовых мест ДхШхВ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "10",
|
||||||
|
"label": "Штабелирвоание с другими отправками"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "11",
|
||||||
|
"label": "Штабелирвоание между собой"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "9",
|
||||||
|
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "12",
|
||||||
|
"label": "Батарейки в составе груза / отдельно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "13",
|
||||||
|
"label": "MSDS"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "14",
|
||||||
|
"label": "DGM"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "15",
|
||||||
|
"label": "Название бренда"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "16",
|
||||||
|
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "17",
|
||||||
|
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "18",
|
||||||
|
"label": "Требуется ли замена документов"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Автомобильная перевозка (FTL)": {
|
||||||
|
"sheet": "Авто FTL интерфейс",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "6",
|
||||||
|
"label": "Количество и типоразмер машин"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "7",
|
||||||
|
"label": "Количество грузовых мест и вес, объем"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "left_12",
|
||||||
|
"num": "8",
|
||||||
|
"label": "Габариты грузовых мест ДхШхВ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "10",
|
||||||
|
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "12",
|
||||||
|
"label": "Батарейки в составе груза / отдельно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "11",
|
||||||
|
"label": "MSDS"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "13",
|
||||||
|
"label": "DGM"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "14",
|
||||||
|
"label": "Название бренда"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "15",
|
||||||
|
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "16",
|
||||||
|
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "17",
|
||||||
|
"label": "Требуется ли замена документов"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Морская перевозка (LCL)": {
|
||||||
|
"sheet": "Море, море+жд, жд LCL интерфейc",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "6",
|
||||||
|
"label": "Количество и типоразмер машин"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "7",
|
||||||
|
"label": "Количество грузовых мест и вес, объем"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "10",
|
||||||
|
"label": "Штабелирвоание с другими отправками"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "11",
|
||||||
|
"label": "Штабелирвоание между собой"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "9",
|
||||||
|
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "12",
|
||||||
|
"label": "Батарейки в составе груза / отдельно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "13",
|
||||||
|
"label": "MSDS"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "14",
|
||||||
|
"label": "DGM"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "15",
|
||||||
|
"label": "Название бренда"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "16",
|
||||||
|
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "17",
|
||||||
|
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "18",
|
||||||
|
"label": "Требуется ли замена документов"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Железнодорожная перевозка (LCL)": {
|
||||||
|
"sheet": "Море, море+жд, жд LCL интерфейc",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "6",
|
||||||
|
"label": "Количество и типоразмер машин"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "7",
|
||||||
|
"label": "Количество грузовых мест и вес, объем"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "10",
|
||||||
|
"label": "Штабелирвоание с другими отправками"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "11",
|
||||||
|
"label": "Штабелирвоание между собой"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "9",
|
||||||
|
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "12",
|
||||||
|
"label": "Батарейки в составе груза / отдельно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "13",
|
||||||
|
"label": "MSDS"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "14",
|
||||||
|
"label": "DGM"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "15",
|
||||||
|
"label": "Название бренда"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "16",
|
||||||
|
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "17",
|
||||||
|
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "18",
|
||||||
|
"label": "Требуется ли замена документов"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Мультимодальная перевозка море + ж/д (LCL)": {
|
||||||
|
"sheet": "Море, море+жд, жд LCL интерфейc",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "6",
|
||||||
|
"label": "Количество и типоразмер машин"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "7",
|
||||||
|
"label": "Количество грузовых мест и вес, объем"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "10",
|
||||||
|
"label": "Штабелирвоание с другими отправками"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "11",
|
||||||
|
"label": "Штабелирвоание между собой"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "9",
|
||||||
|
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "12",
|
||||||
|
"label": "Батарейки в составе груза / отдельно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "13",
|
||||||
|
"label": "MSDS"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "14",
|
||||||
|
"label": "DGM"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "15",
|
||||||
|
"label": "Название бренда"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "16",
|
||||||
|
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "17",
|
||||||
|
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "18",
|
||||||
|
"label": "Требуется ли замена документов"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Морская перевозка (FCL)": {
|
||||||
|
"sheet": "Море, море+жд, жд FCL интерфейc",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "6",
|
||||||
|
"label": "Количество и типоразмер контейнеров"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "7",
|
||||||
|
"label": "Количество грузовых мест и вес, объем"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "left_12",
|
||||||
|
"num": "8",
|
||||||
|
"label": "Габариты грузовых мест ДхШхВ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "10",
|
||||||
|
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "12",
|
||||||
|
"label": "Батарейки в составе груза / отдельно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "11",
|
||||||
|
"label": "MSDS"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "13",
|
||||||
|
"label": "Technical Description of Goods in Railway Transport"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "14",
|
||||||
|
"label": "Название бренда"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "15",
|
||||||
|
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "16",
|
||||||
|
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "17",
|
||||||
|
"label": "Требуется ли замена документов"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "18",
|
||||||
|
"label": "Место таможенного оформления при экспорте из РФ"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Наличие Фумигации на деревянной таре при экспорте из РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "left_12",
|
||||||
|
"num": "20",
|
||||||
|
"label": "Превышение габаритов дверного проема контейнеров 20DV, 40DV, 40HC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Железнодорожная перевозка (FCL)": {
|
||||||
|
"sheet": "Море, море+жд, жд FCL интерфейc",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "6",
|
||||||
|
"label": "Количество и типоразмер контейнеров"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "7",
|
||||||
|
"label": "Количество грузовых мест и вес, объем"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "left_12",
|
||||||
|
"num": "8",
|
||||||
|
"label": "Габариты грузовых мест ДхШхВ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "10",
|
||||||
|
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "12",
|
||||||
|
"label": "Батарейки в составе груза / отдельно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "11",
|
||||||
|
"label": "MSDS"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "13",
|
||||||
|
"label": "Technical Description of Goods in Railway Transport"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "14",
|
||||||
|
"label": "Название бренда"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "15",
|
||||||
|
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "16",
|
||||||
|
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "17",
|
||||||
|
"label": "Требуется ли замена документов"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "18",
|
||||||
|
"label": "Место таможенного оформления при экспорте из РФ"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Наличие Фумигации на деревянной таре при экспорте из РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "left_12",
|
||||||
|
"num": "20",
|
||||||
|
"label": "Превышение габаритов дверного проема контейнеров 20DV, 40DV, 40HC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Мультимодальная перевозка море + ж/д (FCL)": {
|
||||||
|
"sheet": "Море, море+жд, жд FCL интерфейc",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "6",
|
||||||
|
"label": "Количество и типоразмер контейнеров"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "7",
|
||||||
|
"label": "Количество грузовых мест и вес, объем"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "left_12",
|
||||||
|
"num": "8",
|
||||||
|
"label": "Габариты грузовых мест ДхШхВ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "10",
|
||||||
|
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "12",
|
||||||
|
"label": "Батарейки в составе груза / отдельно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "11",
|
||||||
|
"label": "MSDS"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "13",
|
||||||
|
"label": "Technical Description of Goods in Railway Transport"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "14",
|
||||||
|
"label": "Название бренда"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "15",
|
||||||
|
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "16",
|
||||||
|
"label": "Требуется ли дополнительный сервис - фитоконтроль, иное"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "17",
|
||||||
|
"label": "Требуется ли замена документов"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "18",
|
||||||
|
"label": "Место таможенного оформления при экспорте из РФ"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Наличие Фумигации на деревянной таре при экспорте из РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "left_12",
|
||||||
|
"num": "20",
|
||||||
|
"label": "Превышение габаритов дверного проема контейнеров 20DV, 40DV, 40HC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Авиаперевозка": {
|
||||||
|
"sheet": "Авиа Интерфейс",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Страна, город, точный адрес доставки, Место ТО в РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "6",
|
||||||
|
"label": "Количество грузовых мест и вес, объем"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "7",
|
||||||
|
"label": "Габариты грузовых мест ДхШхВ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "10",
|
||||||
|
"label": "Штабелирвоание с другими отправками"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "11",
|
||||||
|
"label": "Штабелирвоание между собой"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "9",
|
||||||
|
"label": "Наличие батарейк, газов, жидкостей, аэрозолей"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "13",
|
||||||
|
"label": "Батарейки в составе груза / отдельно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "12",
|
||||||
|
"label": "MSDS"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "14",
|
||||||
|
"label": "DGM"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "15",
|
||||||
|
"label": "Название бренда"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "16",
|
||||||
|
"label": "Авторизационное письмо/разрешение на вывоз этих брендов из Китая"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "17",
|
||||||
|
"label": "Перелет через третью страну со сменой авинаклданых"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "18",
|
||||||
|
"label": "Наличие экспортной лицезии у отправителя"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "19",
|
||||||
|
"label": "Дополнительный сервис"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "20",
|
||||||
|
"label": "Экспедирование в аэропорту назначения"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "22",
|
||||||
|
"label": "Спец требования при доставке до адреса в РФ"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "23",
|
||||||
|
"label": "Таможенное оформление"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "24",
|
||||||
|
"label": "Место таможенного оформления при экспорте из РФ"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "25",
|
||||||
|
"label": "Наличие Фумигации на деревянной таре при экспорте из РФ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
from pypdf import PdfReader
|
||||||
|
from docx import Document
|
||||||
|
import openpyxl
|
||||||
|
from typing import Union
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DocumentProcessor:
|
||||||
|
@staticmethod
|
||||||
|
def _cell_to_text(v) -> str:
|
||||||
|
if v is None:
|
||||||
|
return ""
|
||||||
|
s = str(v).strip()
|
||||||
|
return s if s else ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sheet_rows_text_openpyxl(sheet) -> list[str]:
|
||||||
|
out: list[str] = []
|
||||||
|
for row in sheet.iter_rows(values_only=True):
|
||||||
|
vals = [DocumentProcessor._cell_to_text(c) for c in row]
|
||||||
|
vals = [x for x in vals if x]
|
||||||
|
if vals:
|
||||||
|
out.append(" | ".join(vals))
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sheet_cols_text_openpyxl(sheet) -> list[str]:
|
||||||
|
out: list[str] = []
|
||||||
|
max_col = sheet.max_column or 0
|
||||||
|
max_row = sheet.max_row or 0
|
||||||
|
for c in range(1, max_col + 1):
|
||||||
|
vals: list[str] = []
|
||||||
|
for r in range(1, max_row + 1):
|
||||||
|
v = DocumentProcessor._cell_to_text(sheet.cell(r, c).value)
|
||||||
|
if v:
|
||||||
|
vals.append(v)
|
||||||
|
if vals:
|
||||||
|
col_letter = openpyxl.utils.get_column_letter(c)
|
||||||
|
out.append(f"COL {col_letter} TOP_DOWN: " + " || ".join(vals))
|
||||||
|
out.append(f"COL {col_letter} BOTTOM_UP: " + " || ".join(reversed(vals)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sheet_rows_text_xlrd(sheet) -> list[str]:
|
||||||
|
out: list[str] = []
|
||||||
|
for row_idx in range(sheet.nrows):
|
||||||
|
row = sheet.row_values(row_idx)
|
||||||
|
vals = [DocumentProcessor._cell_to_text(c) for c in row]
|
||||||
|
vals = [x for x in vals if x]
|
||||||
|
if vals:
|
||||||
|
out.append(" | ".join(vals))
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sheet_cols_text_xlrd(sheet) -> list[str]:
|
||||||
|
out: list[str] = []
|
||||||
|
for c in range(sheet.ncols):
|
||||||
|
vals: list[str] = []
|
||||||
|
for r in range(sheet.nrows):
|
||||||
|
v = DocumentProcessor._cell_to_text(sheet.cell_value(r, c))
|
||||||
|
if v:
|
||||||
|
vals.append(v)
|
||||||
|
if vals:
|
||||||
|
out.append(f"COL {c+1} TOP_DOWN: " + " || ".join(vals))
|
||||||
|
out.append(f"COL {c+1} BOTTOM_UP: " + " || ".join(reversed(vals)))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def normalize_email_html(html_content: str) -> str:
|
||||||
|
"""
|
||||||
|
Очищает HTML письма и нормализует таблицы
|
||||||
|
"""
|
||||||
|
|
||||||
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
|
|
||||||
|
# удаляем скрипты и стили
|
||||||
|
for tag in soup(["script", "style"]):
|
||||||
|
tag.decompose()
|
||||||
|
|
||||||
|
# нормализуем таблицы
|
||||||
|
for table in soup.find_all("table"):
|
||||||
|
table["style"] = "border-collapse: collapse; width: 100%;"
|
||||||
|
|
||||||
|
for cell in table.find_all(["td", "th"]):
|
||||||
|
cell["style"] = "border:1px solid #ccc;padding:6px;"
|
||||||
|
|
||||||
|
return str(soup)
|
||||||
|
def extract_text(self, content: bytes, filename: str) -> str:
|
||||||
|
ext = filename.lower().split('.')[-1]
|
||||||
|
|
||||||
|
# Изображения не обрабатываем
|
||||||
|
if ext in ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'webp']:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if ext == 'pdf':
|
||||||
|
return self._extract_pdf(content)
|
||||||
|
elif ext in ['docx', 'doc']:
|
||||||
|
return self._extract_docx(content)
|
||||||
|
elif ext in ['xlsx', 'xls']:
|
||||||
|
return self._extract_excel(content)
|
||||||
|
elif ext == 'txt':
|
||||||
|
return content.decode('utf-8')
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported format: {ext}")
|
||||||
|
def _extract_pdf(self, content: bytes) -> str:
|
||||||
|
pdf = PdfReader(io.BytesIO(content))
|
||||||
|
return "\n".join(page.extract_text() for page in pdf.pages)
|
||||||
|
|
||||||
|
def _extract_docx(self, content: bytes) -> str:
|
||||||
|
doc = Document(io.BytesIO(content))
|
||||||
|
return "\n".join(paragraph.text for paragraph in doc.paragraphs)
|
||||||
|
|
||||||
|
def _extract_excel(self, content: bytes) -> str:
|
||||||
|
try:
|
||||||
|
# сначала пробуем openpyxl (для .xlsx)
|
||||||
|
wb = openpyxl.load_workbook(io.BytesIO(content))
|
||||||
|
text = []
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
text.append(f"=== SHEET: {sheet.title} ===")
|
||||||
|
# Сохраняем совместимость: построчное чтение.
|
||||||
|
rows_text = self._sheet_rows_text_openpyxl(sheet)
|
||||||
|
if rows_text:
|
||||||
|
text.append("[ROWS]")
|
||||||
|
text.extend(rows_text)
|
||||||
|
# Новый режим: чтение по колонкам (общие данные часто внизу столбца).
|
||||||
|
cols_text = self._sheet_cols_text_openpyxl(sheet)
|
||||||
|
if cols_text:
|
||||||
|
text.append("[COLUMNS]")
|
||||||
|
text.extend(cols_text)
|
||||||
|
return "\n".join(text)
|
||||||
|
except Exception as e:
|
||||||
|
# если не получилось, возможно это .xls, пробуем xlrd
|
||||||
|
try:
|
||||||
|
import xlrd
|
||||||
|
workbook = xlrd.open_workbook(file_contents=content)
|
||||||
|
text = []
|
||||||
|
for sheet in workbook.sheets():
|
||||||
|
text.append(f"=== SHEET: {sheet.name} ===")
|
||||||
|
rows_text = self._sheet_rows_text_xlrd(sheet)
|
||||||
|
if rows_text:
|
||||||
|
text.append("[ROWS]")
|
||||||
|
text.extend(rows_text)
|
||||||
|
cols_text = self._sheet_cols_text_xlrd(sheet)
|
||||||
|
if cols_text:
|
||||||
|
text.append("[COLUMNS]")
|
||||||
|
text.extend(cols_text)
|
||||||
|
return "\n".join(text)
|
||||||
|
except ImportError:
|
||||||
|
logger.error("xlrd not installed, cannot parse .xls files")
|
||||||
|
return ""
|
||||||
|
except Exception as e2:
|
||||||
|
logger.error(f"Failed to parse Excel with xlrd: {e2}")
|
||||||
|
return ""
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
"""
|
||||||
|
Однократная/повторная генерация criteria_interface_layout.json из Excel
|
||||||
|
«Запрос для ИИ Общий(2).xlsx» (листы с «интерфейс» в названии).
|
||||||
|
|
||||||
|
Запуск (путь к xlsx можно передать аргументом):
|
||||||
|
py extract_criteria_interface_layout.py "C:\\Users\\...\\Запрос для ИИ Общий(2).xlsx"
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import openpyxl
|
||||||
|
|
||||||
|
SHEET_TO_SHIPPING_TYPES: dict[str, list[str]] = {
|
||||||
|
"Авто LTL интерфейс": ["Автомобильная перевозка (LTL)"],
|
||||||
|
"Авто FTL интерфейс": ["Автомобильная перевозка (FTL)"],
|
||||||
|
"Море, море+жд, жд LCL интерфейc": [
|
||||||
|
"Морская перевозка (LCL)",
|
||||||
|
"Железнодорожная перевозка (LCL)",
|
||||||
|
"Мультимодальная перевозка море + ж/д (LCL)",
|
||||||
|
],
|
||||||
|
"Море, море+жд, жд FCL интерфейc": [
|
||||||
|
"Морская перевозка (FCL)",
|
||||||
|
"Железнодорожная перевозка (FCL)",
|
||||||
|
"Мультимодальная перевозка море + ж/д (FCL)",
|
||||||
|
],
|
||||||
|
"Авиа Интерфейс": ["Авиаперевозка"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _row_cells(ws, r: int, max_c: int = 12) -> list[tuple[int, str]]:
|
||||||
|
out: list[tuple[int, str]] = []
|
||||||
|
for c in range(1, max_c + 1):
|
||||||
|
v = ws.cell(r, c).value
|
||||||
|
if v is not None and str(v).strip():
|
||||||
|
out.append((c, str(v).strip()))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def parse_interface_sheet(ws) -> list[dict]:
|
||||||
|
"""Разбор листа интерфейса в последовательность визуальных блоков."""
|
||||||
|
blocks: list[dict] = []
|
||||||
|
r = 1
|
||||||
|
max_r = ws.max_row or 1
|
||||||
|
while r <= max_r:
|
||||||
|
cells = _row_cells(ws, r)
|
||||||
|
if not cells:
|
||||||
|
r += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Заголовок: 7 ячеек в колонках 1,2,3,4,5,7,9 и все значения — «номера пунктов»
|
||||||
|
if (
|
||||||
|
len(cells) == 7
|
||||||
|
and {c[0] for c in cells} == {1, 2, 3, 4, 5, 7, 9}
|
||||||
|
and all(str(c[1]).strip().isdigit() or c[1].strip().replace(".", "").isdigit() for c in cells)
|
||||||
|
):
|
||||||
|
nums = [c[1] for c in sorted(cells, key=lambda x: (x[0] not in (1, 2, 3, 4, 5, 7, 9), x[0]))]
|
||||||
|
# порядок как в Excel слева направо по колонкам
|
||||||
|
nums = [c[1] for c in sorted(cells, key=lambda x: x[0])]
|
||||||
|
r2 = r + 1
|
||||||
|
cells2 = _row_cells(ws, r2)
|
||||||
|
if len(cells2) == 7 and {c[0] for c in cells2} == {1, 2, 3, 4, 5, 7, 9}:
|
||||||
|
labels = [c[1] for c in sorted(cells2, key=lambda x: x[0])]
|
||||||
|
blocks.append({"type": "header", "nums": nums, "labels": labels})
|
||||||
|
r += 2
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Только правая пара (6 = номер, 7 = текст) — например «20 Место таможни»
|
||||||
|
if len(cells) == 2 and cells[0][0] == 6 and cells[1][0] == 7:
|
||||||
|
blocks.append({"type": "right_67", "num": cells[0][1], "label": cells[1][1]})
|
||||||
|
r += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Две пары: 1-2 и 6-7
|
||||||
|
if len(cells) == 4 and {cells[i][0] for i in range(4)} == {1, 2, 6, 7}:
|
||||||
|
blocks.append(
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {"num": cells[0][1], "label": cells[1][1]},
|
||||||
|
"right": {"num": cells[2][1], "label": cells[3][1]},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
r += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Только левая пара 1-2 (один блок на строку)
|
||||||
|
if len(cells) == 2 and cells[0][0] == 1 and cells[1][0] == 2:
|
||||||
|
blocks.append({"type": "left_12", "num": cells[0][1], "label": cells[1][1]})
|
||||||
|
r += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
r += 1
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
xlsx = (
|
||||||
|
sys.argv[1]
|
||||||
|
if len(sys.argv) > 1
|
||||||
|
else os.path.join(os.path.dirname(__file__), "Запрос для ИИ Общий(3).xlsx")
|
||||||
|
)
|
||||||
|
if not os.path.isfile(xlsx):
|
||||||
|
print("Файл не найден:", xlsx)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
wb = openpyxl.load_workbook(xlsx, read_only=False, data_only=True)
|
||||||
|
out: dict = {"version": 1, "source_xlsx": os.path.basename(xlsx), "by_shipping_type": {}}
|
||||||
|
|
||||||
|
for sheet_name, type_names in SHEET_TO_SHIPPING_TYPES.items():
|
||||||
|
if sheet_name not in wb.sheetnames:
|
||||||
|
print("Пропуск: лист не найден:", sheet_name)
|
||||||
|
continue
|
||||||
|
blocks = parse_interface_sheet(wb[sheet_name])
|
||||||
|
for tn in type_names:
|
||||||
|
out["by_shipping_type"][tn] = {"sheet": sheet_name, "blocks": blocks}
|
||||||
|
|
||||||
|
out_path = os.path.join(os.path.dirname(__file__), "criteria_interface_layout.json")
|
||||||
|
with open(out_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(out, f, ensure_ascii=False, indent=2)
|
||||||
|
print("Записано:", out_path, "типов:", len(out["by_shipping_type"]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue