Загрузить файлы в «/»
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