NEKReport/container_reference.py

179 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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