Загрузить файлы в «/»
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
|
||||
«Запрос для ИИ Общий(2).xlsx» (листы с «интерфейс» в названии).
|
||||
|
||||
Запуск (путь к xlsx можно передать аргументом):
|
||||
py extract_criteria_interface_layout.py "C:\\Users\\...\\Запрос для ИИ Общий(2).xlsx"
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import openpyxl
|
||||
|
||||
SHEET_TO_SHIPPING_TYPES: dict[str, list[str]] = {
|
||||
"Авто LTL интерфейс": ["Автомобильная перевозка (LTL)"],
|
||||
"Авто FTL интерфейс": ["Автомобильная перевозка (FTL)"],
|
||||
"Море, море+жд, жд LCL интерфейc": [
|
||||
"Морская перевозка (LCL)",
|
||||
"Железнодорожная перевозка (LCL)",
|
||||
"Мультимодальная перевозка море + ж/д (LCL)",
|
||||
],
|
||||
"Море, море+жд, жд FCL интерфейc": [
|
||||
"Морская перевозка (FCL)",
|
||||
"Железнодорожная перевозка (FCL)",
|
||||
"Мультимодальная перевозка море + ж/д (FCL)",
|
||||
],
|
||||
"Авиа Интерфейс": ["Авиаперевозка"],
|
||||
}
|
||||
|
||||
|
||||
def _row_cells(ws, r: int, max_c: int = 12) -> list[tuple[int, str]]:
|
||||
out: list[tuple[int, str]] = []
|
||||
for c in range(1, max_c + 1):
|
||||
v = ws.cell(r, c).value
|
||||
if v is not None and str(v).strip():
|
||||
out.append((c, str(v).strip()))
|
||||
return out
|
||||
|
||||
|
||||
def parse_interface_sheet(ws) -> list[dict]:
|
||||
"""Разбор листа интерфейса в последовательность визуальных блоков."""
|
||||
blocks: list[dict] = []
|
||||
r = 1
|
||||
max_r = ws.max_row or 1
|
||||
while r <= max_r:
|
||||
cells = _row_cells(ws, r)
|
||||
if not cells:
|
||||
r += 1
|
||||
continue
|
||||
|
||||
# Заголовок: 7 ячеек в колонках 1,2,3,4,5,7,9 и все значения — «номера пунктов»
|
||||
if (
|
||||
len(cells) == 7
|
||||
and {c[0] for c in cells} == {1, 2, 3, 4, 5, 7, 9}
|
||||
and all(str(c[1]).strip().isdigit() or c[1].strip().replace(".", "").isdigit() for c in cells)
|
||||
):
|
||||
nums = [c[1] for c in sorted(cells, key=lambda x: (x[0] not in (1, 2, 3, 4, 5, 7, 9), x[0]))]
|
||||
# порядок как в Excel слева направо по колонкам
|
||||
nums = [c[1] for c in sorted(cells, key=lambda x: x[0])]
|
||||
r2 = r + 1
|
||||
cells2 = _row_cells(ws, r2)
|
||||
if len(cells2) == 7 and {c[0] for c in cells2} == {1, 2, 3, 4, 5, 7, 9}:
|
||||
labels = [c[1] for c in sorted(cells2, key=lambda x: x[0])]
|
||||
blocks.append({"type": "header", "nums": nums, "labels": labels})
|
||||
r += 2
|
||||
continue
|
||||
|
||||
# Только правая пара (6 = номер, 7 = текст) — например «20 Место таможни»
|
||||
if len(cells) == 2 and cells[0][0] == 6 and cells[1][0] == 7:
|
||||
blocks.append({"type": "right_67", "num": cells[0][1], "label": cells[1][1]})
|
||||
r += 1
|
||||
continue
|
||||
|
||||
# Две пары: 1-2 и 6-7
|
||||
if len(cells) == 4 and {cells[i][0] for i in range(4)} == {1, 2, 6, 7}:
|
||||
blocks.append(
|
||||
{
|
||||
"type": "pair",
|
||||
"left": {"num": cells[0][1], "label": cells[1][1]},
|
||||
"right": {"num": cells[2][1], "label": cells[3][1]},
|
||||
}
|
||||
)
|
||||
r += 1
|
||||
continue
|
||||
|
||||
# Только левая пара 1-2 (один блок на строку)
|
||||
if len(cells) == 2 and cells[0][0] == 1 and cells[1][0] == 2:
|
||||
blocks.append({"type": "left_12", "num": cells[0][1], "label": cells[1][1]})
|
||||
r += 1
|
||||
continue
|
||||
|
||||
r += 1
|
||||
return blocks
|
||||
|
||||
|
||||
def main() -> None:
|
||||
xlsx = (
|
||||
sys.argv[1]
|
||||
if len(sys.argv) > 1
|
||||
else os.path.join(os.path.dirname(__file__), "Запрос для ИИ Общий(3).xlsx")
|
||||
)
|
||||
if not os.path.isfile(xlsx):
|
||||
print("Файл не найден:", xlsx)
|
||||
sys.exit(1)
|
||||
|
||||
wb = openpyxl.load_workbook(xlsx, read_only=False, data_only=True)
|
||||
out: dict = {"version": 1, "source_xlsx": os.path.basename(xlsx), "by_shipping_type": {}}
|
||||
|
||||
for sheet_name, type_names in SHEET_TO_SHIPPING_TYPES.items():
|
||||
if sheet_name not in wb.sheetnames:
|
||||
print("Пропуск: лист не найден:", sheet_name)
|
||||
continue
|
||||
blocks = parse_interface_sheet(wb[sheet_name])
|
||||
for tn in type_names:
|
||||
out["by_shipping_type"][tn] = {"sheet": sheet_name, "blocks": blocks}
|
||||
|
||||
out_path = os.path.join(os.path.dirname(__file__), "criteria_interface_layout.json")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, ensure_ascii=False, indent=2)
|
||||
print("Записано:", out_path, "типов:", len(out["by_shipping_type"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
Однократная/повторная генерация criteria_interface_layout.json из Excel
|
||||
«Запрос для ИИ Общий(2).xlsx» (листы с «интерфейс» в названии).
|
||||
|
||||
Запуск (путь к xlsx можно передать аргументом):
|
||||
py extract_criteria_interface_layout.py "C:\\Users\\...\\Запрос для ИИ Общий(2).xlsx"
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import openpyxl
|
||||
|
||||
SHEET_TO_SHIPPING_TYPES: dict[str, list[str]] = {
|
||||
"Авто LTL интерфейс": ["Автомобильная перевозка (LTL)"],
|
||||
"Авто FTL интерфейс": ["Автомобильная перевозка (FTL)"],
|
||||
"Море, море+жд, жд LCL интерфейc": [
|
||||
"Морская перевозка (LCL)",
|
||||
"Железнодорожная перевозка (LCL)",
|
||||
"Мультимодальная перевозка море + ж/д (LCL)",
|
||||
],
|
||||
"Море, море+жд, жд FCL интерфейc": [
|
||||
"Морская перевозка (FCL)",
|
||||
"Железнодорожная перевозка (FCL)",
|
||||
"Мультимодальная перевозка море + ж/д (FCL)",
|
||||
],
|
||||
"Авиа Интерфейс": ["Авиаперевозка"],
|
||||
}
|
||||
|
||||
|
||||
def _row_cells(ws, r: int, max_c: int = 12) -> list[tuple[int, str]]:
|
||||
out: list[tuple[int, str]] = []
|
||||
for c in range(1, max_c + 1):
|
||||
v = ws.cell(r, c).value
|
||||
if v is not None and str(v).strip():
|
||||
out.append((c, str(v).strip()))
|
||||
return out
|
||||
|
||||
|
||||
def _is_num_like(s: str) -> bool:
|
||||
if not isinstance(s, str):
|
||||
return False
|
||||
t = s.strip()
|
||||
if not t:
|
||||
return False
|
||||
# 1, 1., 1.0
|
||||
return t.isdigit() or t.replace(".", "", 1).isdigit()
|
||||
|
||||
|
||||
def parse_interface_sheet(ws) -> list[dict]:
|
||||
"""Разбор листа интерфейса в последовательность визуальных блоков."""
|
||||
blocks: list[dict] = []
|
||||
r = 1
|
||||
max_r = ws.max_row or 1
|
||||
while r <= max_r:
|
||||
cells = _row_cells(ws, r)
|
||||
if not cells:
|
||||
r += 1
|
||||
continue
|
||||
|
||||
# Заголовок верхнего ряда:
|
||||
# в новых версиях файла набор колонок может отличаться, поэтому
|
||||
# распознаём динамически: строка с номерами + следующая строка с подписями
|
||||
# в тех же колонках.
|
||||
cols = [c[0] for c in cells]
|
||||
vals = [c[1] for c in cells]
|
||||
if len(cells) >= 4 and all(_is_num_like(v) for v in vals):
|
||||
r2 = r + 1
|
||||
cells2 = _row_cells(ws, r2)
|
||||
map2 = {c: v for c, v in cells2}
|
||||
if all(c in map2 and str(map2[c]).strip() for c in cols):
|
||||
labels = [str(map2[c]).strip() for c in cols]
|
||||
# Если в следующей строке снова только числа — это не шапка.
|
||||
if not all(_is_num_like(v) for v in labels):
|
||||
nums = [str(v).strip() for v in vals]
|
||||
blocks.append({"type": "header", "nums": nums, "labels": labels})
|
||||
r += 2
|
||||
continue
|
||||
|
||||
# Только правая пара (6 = номер, 7 = текст) — например «20 Место таможни»
|
||||
if len(cells) == 2 and cells[0][0] == 6 and cells[1][0] == 7:
|
||||
blocks.append({"type": "right_67", "num": cells[0][1], "label": cells[1][1]})
|
||||
r += 1
|
||||
continue
|
||||
|
||||
# Две пары: 1-2 и 6-7
|
||||
if len(cells) == 4 and {cells[i][0] for i in range(4)} == {1, 2, 6, 7}:
|
||||
blocks.append(
|
||||
{
|
||||
"type": "pair",
|
||||
"left": {"num": cells[0][1], "label": cells[1][1]},
|
||||
"right": {"num": cells[2][1], "label": cells[3][1]},
|
||||
}
|
||||
)
|
||||
r += 1
|
||||
continue
|
||||
|
||||
# Только левая пара 1-2 (один блок на строку)
|
||||
if len(cells) == 2 and cells[0][0] == 1 and cells[1][0] == 2:
|
||||
blocks.append({"type": "left_12", "num": cells[0][1], "label": cells[1][1]})
|
||||
r += 1
|
||||
continue
|
||||
|
||||
r += 1
|
||||
return blocks
|
||||
|
||||
|
||||
def main() -> None:
|
||||
xlsx = (
|
||||
sys.argv[1]
|
||||
if len(sys.argv) > 1
|
||||
else os.path.join(os.path.dirname(__file__), "Запрос для ИИ Общий(3).xlsx")
|
||||
)
|
||||
if not os.path.isfile(xlsx):
|
||||
print("Файл не найден:", xlsx)
|
||||
sys.exit(1)
|
||||
|
||||
wb = openpyxl.load_workbook(xlsx, read_only=False, data_only=True)
|
||||
out: dict = {"version": 1, "source_xlsx": os.path.basename(xlsx), "by_shipping_type": {}}
|
||||
|
||||
for sheet_name, type_names in SHEET_TO_SHIPPING_TYPES.items():
|
||||
if sheet_name not in wb.sheetnames:
|
||||
print("Пропуск: лист не найден:", sheet_name)
|
||||
continue
|
||||
blocks = parse_interface_sheet(wb[sheet_name])
|
||||
for tn in type_names:
|
||||
out["by_shipping_type"][tn] = {"sheet": sheet_name, "blocks": blocks}
|
||||
|
||||
out_path = os.path.join(os.path.dirname(__file__), "criteria_interface_layout.json")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, ensure_ascii=False, indent=2)
|
||||
print("Записано:", out_path, "типов:", len(out["by_shipping_type"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
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