Загрузить файлы в «/»
This commit is contained in:
parent
b0111c451e
commit
f554962d50
|
|
@ -0,0 +1,502 @@
|
|||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, Response
|
||||
from typing import List, Optional, Dict
|
||||
from fastapi import FastAPI, UploadFile, File, HTTPException, Form, Body
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
import uvicorn
|
||||
from rag_engine_gemini import RAGEngineGemini
|
||||
import logging
|
||||
from prometheus_fastapi_instrumentator import Instrumentator
|
||||
import asyncio
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
from urllib.parse import quote
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
os.makedirs("/opt/rag-gemini/app/reports", exist_ok=True)
|
||||
SHIPPING_TYPES_FILE = os.path.join(os.path.dirname(__file__), "shipping_types.json")
|
||||
PROCESSED_SHIPPING_TYPES_FILE = os.path.join(
|
||||
os.path.dirname(__file__), "shipping_types_processed.json"
|
||||
)
|
||||
|
||||
|
||||
class ShippingTypeBase(BaseModel):
|
||||
name: str
|
||||
criteria: str = ""
|
||||
keywords: List[str] = []
|
||||
employee_email: str = ""
|
||||
confirmation_template: str = ""
|
||||
info_request_template: str = ""
|
||||
|
||||
|
||||
class ShippingTypeCreate(ShippingTypeBase):
|
||||
pass
|
||||
|
||||
|
||||
class ShippingTypeUpdate(ShippingTypeBase):
|
||||
pass
|
||||
|
||||
|
||||
class ShippingType(ShippingTypeBase):
|
||||
id: int
|
||||
|
||||
|
||||
def load_shipping_types():
|
||||
"""Загружает типы перевозок с нормализацией ключей"""
|
||||
if not os.path.exists(SHIPPING_TYPES_FILE):
|
||||
return []
|
||||
with open(SHIPPING_TYPES_FILE, "r", encoding="utf-8") as f:
|
||||
types = json.load(f)
|
||||
|
||||
# Нормализуем ключи (убираем пробелы) и keywords
|
||||
normalized_types = []
|
||||
for t in types:
|
||||
normalized = {}
|
||||
for key, value in t.items():
|
||||
clean_key = key.strip()
|
||||
if clean_key == "keywords" and isinstance(value, list):
|
||||
# Нормализуем keywords - каждый элемент отдельно, убираем пробелы и кавычки
|
||||
normalized[clean_key] = [kw.strip().strip('"').strip("'") for kw in value if kw.strip()]
|
||||
elif isinstance(value, str):
|
||||
normalized[clean_key] = value.strip()
|
||||
else:
|
||||
normalized[clean_key] = value
|
||||
normalized_types.append(normalized)
|
||||
|
||||
return normalized_types
|
||||
|
||||
|
||||
def save_shipping_types(types):
|
||||
"""Сохраняет типы перевозок в файл"""
|
||||
with open(SHIPPING_TYPES_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(types, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def load_processed_shipping_criteria() -> Dict[str, str]:
|
||||
"""Loads AI-processed criteria per shipping type name."""
|
||||
if not os.path.exists(PROCESSED_SHIPPING_TYPES_FILE):
|
||||
return {}
|
||||
try:
|
||||
with open(PROCESSED_SHIPPING_TYPES_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if isinstance(data, dict):
|
||||
return {str(k): str(v) for k, v in data.items() if v is not None}
|
||||
return {}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load processed shipping criteria: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def save_processed_shipping_criteria(mapping: Dict[str, str]) -> None:
|
||||
"""Saves AI-processed criteria per shipping type name."""
|
||||
with open(PROCESSED_SHIPPING_TYPES_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(mapping, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def upsert_processed_shipping_criteria(type_name: str, processed_criteria: str) -> None:
|
||||
if not isinstance(type_name, str) or not type_name.strip():
|
||||
return
|
||||
processed_criteria = processed_criteria if isinstance(processed_criteria, str) else ""
|
||||
mapping = load_processed_shipping_criteria()
|
||||
mapping[type_name] = processed_criteria
|
||||
save_processed_shipping_criteria(mapping)
|
||||
|
||||
|
||||
app = FastAPI(title="SEPTEM Cargo RAG System")
|
||||
app.mount("/addin", StaticFiles(directory=os.path.join(os.getcwd(), "addin"), html=True), name="addin")
|
||||
|
||||
OLD_HOST = "nec.septem.pro"
|
||||
NEW_HOST = "nec.clients.septem.pro"
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def redirect_legacy_host(request, call_next):
|
||||
host = request.headers.get("host", "").split(":")[0].lower()
|
||||
if host == OLD_HOST:
|
||||
target_url = f"https://{NEW_HOST}{request.url.path}"
|
||||
if request.url.query:
|
||||
target_url = f"{target_url}?{request.url.query}"
|
||||
return JSONResponse(
|
||||
status_code=308,
|
||||
content={"detail": "Permanent Redirect"},
|
||||
headers={"Location": target_url},
|
||||
)
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
@app.get("/addin/taskpane.html")
|
||||
async def get_taskpane():
|
||||
if os.path.exists("addin/taskpane.html"):
|
||||
return FileResponse("addin/taskpane.html")
|
||||
elif os.path.exists("frontend/taskpane.html"):
|
||||
return FileResponse("frontend/taskpane.html")
|
||||
else:
|
||||
return {"error": "taskpane.html not found"}
|
||||
|
||||
|
||||
@app.get("/addin/commands.html")
|
||||
async def get_commands():
|
||||
if os.path.exists("addin/commands.html"):
|
||||
return FileResponse("addin/commands.html")
|
||||
elif os.path.exists("frontend/commands.html"):
|
||||
return FileResponse("frontend/commands.html")
|
||||
else:
|
||||
return {"error": "commands.html not found"}
|
||||
|
||||
|
||||
rag = RAGEngineGemini()
|
||||
Instrumentator().instrument(app).expose(app)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[
|
||||
"https://nec.clients.septem.pro",
|
||||
"https://localhost:3000",
|
||||
"http://localhost:8501"
|
||||
],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["Content-Disposition", "Content-Length"]
|
||||
)
|
||||
|
||||
|
||||
class EmailAttachment(BaseModel):
|
||||
filename: str
|
||||
size: int
|
||||
content: Optional[str] = None
|
||||
|
||||
|
||||
class OutlookEmail(BaseModel):
|
||||
id: str
|
||||
subject: str
|
||||
sender: str
|
||||
senderName: Optional[str] = None
|
||||
body: str
|
||||
body_html: Optional[str] = None
|
||||
receivedTime: Optional[str] = None
|
||||
to: Optional[str] = None
|
||||
cc: Optional[str] = None
|
||||
attachments: List[EmailAttachment] = []
|
||||
|
||||
class OutlookEmailsRequest(BaseModel):
|
||||
emails: List[OutlookEmail]
|
||||
session_id: Optional[str] = None
|
||||
|
||||
|
||||
class CargoQueryRequest(BaseModel):
|
||||
query: str
|
||||
session_id: Optional[str] = None
|
||||
top_k: int = 10
|
||||
|
||||
|
||||
class CargoQueryResponse(BaseModel):
|
||||
answer: str
|
||||
structured_data: dict
|
||||
sources: list
|
||||
total_emails_analyzed: int
|
||||
|
||||
|
||||
class AnalyzeCargoRequest(BaseModel):
|
||||
email_ids: List[str]
|
||||
|
||||
|
||||
class CargoLearningRequest(BaseModel):
|
||||
"""Сохранение примера для обучения: переписка + структурированный ответ (как в отчёте)."""
|
||||
structured_data: dict
|
||||
session_id: Optional[str] = None
|
||||
context_preview: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
@app.post("/record-cargo-learning")
|
||||
async def record_cargo_learning(req: CargoLearningRequest):
|
||||
"""
|
||||
Записывает пару «контекст писем → JSON отчёта» в локальное хранилище few-shot.
|
||||
Если передан session_id, текст писем подставляется из текущей сессии сервера.
|
||||
"""
|
||||
try:
|
||||
ok = rag.record_cargo_learning(
|
||||
structured_data=req.structured_data,
|
||||
session_id=req.session_id,
|
||||
context_preview=req.context_preview,
|
||||
notes=req.notes,
|
||||
)
|
||||
return {"status": "ok" if ok else "skipped", "stored": ok}
|
||||
except Exception as e:
|
||||
logger.error(f"record-cargo-learning error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/process-outlook-emails")
|
||||
async def process_outlook_emails(request: OutlookEmailsRequest):
|
||||
try:
|
||||
session_id = await rag.process_outlook_emails(
|
||||
[email.model_dump() for email in request.emails],
|
||||
session_id=request.session_id
|
||||
)
|
||||
return {
|
||||
"status": "success",
|
||||
"session_id": session_id,
|
||||
"emails_processed": len(request.emails)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Process Outlook emails error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/query-cargo", response_model=CargoQueryResponse)
|
||||
async def query_cargo(request: CargoQueryRequest):
|
||||
try:
|
||||
result = await rag.query_cargo_info(
|
||||
request.query,
|
||||
request.session_id,
|
||||
request.top_k
|
||||
)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Cargo query error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/generate-cargo-report")
|
||||
async def generate_cargo_report(session_id: str = Body(..., embed=True)):
|
||||
try:
|
||||
result = await rag.generate_cargo_report(session_id)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Generate report error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/email-sessions/{session_id}")
|
||||
async def get_session_info(session_id: str):
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"emails_count": 10
|
||||
}
|
||||
|
||||
|
||||
@app.post("/upload", include_in_schema=False)
|
||||
async def upload_document(file: UploadFile = File(...)):
|
||||
try:
|
||||
content = await file.read()
|
||||
doc_id = hashlib.md5(file.filename.encode()).hexdigest()
|
||||
asyncio.create_task(rag.process_document(content, file.filename))
|
||||
return {"status": "processing", "document_id": doc_id}
|
||||
except Exception as e:
|
||||
logger.error(f"Upload error: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 🔥 ENDPOINT ДЛЯ ПРОСМОТРА ВЛОЖЕНИЙ В БРАУЗЕРЕ (ИСПРАВЛЕННЫЙ)
|
||||
# =============================================================================
|
||||
@app.get("/attachments/{session_id}/{email_index}/{attachment_index}")
|
||||
async def get_attachment(session_id: str, email_index: int, attachment_index: int):
|
||||
"""
|
||||
Возвращает оригинальный файл вложения для просмотра в браузере.
|
||||
Поддерживает PDF, DOCX, XLSX, изображения и другие форматы.
|
||||
Корректно обрабатывает русские имена файлов (RFC 5987).
|
||||
"""
|
||||
att = rag.get_attachment(session_id, email_index, attachment_index)
|
||||
if att is None:
|
||||
raise HTTPException(status_code=404, detail="Attachment not found")
|
||||
|
||||
filename = att.get("filename", "attachment")
|
||||
content_base64 = att.get("content_base64")
|
||||
|
||||
# 🔥 Функция для безопасного формирования заголовка Content-Disposition
|
||||
def make_content_disposition(filename: str, disposition: str = "inline") -> str:
|
||||
"""
|
||||
Формирует Content-Disposition с поддержкой UTF-8 имён файлов (RFC 5987).
|
||||
Совместимо со старыми и новыми браузерами.
|
||||
"""
|
||||
# ASCII-версия для старых браузеров (fallback)
|
||||
ascii_filename = filename.encode('ascii', 'ignore').decode('ascii') or 'attachment'
|
||||
|
||||
# UTF-8 версия с URL-кодированием для современных браузеров (RFC 5987)
|
||||
utf8_filename = quote(filename, safe='')
|
||||
|
||||
return f'{disposition}; filename="{ascii_filename}"; filename*=UTF-8\'\'{utf8_filename}'
|
||||
|
||||
# Если нет оригинального содержимого, возвращаем извлечённый текст
|
||||
if not content_base64:
|
||||
text = att.get("text", "")
|
||||
return Response(
|
||||
content=text,
|
||||
media_type="text/plain; charset=utf-8",
|
||||
headers={
|
||||
"Content-Disposition": make_content_disposition(filename + ".txt"),
|
||||
"Content-Length": str(len(text.encode('utf-8'))),
|
||||
"Access-Control-Expose-Headers": "Content-Disposition, Content-Length"
|
||||
}
|
||||
)
|
||||
|
||||
# 🔥 Декодируем base64 и определяем MIME тип по расширению
|
||||
try:
|
||||
file_content = base64.b64decode(content_base64)
|
||||
|
||||
# Определяем MIME тип по расширению файла
|
||||
ext = filename.lower().split('.')[-1] if '.' in filename else ''
|
||||
mime_types = {
|
||||
'pdf': 'application/pdf',
|
||||
'doc': 'application/msword',
|
||||
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'xls': 'application/vnd.ms-excel',
|
||||
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'txt': 'text/plain',
|
||||
'csv': 'text/csv',
|
||||
'png': 'image/png',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'gif': 'image/gif',
|
||||
'bmp': 'image/bmp',
|
||||
'zip': 'application/zip',
|
||||
'rar': 'application/vnd.rar',
|
||||
'ppt': 'application/vnd.ms-powerpoint',
|
||||
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'rtf': 'application/rtf',
|
||||
'xml': 'application/xml',
|
||||
'json': 'application/json',
|
||||
}
|
||||
|
||||
media_type = mime_types.get(ext, 'application/octet-stream')
|
||||
|
||||
# 🔥 Возвращаем файл с правильным заголовком Content-Disposition
|
||||
return Response(
|
||||
content=file_content,
|
||||
media_type=media_type,
|
||||
headers={
|
||||
"Content-Disposition": make_content_disposition(filename),
|
||||
"Content-Length": str(len(file_content)),
|
||||
"Access-Control-Expose-Headers": "Content-Disposition, Content-Length"
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error serving attachment: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error processing file: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/shipping-types", response_model=List[ShippingType])
|
||||
async def get_shipping_types():
|
||||
return load_shipping_types()
|
||||
|
||||
|
||||
@app.post("/shipping-types", response_model=ShippingType)
|
||||
async def create_shipping_type(item: ShippingTypeCreate):
|
||||
types = load_shipping_types()
|
||||
new_id = max([t["id"] for t in types], default=0) + 1
|
||||
new_item = item.model_dump()
|
||||
new_item["id"] = new_id
|
||||
types.append(new_item)
|
||||
save_shipping_types(types)
|
||||
|
||||
# AI-обработка criteria и сохранение результата
|
||||
try:
|
||||
type_name = new_item.get("name", "")
|
||||
criteria_text = new_item.get("criteria", "") or ""
|
||||
processed = rag.process_shipping_type_criteria(criteria_text)
|
||||
upsert_processed_shipping_criteria(type_name, processed)
|
||||
rag.reload_shipping_types()
|
||||
except Exception as e:
|
||||
logger.warning(f"AI criteria processing failed on create: {e}")
|
||||
return new_item
|
||||
|
||||
|
||||
@app.put("/shipping-types/{item_id}", response_model=ShippingType)
|
||||
async def update_shipping_type(item_id: int, item: ShippingTypeUpdate):
|
||||
types = load_shipping_types()
|
||||
for t in types:
|
||||
if t["id"] == item_id:
|
||||
old_name = t.get("name", "")
|
||||
t.update(item.model_dump())
|
||||
save_shipping_types(types)
|
||||
|
||||
# AI-обработка criteria и сохранение результата
|
||||
try:
|
||||
type_name = t.get("name", "")
|
||||
criteria_text = t.get("criteria", "") or ""
|
||||
processed = rag.process_shipping_type_criteria(criteria_text)
|
||||
|
||||
# если поменяли имя типа — чистим старый ключ
|
||||
if isinstance(old_name, str) and old_name.strip() and old_name != type_name:
|
||||
mapping = load_processed_shipping_criteria()
|
||||
if old_name in mapping:
|
||||
mapping.pop(old_name, None)
|
||||
save_processed_shipping_criteria(mapping)
|
||||
|
||||
upsert_processed_shipping_criteria(type_name, processed)
|
||||
rag.reload_shipping_types()
|
||||
except Exception as e:
|
||||
logger.warning(f"AI criteria processing failed on update: {e}")
|
||||
return t
|
||||
raise HTTPException(status_code=404, detail="Type not found")
|
||||
|
||||
@app.get("/email-attachments/{session_id}")
|
||||
async def get_email_attachments(session_id: str):
|
||||
|
||||
emails = rag.sessions.get(session_id, [])
|
||||
|
||||
files = []
|
||||
|
||||
for email in emails:
|
||||
for att in email.get("attachments", []):
|
||||
|
||||
filename = att.get("filename","")
|
||||
ext = filename.split(".")[-1].lower()
|
||||
|
||||
# игнорируем изображения
|
||||
if ext in ["png","jpg","jpeg","gif","bmp","tiff","webp"]:
|
||||
continue
|
||||
|
||||
if att.get("content_base64"):
|
||||
files.append({
|
||||
"filename": filename,
|
||||
"content_base64": att["content_base64"]
|
||||
})
|
||||
|
||||
return files
|
||||
|
||||
@app.delete("/shipping-types/{item_id}")
|
||||
async def delete_shipping_type(item_id: int):
|
||||
types = load_shipping_types()
|
||||
removed = None
|
||||
for t in types:
|
||||
if t.get("id") == item_id:
|
||||
removed = t
|
||||
break
|
||||
new_types = [t for t in types if t["id"] != item_id]
|
||||
if len(new_types) == len(types):
|
||||
raise HTTPException(status_code=404, detail="Type not found")
|
||||
save_shipping_types(new_types)
|
||||
|
||||
# Удаляем обработанные criteria
|
||||
try:
|
||||
if removed:
|
||||
type_name = removed.get("name", "")
|
||||
mapping = load_processed_shipping_criteria()
|
||||
if type_name in mapping:
|
||||
mapping.pop(type_name, None)
|
||||
save_processed_shipping_criteria(mapping)
|
||||
rag.reload_shipping_types()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete processed criteria on delete: {e}")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import logging
|
||||
import os
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
import httpx
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
UPSTREAM_URL = os.getenv(
|
||||
"LLM_UPSTREAM_URL",
|
||||
"https://llm.corp.septem.pro/v1/chat/completions",
|
||||
).strip()
|
||||
OPENROUTER_KEY = os.getenv("OPENROUTER_API_KEY", "").strip()
|
||||
|
||||
|
||||
def _client_timeout() -> httpx.Timeout:
|
||||
total = float(os.getenv("LLM_PROXY_TIMEOUT_SECONDS", "120"))
|
||||
connect = float(os.getenv("LLM_PROXY_CONNECT_TIMEOUT", "15"))
|
||||
return httpx.Timeout(total, connect=connect)
|
||||
|
||||
|
||||
@app.post("/v1/chat/completions")
|
||||
async def proxy(req: Request):
|
||||
if not OPENROUTER_KEY:
|
||||
logger.error("OPENROUTER_API_KEY is not set (env or .env)")
|
||||
return JSONResponse(
|
||||
status_code=503,
|
||||
content={
|
||||
"error": "Service unavailable",
|
||||
"details": "Set OPENROUTER_API_KEY for the LLM proxy (or add it to .env).",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
body = await req.json()
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"error": "Invalid JSON", "details": str(e)},
|
||||
)
|
||||
|
||||
verify = os.getenv("HTTPX_VERIFY", "true").lower() not in ("0", "false", "no")
|
||||
|
||||
async with httpx.AsyncClient(verify=verify) as client:
|
||||
try:
|
||||
r = await client.post(
|
||||
UPSTREAM_URL,
|
||||
headers={
|
||||
"Authorization": f"Bearer {OPENROUTER_KEY}",
|
||||
"HTTP-Referer": os.getenv("LLM_HTTP_REFERER", "http://localhost"),
|
||||
"X-Title": os.getenv("LLM_PROXY_X_TITLE", "rag-engine"),
|
||||
},
|
||||
json=body,
|
||||
timeout=_client_timeout(),
|
||||
)
|
||||
except httpx.RequestError as e:
|
||||
logger.error(
|
||||
"LLM upstream unreachable (%s): %s",
|
||||
UPSTREAM_URL,
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=502,
|
||||
content={
|
||||
"error": "Bad gateway",
|
||||
"details": str(e),
|
||||
"upstream": UPSTREAM_URL,
|
||||
"hint": "Проверьте VPN, DNS, доступность хоста и переменную LLM_UPSTREAM_URL.",
|
||||
},
|
||||
)
|
||||
|
||||
if r.status_code >= 500:
|
||||
logger.warning(
|
||||
"LLM upstream HTTP %s, body (prefix): %s",
|
||||
r.status_code,
|
||||
(r.text or "")[:800],
|
||||
)
|
||||
|
||||
return Response(
|
||||
content=r.content,
|
||||
status_code=r.status_code,
|
||||
media_type=r.headers.get("content-type", "application/json"),
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,119 @@
|
|||
import math
|
||||
import re
|
||||
from typing import Dict, Optional
|
||||
|
||||
def calculate_shipping_cost(shipment: Dict) -> Optional[Dict]:
|
||||
"""
|
||||
Расчёт стоимости авиаперевозки на основе параметров груза.
|
||||
Возвращает словарь с ключами:
|
||||
total_usd, total_rub, chargeable_weight, actual_weight, volume_weight
|
||||
или None, если не хватает данных.
|
||||
"""
|
||||
try:
|
||||
# Проверяем наличие обязательных данных
|
||||
if not all(k in shipment for k in ['total_weight_kg', 'dimensions', 'cargo_value']):
|
||||
return None
|
||||
|
||||
# Параметры груза
|
||||
actual_weight = shipment.get('total_weight_kg', 0)
|
||||
if not actual_weight or actual_weight <= 0:
|
||||
return None
|
||||
|
||||
# Расчёт объёмного веса
|
||||
dimensions = shipment.get('dimensions', [])
|
||||
if not dimensions:
|
||||
return None
|
||||
|
||||
total_volume_weight = 0
|
||||
for dim in dimensions:
|
||||
if dim.get('length_cm') and dim.get('width_cm') and dim.get('height_cm'):
|
||||
# Объёмный вес = (Д×Ш×В в метрах) × 167
|
||||
length_m = dim['length_cm'] / 100
|
||||
width_m = dim['width_cm'] / 100
|
||||
height_m = dim['height_cm'] / 100
|
||||
volume = length_m * width_m * height_m
|
||||
volume_weight = volume * 167
|
||||
total_volume_weight += volume_weight
|
||||
|
||||
# Платный вес (оплачивается по большему)
|
||||
chargeable_weight = max(actual_weight, total_volume_weight)
|
||||
chargeable_weight = math.ceil(chargeable_weight) # округляем вверх
|
||||
|
||||
# Базовый тариф в зависимости от веса (USD/кг)
|
||||
if chargeable_weight < 45:
|
||||
base_rate = 6.5
|
||||
elif chargeable_weight < 100:
|
||||
base_rate = 5.5
|
||||
elif chargeable_weight < 300:
|
||||
base_rate = 4.5
|
||||
elif chargeable_weight < 500:
|
||||
base_rate = 3.8
|
||||
elif chargeable_weight < 1000:
|
||||
base_rate = 3.2
|
||||
else:
|
||||
base_rate = 2.8
|
||||
|
||||
# Топливный сбор и сбор за безопасность
|
||||
fuel_surcharge_rate = 0.9 # USD/кг
|
||||
min_fuel_surcharge = 90 # USD минимум
|
||||
|
||||
# Терминальные сборы
|
||||
terminal_export_rate = 0.22 # USD/кг (~20 руб/кг)
|
||||
terminal_export_fixed = 25 # USD фиксированных сборов (~2300 руб)
|
||||
terminal_import_rate = 0.35 # EUR/кг ≈ 0.38 USD/кг по курсу 1.1
|
||||
|
||||
# Таможенное оформление
|
||||
customs_export = 170 # USD (~15500 руб)
|
||||
customs_import = 220 # EUR ≈ 242 USD
|
||||
|
||||
# Надбавки за особые условия
|
||||
special_rate_multiplier = 1.0
|
||||
|
||||
# Терморежим (+15…+25 °C)
|
||||
if shipment.get('special_transport_requirements') and 'термо' in str(shipment.get('special_transport_requirements', '')).lower():
|
||||
special_rate_multiplier *= 1.3 # +30%
|
||||
|
||||
# Страхование
|
||||
cargo_value_usd = 0
|
||||
cargo_value_str = shipment.get('cargo_value', '0')
|
||||
# Извлекаем числовое значение из строки (например, "18 500 EUR" -> 18500)
|
||||
value_match = re.search(r'[\d\s]+', cargo_value_str.replace(' ', ''))
|
||||
if value_match:
|
||||
cargo_value_usd = float(value_match.group().replace(' ', ''))
|
||||
# Если указано в EUR, конвертируем
|
||||
if 'EUR' in cargo_value_str or '€' in cargo_value_str:
|
||||
cargo_value_usd *= 1.1 # приблизительный курс EUR/USD
|
||||
|
||||
insurance_cost = cargo_value_usd * 0.01 # 1% от стоимости груза
|
||||
|
||||
# Расчёт компонентов
|
||||
base_freight = chargeable_weight * base_rate * special_rate_multiplier
|
||||
fuel_surcharge = max(chargeable_weight * fuel_surcharge_rate, min_fuel_surcharge)
|
||||
terminal_export = chargeable_weight * terminal_export_rate + terminal_export_fixed
|
||||
terminal_import = chargeable_weight * 0.38 # конвертация EUR->USD
|
||||
customs_total = customs_export + customs_import
|
||||
|
||||
# Дополнительные услуги
|
||||
additional_services = shipment.get('additional_services', [])
|
||||
additional_cost = 0
|
||||
if isinstance(additional_services, list):
|
||||
if 'уведомление о прибытии' in str(additional_services).lower():
|
||||
additional_cost += 25 # USD
|
||||
|
||||
# Итог
|
||||
total_usd = (base_freight + fuel_surcharge + terminal_export +
|
||||
terminal_import + customs_total + additional_cost + insurance_cost)
|
||||
|
||||
# Конвертация в рубли для удобства (курс USD/RUB ~90)
|
||||
total_rub = total_usd * 90
|
||||
|
||||
return {
|
||||
"total_usd": round(total_usd, 2),
|
||||
"total_rub": round(total_rub, 2),
|
||||
"chargeable_weight": chargeable_weight,
|
||||
"actual_weight": actual_weight,
|
||||
"volume_weight": round(total_volume_weight, 2)
|
||||
}
|
||||
except Exception as e:
|
||||
# Логирование ошибки, если нужно
|
||||
return None
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Автомобильная перевозка (LTL)",
|
||||
"employee_email": "denis.fomin@timnet.ch, Yana.Schkabrova@timnet.ch",
|
||||
"extra_required_fields": ["hs_code", "customs_clearance_place_export_rf"],
|
||||
"mandatory_counterparty_criteria": "По макету интерфейса обязательно отражать в письме контрагенту и запрашивать у клиента при отсутствии: название клиента; Incoterms; характер/наименование груза и код ТН ВЭД; страна, город, адрес забора; страна, город, точный адрес доставки; место таможенного оформления в РФ; количество грузовых мест и вес; габариты грузовых мест (Д×Ш×В).",
|
||||
"criteria": "1\tНазвание клиента\n2\tУсловия поставки Incoterms\n3\tДата готовности груза\n4\tСтрана, город, адрес забора\n5\tСтоимость груза\n6\tКоличество грузовых мест и вес\n7\tГабариты каждого грузового места в см или мм (длина × ширина × высота)\n8\tХарактер груза или наименование груза, код ТН ВЭД\n9\tСодержатся ли в грузе батарейки, газы под давлением, жидкости\n10\tМожно ли штабелировать груз с другими отправками\n11\tЕсли груз нельзя штабелировать с другими грузами — можно ли штабелировать грузовые места между собой\n12\tЕсли груз — химия, литиевые батареи, жидкости, аэрозоли, газы или порошки — предоставить MSDS\n13\tЕсли есть батарейки — в составе груза или отдельно упакованы\n14\tЕсли груз из п.13 отгружается из материкового Китая по ж/д — дополнительно DGM\n15\tНазвание бренда\n16\tЕсли бренд зарегистрирован в таможенной системе Китая — есть ли у отправителя авторизационное письмо/разрешение на вывоз бренда\n17\tДополнительные сервисы (фитоконтроль и т.п.)\n18\tТребуется ли замена документов\n19\tСтрана, город, точный адрес доставки\n20\tМесто таможенного оформления в РФ",
|
||||
"keywords": ["ltl", "сборный трак", "догруз", "группаж авто", "less than truckload", "неполная фура", "авто ltl", "мелкий груз авто", "road ltl", "partial truckload"],
|
||||
"confirmation_template": "Dear Partner!\n\nThank you for contacting SEPTEM. We are pleased to confirm the possibility of road freight (LTL) for your cargo under the following terms:\n\n- Route: (Pickup address) -> (Delivery address)\n- Cargo weight: (Cargo weight) kg\n- Number of packages: (Number of packages)\n- Volume: (Volume) m3\n- Dimensions: (Dimensions)\n- Cargo description: (Cargo description)\n- HS code: (HS code)\n- Dangerous properties: (Dangerous properties)\n- MSDS required: (MSDS required)\n- Freight cost: (Estimated cost)\n- Estimated transit time: (Transit time)\n\nTo confirm the order and issue the invoice, please reply to this email or contact your manager.\n\nBest regards,\nSEPTEM Cargo Team",
|
||||
"info_request_template": "Уважаемый (Клиент)!\n\nБлагодарим вас за обращение в компанию SEPTEM. Для расчёта автомобильной перевозки (LTL) нам необходима следующая информация:\n\n(Необходимая информация)\n\nПожалуйста, дополните данные по вашему грузу. Вы можете ответить на это письмо или загрузить недостающие документы через личный кабинет.\n\nС уважением,\nКоманда SEPTEM Cargo"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Автомобильная перевозка (FTL)",
|
||||
"employee_email": "denis.fomin@timnet.ch, Yana.Schkabrova@timnet.ch",
|
||||
"extra_required_fields": ["hs_code", "vehicle_type", "customs_clearance_place_export_rf"],
|
||||
"mandatory_counterparty_criteria": "Обязательно в письме и при запросе клиенту: клиент; Incoterms; характер/наименование груза и ТН ВЭД; адрес забора; адрес доставки; место таможенного оформления в РФ; количество и типоразмер машин; вес груза в машине.",
|
||||
"criteria": "1\tНазвание клиента\n2\tУсловия поставки Incoterms\n3\tДата готовности груза\n4\tСтрана, город, адрес забора\n5\tСтоимость груза\n6\tКоличество и типоразмер машин\n7\tВес груза в машине\n8\tЕсли нет данных о количестве машин — габариты каждого грузового места (Д×Ш×В в см или мм) и вес каждого места\n9\tХарактер груза или наименование груза, код ТН ВЭД\n10\tСодержатся ли в грузе батарейки, газы под давлением, жидкости\n11\tХимия, литиевые батареи, жидкости, аэрозоли, газы или порошки — MSDS\n12\tБатарейки — в составе груза или отдельно упакованы\n13\tОтгрузка из материкового Китая по ж/д — DGM\n14\tНазвание бренда\n15\tЕсли бренд зарегистрирован в таможенной системе Китая — есть ли у отправителя авторизационное письмо/разрешение на вывоз бренда\n16\tДополнительные сервисы (фитоконтроль и т.п.)\n17\tТребуется ли замена документов\n18\tСтрана, город, точный адрес доставки\n19\tМесто таможенного оформления в РФ",
|
||||
"keywords": ["ftl", "полная фура", "целая машина", "full truckload", "отдельный автомобиль", "авто ftl", "тент фура целиком", "road ftl", "exclusive truck"],
|
||||
"confirmation_template": "Dear Partner!\n\nThank you for contacting SEPTEM. We are pleased to confirm the possibility of road freight (FTL) for your cargo under the following terms:\n\n- Route: (Pickup address) -> (Delivery address)\n- Vehicle type: (Vehicle type)\n- Cargo weight: (Cargo weight) kg\n- Number of packages: (Number of packages)\n- Volume: (Volume) m3\n- Dimensions: (Dimensions)\n- Cargo description: (Cargo description)\n- HS code: (HS code)\n- Dangerous properties: (Dangerous properties)\n- MSDS required: (MSDS required)\n- Freight cost: (Estimated cost)\n- Transit time: (Transit time)\n\nBest regards,\nSEPTEM Cargo Team",
|
||||
"info_request_template": "Уважаемый (Клиент)!\n\nБлагодарим за обращение в SEPTEM. Для расчёта автомобильной перевозки (FTL) нам необходима следующая информация:\n\n(Необходимая информация)\n\nПожалуйста, дополните данные по грузу.\n\nС уважением,\nКоманда SEPTEM Cargo"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Морская перевозка (LCL)",
|
||||
"employee_email": "marina.eremina@timnet.ch, ekaterina.kochenkova@timnet.ch, Yana.Schkabrova@timnet.ch",
|
||||
"extra_required_fields": ["hs_code", "stackable_with_others"],
|
||||
"mandatory_counterparty_criteria": "Обязательно: клиент; Incoterms; характер груза и ТН ВЭД; адрес забора; адрес доставки; количество мест и вес; габариты Д×Ш×В; возможность штабелирования с другими отправками.",
|
||||
"criteria": "1\tНазвание клиента\n2\tУсловия поставки Incoterms\n3\tДата готовности груза\n4\tСтрана, город, адрес забора\n5\tСтоимость груза\n6\tКоличество грузовых мест и вес\n7\tГабариты каждого грузового места (Д×Ш×В в см или мм)\n8\tХарактер груза или наименование, код ТН ВЭД\n9\tБатарейки, газы, газы под давлением, жидкости в грузе\n10\tМожно ли штабелировать груз с другими отправками\n11\tЕсли нельзя с другими — можно ли штабелировать места между собой\n12\tХимия, литиевые батареи, жидкости, аэрозоли, газы, порошки — MSDS\n13\tБатарейки — в составе или отдельно\n14\tОтгрузка из материкового Китая по ж/д — Technical Description of Goods in Railway Transport\n15\tНазвание бренда\n16\tЕсли бренд зарегистрирован в таможенной системе Китая — есть ли у отправителя авторизационное письмо/разрешение на вывоз бренда\n17\tДополнительные сервисы (фитоконтроль и т.п.)\n18\tСтрана, город, точный адрес доставки\n\nПрименимо к морской сборной перевозке (LCL): порт погрузки/выгрузки и морской этап явно указаны в переписке; без доминирования ж/д как основного этапа.",
|
||||
"keywords": ["море lcl", "морская сборная", "морской lcl", "sea lcl", "сборный морской", "консолидация море", "groupage sea", "lcl порт", "морской группаж", "неполный контейнер море"],
|
||||
"confirmation_template": "Dear Partner!\n\nThank you for contacting SEPTEM. We confirm the possibility of sea freight (LCL) under the following terms:\n\n- Route: (Port of loading) -> (Port of discharge)\n- Shipment type: LCL\n- Cargo weight: (Cargo weight) kg\n- Packages: (Number of packages)\n- Volume: (Volume) m3\n- Dimensions: (Dimensions)\n- Cargo: (Cargo description)\n- HS code: (HS code)\n- Dangerous properties: (Dangerous properties)\n- MSDS: (MSDS required)\n- Freight: (Estimated cost)\n- Transit: (Transit time)\n\nBest regards,\nSEPTEM Cargo Team",
|
||||
"info_request_template": "Уважаемый (Клиент)!\n\nДля расчёта морской сборной перевозки (LCL) нам необходима следующая информация:\n\n(Необходимая информация)\n\nПожалуйста, дополните данные.\n\nС уважением,\nКоманда SEPTEM Cargo"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Железнодорожная перевозка (LCL)",
|
||||
"employee_email": "marina.eremina@timnet.ch, ekaterina.kochenkova@timnet.ch, Yana.Schkabrova@timnet.ch",
|
||||
"extra_required_fields": ["hs_code", "stackable_with_others"],
|
||||
"mandatory_counterparty_criteria": "Обязательно: клиент; Incoterms; характер груза и ТН ВЭД; адрес забора; адрес доставки; количество мест и вес; габариты; штабелирование с другими отправками.",
|
||||
"criteria": "1\tНазвание клиента\n2\tУсловия поставки Incoterms\n3\tДата готовности груза\n4\tСтрана, город, адрес забора\n5\tСтоимость груза\n6\tКоличество грузовых мест и вес\n7\tГабариты каждого грузового места (Д×Ш×В в см или мм)\n8\tХарактер груза или наименование, код ТН ВЭД\n9\tБатарейки, газы, газы под давлением, жидкости в грузе\n10\tМожно ли штабелировать груз с другими отправками\n11\tЕсли нельзя с другими — можно ли штабелировать места между собой\n12\tХимия, литиевые батареи, жидкости, аэрозоли, газы, порошки — MSDS\n13\tБатарейки — в составе или отдельно\n14\tОтгрузка из материкового Китая по ж/д — Technical Description of Goods in Railway Transport\n15\tНазвание бренда\n16\tЕсли бренд зарегистрирован в таможенной системе Китая — есть ли у отправителя авторизационное письмо/разрешение на вывоз бренда\n17\tДополнительные сервисы (фитоконтроль и т.п.)\n18\tСтрана, город, точный адрес доставки\n\nПрименимо к ж/д сборной перевозке (LCL): станции/терминалы ж/д, контейнер сборный по ж/д; морской этап в запросе не основной.",
|
||||
"keywords": ["жд lcl", "ж/д lcl", "rail lcl", "сборный контейнер жд", "сборная жд", "lcl поезд", "группаж жд", "железная дорога сборный"],
|
||||
"confirmation_template": "Dear Partner!\n\nThank you for contacting SEPTEM. We confirm rail freight (LCL) under the following terms:\n\n- Route: (Pickup address) -> (Delivery address)\n- Shipment type: LCL\n- Weight: (Cargo weight) kg\n- Packages: (Number of packages)\n- Volume: (Volume) m3\n- Dimensions: (Dimensions)\n- Cargo: (Cargo description)\n- HS: (HS code)\n- MSDS: (MSDS required)\n- Cost: (Estimated cost)\n- Transit: (Transit time)\n\nBest regards,\nSEPTEM Cargo Team",
|
||||
"info_request_template": "Уважаемый (Клиент)!\n\nДля расчёта железнодорожной сборной перевозки (LCL) нам необходима следующая информация:\n\n(Необходимая информация)\n\nС уважением,\nКоманда SEPTEM Cargo"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Мультимодальная перевозка море + ж/д (LCL)",
|
||||
"employee_email": "marina.eremina@timnet.ch, ekaterina.kochenkova@timnet.ch, Yana.Schkabrova@timnet.ch",
|
||||
"extra_required_fields": ["hs_code", "stackable_with_others"],
|
||||
"mandatory_counterparty_criteria": "Обязательно: клиент; Incoterms; характер груза и ТН ВЭД; адрес забора; адрес доставки; количество мест и вес; габариты; штабелирование с другими отправками.",
|
||||
"criteria": "1\tНазвание клиента\n2\tУсловия поставки Incoterms\n3\tДата готовности груза\n4\tСтрана, город, адрес забора\n5\tСтоимость груза\n6\tКоличество грузовых мест и вес\n7\tГабариты каждого грузового места (Д×Ш×В в см или мм)\n8\tХарактер груза или наименование, код ТН ВЭД\n9\tБатарейки, газы, газы под давлением, жидкости в грузе\n10\tМожно ли штабелировать груз с другими отправками\n11\tЕсли нельзя с другими — можно ли штабелировать места между собой\n12\tХимия, литиевые батареи, жидкости, аэрозоли, газы, порошки — MSDS\n13\tБатарейки — в составе или отдельно\n14\tОтгрузка из материкового Китая по ж/д — Technical Description of Goods in Railway Transport\n15\tНазвание бренда\n16\tЕсли бренд зарегистрирован в таможенной системе Китая — есть ли у отправителя авторизационное письмо/разрешение на вывоз бренда\n17\tДополнительные сервисы (фитоконтроль и т.п.)\n18\tСтрана, город, точный адрес доставки\n\nКритерии как для сборного груза при цепочке море и ж/д (интермодаль): в переписке явно присутствуют и морской, и железнодорожный этапы.",
|
||||
"keywords": ["море жд", "море+жд", "море и жд", "sea rail", "интермодальн", "multimodal lcl", "морской и жд этап", "порт и станция сборный"],
|
||||
"confirmation_template": "Dear Partner!\n\nThank you for contacting SEPTEM. We confirm multimodal sea+rail (LCL) under the following terms:\n\n- Route: (Pickup address) -> (Delivery address)\n- Modes: sea + rail (LCL)\n- Weight: (Cargo weight) kg\n- Packages: (Number of packages)\n- Volume: (Volume) m3\n- Dimensions: (Dimensions)\n- Cargo: (Cargo description)\n- HS: (HS code)\n- MSDS: (MSDS required)\n- Cost: (Estimated cost)\n- Transit: (Transit time)\n\nBest regards,\nSEPTEM Cargo Team",
|
||||
"info_request_template": "Уважаемый (Клиент)!\n\nДля расчёта мультимодальной перевозки (море + ж/д, LCL) нам необходима следующая информация:\n\n(Необходимая информация)\n\nС уважением,\nКоманда SEPTEM Cargo"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Морская перевозка (FCL)",
|
||||
"employee_email": "marina.eremina@timnet.ch, ekaterina.kochenkova@timnet.ch, Yana.Schkabrova@timnet.ch",
|
||||
"extra_required_fields": ["hs_code", "container_type"],
|
||||
"mandatory_counterparty_criteria": "Обязательно: клиент; Incoterms; характер груза и ТН ВЭД; адрес забора; адрес доставки; количество и типоразмер контейнеров; вес груза в контейнере.",
|
||||
"criteria": "1\tНазвание клиента\n2\tУсловия поставки Incoterms\n3\tДата готовности груза\n4\tСтрана, город, адрес забора\n5\tСтоимость груза\n6\tКоличество и типоразмер контейнеров\n7\tВес груза в контейнере\n8\tЕсли нет данных о контейнерах — габариты каждого грузового места (Д×Ш×В) и вес мест\n9\tХарактер груза или наименование, код ТН ВЭД\n10\tБатарейки, газы, жидкости\n11\tMSDS при необходимости\n12\tБатарейки в составе или отдельно\n13\tИз материкового Китая по ж/д — Technical Description of Goods in Railway Transport\n14\tНазвание бренда\n15\tЕсли бренд зарегистрирован в таможенной системе Китая — есть ли у отправителя авторизационное письмо/разрешение на вывоз бренда\n16\tДополнительные сервисы (крепление в контейнере, фитосанитарный контроль и т.п.)\n17\tТребуется ли замена документов\n18\tСтрана, город, точный адрес доставки\n19\tМесто таможенного оформления при экспорте из РФ\n20\tФумигация деревянной тары при экспорте из РФ\n21\tПроверка габаритов грузовых мест на проходимость в дверной проём контейнеров 20DV, 40DV, 40HC\n\nМорской FCL: ISO-контейнеры под морской фрахт и погрузку на судно.",
|
||||
"keywords": ["морской fcl", "море fcl", "sea fcl", "полный контейнер море", "морской контейнер", "fcl порт", "цельный контейнер море", "ocean fcl"],
|
||||
"confirmation_template": "Dear Partner!\n\nThank you for contacting SEPTEM. We confirm sea freight (FCL):\n\n- Port: (Port of loading) -> (Port of discharge)\n- Container: (Container type)\n- Weight: (Cargo weight) kg\n- Cargo: (Cargo description)\n- HS: (HS code)\n- MSDS/IMDG: (MSDS required)\n- Cost: (Estimated cost)\n- Transit: (Transit time)\n\nBest regards,\nSEPTEM Cargo Team",
|
||||
"info_request_template": "Уважаемый (Клиент)!\n\nДля расчёта морской перевозки (FCL) нам необходима следующая информация:\n\n(Необходимая информация)\n\nС уважением,\nКоманда SEPTEM Cargo"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Железнодорожная перевозка (FCL)",
|
||||
"employee_email": "marina.eremina@timnet.ch, ekaterina.kochenkova@timnet.ch, Yana.Schkabrova@timnet.ch",
|
||||
"extra_required_fields": ["hs_code", "container_type"],
|
||||
"mandatory_counterparty_criteria": "Обязательно: клиент; Incoterms; характер груза и ТН ВЭД; адрес забора; адрес доставки; количество и типоразмер контейнеров; вес груза в контейнере.",
|
||||
"criteria": "1\tНазвание клиента\n2\tУсловия поставки Incoterms\n3\tДата готовности груза\n4\tСтрана, город, адрес забора\n5\tСтоимость груза\n6\tКоличество и типоразмер контейнеров\n7\tВес груза в контейнере\n8\tЕсли нет данных о контейнерах — габариты каждого грузового места (Д×Ш×В) и вес мест\n9\tХарактер груза или наименование, код ТН ВЭД\n10\tБатарейки, газы, жидкости\n11\tMSDS при необходимости\n12\tБатарейки в составе или отдельно\n13\tИз материкового Китая по ж/д — Technical Description of Goods in Railway Transport\n14\tНазвание бренда\n15\tЕсли бренд зарегистрирован в таможенной системе Китая — есть ли у отправителя авторизационное письмо/разрешение на вывоз бренда\n16\tДополнительные сервисы (крепление в контейнере, фитосанитарный контроль и т.п.)\n17\tТребуется ли замена документов\n18\tСтрана, город, точный адрес доставки\n19\tМесто таможенного оформления при экспорте из РФ\n20\tФумигация деревянной тары при экспорте из РФ\n21\tПроверка габаритов грузовых мест на проходимость в дверной проём контейнеров 20DV, 40DV, 40HC\n\nЖ/д FCL: ISO 20'/40' на платформах/вагонных комплектах (1520 мм); без обязательного морского этапа.",
|
||||
"keywords": ["жд fcl", "ж/д fcl", "rail fcl", "контейнер по жд целый", "полный контейнер жд", "fcl поезд", "вагон контейнер"],
|
||||
"confirmation_template": "Dear Partner!\n\nThank you for contacting SEPTEM. We confirm rail freight (FCL):\n\n- Route: (Pickup address) -> (Delivery address)\n- Container: (Container type)\n- Weight: (Cargo weight) kg\n- Cargo: (Cargo description)\n- HS: (HS code)\n- MSDS: (MSDS required)\n- Cost: (Estimated cost)\n- Transit: (Transit time)\n\nBest regards,\nSEPTEM Cargo Team",
|
||||
"info_request_template": "Уважаемый (Клиент)!\n\nДля расчёта железнодорожной перевозки (FCL) нам необходима следующая информация:\n\n(Необходимая информация)\n\nС уважением,\nКоманда SEPTEM Cargo"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "Мультимодальная перевозка море + ж/д (FCL)",
|
||||
"employee_email": "marina.eremina@timnet.ch, ekaterina.kochenkova@timnet.ch, Yana.Schkabrova@timnet.ch",
|
||||
"extra_required_fields": ["hs_code", "container_type"],
|
||||
"mandatory_counterparty_criteria": "Обязательно: клиент; Incoterms; характер груза и ТН ВЭД; адрес забора; адрес доставки; количество и типоразмер контейнеров; вес груза в контейнере.",
|
||||
"criteria": "1\tНазвание клиента\n2\tУсловия поставки Incoterms\n3\tДата готовности груза\n4\tСтрана, город, адрес забора\n5\tСтоимость груза\n6\tКоличество и типоразмер контейнеров\n7\tВес груза в контейнере\n8\tЕсли нет данных о контейнерах — габариты каждого грузового места (Д×Ш×В) и вес мест\n9\tХарактер груза или наименование, код ТН ВЭД\n10\tБатарейки, газы, жидкости\n11\tMSDS при необходимости\n12\tБатарейки в составе или отдельно\n13\tИз материкового Китая по ж/д — Technical Description of Goods in Railway Transport\n14\tНазвание бренда\n15\tЕсли бренд зарегистрирован в таможенной системе Китая — есть ли у отправителя авторизационное письмо/разрешение на вывоз бренда\n16\tДополнительные сервисы (крепление в контейнере, фитосанитарный контроль и т.п.)\n17\tТребуется ли замена документов\n18\tСтрана, город, точный адрес доставки\n19\tМесто таможенного оформления при экспорте из РФ\n20\tФумигация деревянной тары при экспорте из РФ\n21\tПроверка габаритов грузовых мест на проходимость в дверной проём контейнеров 20DV, 40DV, 40HC\n\nМультимодальная перевозка море + ж/д (FCL): в запросе явно сочетаются морской и ж/д этапы с полным контейнером (интермодаль).",
|
||||
"keywords": ["море жд fcl", "море+жд fcl", "sea rail fcl", "интермодальн fcl", "контейнер море и жд", "порт станция fcl"],
|
||||
"confirmation_template": "Dear Partner!\n\nThank you for contacting SEPTEM. We confirm multimodal sea+rail (FCL):\n\n- Route: (Port of loading) / rail -> (Delivery address)\n- Container: (Container type)\n- Weight: (Cargo weight) kg\n- Cargo: (Cargo description)\n- HS: (HS code)\n- MSDS: (MSDS required)\n- Cost: (Estimated cost)\n- Transit: (Transit time)\n\nBest regards,\nSEPTEM Cargo Team",
|
||||
"info_request_template": "Уважаемый (Клиент)!\n\nДля расчёта мультимодальной перевозки (море + ж/д, FCL) нам необходима следующая информация:\n\n(Необходимая информация)\n\nС уважением,\nКоманда SEPTEM Cargo"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "Авиаперевозка",
|
||||
"employee_email": "evgeniy.domashnev@timnet.ch, olga.ermakova@timnet.ch, andrey.reshetnyak@timnet.ch, anastasia.fatkina@timnet.ch",
|
||||
"extra_required_fields": ["hs_code", "stackable_with_others", "total_volume_cbm", "dangerous_goods_clarification", "customs_clearance_place_export_rf"],
|
||||
"mandatory_counterparty_criteria": "Обязательно: клиент; Incoterms; характер груза и ТН ВЭД; адрес забора; адрес доставки; количество мест, вес и объём; габариты Д×Ш×В; штабелирование с другими отправками; сведения о батарейках, газах, жидкостях, аэрозолях; место таможенного оформления при экспорте из РФ (и прочие пункты чек-листа авиа по полному criteria).",
|
||||
"criteria": "1\tНазвание клиента\n2\tУсловия поставки Incoterms\n3\tДата готовности груза\n4\tСтрана, город, адрес забора\n5\tСтоимость груза\n6\tКоличество грузовых мест, вес, объём\n7\tГабариты каждого грузового места (Д×Ш×В в см или мм)\n8\tХарактер груза или наименование, код ТН ВЭД\n9\tБатарейки, газы, жидкости, хладоагенты, сухой лёд\n10\tШтабелирование с другими отправками\n11\tШтабелирование мест между собой\n12\tMSDS при химии, батареях, жидкостях, аэрозолях, порошках\n13\tБатарейки — в составе или отдельно\n14\tБатарейки и вылет из материкового Китая — DGM (Dangerous Goods Management) report\n15\tНазвание бренда\n16\tЕсли бренд зарегистрирован в таможенной системе Китая — есть ли у отправителя авторизационное письмо/разрешение на вывоз бренда\n17\tПерелёт через третью страну со сменой авианакладных\n18\tЭкспортная лицензия у отправителя\n19\tДоп. сервис: переупаковка, маркировка, логгеры и т.д.\n20\tКто осуществляет экспедирование в аэропорту прибытия (терминал, сборы авиакомпании)\n21\tСтрана, город, точный адрес доставки\n22\tВывоз из аэропорта — особые требования к автотранспорту, время подачи\n23\tНужно ли таможенное оформление\n24\tМесто таможенного оформления при экспорте из РФ\n25\tФумигация деревянной тары при экспорте из РФ",
|
||||
"keywords": ["авиа", "авиаперевозка", "самолёт", "air freight", "air cargo", "аэропорт", "flight", "авианакладная", "awb"],
|
||||
"confirmation_template": "Dear Partner!\n\nThank you for contacting SEPTEM. We confirm air freight:\n\n- Route: (Pickup address) -> (Delivery address)\n- Weight: (Cargo weight) kg\n- Packages: (Number of packages)\n- Volume: (Volume) m3\n- Dimensions: (Dimensions)\n- Cargo: (Cargo description)\n- HS: (HS code)\n- Dangerous goods: (Dangerous properties)\n- MSDS: (MSDS required)\n- DGM (при опасном грузе / вылет из материкового Китая — по правилам перевозчика)\n- Cost: (Estimated cost)\n- Transit: (Transit time)\n\nBest regards,\nSEPTEM Cargo Team",
|
||||
"info_request_template": "Уважаемый (Клиент)!\n\nДля расчёта авиаперевозки нам необходима следующая информация:\n\n(Необходимая информация)\n\nС уважением,\nКоманда SEPTEM Cargo"
|
||||
}
|
||||
]
|
||||
Loading…
Reference in New Issue