statuses_repair_ca работает корректно
This commit is contained in:
@@ -2,10 +2,12 @@ from .monitoring_fuel import MonitoringFuelParser
|
|||||||
from .svodka_ca import SvodkaCAParser
|
from .svodka_ca import SvodkaCAParser
|
||||||
from .svodka_pm import SvodkaPMParser
|
from .svodka_pm import SvodkaPMParser
|
||||||
from .svodka_repair_ca import SvodkaRepairCAParser
|
from .svodka_repair_ca import SvodkaRepairCAParser
|
||||||
|
from .statuses_repair_ca import StatusesRepairCAParser
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'MonitoringFuelParser',
|
'MonitoringFuelParser',
|
||||||
'SvodkaCAParser',
|
'SvodkaCAParser',
|
||||||
'SvodkaPMParser',
|
'SvodkaPMParser',
|
||||||
'SvodkaRepairCAParser'
|
'SvodkaRepairCAParser',
|
||||||
|
'StatusesRepairCAParser'
|
||||||
]
|
]
|
||||||
|
|||||||
341
python_parser/adapters/parsers/statuses_repair_ca.py
Normal file
341
python_parser/adapters/parsers/statuses_repair_ca.py
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
from typing import Dict, Any, List, Tuple, Optional
|
||||||
|
from core.ports import ParserPort
|
||||||
|
from core.schema_utils import register_getter_from_schema, validate_params_with_schema
|
||||||
|
from app.schemas.statuses_repair_ca import StatusesRepairCARequest
|
||||||
|
from adapters.pconfig import find_header_row, get_og_by_name, data_to_json
|
||||||
|
|
||||||
|
|
||||||
|
class StatusesRepairCAParser(ParserPort):
|
||||||
|
"""Парсер для статусов ремонта СА"""
|
||||||
|
|
||||||
|
name = "Статусы ремонта СА"
|
||||||
|
|
||||||
|
def _register_default_getters(self):
|
||||||
|
"""Регистрация геттеров по умолчанию"""
|
||||||
|
register_getter_from_schema(
|
||||||
|
parser_instance=self,
|
||||||
|
getter_name="get_repair_statuses",
|
||||||
|
method=self._get_repair_statuses_wrapper,
|
||||||
|
schema_class=StatusesRepairCARequest,
|
||||||
|
description="Получение статусов ремонта по ОГ и ключам"
|
||||||
|
)
|
||||||
|
|
||||||
|
def parse(self, file_path: str, params: dict) -> Dict[str, Any]:
|
||||||
|
"""Парсинг файла статусов ремонта СА"""
|
||||||
|
print(f"🔍 DEBUG: StatusesRepairCAParser.parse вызван с файлом: {file_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Определяем тип файла
|
||||||
|
if file_path.endswith('.zip'):
|
||||||
|
return self._parse_zip_file(file_path)
|
||||||
|
elif file_path.endswith(('.xlsx', '.xls')):
|
||||||
|
return self._parse_excel_file(file_path)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Неподдерживаемый формат файла: {file_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка при парсинге файла {file_path}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _parse_zip_file(self, zip_path: str) -> Dict[str, Any]:
|
||||||
|
"""Парсинг ZIP архива"""
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(temp_dir)
|
||||||
|
|
||||||
|
# Ищем Excel файл в архиве
|
||||||
|
excel_files = []
|
||||||
|
for root, dirs, files in os.walk(temp_dir):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith(('.xlsx', '.xls')):
|
||||||
|
excel_files.append(os.path.join(root, file))
|
||||||
|
|
||||||
|
if not excel_files:
|
||||||
|
raise ValueError("В архиве не найдено Excel файлов")
|
||||||
|
|
||||||
|
# Берем первый найденный Excel файл
|
||||||
|
excel_file = excel_files[0]
|
||||||
|
print(f"🔍 DEBUG: Найден Excel файл в архиве: {excel_file}")
|
||||||
|
|
||||||
|
return self._parse_excel_file(excel_file)
|
||||||
|
|
||||||
|
def _parse_excel_file(self, file_path: str) -> Dict[str, Any]:
|
||||||
|
"""Парсинг Excel файла"""
|
||||||
|
print(f"🔍 DEBUG: Парсинг Excel файла: {file_path}")
|
||||||
|
|
||||||
|
# Парсим данные
|
||||||
|
df_statuses = self._parse_statuses_repair_ca(file_path, 0)
|
||||||
|
|
||||||
|
if df_statuses.empty:
|
||||||
|
print("⚠️ Нет данных после парсинга")
|
||||||
|
return {"data": [], "records_count": 0}
|
||||||
|
|
||||||
|
# Преобразуем в список словарей для хранения
|
||||||
|
data_list = self._data_to_structured_json(df_statuses)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"data": data_list,
|
||||||
|
"records_count": len(data_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Устанавливаем данные в парсер для использования в геттерах
|
||||||
|
self.data_dict = result
|
||||||
|
|
||||||
|
print(f"✅ Парсинг завершен. Получено {len(data_list)} записей")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _parse_statuses_repair_ca(self, file: str, sheet: int, header_num: Optional[int] = None) -> pd.DataFrame:
|
||||||
|
"""Парсинг отчетов статусов ремонта"""
|
||||||
|
|
||||||
|
# === ШАГ 1: Создание MultiIndex ===
|
||||||
|
columns_level_1 = [
|
||||||
|
'id',
|
||||||
|
'ОГ',
|
||||||
|
'Дата начала ремонта',
|
||||||
|
'Готовность к КР',
|
||||||
|
'Отставание / опережение подготовки к КР',
|
||||||
|
'Заключение договоров на СМР',
|
||||||
|
'Поставка МТР'
|
||||||
|
]
|
||||||
|
|
||||||
|
sub_columns_cmp = {
|
||||||
|
'ДВ': ['всего', 'плановая дата', 'факт', '%'],
|
||||||
|
'Сметы': ['всего', 'плановая дата', 'факт', '%'],
|
||||||
|
'Формирование лотов': ['всего', 'плановая дата', 'факт', '%'],
|
||||||
|
'Договор': ['всего', 'плановая дата', 'факт', '%']
|
||||||
|
}
|
||||||
|
|
||||||
|
sub_columns_mtp = {
|
||||||
|
'Выполнение плана на текущую дату': ['инициирования закупок', 'заключения договоров', 'поставки'],
|
||||||
|
'На складе, позиций': ['всего', 'поставлено', '%', 'динамика за прошедшую неделю, поз.']
|
||||||
|
}
|
||||||
|
|
||||||
|
# Формируем MultiIndex — ВСЕ кортежи длиной 3
|
||||||
|
cols = []
|
||||||
|
for col1 in columns_level_1:
|
||||||
|
if col1 == 'id':
|
||||||
|
cols.append((col1, '', ''))
|
||||||
|
elif col1 == 'ОГ':
|
||||||
|
cols.append((col1, '', ''))
|
||||||
|
elif col1 == 'Дата начала ремонта':
|
||||||
|
cols.append((col1, '', ''))
|
||||||
|
elif col1 == 'Готовность к КР':
|
||||||
|
cols.extend([(col1, 'План', ''), (col1, 'Факт', '')])
|
||||||
|
elif col1 == 'Отставание / опережение подготовки к КР':
|
||||||
|
cols.extend([
|
||||||
|
(col1, 'Отставание / опережение', ''),
|
||||||
|
(col1, 'Динамика за прошедшую неделю', '')
|
||||||
|
])
|
||||||
|
elif col1 == 'Заключение договоров на СМР':
|
||||||
|
for subcol, sub_sub_cols in sub_columns_cmp.items():
|
||||||
|
for ssc in sub_sub_cols:
|
||||||
|
cols.append((col1, subcol, ssc))
|
||||||
|
elif col1 == 'Поставка МТР':
|
||||||
|
for subcol, sub_sub_cols in sub_columns_mtp.items():
|
||||||
|
for ssc in sub_sub_cols:
|
||||||
|
cols.append((col1, subcol, ssc))
|
||||||
|
else:
|
||||||
|
cols.append((col1, '', ''))
|
||||||
|
|
||||||
|
# Создаем MultiIndex
|
||||||
|
multi_index = pd.MultiIndex.from_tuples(cols, names=['Level1', 'Level2', 'Level3'])
|
||||||
|
|
||||||
|
# === ШАГ 2: Читаем данные из Excel ===
|
||||||
|
if header_num is None:
|
||||||
|
header_num = find_header_row(file, sheet, search_value="ОГ")
|
||||||
|
|
||||||
|
df_data = pd.read_excel(
|
||||||
|
file,
|
||||||
|
skiprows=header_num + 3,
|
||||||
|
header=None,
|
||||||
|
index_col=0,
|
||||||
|
engine='openpyxl'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Убираем строки с пустыми данными
|
||||||
|
df_data.dropna(how='all', inplace=True)
|
||||||
|
|
||||||
|
# Применяем функцию get_og_by_name для 'id'
|
||||||
|
df_data['id'] = df_data.iloc[:, 0].copy()
|
||||||
|
df_data['id'] = df_data['id'].apply(get_og_by_name)
|
||||||
|
|
||||||
|
# Перемещаем 'id' на первое место
|
||||||
|
cols = ['id'] + [col for col in df_data.columns if col != 'id']
|
||||||
|
df_data = df_data[cols]
|
||||||
|
|
||||||
|
# Удаляем строки с пустым id
|
||||||
|
df_data = df_data.dropna(subset=['id'])
|
||||||
|
df_data = df_data[df_data['id'].astype(str).str.strip() != '']
|
||||||
|
|
||||||
|
# Сбрасываем индекс
|
||||||
|
df_data = df_data.reset_index(drop=True)
|
||||||
|
|
||||||
|
# Выбираем 4-ю колонку (индекс 3) для фильтрации
|
||||||
|
col_index = 3
|
||||||
|
numeric_series = pd.to_numeric(df_data.iloc[:, col_index], errors='coerce')
|
||||||
|
|
||||||
|
# Фильтруем: оставляем только строки, где значение — число
|
||||||
|
mask = pd.notna(numeric_series)
|
||||||
|
df_data = df_data[mask].copy()
|
||||||
|
|
||||||
|
# === ШАГ 3: Применяем MultiIndex к данным ===
|
||||||
|
df_data.columns = multi_index
|
||||||
|
|
||||||
|
return df_data
|
||||||
|
|
||||||
|
def _data_to_structured_json(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
|
||||||
|
"""Преобразование DataFrame с MultiIndex в структурированный JSON"""
|
||||||
|
if df.empty:
|
||||||
|
return []
|
||||||
|
|
||||||
|
result_list = []
|
||||||
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
result = {}
|
||||||
|
for col in df.columns:
|
||||||
|
value = row[col]
|
||||||
|
# Пропускаем NaN
|
||||||
|
if pd.isna(value):
|
||||||
|
value = None
|
||||||
|
|
||||||
|
# Распаковываем уровни
|
||||||
|
level1, level2, level3 = col
|
||||||
|
|
||||||
|
# Убираем пустые/неинформативные значения
|
||||||
|
level1 = str(level1).strip() if level1 else ""
|
||||||
|
level2 = str(level2).strip() if level2 else None
|
||||||
|
level3 = str(level3).strip() if level3 else None
|
||||||
|
|
||||||
|
# Обработка id и ОГ — выносим на верх
|
||||||
|
if level1 == "id":
|
||||||
|
result["id"] = value
|
||||||
|
elif level1 == "ОГ":
|
||||||
|
result["name"] = value
|
||||||
|
else:
|
||||||
|
# Группируем по Level1
|
||||||
|
if level1 not in result:
|
||||||
|
result[level1] = {}
|
||||||
|
|
||||||
|
# Вложенные уровни
|
||||||
|
if level2 and level3:
|
||||||
|
if level2 not in result[level1]:
|
||||||
|
result[level1][level2] = {}
|
||||||
|
result[level1][level2][level3] = value
|
||||||
|
elif level2:
|
||||||
|
result[level1][level2] = value
|
||||||
|
else:
|
||||||
|
result[level1] = value
|
||||||
|
|
||||||
|
result_list.append(result)
|
||||||
|
|
||||||
|
return result_list
|
||||||
|
|
||||||
|
def _get_repair_statuses_wrapper(self, params: dict):
|
||||||
|
"""Обертка для получения статусов ремонта"""
|
||||||
|
print(f"🔍 DEBUG: _get_repair_statuses_wrapper вызван с параметрами: {params}")
|
||||||
|
|
||||||
|
# Валидация параметров
|
||||||
|
validated_params = validate_params_with_schema(params, StatusesRepairCARequest)
|
||||||
|
|
||||||
|
ids = validated_params.get('ids')
|
||||||
|
keys = validated_params.get('keys')
|
||||||
|
|
||||||
|
print(f"🔍 DEBUG: Запрошенные ОГ: {ids}")
|
||||||
|
print(f"🔍 DEBUG: Запрошенные ключи: {keys}")
|
||||||
|
|
||||||
|
# Получаем данные из парсера
|
||||||
|
if hasattr(self, 'df') and self.df is not None:
|
||||||
|
# Данные загружены из MinIO
|
||||||
|
if isinstance(self.df, dict):
|
||||||
|
# Это словарь (как в других парсерах)
|
||||||
|
data_source = self.df.get('data', [])
|
||||||
|
elif hasattr(self.df, 'columns') and 'data' in self.df.columns:
|
||||||
|
# Это DataFrame
|
||||||
|
data_source = []
|
||||||
|
for _, row in self.df.iterrows():
|
||||||
|
if row['data']:
|
||||||
|
data_source.extend(row['data'])
|
||||||
|
else:
|
||||||
|
data_source = []
|
||||||
|
elif hasattr(self, 'data_dict') and self.data_dict:
|
||||||
|
# Данные из локального парсинга
|
||||||
|
data_source = self.data_dict.get('data', [])
|
||||||
|
else:
|
||||||
|
print("⚠️ Нет данных в парсере")
|
||||||
|
return []
|
||||||
|
|
||||||
|
print(f"🔍 DEBUG: Используем данные с {len(data_source)} записями")
|
||||||
|
|
||||||
|
# Фильтруем данные
|
||||||
|
filtered_data = self._filter_statuses_data(data_source, ids, keys)
|
||||||
|
|
||||||
|
print(f"🔍 DEBUG: Отфильтровано {len(filtered_data)} записей")
|
||||||
|
return filtered_data
|
||||||
|
|
||||||
|
def _filter_statuses_data(self, data_source: List[Dict], ids: Optional[List[str]], keys: Optional[List[List[str]]]) -> List[Dict]:
|
||||||
|
"""Фильтрация данных по ОГ и ключам"""
|
||||||
|
if not data_source:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Если не указаны фильтры, возвращаем все данные
|
||||||
|
if not ids and not keys:
|
||||||
|
return data_source
|
||||||
|
|
||||||
|
filtered_data = []
|
||||||
|
|
||||||
|
for item in data_source:
|
||||||
|
# Фильтр по ОГ
|
||||||
|
if ids is not None:
|
||||||
|
item_id = item.get('id')
|
||||||
|
if item_id not in ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Если указаны ключи, извлекаем только нужные поля
|
||||||
|
if keys is not None:
|
||||||
|
filtered_item = self._extract_keys_from_item(item, keys)
|
||||||
|
if filtered_item:
|
||||||
|
filtered_data.append(filtered_item)
|
||||||
|
else:
|
||||||
|
filtered_data.append(item)
|
||||||
|
|
||||||
|
return filtered_data
|
||||||
|
|
||||||
|
def _extract_keys_from_item(self, item: Dict[str, Any], keys: List[List[str]]) -> Dict[str, Any]:
|
||||||
|
"""Извлечение указанных ключей из элемента"""
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Всегда добавляем id и name
|
||||||
|
if 'id' in item:
|
||||||
|
result['id'] = item['id']
|
||||||
|
if 'name' in item:
|
||||||
|
result['name'] = item['name']
|
||||||
|
|
||||||
|
# Извлекаем указанные ключи
|
||||||
|
for key_path in keys:
|
||||||
|
if not key_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = item
|
||||||
|
for key in key_path:
|
||||||
|
if isinstance(value, dict) and key in value:
|
||||||
|
value = value[key]
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
break
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
# Строим вложенную структуру
|
||||||
|
current = result
|
||||||
|
for i, key in enumerate(key_path):
|
||||||
|
if i == len(key_path) - 1:
|
||||||
|
current[key] = value
|
||||||
|
else:
|
||||||
|
if key not in current:
|
||||||
|
current[key] = {}
|
||||||
|
current = current[key]
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -6,7 +6,7 @@ from fastapi import FastAPI, File, UploadFile, HTTPException, status
|
|||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from adapters.storage import MinIOStorageAdapter
|
from adapters.storage import MinIOStorageAdapter
|
||||||
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, SvodkaRepairCAParser
|
from adapters.parsers import SvodkaPMParser, SvodkaCAParser, MonitoringFuelParser, SvodkaRepairCAParser, StatusesRepairCAParser
|
||||||
|
|
||||||
from core.models import UploadRequest, DataRequest
|
from core.models import UploadRequest, DataRequest
|
||||||
from core.services import ReportService, PARSERS
|
from core.services import ReportService, PARSERS
|
||||||
@@ -19,6 +19,7 @@ from app.schemas import (
|
|||||||
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
|
MonitoringFuelMonthRequest, MonitoringFuelTotalRequest
|
||||||
)
|
)
|
||||||
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
|
from app.schemas.svodka_repair_ca import SvodkaRepairCARequest
|
||||||
|
from app.schemas.statuses_repair_ca import StatusesRepairCARequest
|
||||||
|
|
||||||
|
|
||||||
# Парсеры
|
# Парсеры
|
||||||
@@ -27,6 +28,7 @@ PARSERS.update({
|
|||||||
'svodka_ca': SvodkaCAParser,
|
'svodka_ca': SvodkaCAParser,
|
||||||
'monitoring_fuel': MonitoringFuelParser,
|
'monitoring_fuel': MonitoringFuelParser,
|
||||||
'svodka_repair_ca': SvodkaRepairCAParser,
|
'svodka_repair_ca': SvodkaRepairCAParser,
|
||||||
|
'statuses_repair_ca': StatusesRepairCAParser,
|
||||||
# 'svodka_plan_sarnpz': SvodkaPlanSarnpzParser,
|
# 'svodka_plan_sarnpz': SvodkaPlanSarnpzParser,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -730,6 +732,121 @@ async def get_svodka_repair_ca_data(
|
|||||||
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/statuses_repair_ca/upload", tags=[StatusesRepairCAParser.name],
|
||||||
|
summary="Загрузка отчета статусов ремонта СА")
|
||||||
|
async def upload_statuses_repair_ca(
|
||||||
|
file: UploadFile = File(...)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Загрузка отчета статусов ремонта СА
|
||||||
|
|
||||||
|
### Поддерживаемые форматы:
|
||||||
|
- **Excel файлы**: `.xlsx`, `.xlsm`, `.xls`
|
||||||
|
- **ZIP архивы**: `.zip` (содержащие Excel файлы)
|
||||||
|
|
||||||
|
### Пример использования:
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/statuses_repair_ca/upload" \
|
||||||
|
-H "accept: application/json" \
|
||||||
|
-H "Content-Type: multipart/form-data" \
|
||||||
|
-F "file=@statuses_repair_ca.xlsx"
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
report_service = get_report_service()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Проверяем тип файла
|
||||||
|
if not file.filename.endswith(('.xlsx', '.xlsm', '.xls', '.zip')):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Поддерживаются только Excel файлы (.xlsx, .xlsm, .xls) или архивы (.zip)"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Читаем содержимое файла
|
||||||
|
file_content = await file.read()
|
||||||
|
|
||||||
|
# Создаем запрос на загрузку
|
||||||
|
upload_request = UploadRequest(
|
||||||
|
report_type='statuses_repair_ca',
|
||||||
|
file_content=file_content,
|
||||||
|
file_name=file.filename
|
||||||
|
)
|
||||||
|
|
||||||
|
# Загружаем отчет
|
||||||
|
result = report_service.upload_report(upload_request)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
return UploadResponse(
|
||||||
|
success=True,
|
||||||
|
message="Отчет успешно загружен и обработан",
|
||||||
|
report_id=result.object_id,
|
||||||
|
filename=file.filename
|
||||||
|
).model_dump()
|
||||||
|
else:
|
||||||
|
return UploadErrorResponse(
|
||||||
|
success=False,
|
||||||
|
message=result.message,
|
||||||
|
error_code="ERR_UPLOAD",
|
||||||
|
details=None
|
||||||
|
).model_dump()
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/statuses_repair_ca/get_data", tags=[StatusesRepairCAParser.name],
|
||||||
|
summary="Получение данных из отчета статусов ремонта СА")
|
||||||
|
async def get_statuses_repair_ca_data(
|
||||||
|
request_data: StatusesRepairCARequest
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Получение данных из отчета статусов ремонта СА
|
||||||
|
|
||||||
|
### Структура параметров:
|
||||||
|
- `ids`: **Массив ID ОГ** для фильтрации (опциональный)
|
||||||
|
- `keys`: **Массив ключей** для извлечения данных (опциональный)
|
||||||
|
|
||||||
|
### Пример тела запроса:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": ["SNPZ", "KNPZ", "ANHK"],
|
||||||
|
"keys": [
|
||||||
|
["Дата начала ремонта"],
|
||||||
|
["Готовность к КР", "Факт"],
|
||||||
|
["Заключение договоров на СМР", "Договор", "%"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
report_service = get_report_service()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Создаем запрос
|
||||||
|
request_dict = request_data.model_dump()
|
||||||
|
request = DataRequest(
|
||||||
|
report_type='statuses_repair_ca',
|
||||||
|
get_params=request_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
# Получаем данные
|
||||||
|
result = report_service.get_data(request)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": result.data
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail=result.message)
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Внутренняя ошибка сервера: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
# @app.post("/monitoring_fuel/upload", tags=[MonitoringFuelParser.name])
|
# @app.post("/monitoring_fuel/upload", tags=[MonitoringFuelParser.name])
|
||||||
# async def upload_monitoring_fuel(
|
# async def upload_monitoring_fuel(
|
||||||
# file: UploadFile = File(...),
|
# file: UploadFile = File(...),
|
||||||
|
|||||||
34
python_parser/app/schemas/statuses_repair_ca.py
Normal file
34
python_parser/app/schemas/statuses_repair_ca.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class StatusesRepairCARequest(BaseModel):
|
||||||
|
ids: Optional[List[str]] = Field(
|
||||||
|
None,
|
||||||
|
description="Массив ID ОГ для фильтрации (например, ['SNPZ', 'KNPZ'])",
|
||||||
|
example=["SNPZ", "KNPZ", "ANHK"]
|
||||||
|
)
|
||||||
|
keys: Optional[List[List[str]]] = Field(
|
||||||
|
None,
|
||||||
|
description="Массив ключей для извлечения данных (например, [['Дата начала ремонта'], ['Готовность к КР', 'Факт']])",
|
||||||
|
example=[
|
||||||
|
["Дата начала ремонта"],
|
||||||
|
["Отставание / опережение подготовки к КР", "Отставание / опережение"],
|
||||||
|
["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"],
|
||||||
|
["Готовность к КР", "Факт"],
|
||||||
|
["Заключение договоров на СМР", "Договор", "%"],
|
||||||
|
["Поставка МТР", "На складе, позиций", "%"]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
json_schema_extra = {
|
||||||
|
"example": {
|
||||||
|
"ids": ["SNPZ", "KNPZ", "ANHK"],
|
||||||
|
"keys": [
|
||||||
|
["Дата начала ремонта"],
|
||||||
|
["Готовность к КР", "Факт"],
|
||||||
|
["Заключение договоров на СМР", "Договор", "%"]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -139,6 +139,9 @@ class ReportService:
|
|||||||
elif request.report_type == 'svodka_repair_ca':
|
elif request.report_type == 'svodka_repair_ca':
|
||||||
# Для svodka_repair_ca используем геттер get_repair_data
|
# Для svodka_repair_ca используем геттер get_repair_data
|
||||||
getter_name = 'get_repair_data'
|
getter_name = 'get_repair_data'
|
||||||
|
elif request.report_type == 'statuses_repair_ca':
|
||||||
|
# Для statuses_repair_ca используем геттер get_repair_statuses
|
||||||
|
getter_name = 'get_repair_statuses'
|
||||||
elif request.report_type == 'monitoring_fuel':
|
elif request.report_type == 'monitoring_fuel':
|
||||||
# Для monitoring_fuel определяем геттер из параметра mode
|
# Для monitoring_fuel определяем геттер из параметра mode
|
||||||
getter_name = get_params.pop("mode", None)
|
getter_name = get_params.pop("mode", None)
|
||||||
|
|||||||
@@ -115,11 +115,12 @@ def main():
|
|||||||
st.write(f"• {parser}")
|
st.write(f"• {parser}")
|
||||||
|
|
||||||
# Основные вкладки - по одной на каждый парсер
|
# Основные вкладки - по одной на каждый парсер
|
||||||
tab1, tab2, tab3, tab4 = st.tabs([
|
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
||||||
"📊 Сводки ПМ",
|
"📊 Сводки ПМ",
|
||||||
"🏭 Сводки СА",
|
"🏭 Сводки СА",
|
||||||
"⛽ Мониторинг топлива",
|
"⛽ Мониторинг топлива",
|
||||||
"🔧 Ремонт СА"
|
"🔧 Ремонт СА",
|
||||||
|
"📋 Статусы ремонта СА"
|
||||||
])
|
])
|
||||||
|
|
||||||
# Вкладка 1: Сводки ПМ - полный функционал
|
# Вкладка 1: Сводки ПМ - полный функционал
|
||||||
@@ -493,6 +494,145 @@ def main():
|
|||||||
else:
|
else:
|
||||||
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
|
||||||
|
# Вкладка 5: Статусы ремонта СА
|
||||||
|
with tab5:
|
||||||
|
st.header("📋 Статусы ремонта СА")
|
||||||
|
|
||||||
|
# Секция загрузки файлов
|
||||||
|
st.subheader("📤 Загрузка файлов")
|
||||||
|
uploaded_file = st.file_uploader(
|
||||||
|
"Выберите файл статусов ремонта СА",
|
||||||
|
type=['xlsx', 'xlsm', 'xls', 'zip'],
|
||||||
|
key="statuses_repair_ca_upload"
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_file is not None:
|
||||||
|
if st.button("📤 Загрузить файл", key="statuses_repair_ca_upload_btn"):
|
||||||
|
with st.spinner("Загружаем файл..."):
|
||||||
|
file_data = uploaded_file.read()
|
||||||
|
result, status_code = upload_file_to_api("/statuses_repair_ca/upload", file_data, uploaded_file.name)
|
||||||
|
|
||||||
|
if status_code == 200:
|
||||||
|
st.success("✅ Файл успешно загружен!")
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка загрузки: {result}")
|
||||||
|
|
||||||
|
# Секция получения данных
|
||||||
|
st.subheader("📊 Получение данных")
|
||||||
|
|
||||||
|
# Получаем доступные ОГ динамически
|
||||||
|
available_ogs = get_available_ogs("statuses_repair_ca")
|
||||||
|
|
||||||
|
# Фильтр по ОГ
|
||||||
|
og_ids = st.multiselect(
|
||||||
|
"Выберите ОГ (оставьте пустым для всех)",
|
||||||
|
available_ogs if available_ogs else ["KNPZ", "ANHK", "SNPZ", "BASH", "UNH", "NOV"], # fallback
|
||||||
|
key="statuses_repair_ca_og_ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Предустановленные ключи для извлечения
|
||||||
|
st.subheader("🔑 Ключи для извлечения данных")
|
||||||
|
|
||||||
|
# Основные ключи
|
||||||
|
include_basic_keys = st.checkbox("Основные данные", value=True, key="statuses_basic_keys")
|
||||||
|
include_readiness_keys = st.checkbox("Готовность к КР", value=True, key="statuses_readiness_keys")
|
||||||
|
include_contract_keys = st.checkbox("Заключение договоров", value=True, key="statuses_contract_keys")
|
||||||
|
include_supply_keys = st.checkbox("Поставка МТР", value=True, key="statuses_supply_keys")
|
||||||
|
|
||||||
|
# Формируем ключи на основе выбора
|
||||||
|
keys = []
|
||||||
|
if include_basic_keys:
|
||||||
|
keys.append(["Дата начала ремонта"])
|
||||||
|
keys.append(["Отставание / опережение подготовки к КР", "Отставание / опережение"])
|
||||||
|
keys.append(["Отставание / опережение подготовки к КР", "Динамика за прошедшую неделю"])
|
||||||
|
|
||||||
|
if include_readiness_keys:
|
||||||
|
keys.append(["Готовность к КР", "Факт"])
|
||||||
|
|
||||||
|
if include_contract_keys:
|
||||||
|
keys.append(["Заключение договоров на СМР", "Договор", "%"])
|
||||||
|
|
||||||
|
if include_supply_keys:
|
||||||
|
keys.append(["Поставка МТР", "На складе, позиций", "%"])
|
||||||
|
|
||||||
|
# Кнопка получения данных
|
||||||
|
if st.button("📊 Получить данные", key="statuses_repair_ca_get_data_btn"):
|
||||||
|
if not keys:
|
||||||
|
st.warning("⚠️ Выберите хотя бы одну группу ключей для извлечения")
|
||||||
|
else:
|
||||||
|
with st.spinner("Получаем данные..."):
|
||||||
|
request_data = {
|
||||||
|
"ids": og_ids if og_ids else None,
|
||||||
|
"keys": keys
|
||||||
|
}
|
||||||
|
|
||||||
|
result, status_code = make_api_request("/statuses_repair_ca/get_data", request_data)
|
||||||
|
|
||||||
|
if status_code == 200 and result.get("success"):
|
||||||
|
st.success("✅ Данные успешно получены!")
|
||||||
|
|
||||||
|
data = result.get("data", {}).get("value", [])
|
||||||
|
if data:
|
||||||
|
# Отображаем данные в виде таблицы
|
||||||
|
if isinstance(data, list) and len(data) > 0:
|
||||||
|
# Преобразуем в DataFrame для лучшего отображения
|
||||||
|
df_data = []
|
||||||
|
for item in data:
|
||||||
|
row = {
|
||||||
|
"ID": item.get("id", ""),
|
||||||
|
"Название": item.get("name", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавляем основные поля
|
||||||
|
if "Дата начала ремонта" in item:
|
||||||
|
row["Дата начала ремонта"] = item["Дата начала ремонта"]
|
||||||
|
|
||||||
|
# Добавляем готовность к КР
|
||||||
|
if "Готовность к КР" in item:
|
||||||
|
readiness = item["Готовность к КР"]
|
||||||
|
if isinstance(readiness, dict) and "Факт" in readiness:
|
||||||
|
row["Готовность к КР (Факт)"] = readiness["Факт"]
|
||||||
|
|
||||||
|
# Добавляем отставание/опережение
|
||||||
|
if "Отставание / опережение подготовки к КР" in item:
|
||||||
|
delay = item["Отставание / опережение подготовки к КР"]
|
||||||
|
if isinstance(delay, dict):
|
||||||
|
if "Отставание / опережение" in delay:
|
||||||
|
row["Отставание/опережение"] = delay["Отставание / опережение"]
|
||||||
|
if "Динамика за прошедшую неделю" in delay:
|
||||||
|
row["Динамика за неделю"] = delay["Динамика за прошедшую неделю"]
|
||||||
|
|
||||||
|
# Добавляем договоры
|
||||||
|
if "Заключение договоров на СМР" in item:
|
||||||
|
contracts = item["Заключение договоров на СМР"]
|
||||||
|
if isinstance(contracts, dict) and "Договор" in contracts:
|
||||||
|
contract = contracts["Договор"]
|
||||||
|
if isinstance(contract, dict) and "%" in contract:
|
||||||
|
row["Договоры (%)"] = contract["%"]
|
||||||
|
|
||||||
|
# Добавляем поставки МТР
|
||||||
|
if "Поставка МТР" in item:
|
||||||
|
supply = item["Поставка МТР"]
|
||||||
|
if isinstance(supply, dict) and "На складе, позиций" in supply:
|
||||||
|
warehouse = supply["На складе, позиций"]
|
||||||
|
if isinstance(warehouse, dict) and "%" in warehouse:
|
||||||
|
row["МТР на складе (%)"] = warehouse["%"]
|
||||||
|
|
||||||
|
df_data.append(row)
|
||||||
|
|
||||||
|
if df_data:
|
||||||
|
df = pd.DataFrame(df_data)
|
||||||
|
st.dataframe(df, use_container_width=True)
|
||||||
|
else:
|
||||||
|
st.info("📋 Нет данных для отображения")
|
||||||
|
else:
|
||||||
|
st.json(result)
|
||||||
|
else:
|
||||||
|
st.info("📋 Нет данных для отображения")
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Ошибка: {result.get('message', 'Неизвестная ошибка')}")
|
||||||
|
|
||||||
# Футер
|
# Футер
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("### 📚 Документация API")
|
st.markdown("### 📚 Документация API")
|
||||||
@@ -508,6 +648,7 @@ def main():
|
|||||||
- 🏭 Парсинг сводок СА
|
- 🏭 Парсинг сводок СА
|
||||||
- ⛽ Мониторинг топлива
|
- ⛽ Мониторинг топлива
|
||||||
- 🔧 Управление ремонтными работами СА
|
- 🔧 Управление ремонтными работами СА
|
||||||
|
- 📋 Мониторинг статусов ремонта СА
|
||||||
|
|
||||||
**Технологии:**
|
**Технологии:**
|
||||||
- FastAPI
|
- FastAPI
|
||||||
|
|||||||
Reference in New Issue
Block a user