Загрузка данных
from fastapi import FastAPI, Request, Form, HTTPException, Depends, status
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, Field, validator
from typing import List, Annotated, Optional
import uuid
from datetime import datetime
import json
from pathlib import Path
import uvicorn
import os
import pandas as pd
from excel_database import excel_db
from openpyxl.styles import Font, PatternFill
app = FastAPI()
# Создаем директории если их нет
Path("static").mkdir(exist_ok=True)
Path("templates").mkdir(exist_ok=True)
EXPORT_DIR = Path("saved_configurations")
EXPORT_DIR.mkdir(exist_ok=True)
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
shelf_db: List["Shelf"] = []
class GPFaddon(BaseModel):
isAddon: bool
PumpCount: int
class PLC(BaseModel):
isDomestic: bool
isModule: bool
name: str
manufacturer: str
price: float
di_count: int = Field(..., gt=0)
do_count: int = Field(..., gt=0)
ai_count: int = Field(..., gt=0)
ao_count: int = Field(..., gt=0)
@validator('name', 'manufacturer')
def validate_strings(cls, v):
if not v.strip():
raise ValueError("Не может быть пустым")
return v.strip()
class Component(BaseModel):
name: str
price: float = Field(..., gt=0)
io_type: str
@validator('io_type')
def validate_io_type(cls, v):
if v not in ['DI', 'DO', 'AI', 'AO']:
raise ValueError("Недопустимый тип I/O")
return v
class Shelf(BaseModel):
name: str
material: bool
size: str
class Compress(BaseModel):
is_present: bool = Field(default=False)
ratemeter: bool = Field(default=False)
class PumpStation(BaseModel):
is_present: bool = Field(default=False)
pump_count: Optional[int] = Field(default=None, ge=1, le=2)
pump_powers: Optional[List[float]] = Field(default=None)
@validator('pump_powers', always=True)
def validate_powers(cls, v, values):
is_present = values.get('is_present', False)
pump_count = values.get('pump_count')
if is_present and pump_count is not None:
if v is None:
raise ValueError("Для работающей станции необходимо указать мощности насосов")
return v
class Questionnaire(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
Org_name: str
gpf_size: int
isDomestic_controller: bool
PLC_manufactorer: str
Shelf: Shelf
Compress: Compress
regen_station: PumpStation = Field(default_factory=PumpStation)
pulp_station: PumpStation = Field(default_factory=PumpStation)
press_station: PumpStation = Field(default_factory=PumpStation)
Shaker: bool
UZIP: bool
Light_siren: bool
IBP: bool
Drip_tray_drive: bool
Commentary: str
created_at: datetime = Field(default_factory=datetime.now)
class Config:
allow_population_by_field_name = True
json_encoders = {
datetime: lambda v: v.isoformat()
}
# Pydantic схемы для конфигураций
class ConfigurationBase(BaseModel):
name: str
questionnaire_id: Optional[str] = None
configuration_data: dict
class ConfigurationCreate(ConfigurationBase):
pass
class Configuration(ConfigurationBase):
id: int
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
# Функции для работы с JSON (анкеты)
JSON_FILE = Path("questionnaires.json")
def save_to_json(questionnaire: Questionnaire):
try:
data = []
if JSON_FILE.exists():
data = json.loads(JSON_FILE.read_text(encoding="utf-8"))
data.append(questionnaire.dict())
JSON_FILE.write_text(
json.dumps(data, indent=2, ensure_ascii=False, default=str),
encoding="utf-8"
)
print(f"Данные успешно сохранены в {JSON_FILE}")
except Exception as e:
print(f"Ошибка при сохранении: {str(e)}")
raise
def load_from_json() -> List[Questionnaire]:
if not JSON_FILE.exists():
return []
try:
with open(JSON_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
return [Questionnaire.parse_obj(item) for item in data]
except Exception as e:
raise ValueError(f"Ошибка загрузки: {str(e)}")
# API для анкет
@app.post("/questionnaires/",
response_model=Questionnaire,
status_code=status.HTTP_201_CREATED)
async def create_questionnaire(questionnaire: Questionnaire):
try:
save_to_json(questionnaire)
return questionnaire
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при сохранении: {str(e)}")
# API для PLC (Excel)
@app.post("/plcs/")
async def create_plc(
request: Request,
isDomestic: bool = Form(...),
name: str = Form(...),
manufacturer: str = Form(...),
price: float = Form(...),
di_count: int = Form(...),
do_count: int = Form(...),
ai_count: int = Form(...),
ao_count: int = Form(...)
):
try:
plc_data = {
'isDomestic': isDomestic,
'name': name,
'manufacturer': manufacturer,
'price': price,
'di_count': di_count,
'do_count': do_count,
'ai_count': ai_count,
'ao_count': ao_count
}
new_plc = excel_db.create_plc(plc_data)
return RedirectResponse(url="/create_components", status_code=303)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
# API для Components (Excel)
@app.post("/components/")
async def create_component(
request: Request,
name: str = Form(...),
price: float = Form(...),
io_type: str = Form(...)
):
try:
component_data = {
'name': name,
'price': price,
'io_type': io_type
}
new_component = excel_db.create_component(component_data)
return RedirectResponse(url="/create_components", status_code=303)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.delete("/components/component_deletebyid/{id}")
async def delete_component(id: int):
try:
excel_db.delete_component(id)
return {"message": f"Component with id {id} deleted successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/components/plc_deletebyid/{id}")
async def delete_plc(id: int):
try:
excel_db.delete_plc(id)
return {"message": f"PLC with id {id} deleted successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# API для Configurations (Excel)
@app.post("/configurations/", response_model=dict)
async def create_configuration(configuration: ConfigurationCreate):
try:
# Сохраняем ID анкеты для последующего удаления
questionnaire_id = configuration.questionnaire_id
config_data = {
'name': configuration.name,
'questionnaire_id': questionnaire_id,
'configuration_data': json.dumps(configuration.configuration_data)
}
# Создаем конфигурацию
new_config = excel_db.create_configuration(config_data)
new_config['configuration_data'] = json.loads(new_config['configuration_data'])
# УДАЛЯЕМ АНКЕТУ ПОСЛЕ УСПЕШНОГО СОЗДАНИЯ КОНФИГУРАЦИИ
if questionnaire_id:
deleted = await delete_questionnaire_directly(questionnaire_id)
if deleted:
print(f"✅ Анкета {questionnaire_id} удалена после создания конфигурации")
else:
print(f"⚠️ Не удалось удалить анкету {questionnaire_id}")
return new_config
except Exception as e:
print(f"Ошибка при создании конфигурации: {str(e)}")
raise HTTPException(status_code=400, detail=str(e))
async def delete_questionnaire_directly(questionnaire_id: str) -> bool:
"""Прямое удаление анкеты из JSON файла"""
try:
if not JSON_FILE.exists():
return False
# Читаем текущие анкеты
with open(JSON_FILE, 'r', encoding='utf-8') as f:
questionnaires = json.load(f)
# Фильтруем анкеты
updated_questionnaires = [q for q in questionnaires if q['id'] != questionnaire_id]
# Если список изменился - записываем обратно
if len(updated_questionnaires) != len(questionnaires):
with open(JSON_FILE, 'w', encoding='utf-8') as f:
json.dump(updated_questionnaires, f, ensure_ascii=False, indent=2, default=str)
return True
return False
except Exception as e:
print(f"Ошибка при прямом удалении анкеты: {str(e)}")
return False
@app.delete("/force-delete-questionnaire/{item_id}")
async def force_delete_questionnaire(item_id: str):
"""Принудительное удаление анкеты"""
try:
if not JSON_FILE.exists():
raise HTTPException(status_code=404, detail="Файл анкет не найден")
with open(JSON_FILE, 'r', encoding='utf-8') as f:
questionnaires = json.load(f)
initial_count = len(questionnaires)
updated_questionnaires = [q for q in questionnaires if q['id'] != item_id]
if len(updated_questionnaires) == initial_count:
raise HTTPException(status_code=404, detail="Анкета не найдена")
with open(JSON_FILE, 'w', encoding='utf-8') as f:
json.dump(updated_questionnaires, f, ensure_ascii=False, indent=2, default=str)
return {"message": f"Анкета {item_id} успешно удалена", "deleted": True}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при удалении: {str(e)}")
async def delete_questionnaire_after_config(questionnaire_id: str):
"""Удаляет анкету из JSON файла после создания конфигурации"""
try:
if not JSON_FILE.exists():
print("Файл анкет не существует")
return False
# Читаем текущие анкеты
with open(JSON_FILE, 'r', encoding='utf-8') as f:
questionnaires = json.load(f)
# Сохраняем исходное количество
initial_count = len(questionnaires)
# Фильтруем анкеты, убирая ту, которую нужно удалить
updated_questionnaires = [q for q in questionnaires if q['id'] != questionnaire_id]
# Проверяем, была ли анкета найдена и удалена
if len(updated_questionnaires) < initial_count:
# Записываем обновленный список обратно в файл
with open(JSON_FILE, 'w', encoding='utf-8') as f:
json.dump(updated_questionnaires, f, ensure_ascii=False, indent=2, default=str)
print(f"✅ Анкета {questionnaire_id} успешно удалена")
return True
else:
print(f"⚠️ Анкета {questionnaire_id} не найдена в файле")
return False
except Exception as e:
print(f"❌ Ошибка при удалении анкеты: {str(e)}")
return False
@app.get("/api/plcs", response_model=List[dict])
async def get_all_plcs_api():
"""API для получения всех контроллеров"""
try:
plcs = excel_db.get_all_plcs()
return plcs
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка получения контроллеров: {str(e)}")
@app.get("/api/components", response_model=List[dict])
async def get_all_components_api():
"""API для получения всех компонентов"""
try:
components = excel_db.get_all_components()
return components
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка получения компонентов: {str(e)}")
@app.get("/configurations/", response_model=List[dict])
async def get_configurations():
try:
configurations = excel_db.get_all_configurations()
# Преобразуем JSON строки обратно в dict и добавляем стоимость
for config in configurations:
config['configuration_data'] = json.loads(config['configuration_data'])
# Добавляем расчет стоимости
config['total_cost'] = calculate_configuration_cost(config['configuration_data'])
# Добавляем детальный расчет стоимости
config['detailed_cost'] = calculate_detailed_cost(config['configuration_data'])
return configurations
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/configurations/{config_id}")
async def delete_configuration(config_id: int):
try:
# Преобразуем config_id в int на случай если приходит строка
config_id = int(config_id)
excel_db.delete_configuration(config_id)
return {"message": f"Configuration with id {config_id} deleted successfully"}
except ValueError:
raise HTTPException(status_code=400, detail="Invalid configuration ID")
except Exception as e:
print(f"Error deleting configuration {config_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error deleting configuration: {str(e)}")
# HTML endpoints
@app.get("/create_components", response_class=HTMLResponse)
async def components_page(request: Request):
plcs = excel_db.get_all_plcs()
components = excel_db.get_all_components()
return templates.TemplateResponse("add_components.html", {
"request": request,
"plcs": plcs,
"components": components
})
@app.get("/menu", response_class=HTMLResponse)
async def menu_page(request: Request):
plcs = excel_db.get_all_plcs()
components = excel_db.get_all_components()
return templates.TemplateResponse("menu_template.html", {
"request": request,
"plcs": plcs,
"components": components,
"shelves": shelf_db
})
@app.get("/new_list", response_class=HTMLResponse)
async def new_list_page(request: Request):
return templates.TemplateResponse("new_list.html", {"request": request})
@app.get("/saved_configurations", response_class=HTMLResponse)
async def saved_configs_page(request: Request):
return templates.TemplateResponse("saved_configurations.html", {"request": request})
@app.get("/new_config", response_class=HTMLResponse)
async def new_config_page(request: Request):
plcs = excel_db.get_all_plcs()
components = excel_db.get_all_components()
return templates.TemplateResponse("new_config.html", {
"request": request,
"plcs": plcs,
"components": components,
"shelves": shelf_db
})
@app.get("/get-questionnaire")
async def get_questionnaires():
if not os.path.exists(JSON_FILE):
return JSONResponse(status_code=404, content={"error": "File not found"})
return FileResponse(JSON_FILE, media_type="application/json; charset=utf-8")
@app.get("/get-questionnaire/{item_id}")
async def get_questionnaire(item_id: str):
try:
questionnaires = load_from_json()
for q in questionnaires:
if q.id == item_id:
return JSONResponse(
content=jsonable_encoder(q),
media_type="application/json; charset=utf-8"
)
raise HTTPException(status_code=404, detail="Анкета не найдена")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/config_create", response_class=HTMLResponse)
async def config_create_page(request: Request, questionnaireId: Optional[str] = None):
questionnaire_data = None
if questionnaireId:
try:
questionnaires = load_from_json()
questionnaire = next((q for q in questionnaires if q.id == questionnaireId), None)
if questionnaire:
questionnaire_data = questionnaire.dict()
except Exception as e:
print(f"Ошибка при загрузке анкеты: {str(e)}")
return templates.TemplateResponse("config_create.html", {
"request": request,
"questionnaire": questionnaire_data
})
def calculate_configuration_cost(config_data: dict) -> float:
"""Рассчитывает общую стоимость конфигурации"""
total_cost = 0.0
try:
# Стоимость выбранного контроллера
selected_plc = config_data.get('selected_plc', {})
if selected_plc and selected_plc.get('data'):
plc_price = selected_plc['data'].get('price', 0)
if plc_price:
total_cost += float(plc_price)
# Стоимость компонентов
selected_components = config_data.get('selected_components', [])
for component in selected_components:
if isinstance(component, dict):
price = component.get('price', 0)
quantity = component.get('quantity', 1)
total_cost += float(price) * quantity
# Дополнительное оборудование (условные стоимости)
equipment_costs = {
'shaker': 5000.0, # Примерная стоимость механизма встряхивания
'uzip': 3000.0, # Примерная стоимость УЗИП
'light_siren': 2000.0, # Примерная стоимость сирены
'ibp': 15000.0, # Примерная стоимость ИБП
}
for eq_key, cost in equipment_costs.items():
if config_data.get(eq_key):
total_cost += cost
# Стоимость станций (упрощенный расчет)
stations = ['compress_station', 'regen_station', 'pulp_station', 'press_station']
station_base_cost = 20000.0 # Базовая стоимость станции
for station_key in stations:
station = config_data.get(station_key, {})
if station and station.get('is_present'):
total_cost += station_base_cost
# Дополнительная стоимость за насосы
pump_count = station.get('pump_count', 0)
total_cost += pump_count * 5000.0 # Примерная стоимость насоса
return round(total_cost, 2)
except Exception as e:
print(f"Ошибка расчета стоимости: {e}")
return 0.0
def calculate_detailed_cost(config_data: dict) -> dict:
"""Детальный расчет стоимости с разбивкой по компонентам"""
cost_breakdown = {
'controller': 0.0,
'components': 0.0,
'equipment': 0.0,
'stations': 0.0,
'sensors': 0.0,
'total': 0.0
}
# Расчет стоимости контроллера
selected_plc = config_data.get('selected_plc', {})
if selected_plc and selected_plc.get('data'):
plc_cost = selected_plc['data'].get('price', 0)
cost_breakdown['controller'] = float(plc_cost) if plc_cost else 0.0
# Расчет стоимости компонентов
selected_components = config_data.get('selected_components', [])
components_cost = 0.0
components_details = []
for component in selected_components:
if isinstance(component, dict):
price = component.get('price', 0)
quantity = component.get('quantity', 1)
component_total = float(price) * quantity
components_cost += component_total
components_details.append({
'id': component.get('id'),
'name': component.get('name'),
'type': component.get('io_type'),
'price': float(price),
'quantity': quantity,
'total': component_total
})
cost_breakdown['components'] = components_cost
cost_breakdown['components_details'] = components_details
# Расчет стоимости дополнительного оборудования
equipment_prices = {
'shaker': 5000.0, 'uzip': 3000.0,
'light_siren': 2000.0, 'ibp': 15000.0
}
for eq_key, price in equipment_prices.items():
if config_data.get(eq_key):
cost_breakdown['equipment'] += price
# Расчет стоимости станций
station_prices = {
'compress_station': 25000.0,
'regen_station': 30000.0,
'pulp_station': 28000.0,
'press_station': 35000.0
}
for station_key, base_price in station_prices.items():
station = config_data.get(station_key, {})
if station and station.get('is_present'):
station_cost = base_price
# Добавляем стоимость насосов
pump_count = station.get('pump_count', 0)
station_cost += pump_count * 8000.0
cost_breakdown['stations'] += station_cost
# Итоговая сумма
cost_breakdown['total'] = sum([
cost_breakdown['controller'],
cost_breakdown['components'],
cost_breakdown['equipment'],
cost_breakdown['stations'],
cost_breakdown['sensors']
])
return cost_breakdown
@app.post("/export/single-config/{config_id}")
async def export_single_config(config_id: int):
try:
# Получаем все конфигурации
configurations = excel_db.get_all_configurations()
config = next((c for c in configurations if c['id'] == config_id), None)
if not config:
raise HTTPException(status_code=404, detail="Конфигурация не найдена")
# Безопасно преобразуем JSON строку обратно в dict
try:
if isinstance(config['configuration_data'], str):
config_data = json.loads(config['configuration_data'])
else:
config_data = config['configuration_data']
except (json.JSONDecodeError, TypeError) as e:
print(f"Ошибка парсинга JSON для конфигурации {config_id}: {e}")
config_data = {}
# Формируем путь для сохранения
export_file = EXPORT_DIR / f"configuration_{config_id}.xlsx"
with pd.ExcelWriter(export_file, engine='openpyxl') as writer:
# Лист 1: Основная информация
basic_data = [
['ID конфигурации', config['id']],
['Название', config['name']],
['ID анкеты', config.get('questionnaire_id', 'Не указан')],
['Дата создания', config['created_at']],
['', ''],
['Основные параметры', '']
]
# Безопасно добавляем основные параметры
safe_data = config_data or {}
basic_data.extend([
['Заказчик', safe_data.get('org_name', safe_data.get('Org_name', ''))],
['Размер ГПФ', f"{safe_data.get('gpf_size', '')} мм"],
['Отечественный контроллер', 'Да' if safe_data.get('is_domestic_controller') else 'Нет'],
['Производитель ПЛК', safe_data.get('plc_manufacturer', safe_data.get('PLC_manufactorer', ''))],
['Материал шкафа', 'Материал 1' if safe_data.get('shelf_material') else 'Материал 2'],
['Комментарий', safe_data.get('commentary', safe_data.get('Commentary', ''))]
])
# Добавляем информацию о выбранном контроллере (если есть)
if isinstance(safe_data, dict) and safe_data.get('selected_plc'):
selected_plc = safe_data.get('selected_plc', {})
plc_data = selected_plc.get('data', {})
basic_data.extend([
['', ''],
['Выбранный контроллер', ''],
['ID контроллера', selected_plc.get('id', '')],
['Производитель контроллера', plc_data.get('manufacturer', '')],
['Модель контроллера', plc_data.get('name', '')],
['Тип контроллера', 'Отечественный' if plc_data.get('isDomestic') else 'Импортный'],
['Цена контроллера', plc_data.get('price', '')],
['DI порты', plc_data.get('di_count', '')],
['DO порты', plc_data.get('do_count', '')],
['AI порты', plc_data.get('ai_count', '')],
['AO порты', plc_data.get('ao_count', '')]
])
basic_df = pd.DataFrame(basic_data, columns=['Параметр', 'Значение'])
basic_df.to_excel(writer, sheet_name='Основная информация', index=False)
# Автоматическая ширина столбцов для основного листа
worksheet = writer.sheets['Основная информация']
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50) # Максимальная ширина 50 символов
worksheet.column_dimensions[column_letter].width = adjusted_width
# Лист 2: Станции и оборудование
stations_data = []
# Доп. компрессор
compress = safe_data.get('compress_station', safe_data.get('Compress', {}))
if isinstance(compress, dict) and compress.get('is_present'):
stations_data.append(['Дополнительный компрессор', 'Да'])
stations_data.append(['Расходомер на воздух', 'Да' if compress.get('ratemeter') else 'Нет'])
else:
stations_data.append(['Дополнительный компрессор', 'Нет'])
stations_data.append(['', ''])
# Станции
stations = [
('regen_station', 'Станция регенерации'),
('pulp_station', 'Станция пульпы'),
('press_station', 'Станция прессования')
]
for station_key, station_name in stations:
station = safe_data.get(station_key, {})
if isinstance(station, dict) and station.get('is_present'):
stations_data.append([station_name, 'Да'])
stations_data.append(['Количество насосов', station.get('pump_count', 0)])
pump_powers = station.get('pump_powers', [])
if pump_powers and isinstance(pump_powers, list):
stations_data.append(['Мощности насосов', ', '.join(map(str, pump_powers)) + ' кВт'])
else:
stations_data.append([station_name, 'Нет'])
stations_data.append(['', ''])
# Дополнительное оборудование
equipment = [
('shaker', 'Механизм встряхивания плит'),
('uzip', 'УЗИП'),
('light_siren', 'Сирена с светоизлучателем'),
('ibp', 'ИБП')
]
stations_data.append(['Дополнительное оборудование', ''])
for eq_key, eq_name in equipment:
value = safe_data.get(eq_key)
stations_data.append([eq_name, 'Да' if value else 'Нет'])
stations_data.append(['', ''])
# Привод каплесборника
drive = safe_data.get('drip_tray_drive')
stations_data.append(['Привод каплесборника', 'Гидравлический' if drive else 'Электрический'])
stations_df = pd.DataFrame(stations_data, columns=['Оборудование', 'Статус'])
stations_df.to_excel(writer, sheet_name='Оборудование', index=False)
# Автоматическая ширина столбцов для листа оборудования
worksheet = writer.sheets['Оборудование']
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
worksheet.column_dimensions[column_letter].width = adjusted_width
# Лист 3: Датчики (если есть)
sensors_data = []
sensors = safe_data.get('sensors', {})
if isinstance(sensors, dict):
for section_name, sensor_list in sensors.items():
if sensor_list and isinstance(sensor_list, list):
sensors_data.append([section_name.replace('_', ' ').title(), ''])
for sensor in sensor_list:
if isinstance(sensor, dict):
sensors_data.append([
sensor.get('name', ''),
f"{sensor.get('type', '')} ({sensor.get('range', '')})"
])
sensors_data.append(['', ''])
if sensors_data:
sensors_df = pd.DataFrame(sensors_data, columns=['Датчик', 'Параметры'])
sensors_df.to_excel(writer, sheet_name='Датчики', index=False)
# Автоматическая ширина столбцов для листа датчиков
worksheet = writer.sheets['Датчики']
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
worksheet.column_dimensions[column_letter].width = adjusted_width
else:
pd.DataFrame([['Нет данных о датчиках', '']], columns=['Датчик', 'Параметры']).to_excel(
writer, sheet_name='Датчики', index=False
)
# Лист 4: Компоненты (если есть)
selected_components = safe_data.get('selected_components', [])
if selected_components and isinstance(selected_components, list):
components_data = []
# Заголовок таблицы
components_data.append(['ID', 'Название', 'Тип I/O', 'Цена за шт. (₽)', 'Количество', 'Общая стоимость (₽)'])
# Данные компонентов
total_components_cost = 0
for component in selected_components:
if isinstance(component, dict):
price = component.get('price', 0)
quantity = component.get('quantity', 1)
total_cost = price * quantity
total_components_cost += total_cost
components_data.append([
component.get('id', ''),
component.get('name', ''),
component.get('io_type', ''),
f"{price:.2f}",
quantity,
f"{total_cost:.2f}"
])
# Итоговая строка
components_data.append(['', '', '', '', 'ИТОГО:', f"{total_components_cost:.2f}"])
components_df = pd.DataFrame(components_data)
components_df.to_excel(writer, sheet_name='Компоненты', index=False, header=False)
# Автоматическая ширина столбцов для листа компонентов
worksheet = writer.sheets['Компоненты']
for column in worksheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = min(max_length + 2, 50)
worksheet.column_dimensions[column_letter].width = adjusted_width
# Стилизация заголовка и итогов
header_row = worksheet[1]
for cell in header_row:
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color="E0E0E0", end_color="E0E0E0", fill_type="solid")
# Выделение итоговой строки
last_row = worksheet[worksheet.max_row]
for cell in last_row:
if cell.column_letter in ['F']: # Столбец с итоговой стоимостью
cell.font = Font(bold=True, color="FF0000")
elif cell.column_letter in ['E']: # Столбец с текстом "ИТОГО:"
cell.font = Font(bold=True)
print(f"Конфигурация {config_id} успешно экспортирована в {export_file}")
# Возвращаем файл для скачивания
return FileResponse(
export_file,
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
filename=f"configuration_{config_id}.xlsx"
)
except Exception as e:
print(f"Ошибка экспорта конфигурации {config_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Ошибка экспорта: {str(e)}")
@app.delete("/delete-questionnaire/{item_id}")
async def delete_questionnaire(item_id: str):
try:
if not JSON_FILE.exists():
raise HTTPException(status_code=404, detail="Файл не найден")
with open(JSON_FILE, 'r', encoding='utf-8') as f:
questionnaires = json.load(f)
updated_questionnaires = [q for q in questionnaires if q['id'] != item_id]
with open(JSON_FILE, 'w', encoding='utf-8') as f:
json.dump(updated_questionnaires, f, ensure_ascii=False, indent=2, default=str)
return {"message": "Анкета успешно удалена"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при удалении: {str(e)}")
@app.post("/export/config-data")
async def export_config_data(data: dict):
try:
export_file = f"configuration_export_{data.get('export_config_id', 'unknown')}.xlsx"
with pd.ExcelWriter(export_file, engine='openpyxl') as writer:
# Лист с основной информацией о конфигурации
if 'configurations' in data and data['configurations']:
config = data['configurations'][0]
# Основная информация
basic_info = {
'ID конфигурации': [config['id']],
'Название': [config['name']],
'ID анкеты': [config.get('questionnaire_id', 'Не указан')],
'Дата создания': [config['created_at']],
'Дата обновления': [config['updated_at']]
}
basic_df = pd.DataFrame(basic_info)
basic_df.to_excel(writer, sheet_name='Основная информация', index=False)
# Детали конфигурации
config_data = config.get('configuration_data', {})
if config_data:
# Преобразуем вложенные данные в плоскую структуру
flat_data = flatten_config_data(config_data)
config_df = pd.DataFrame([flat_data])
config_df.to_excel(writer, sheet_name='Данные конфигурации', index=False)
# Лист с сырыми данными (для полноты)
raw_df = pd.DataFrame([data])
raw_df.to_excel(writer, sheet_name='Полные данные', index=False)
return FileResponse(
export_file,
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
filename=export_file
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка экспорта: {str(e)}")
def flatten_config_data(data, parent_key='', sep='.'):
#Рекурсивно преобразует вложенный словарь в плоский
items = []
for k, v in data.items():
new_key = f"{parent_key}{sep}{k}" if parent_key else k
if isinstance(v, dict):
items.extend(flatten_config_data(v, new_key, sep=sep).items())
elif isinstance(v, list):
# Для списков создаем строку с перечислением
items.append((new_key, '; '.join(map(str, v))))
else:
items.append((new_key, v))
return dict(items)
@app.get("/")
async def root():
return RedirectResponse(url="/menu")
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=1234, reload=True)