Загрузить файлы в «/»
This commit is contained in:
parent
4fedd08e8f
commit
386d5e03ea
|
|
@ -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": "18",
|
||||||
|
"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": "19",
|
||||||
|
"label": "Место таможенного оформления при экспорте из РФ"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "20",
|
||||||
|
"label": "Наличие Фумигации на деревянной таре при экспорте из РФ"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "left_12",
|
||||||
|
"num": "21",
|
||||||
|
"label": "Превышение габаритов дверного проема контейнеров 20DV, 40DV, 40HC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Железнодорожная перевозка (FCL)": {
|
||||||
|
"sheet": "Море, море+жд, жд FCL интерфейc",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "pair",
|
||||||
|
"left": {
|
||||||
|
"num": "4",
|
||||||
|
"label": "Страна, город, адрес забора"
|
||||||
|
},
|
||||||
|
"right": {
|
||||||
|
"num": "18",
|
||||||
|
"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": "21",
|
||||||
|
"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,815 @@
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import csv
|
||||||
|
import tempfile
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from pypdf import PdfReader
|
||||||
|
from docx import Document
|
||||||
|
|
||||||
|
import openpyxl
|
||||||
|
import pdfplumber
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
import pytesseract
|
||||||
|
|
||||||
|
from striprtf.striprtf import rtf_to_text
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentProcessor:
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# HELPERS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cleanup_text(text: str) -> str:
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
text = text.replace("\xa0", " ")
|
||||||
|
text = text.replace("\t", " ")
|
||||||
|
|
||||||
|
text = re.sub(r"[ \t]+", " ", text)
|
||||||
|
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||||
|
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _cell_to_text(v) -> str:
|
||||||
|
|
||||||
|
if v is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
s = str(v).strip()
|
||||||
|
|
||||||
|
return s if s else ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# HTML
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
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)
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# MAIN
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def extract_text(
|
||||||
|
self,
|
||||||
|
content: bytes,
|
||||||
|
filename: str
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
ext = filename.lower().split(".")[-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# =================================================
|
||||||
|
# PDF
|
||||||
|
# =================================================
|
||||||
|
|
||||||
|
if ext == "pdf":
|
||||||
|
return self._extract_pdf(content)
|
||||||
|
|
||||||
|
# =================================================
|
||||||
|
# WORD
|
||||||
|
# =================================================
|
||||||
|
|
||||||
|
elif ext in ["docx", "docm"]:
|
||||||
|
return self._extract_docx(content)
|
||||||
|
|
||||||
|
elif ext == "doc":
|
||||||
|
return self._extract_doc(content)
|
||||||
|
|
||||||
|
elif ext == "rtf":
|
||||||
|
return self._extract_rtf(content)
|
||||||
|
|
||||||
|
# =================================================
|
||||||
|
# EXCEL
|
||||||
|
# =================================================
|
||||||
|
|
||||||
|
elif ext in ["xlsx", "xlsm", "xlsxm"]:
|
||||||
|
return self._extract_excel_openpyxl(content)
|
||||||
|
|
||||||
|
elif ext == "xls":
|
||||||
|
return self._extract_excel_xls(content)
|
||||||
|
|
||||||
|
elif ext == "csv":
|
||||||
|
return self._extract_csv(content)
|
||||||
|
|
||||||
|
# =================================================
|
||||||
|
# IMAGES / OCR
|
||||||
|
# =================================================
|
||||||
|
|
||||||
|
elif ext in [
|
||||||
|
"png",
|
||||||
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
|
"gif",
|
||||||
|
"bmp",
|
||||||
|
"webp"
|
||||||
|
]:
|
||||||
|
return self._extract_image(content)
|
||||||
|
|
||||||
|
elif ext in ["tif", "tiff"]:
|
||||||
|
return self._extract_tiff(content)
|
||||||
|
|
||||||
|
# =================================================
|
||||||
|
# TEXT
|
||||||
|
# =================================================
|
||||||
|
|
||||||
|
elif ext == "txt":
|
||||||
|
|
||||||
|
try:
|
||||||
|
return content.decode("utf-8")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return content.decode(
|
||||||
|
"cp1251",
|
||||||
|
errors="ignore"
|
||||||
|
)
|
||||||
|
|
||||||
|
# =================================================
|
||||||
|
# UNKNOWN
|
||||||
|
# =================================================
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"Unsupported attachment format: {ext}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
|
||||||
|
logger.exception(
|
||||||
|
f"Attachment processing failed: {filename}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# PDF
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _extract_pdf(
|
||||||
|
self,
|
||||||
|
content: bytes
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
with pdfplumber.open(
|
||||||
|
io.BytesIO(content)
|
||||||
|
) as pdf:
|
||||||
|
|
||||||
|
for page_num, page in enumerate(
|
||||||
|
pdf.pages,
|
||||||
|
start=1
|
||||||
|
):
|
||||||
|
|
||||||
|
parts.append(
|
||||||
|
f"=== PAGE {page_num} ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# TEXT
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
text = page.extract_text()
|
||||||
|
|
||||||
|
if text:
|
||||||
|
parts.append(text)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"PDF text extraction failed: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# TABLES
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
tables = page.extract_tables()
|
||||||
|
|
||||||
|
for table_idx, table in enumerate(
|
||||||
|
tables,
|
||||||
|
start=1
|
||||||
|
):
|
||||||
|
|
||||||
|
parts.append(
|
||||||
|
f"=== TABLE {table_idx} ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in table:
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cells = []
|
||||||
|
|
||||||
|
for cell in row:
|
||||||
|
|
||||||
|
if cell is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
txt = str(cell).strip()
|
||||||
|
|
||||||
|
txt = " ".join(
|
||||||
|
txt.split()
|
||||||
|
)
|
||||||
|
|
||||||
|
if txt:
|
||||||
|
cells.append(txt)
|
||||||
|
|
||||||
|
if cells:
|
||||||
|
|
||||||
|
parts.append(
|
||||||
|
" | ".join(cells)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"PDF table extraction failed: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
text = "\n".join(parts)
|
||||||
|
|
||||||
|
return self._cleanup_text(text)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(f"PDF parse failed: {e}")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# DOCX / DOCM
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _extract_docx(
|
||||||
|
self,
|
||||||
|
content: bytes
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
doc = Document(io.BytesIO(content))
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# PARAGRAPHS
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
for p in doc.paragraphs:
|
||||||
|
|
||||||
|
txt = p.text.strip()
|
||||||
|
|
||||||
|
if txt:
|
||||||
|
parts.append(txt)
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# TABLES
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
for table_idx, table in enumerate(
|
||||||
|
doc.tables
|
||||||
|
):
|
||||||
|
|
||||||
|
parts.append(
|
||||||
|
f"=== TABLE {table_idx + 1} ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
for row in table.rows:
|
||||||
|
|
||||||
|
cells = []
|
||||||
|
|
||||||
|
for cell in row.cells:
|
||||||
|
|
||||||
|
cell_parts = []
|
||||||
|
|
||||||
|
# =================================
|
||||||
|
# DIRECT CELL TEXT
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
txt = "\n".join(
|
||||||
|
p.text.strip()
|
||||||
|
for p in cell.paragraphs
|
||||||
|
if p.text.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
txt = " ".join(txt.split())
|
||||||
|
|
||||||
|
if txt:
|
||||||
|
cell_parts.append(txt)
|
||||||
|
|
||||||
|
# =================================
|
||||||
|
# NESTED TABLES
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
for nested_table in cell.tables:
|
||||||
|
|
||||||
|
for nested_row in nested_table.rows:
|
||||||
|
|
||||||
|
nested_vals = []
|
||||||
|
|
||||||
|
for nested_cell in nested_row.cells:
|
||||||
|
|
||||||
|
nested_txt = "\n".join(
|
||||||
|
p.text.strip()
|
||||||
|
for p in nested_cell.paragraphs
|
||||||
|
if p.text.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
nested_txt = " ".join(
|
||||||
|
nested_txt.split()
|
||||||
|
)
|
||||||
|
|
||||||
|
if nested_txt:
|
||||||
|
nested_vals.append(
|
||||||
|
nested_txt
|
||||||
|
)
|
||||||
|
|
||||||
|
if nested_vals:
|
||||||
|
|
||||||
|
cell_parts.append(
|
||||||
|
" || ".join(
|
||||||
|
nested_vals
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# =================================
|
||||||
|
# FINAL CELL
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
if cell_parts:
|
||||||
|
|
||||||
|
cells.append(
|
||||||
|
" <<<TABLE>>> ".join(
|
||||||
|
cell_parts
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if cells:
|
||||||
|
|
||||||
|
parts.append(
|
||||||
|
" | ".join(cells)
|
||||||
|
)
|
||||||
|
|
||||||
|
text = "\n".join(parts)
|
||||||
|
|
||||||
|
return self._cleanup_text(text)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(f"DOCX parse failed: {e}")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# DOC
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _extract_doc(
|
||||||
|
self,
|
||||||
|
content: bytes
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
|
||||||
|
input_path = os.path.join(
|
||||||
|
tmpdir,
|
||||||
|
"temp.doc"
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(input_path, "wb") as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"libreoffice",
|
||||||
|
"--headless",
|
||||||
|
"--convert-to",
|
||||||
|
"docx",
|
||||||
|
input_path,
|
||||||
|
"--outdir",
|
||||||
|
tmpdir,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
converted_path = os.path.join(
|
||||||
|
tmpdir,
|
||||||
|
"temp.docx"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(
|
||||||
|
converted_path
|
||||||
|
):
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
"DOC conversion failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
with open(
|
||||||
|
converted_path,
|
||||||
|
"rb"
|
||||||
|
) as f:
|
||||||
|
|
||||||
|
converted_content = f.read()
|
||||||
|
|
||||||
|
return self._extract_docx(
|
||||||
|
converted_content
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(f"DOC parse failed: {e}")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# RTF
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _extract_rtf(
|
||||||
|
self,
|
||||||
|
content: bytes
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = content.decode("utf-8")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
text = content.decode(
|
||||||
|
"cp1251",
|
||||||
|
errors="ignore"
|
||||||
|
)
|
||||||
|
|
||||||
|
text = rtf_to_text(text)
|
||||||
|
|
||||||
|
return self._cleanup_text(text)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(f"RTF parse failed: {e}")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# XLSX
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _extract_excel_openpyxl(
|
||||||
|
self,
|
||||||
|
content: bytes
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
wb = openpyxl.load_workbook(
|
||||||
|
io.BytesIO(content),
|
||||||
|
data_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
|
||||||
|
for sheet in wb.worksheets:
|
||||||
|
|
||||||
|
text.append(
|
||||||
|
f"=== SHEET: {sheet.title} ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# MERGED CELLS
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
merged_ranges = list(
|
||||||
|
sheet.merged_cells.ranges
|
||||||
|
)
|
||||||
|
|
||||||
|
if merged_ranges:
|
||||||
|
|
||||||
|
text.append(
|
||||||
|
"=== MERGED CELLS ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
for r in merged_ranges:
|
||||||
|
text.append(str(r))
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# ROWS
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
text.append("[ROWS]")
|
||||||
|
|
||||||
|
for row in sheet.iter_rows(
|
||||||
|
values_only=True
|
||||||
|
):
|
||||||
|
|
||||||
|
vals = [
|
||||||
|
self._cell_to_text(c)
|
||||||
|
for c in row
|
||||||
|
]
|
||||||
|
|
||||||
|
vals = [x for x in vals if x]
|
||||||
|
|
||||||
|
if vals:
|
||||||
|
|
||||||
|
text.append(
|
||||||
|
" | ".join(vals)
|
||||||
|
)
|
||||||
|
|
||||||
|
# =====================================
|
||||||
|
# COLUMNS
|
||||||
|
# =====================================
|
||||||
|
|
||||||
|
text.append("[COLUMNS]")
|
||||||
|
|
||||||
|
max_col = sheet.max_column or 0
|
||||||
|
max_row = sheet.max_row or 0
|
||||||
|
|
||||||
|
for c in range(1, max_col + 1):
|
||||||
|
|
||||||
|
vals = []
|
||||||
|
|
||||||
|
for r in range(1, max_row + 1):
|
||||||
|
|
||||||
|
v = self._cell_to_text(
|
||||||
|
sheet.cell(r, c).value
|
||||||
|
)
|
||||||
|
|
||||||
|
if v:
|
||||||
|
vals.append(v)
|
||||||
|
|
||||||
|
if vals:
|
||||||
|
|
||||||
|
col_letter = (
|
||||||
|
openpyxl.utils
|
||||||
|
.get_column_letter(c)
|
||||||
|
)
|
||||||
|
|
||||||
|
text.append(
|
||||||
|
f"COL {col_letter}: "
|
||||||
|
+ " || ".join(vals)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._cleanup_text(
|
||||||
|
"\n".join(text)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
f"XLSX parse failed: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# XLS
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _extract_excel_xls(
|
||||||
|
self,
|
||||||
|
content: bytes
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
import xlrd
|
||||||
|
|
||||||
|
workbook = xlrd.open_workbook(
|
||||||
|
file_contents=content
|
||||||
|
)
|
||||||
|
|
||||||
|
text = []
|
||||||
|
|
||||||
|
for sheet in workbook.sheets():
|
||||||
|
|
||||||
|
text.append(
|
||||||
|
f"=== SHEET: {sheet.name} ==="
|
||||||
|
)
|
||||||
|
|
||||||
|
for row_idx in range(
|
||||||
|
sheet.nrows
|
||||||
|
):
|
||||||
|
|
||||||
|
row = sheet.row_values(
|
||||||
|
row_idx
|
||||||
|
)
|
||||||
|
|
||||||
|
vals = [
|
||||||
|
self._cell_to_text(c)
|
||||||
|
for c in row
|
||||||
|
]
|
||||||
|
|
||||||
|
vals = [x for x in vals if x]
|
||||||
|
|
||||||
|
if vals:
|
||||||
|
|
||||||
|
text.append(
|
||||||
|
" | ".join(vals)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._cleanup_text(
|
||||||
|
"\n".join(text)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(f"XLS parse failed: {e}")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# CSV
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _extract_csv(
|
||||||
|
self,
|
||||||
|
content: bytes
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = content.decode("utf-8")
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
text = content.decode(
|
||||||
|
"cp1251",
|
||||||
|
errors="ignore"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader = csv.reader(
|
||||||
|
io.StringIO(text)
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
|
||||||
|
for row in reader:
|
||||||
|
|
||||||
|
vals = [
|
||||||
|
str(x).strip()
|
||||||
|
for x in row
|
||||||
|
if str(x).strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
if vals:
|
||||||
|
|
||||||
|
rows.append(
|
||||||
|
" | ".join(vals)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._cleanup_text(
|
||||||
|
"\n".join(rows)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(f"CSV parse failed: {e}")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# OCR
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _ocr_image(
|
||||||
|
self,
|
||||||
|
image: Image.Image
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
text = pytesseract.image_to_string(
|
||||||
|
image,
|
||||||
|
lang="rus+eng",
|
||||||
|
config="--psm 6"
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._cleanup_text(text)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(f"OCR failed: {e}")
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# IMAGE
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _extract_image(
|
||||||
|
self,
|
||||||
|
content: bytes
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
image = Image.open(
|
||||||
|
io.BytesIO(content)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._ocr_image(image)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
f"Image parse failed: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# TIFF
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
def _extract_tiff(
|
||||||
|
self,
|
||||||
|
content: bytes
|
||||||
|
) -> str:
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
image = Image.open(
|
||||||
|
io.BytesIO(content)
|
||||||
|
)
|
||||||
|
|
||||||
|
texts = []
|
||||||
|
|
||||||
|
for frame in range(
|
||||||
|
getattr(image, "n_frames", 1)
|
||||||
|
):
|
||||||
|
|
||||||
|
image.seek(frame)
|
||||||
|
|
||||||
|
texts.append(
|
||||||
|
self._ocr_image(image)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._cleanup_text(
|
||||||
|
"\n".join(texts)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
f"TIFF parse failed: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
@ -1,126 +1,138 @@
|
||||||
"""
|
"""
|
||||||
Однократная/повторная генерация criteria_interface_layout.json из Excel
|
Однократная/повторная генерация criteria_interface_layout.json из Excel
|
||||||
«Запрос для ИИ Общий(2).xlsx» (листы с «интерфейс» в названии).
|
«Запрос для ИИ Общий(2).xlsx» (листы с «интерфейс» в названии).
|
||||||
|
|
||||||
Запуск (путь к xlsx можно передать аргументом):
|
Запуск (путь к xlsx можно передать аргументом):
|
||||||
py extract_criteria_interface_layout.py "C:\\Users\\...\\Запрос для ИИ Общий(2).xlsx"
|
py extract_criteria_interface_layout.py "C:\\Users\\...\\Запрос для ИИ Общий(2).xlsx"
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import openpyxl
|
import openpyxl
|
||||||
|
|
||||||
SHEET_TO_SHIPPING_TYPES: dict[str, list[str]] = {
|
SHEET_TO_SHIPPING_TYPES: dict[str, list[str]] = {
|
||||||
"Авто LTL интерфейс": ["Автомобильная перевозка (LTL)"],
|
"Авто LTL интерфейс": ["Автомобильная перевозка (LTL)"],
|
||||||
"Авто FTL интерфейс": ["Автомобильная перевозка (FTL)"],
|
"Авто FTL интерфейс": ["Автомобильная перевозка (FTL)"],
|
||||||
"Море, море+жд, жд LCL интерфейc": [
|
"Море, море+жд, жд LCL интерфейc": [
|
||||||
"Морская перевозка (LCL)",
|
"Морская перевозка (LCL)",
|
||||||
"Железнодорожная перевозка (LCL)",
|
"Железнодорожная перевозка (LCL)",
|
||||||
"Мультимодальная перевозка море + ж/д (LCL)",
|
"Мультимодальная перевозка море + ж/д (LCL)",
|
||||||
],
|
],
|
||||||
"Море, море+жд, жд FCL интерфейc": [
|
"Море, море+жд, жд FCL интерфейc": [
|
||||||
"Морская перевозка (FCL)",
|
"Морская перевозка (FCL)",
|
||||||
"Железнодорожная перевозка (FCL)",
|
"Железнодорожная перевозка (FCL)",
|
||||||
"Мультимодальная перевозка море + ж/д (FCL)",
|
"Мультимодальная перевозка море + ж/д (FCL)",
|
||||||
],
|
],
|
||||||
"Авиа Интерфейс": ["Авиаперевозка"],
|
"Авиа Интерфейс": ["Авиаперевозка"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _row_cells(ws, r: int, max_c: int = 12) -> list[tuple[int, str]]:
|
def _row_cells(ws, r: int, max_c: int = 12) -> list[tuple[int, str]]:
|
||||||
out: list[tuple[int, str]] = []
|
out: list[tuple[int, str]] = []
|
||||||
for c in range(1, max_c + 1):
|
for c in range(1, max_c + 1):
|
||||||
v = ws.cell(r, c).value
|
v = ws.cell(r, c).value
|
||||||
if v is not None and str(v).strip():
|
if v is not None and str(v).strip():
|
||||||
out.append((c, str(v).strip()))
|
out.append((c, str(v).strip()))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def parse_interface_sheet(ws) -> list[dict]:
|
def _is_num_like(s: str) -> bool:
|
||||||
"""Разбор листа интерфейса в последовательность визуальных блоков."""
|
if not isinstance(s, str):
|
||||||
blocks: list[dict] = []
|
return False
|
||||||
r = 1
|
t = s.strip()
|
||||||
max_r = ws.max_row or 1
|
if not t:
|
||||||
while r <= max_r:
|
return False
|
||||||
cells = _row_cells(ws, r)
|
# 1, 1., 1.0
|
||||||
if not cells:
|
return t.isdigit() or t.replace(".", "", 1).isdigit()
|
||||||
r += 1
|
|
||||||
continue
|
|
||||||
|
def parse_interface_sheet(ws) -> list[dict]:
|
||||||
# Заголовок: 7 ячеек в колонках 1,2,3,4,5,7,9 и все значения — «номера пунктов»
|
"""Разбор листа интерфейса в последовательность визуальных блоков."""
|
||||||
if (
|
blocks: list[dict] = []
|
||||||
len(cells) == 7
|
r = 1
|
||||||
and {c[0] for c in cells} == {1, 2, 3, 4, 5, 7, 9}
|
max_r = ws.max_row or 1
|
||||||
and all(str(c[1]).strip().isdigit() or c[1].strip().replace(".", "").isdigit() for c in cells)
|
while r <= max_r:
|
||||||
):
|
cells = _row_cells(ws, r)
|
||||||
nums = [c[1] for c in sorted(cells, key=lambda x: (x[0] not in (1, 2, 3, 4, 5, 7, 9), x[0]))]
|
if not cells:
|
||||||
# порядок как в Excel слева направо по колонкам
|
r += 1
|
||||||
nums = [c[1] for c in sorted(cells, key=lambda x: x[0])]
|
continue
|
||||||
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
|
cols = [c[0] for c in cells]
|
||||||
continue
|
vals = [c[1] for c in cells]
|
||||||
|
if len(cells) >= 4 and all(_is_num_like(v) for v in vals):
|
||||||
# Только правая пара (6 = номер, 7 = текст) — например «20 Место таможни»
|
r2 = r + 1
|
||||||
if len(cells) == 2 and cells[0][0] == 6 and cells[1][0] == 7:
|
cells2 = _row_cells(ws, r2)
|
||||||
blocks.append({"type": "right_67", "num": cells[0][1], "label": cells[1][1]})
|
map2 = {c: v for c, v in cells2}
|
||||||
r += 1
|
if all(c in map2 and str(map2[c]).strip() for c in cols):
|
||||||
continue
|
labels = [str(map2[c]).strip() for c in cols]
|
||||||
|
# Если в следующей строке снова только числа — это не шапка.
|
||||||
# Две пары: 1-2 и 6-7
|
if not all(_is_num_like(v) for v in labels):
|
||||||
if len(cells) == 4 and {cells[i][0] for i in range(4)} == {1, 2, 6, 7}:
|
nums = [str(v).strip() for v in vals]
|
||||||
blocks.append(
|
blocks.append({"type": "header", "nums": nums, "labels": labels})
|
||||||
{
|
r += 2
|
||||||
"type": "pair",
|
continue
|
||||||
"left": {"num": cells[0][1], "label": cells[1][1]},
|
|
||||||
"right": {"num": cells[2][1], "label": cells[3][1]},
|
# Только правая пара (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
|
r += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Только левая пара 1-2 (один блок на строку)
|
# Две пары: 1-2 и 6-7
|
||||||
if len(cells) == 2 and cells[0][0] == 1 and cells[1][0] == 2:
|
if len(cells) == 4 and {cells[i][0] for i in range(4)} == {1, 2, 6, 7}:
|
||||||
blocks.append({"type": "left_12", "num": cells[0][1], "label": cells[1][1]})
|
blocks.append(
|
||||||
r += 1
|
{
|
||||||
continue
|
"type": "pair",
|
||||||
|
"left": {"num": cells[0][1], "label": cells[1][1]},
|
||||||
r += 1
|
"right": {"num": cells[2][1], "label": cells[3][1]},
|
||||||
return blocks
|
}
|
||||||
|
)
|
||||||
|
r += 1
|
||||||
def main() -> None:
|
continue
|
||||||
xlsx = (
|
|
||||||
sys.argv[1]
|
# Только левая пара 1-2 (один блок на строку)
|
||||||
if len(sys.argv) > 1
|
if len(cells) == 2 and cells[0][0] == 1 and cells[1][0] == 2:
|
||||||
else os.path.join(os.path.dirname(__file__), "Запрос для ИИ Общий(3).xlsx")
|
blocks.append({"type": "left_12", "num": cells[0][1], "label": cells[1][1]})
|
||||||
)
|
r += 1
|
||||||
if not os.path.isfile(xlsx):
|
continue
|
||||||
print("Файл не найден:", xlsx)
|
|
||||||
sys.exit(1)
|
r += 1
|
||||||
|
return blocks
|
||||||
wb = openpyxl.load_workbook(xlsx, read_only=False, data_only=True)
|
|
||||||
out: dict = {"version": 1, "source_xlsx": os.path.basename(xlsx), "by_shipping_type": {}}
|
|
||||||
|
def main() -> None:
|
||||||
for sheet_name, type_names in SHEET_TO_SHIPPING_TYPES.items():
|
xlsx = (
|
||||||
if sheet_name not in wb.sheetnames:
|
sys.argv[1]
|
||||||
print("Пропуск: лист не найден:", sheet_name)
|
if len(sys.argv) > 1
|
||||||
continue
|
else os.path.join(os.path.dirname(__file__), "Запрос для ИИ Общий(3).xlsx")
|
||||||
blocks = parse_interface_sheet(wb[sheet_name])
|
)
|
||||||
for tn in type_names:
|
if not os.path.isfile(xlsx):
|
||||||
out["by_shipping_type"][tn] = {"sheet": sheet_name, "blocks": blocks}
|
print("Файл не найден:", xlsx)
|
||||||
|
sys.exit(1)
|
||||||
out_path = os.path.join(os.path.dirname(__file__), "criteria_interface_layout.json")
|
|
||||||
with open(out_path, "w", encoding="utf-8") as f:
|
wb = openpyxl.load_workbook(xlsx, read_only=False, data_only=True)
|
||||||
json.dump(out, f, ensure_ascii=False, indent=2)
|
out: dict = {"version": 1, "source_xlsx": os.path.basename(xlsx), "by_shipping_type": {}}
|
||||||
print("Записано:", out_path, "типов:", len(out["by_shipping_type"]))
|
|
||||||
|
for sheet_name, type_names in SHEET_TO_SHIPPING_TYPES.items():
|
||||||
|
if sheet_name not in wb.sheetnames:
|
||||||
if __name__ == "__main__":
|
print("Пропуск: лист не найден:", sheet_name)
|
||||||
main()
|
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()
|
||||||
|
|
|
||||||
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
|
||||||
Loading…
Reference in New Issue