After Graduate Update
This commit is contained in:
parent
b92a91ab37
commit
c6917dd85e
69 changed files with 7540 additions and 0 deletions
BIN
ressult/app/__pycache__/database.cpython-314.pyc
Normal file
BIN
ressult/app/__pycache__/database.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/__pycache__/main.cpython-314.pyc
Normal file
BIN
ressult/app/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
60
ressult/app/database.py
Normal file
60
ressult/app/database.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# app/database.py
|
||||
"""
|
||||
Модуль для работы с базой данных PostgreSQL
|
||||
Соответствует требованиям ТЗ по разработке базы данных
|
||||
"""
|
||||
import os
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from dotenv import load_dotenv
|
||||
import time
|
||||
|
||||
load_dotenv()
|
||||
|
||||
class Database:
|
||||
def __init__(self):
|
||||
self.connection = None
|
||||
self.max_retries = 3
|
||||
self.retry_delay = 1
|
||||
|
||||
def get_connection(self):
|
||||
"""Получение подключения к базе данных с повторными попытками"""
|
||||
if self.connection is None or self.connection.closed:
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
self.connection = psycopg2.connect(
|
||||
os.getenv('DATABASE_URL'),
|
||||
cursor_factory=RealDictCursor
|
||||
)
|
||||
break
|
||||
except psycopg2.OperationalError as e:
|
||||
if attempt < self.max_retries - 1:
|
||||
time.sleep(self.retry_delay)
|
||||
continue
|
||||
else:
|
||||
raise e
|
||||
return self.connection
|
||||
|
||||
def execute_query(self, query, params=None):
|
||||
"""Выполнение SQL запроса с обработкой ошибок"""
|
||||
conn = self.get_connection()
|
||||
try:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(query, params)
|
||||
if query.strip().upper().startswith('SELECT'):
|
||||
return cursor.fetchall()
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
except psycopg2.InterfaceError:
|
||||
self.connection = None
|
||||
raise
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
raise e
|
||||
|
||||
def close(self):
|
||||
"""Закрытие соединения с базой данных"""
|
||||
if self.connection and not self.connection.closed:
|
||||
self.connection.close()
|
||||
|
||||
db = Database()
|
||||
48
ressult/app/main.py
Normal file
48
ressult/app/main.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# app/main.py
|
||||
"""
|
||||
Главный модуль FastAPI приложения
|
||||
Соответствует требованиям ТЗ по интеграции модулей
|
||||
"""
|
||||
import os
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from dotenv import load_dotenv
|
||||
from app.routes import partners, sales, upload, calculations, auth, config
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI(
|
||||
title="MasterPol Partner Management System",
|
||||
description="REST API для системы управления партнерами согласно ТЗ демонстрационного экзамена",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Регистрация маршрутов согласно модулям ТЗ
|
||||
app.include_router(partners.router, prefix="/api/v1/partners", tags=["Partners Management"])
|
||||
app.include_router(sales.router, prefix="/api/v1/sales", tags=["Sales History"])
|
||||
app.include_router(upload.router, prefix="/api/v1/upload", tags=["Data Import"])
|
||||
app.include_router(calculations.router, prefix="/api/v1/calculations", tags=["Calculations"])
|
||||
app.include_router(config.router, prefix="/api/v1/config", tags=["Configuration"])
|
||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["Authentication"])
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Корневой endpoint системы"""
|
||||
return {
|
||||
"message": "MasterPol Partner Management System API",
|
||||
"version": "1.0.0",
|
||||
"description": "Система управления партнерами согласно ТЗ демонстрационного экзамена"
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Проверка здоровья приложения"""
|
||||
return {"status": "healthy"}
|
||||
75
ressult/app/models/__init__.py
Normal file
75
ressult/app/models/__init__.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# app/models/__init__.py
|
||||
"""
|
||||
Модели данных Pydantic для валидации API запросов и ответов
|
||||
Соответствует ТЗ демонстрационного экзамена
|
||||
"""
|
||||
from pydantic import BaseModel, EmailStr, validator, conint
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
class PartnerBase(BaseModel):
|
||||
partner_type: Optional[str] = None
|
||||
company_name: str
|
||||
legal_address: Optional[str] = None
|
||||
inn: str
|
||||
director_name: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
email: Optional[EmailStr] = None
|
||||
rating: conint(ge=0) # Рейтинг должен быть целым неотрицательным числом
|
||||
sales_locations: Optional[str] = None
|
||||
|
||||
@validator('phone')
|
||||
def validate_phone(cls, v):
|
||||
if v and not v.startswith('+'):
|
||||
raise ValueError('Телефон должен начинаться с +')
|
||||
return v
|
||||
|
||||
class PartnerCreate(PartnerBase):
|
||||
pass
|
||||
|
||||
class PartnerUpdate(PartnerBase):
|
||||
pass
|
||||
|
||||
class Partner(PartnerBase):
|
||||
partner_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class SaleBase(BaseModel):
|
||||
partner_id: int
|
||||
product_name: str
|
||||
quantity: Decimal
|
||||
sale_date: str
|
||||
|
||||
class SaleCreate(SaleBase):
|
||||
pass
|
||||
|
||||
class Sale(SaleBase):
|
||||
sale_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class UploadResponse(BaseModel):
|
||||
message: str
|
||||
processed_rows: int
|
||||
errors: list[str] = []
|
||||
|
||||
class MaterialCalculationRequest(BaseModel):
|
||||
product_type_id: int
|
||||
material_type_id: int
|
||||
quantity: conint(ge=1)
|
||||
param1: float
|
||||
param2: float
|
||||
product_coeff: float
|
||||
defect_percent: float
|
||||
|
||||
class MaterialCalculationResponse(BaseModel):
|
||||
material_quantity: int
|
||||
status: str
|
||||
|
||||
class DiscountResponse(BaseModel):
|
||||
partner_id: int
|
||||
total_sales: Decimal
|
||||
discount_percent: int
|
||||
BIN
ressult/app/models/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
ressult/app/models/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
5
ressult/app/routes/__init__.py
Normal file
5
ressult/app/routes/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# app/routes/__init__.py
|
||||
"""
|
||||
Инициализация маршрутов API
|
||||
"""
|
||||
from . import partners, sales, upload, calculations, auth, config
|
||||
BIN
ressult/app/routes/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/auth.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/auth.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/calculations.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/calculations.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/config.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/partners.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/partners.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/sales.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/sales.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/app/routes/__pycache__/upload.cpython-314.pyc
Normal file
BIN
ressult/app/routes/__pycache__/upload.cpython-314.pyc
Normal file
Binary file not shown.
45
ressult/app/routes/auth.py
Normal file
45
ressult/app/routes/auth.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# app/routes/auth.py
|
||||
"""
|
||||
Маршруты API для аутентификации
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from app.database import db
|
||||
import bcrypt
|
||||
|
||||
router = APIRouter()
|
||||
security = HTTPBasic()
|
||||
|
||||
@router.post("/login")
|
||||
async def login(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
"""Аутентификация менеджера"""
|
||||
try:
|
||||
result = db.execute_query(
|
||||
"SELECT manager_id, username, password_hash, full_name FROM managers WHERE username = %s AND is_active = TRUE",
|
||||
(credentials.username,)
|
||||
)
|
||||
|
||||
if not result:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
manager = dict(result[0])
|
||||
stored_hash = manager['password_hash']
|
||||
|
||||
# Проверка пароля
|
||||
if bcrypt.checkpw(credentials.password.encode('utf-8'), stored_hash.encode('utf-8')):
|
||||
return {
|
||||
"manager_id": manager['manager_id'],
|
||||
"username": manager['username'],
|
||||
"full_name": manager['full_name'],
|
||||
"authenticated": True
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/verify")
|
||||
async def verify_token():
|
||||
"""Проверка валидности токена"""
|
||||
return {"verified": True}
|
||||
43
ressult/app/routes/calculations.py
Normal file
43
ressult/app/routes/calculations.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# app/routes/calculations.py
|
||||
"""
|
||||
Маршруты API для расчетов
|
||||
Соответствует модулю 4 ТЗ по расчету материалов
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.models import MaterialCalculationRequest, MaterialCalculationResponse
|
||||
import math
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/calculate-material", response_model=MaterialCalculationResponse)
|
||||
async def calculate_material(request: MaterialCalculationRequest):
|
||||
"""
|
||||
Расчет количества материала для производства продукции
|
||||
Соответствует модулю 4 ТЗ
|
||||
"""
|
||||
try:
|
||||
# Валидация входных параметров
|
||||
if (request.param1 <= 0 or request.param2 <= 0 or
|
||||
request.product_coeff <= 0 or request.defect_percent < 0):
|
||||
return MaterialCalculationResponse(
|
||||
material_quantity=-1,
|
||||
status="error: invalid parameters"
|
||||
)
|
||||
|
||||
# Расчет количества материала на одну единицу продукции
|
||||
material_per_unit = request.param1 * request.param2 * request.product_coeff
|
||||
|
||||
# Расчет общего количества материала с учетом брака
|
||||
total_material = material_per_unit * request.quantity
|
||||
total_material_with_defect = total_material * (1 + request.defect_percent / 100)
|
||||
|
||||
# Округление до целого числа в большую сторону
|
||||
material_quantity = math.ceil(total_material_with_defect)
|
||||
|
||||
return MaterialCalculationResponse(
|
||||
material_quantity=material_quantity,
|
||||
status="success"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
32
ressult/app/routes/config.py
Normal file
32
ressult/app/routes/config.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# app/routes/config.py
|
||||
"""
|
||||
Маршруты API для управления конфигурацией
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
CONFIG_PATH = Path(__file__).parent.parent.parent / "config.json"
|
||||
|
||||
@router.get("/")
|
||||
async def get_config():
|
||||
"""Получение текущей конфигурации"""
|
||||
try:
|
||||
if CONFIG_PATH.exists():
|
||||
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
return {"message": "Config file not found"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error reading config: {str(e)}")
|
||||
|
||||
@router.put("/")
|
||||
async def update_config(config_data: dict):
|
||||
"""Обновление конфигурации"""
|
||||
try:
|
||||
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(config_data, f, indent=4, ensure_ascii=False)
|
||||
return {"message": "Configuration updated successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error saving config: {str(e)}")
|
||||
157
ressult/app/routes/partners.py
Normal file
157
ressult/app/routes/partners.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# app/routes/partners.py
|
||||
"""
|
||||
Маршруты API для управления партнерами
|
||||
Соответствует модулям 1-3 ТЗ
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.database import db
|
||||
from app.models import Partner, PartnerCreate, PartnerUpdate, DiscountResponse
|
||||
from decimal import Decimal
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/")
|
||||
async def get_partners():
|
||||
"""
|
||||
Получение списка всех партнеров
|
||||
Соответствует требованию просмотра списка партнеров
|
||||
"""
|
||||
try:
|
||||
result = db.execute_query("""
|
||||
SELECT partner_id, partner_type, company_name, legal_address,
|
||||
inn, director_name, phone, email, rating, sales_locations
|
||||
FROM partners
|
||||
ORDER BY company_name
|
||||
""")
|
||||
|
||||
partners_list = []
|
||||
for row in result:
|
||||
partner_dict = dict(row)
|
||||
# Преобразуем рейтинг к int если нужно
|
||||
if isinstance(partner_dict.get('rating'), float):
|
||||
partner_dict['rating'] = int(partner_dict['rating'])
|
||||
partners_list.append(partner_dict)
|
||||
|
||||
return partners_list
|
||||
|
||||
except Exception as e:
|
||||
if "relation \"partners\" does not exist" in str(e):
|
||||
return []
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{partner_id}")
|
||||
async def get_partner(partner_id: int):
|
||||
"""Получение информации о конкретном партнере"""
|
||||
try:
|
||||
result = db.execute_query(
|
||||
"SELECT * FROM partners WHERE partner_id = %s",
|
||||
(partner_id,)
|
||||
)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Partner not found")
|
||||
|
||||
partner_data = dict(result[0])
|
||||
# Преобразуем рейтинг к int если нужно
|
||||
if isinstance(partner_data.get('rating'), float):
|
||||
partner_data['rating'] = int(partner_data['rating'])
|
||||
|
||||
return partner_data
|
||||
|
||||
except Exception as e:
|
||||
if "relation \"partners\" does not exist" in str(e):
|
||||
raise HTTPException(status_code=404, detail="Partner not found")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/")
|
||||
async def create_partner(partner: PartnerCreate):
|
||||
"""
|
||||
Создание нового партнера
|
||||
Включает валидацию данных согласно ТЗ
|
||||
"""
|
||||
try:
|
||||
result = db.execute_query("""
|
||||
INSERT INTO partners
|
||||
(partner_type, company_name, legal_address, inn, director_name,
|
||||
phone, email, rating, sales_locations)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING partner_id
|
||||
""", (
|
||||
partner.partner_type, partner.company_name, partner.legal_address,
|
||||
partner.inn, partner.director_name, partner.phone, partner.email,
|
||||
partner.rating, partner.sales_locations
|
||||
))
|
||||
return {"partner_id": result[0]["partner_id"]}
|
||||
except Exception as e:
|
||||
if "duplicate key value violates unique constraint" in str(e):
|
||||
raise HTTPException(status_code=400, detail="Partner with this INN already exists")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.put("/{partner_id}")
|
||||
async def update_partner(partner_id: int, partner: PartnerUpdate):
|
||||
"""
|
||||
Обновление данных партнера
|
||||
Соответствует требованию редактирования данных партнера
|
||||
"""
|
||||
try:
|
||||
db.execute_query("""
|
||||
UPDATE partners SET
|
||||
partner_type = %s, company_name = %s, legal_address = %s,
|
||||
inn = %s, director_name = %s, phone = %s, email = %s,
|
||||
rating = %s, sales_locations = %s
|
||||
WHERE partner_id = %s
|
||||
""", (
|
||||
partner.partner_type, partner.company_name, partner.legal_address,
|
||||
partner.inn, partner.director_name, partner.phone, partner.email,
|
||||
partner.rating, partner.sales_locations, partner_id
|
||||
))
|
||||
return {"message": "Partner updated successfully"}
|
||||
except Exception as e:
|
||||
if "duplicate key value violates unique constraint" in str(e):
|
||||
raise HTTPException(status_code=400, detail="Partner with this INN already exists")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.delete("/{partner_id}")
|
||||
async def delete_partner(partner_id: int):
|
||||
"""Удаление партнера"""
|
||||
try:
|
||||
db.execute_query(
|
||||
"DELETE FROM partners WHERE partner_id = %s",
|
||||
(partner_id,)
|
||||
)
|
||||
return {"message": "Partner deleted successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/{partner_id}/discount", response_model=DiscountResponse)
|
||||
async def calculate_partner_discount(partner_id: int):
|
||||
"""
|
||||
Расчет скидки для партнера на основе общего количества продаж
|
||||
Соответствует модулю 2 ТЗ
|
||||
"""
|
||||
try:
|
||||
# Получаем общее количество продаж партнера
|
||||
result = db.execute_query("""
|
||||
SELECT COALESCE(SUM(quantity), 0) as total_sales
|
||||
FROM sales WHERE partner_id = %s
|
||||
""", (partner_id,))
|
||||
|
||||
total_sales = result[0]["total_sales"] if result else Decimal('0')
|
||||
|
||||
# Расчет скидки согласно бизнес-правилам ТЗ
|
||||
if total_sales < 10000:
|
||||
discount = 0
|
||||
elif total_sales < 50000:
|
||||
discount = 5
|
||||
elif total_sales < 300000:
|
||||
discount = 10
|
||||
else:
|
||||
discount = 15
|
||||
|
||||
return DiscountResponse(
|
||||
partner_id=partner_id,
|
||||
total_sales=total_sales,
|
||||
discount_percent=discount
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
64
ressult/app/routes/sales.py
Normal file
64
ressult/app/routes/sales.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# app/routes/sales.py
|
||||
"""
|
||||
Маршруты API для управления продажами
|
||||
Соответствует требованиям ТЗ по истории реализации продукции
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.database import db
|
||||
from app.models import Sale, SaleCreate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/partner/{partner_id}")
|
||||
async def get_sales_by_partner(partner_id: int):
|
||||
"""
|
||||
Получение истории реализации продукции партнером
|
||||
Соответствует модулю 4 ТЗ
|
||||
"""
|
||||
try:
|
||||
result = db.execute_query("""
|
||||
SELECT sale_id, partner_id, product_name, quantity, sale_date
|
||||
FROM sales
|
||||
WHERE partner_id = %s
|
||||
ORDER BY sale_date DESC
|
||||
""", (partner_id,))
|
||||
return [dict(row) for row in result]
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/")
|
||||
async def get_all_sales():
|
||||
"""Получение всех продаж с информацией о партнерах"""
|
||||
try:
|
||||
result = db.execute_query("""
|
||||
SELECT s.sale_id, s.partner_id, p.company_name, s.product_name,
|
||||
s.quantity, s.sale_date
|
||||
FROM sales s
|
||||
JOIN partners p ON s.partner_id = p.partner_id
|
||||
ORDER BY s.sale_date DESC
|
||||
""")
|
||||
return [dict(row) for row in result]
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/")
|
||||
async def create_sale(sale: SaleCreate):
|
||||
"""Создание новой записи о продаже"""
|
||||
try:
|
||||
result = db.execute_query("""
|
||||
INSERT INTO sales (partner_id, product_name, quantity, sale_date)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING sale_id
|
||||
""", (sale.partner_id, sale.product_name, sale.quantity, sale.sale_date))
|
||||
return {"sale_id": result[0]["sale_id"]}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.delete("/{sale_id}")
|
||||
async def delete_sale(sale_id: int):
|
||||
"""Удаление записи о продаже"""
|
||||
try:
|
||||
db.execute_query("DELETE FROM sales WHERE sale_id = %s", (sale_id,))
|
||||
return {"message": "Sale deleted successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
103
ressult/app/routes/upload.py
Normal file
103
ressult/app/routes/upload.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# app/routes/upload.py
|
||||
"""
|
||||
Маршруты API для загрузки и импорта данных
|
||||
Соответствует требованиям ТЗ по импорту данных
|
||||
"""
|
||||
import pandas as pd
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException
|
||||
from app.database import db
|
||||
from app.models import UploadResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/partners")
|
||||
async def upload_partners(file: UploadFile = File(...)):
|
||||
"""
|
||||
Загрузка партнеров из файла
|
||||
Подготовка данных для импорта согласно ТЗ
|
||||
"""
|
||||
try:
|
||||
if file.filename.endswith('.xlsx'):
|
||||
df = pd.read_excel(file.file)
|
||||
elif file.filename.endswith('.csv'):
|
||||
df = pd.read_csv(file.file)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Unsupported file format")
|
||||
|
||||
processed = 0
|
||||
errors = []
|
||||
|
||||
for index, row in df.iterrows():
|
||||
try:
|
||||
# Валидация и преобразование данных
|
||||
rating = row.get('rating', 0)
|
||||
if pd.isna(rating):
|
||||
rating = 0
|
||||
|
||||
db.execute_query("""
|
||||
INSERT INTO partners
|
||||
(partner_type, company_name, legal_address, inn, director_name,
|
||||
phone, email, rating, sales_locations)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
row.get('partner_type'),
|
||||
row.get('company_name'),
|
||||
row.get('legal_address'),
|
||||
row.get('inn'),
|
||||
row.get('director_name'),
|
||||
row.get('phone'),
|
||||
row.get('email'),
|
||||
int(rating), # Конвертация в целое число
|
||||
row.get('sales_locations')
|
||||
))
|
||||
processed += 1
|
||||
except Exception as e:
|
||||
errors.append(f"Row {index}: {str(e)}")
|
||||
|
||||
return UploadResponse(
|
||||
message="File processed successfully",
|
||||
processed_rows=processed,
|
||||
errors=errors
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/sales")
|
||||
async def upload_sales(file: UploadFile = File(...)):
|
||||
"""Загрузка продаж из файла"""
|
||||
try:
|
||||
if file.filename.endswith('.xlsx'):
|
||||
df = pd.read_excel(file.file)
|
||||
elif file.filename.endswith('.csv'):
|
||||
df = pd.read_csv(file.file)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Unsupported file format")
|
||||
|
||||
processed = 0
|
||||
errors = []
|
||||
|
||||
for index, row in df.iterrows():
|
||||
try:
|
||||
db.execute_query("""
|
||||
INSERT INTO sales
|
||||
(partner_id, product_name, quantity, sale_date)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", (
|
||||
int(row.get('partner_id')),
|
||||
row.get('product_name'),
|
||||
row.get('quantity'),
|
||||
row.get('sale_date')
|
||||
))
|
||||
processed += 1
|
||||
except Exception as e:
|
||||
errors.append(f"Row {index}: {str(e)}")
|
||||
|
||||
return UploadResponse(
|
||||
message="File processed successfully",
|
||||
processed_rows=processed,
|
||||
errors=errors
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Loading…
Add table
Add a link
Reference in a new issue