After Graduate Update
This commit is contained in:
parent
b92a91ab37
commit
c6917dd85e
69 changed files with 7540 additions and 0 deletions
6
ressult/.env
Normal file
6
ressult/.env
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# .env
|
||||
DATABASE_URL=postgresql://postgres:213k2010###@localhost/masterpol
|
||||
SECRET_KEY=your-secret-key-here
|
||||
DEBUG=True
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
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))
|
||||
24
ressult/config.json
Normal file
24
ressult/config.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"application": {
|
||||
"name": "MasterPol Partner Management System",
|
||||
"version": "1.0.0",
|
||||
"company_logo": "resources/logo.png",
|
||||
"app_icon": "resources/icon.png"
|
||||
},
|
||||
"api": {
|
||||
"base_url": "http://localhost:8000",
|
||||
"timeout": 30
|
||||
},
|
||||
"style": {
|
||||
"primary_color": "#007acc",
|
||||
"secondary_color": "#005a9e",
|
||||
"accent_color": "#28a745",
|
||||
"font_family": "Arial",
|
||||
"font_size": "12px"
|
||||
},
|
||||
"features": {
|
||||
"enable_import": true,
|
||||
"enable_export": true,
|
||||
"enable_calculations": true
|
||||
}
|
||||
}
|
||||
196
ressult/database_init.py
Normal file
196
ressult/database_init.py
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# database_init.py
|
||||
"""
|
||||
Скрипт инициализации базы данных с исправлением ошибки типа данных
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
import os
|
||||
from app.database import db
|
||||
import bcrypt
|
||||
|
||||
def parse_arguments():
|
||||
"""Парсинг аргументов командной строки"""
|
||||
parser = argparse.ArgumentParser(description='Инициализация базы данных MasterPol')
|
||||
parser.add_argument('--host', default='localhost', help='Хост PostgreSQL')
|
||||
parser.add_argument('--port', default='5432', help='Порт PostgreSQL')
|
||||
parser.add_argument('--database', default='masterpol', help='Имя базы данных')
|
||||
parser.add_argument('--username', default='postgres', help='Имя пользователя PostgreSQL')
|
||||
parser.add_argument('--password', required=True, help='Пароль пользователя PostgreSQL')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def initialize_database(db_url):
|
||||
"""Инициализация структуры базы данных с тестовыми данными"""
|
||||
|
||||
# Устанавливаем URL базы данных
|
||||
os.environ['DATABASE_URL'] = db_url
|
||||
|
||||
# Удаляем существующие таблицы (для чистой инициализации)
|
||||
drop_tables = """
|
||||
DROP TABLE IF EXISTS sales CASCADE;
|
||||
DROP TABLE IF EXISTS partners CASCADE;
|
||||
DROP TABLE IF EXISTS managers CASCADE;
|
||||
"""
|
||||
|
||||
# Создание таблицы партнеров с правильным типом для rating
|
||||
partners_table = """
|
||||
CREATE TABLE IF NOT EXISTS partners (
|
||||
partner_id SERIAL PRIMARY KEY,
|
||||
partner_type VARCHAR(50),
|
||||
company_name VARCHAR(255) NOT NULL,
|
||||
legal_address TEXT,
|
||||
inn VARCHAR(20) UNIQUE NOT NULL,
|
||||
director_name VARCHAR(255),
|
||||
phone VARCHAR(50),
|
||||
email VARCHAR(255),
|
||||
rating INTEGER NOT NULL DEFAULT 0 CHECK (rating >= 0 AND rating <= 100),
|
||||
sales_locations TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
|
||||
# Создание таблицы продаж
|
||||
sales_table = """
|
||||
CREATE TABLE IF NOT EXISTS sales (
|
||||
sale_id SERIAL PRIMARY KEY,
|
||||
partner_id INTEGER NOT NULL REFERENCES partners(partner_id) ON DELETE CASCADE,
|
||||
product_name VARCHAR(255) NOT NULL,
|
||||
quantity DECIMAL(15,2) NOT NULL,
|
||||
sale_date DATE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
|
||||
# Создание таблицы менеджеров
|
||||
managers_table = """
|
||||
CREATE TABLE IF NOT EXISTS managers (
|
||||
manager_id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(100) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
|
||||
try:
|
||||
# Удаляем существующие таблицы
|
||||
try:
|
||||
db.execute_query(drop_tables)
|
||||
print("✅ Существующие таблицы удалены")
|
||||
except Exception as e:
|
||||
print(f"ℹ️ Таблицы для удаления не найдены: {e}")
|
||||
|
||||
# Создание таблиц
|
||||
db.execute_query(partners_table)
|
||||
db.execute_query(sales_table)
|
||||
db.execute_query(managers_table)
|
||||
print("✅ База данных успешно инициализирована")
|
||||
|
||||
# Создание тестового менеджера
|
||||
password = "pass123"
|
||||
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
db.execute_query("""
|
||||
INSERT INTO managers (username, password_hash, full_name)
|
||||
VALUES ('manager', %s, 'Тестовый Менеджер')
|
||||
ON CONFLICT (username) DO NOTHING
|
||||
""", (hashed_password,))
|
||||
print("✅ Тестовый пользователь создан (manager/pass123)")
|
||||
|
||||
# Добавление тестовых партнеров
|
||||
test_partners = [
|
||||
{
|
||||
'partner_type': 'distributor',
|
||||
'company_name': 'ООО "Ромашка"',
|
||||
'legal_address': 'г. Москва, ул. Ленина, д. 1',
|
||||
'inn': '1234567890',
|
||||
'director_name': 'Иванов Иван Иванович',
|
||||
'phone': '+79991234567',
|
||||
'email': 'info@romashka.ru',
|
||||
'rating': 85, # INTEGER значение от 0 до 100
|
||||
'sales_locations': 'Москва, Санкт-Петербург'
|
||||
},
|
||||
{
|
||||
'partner_type': 'retail',
|
||||
'company_name': 'ИП Петров',
|
||||
'legal_address': 'г. Санкт-Петербург, Невский пр., д. 100',
|
||||
'inn': '0987654321',
|
||||
'director_name': 'Петров Петр Петрович',
|
||||
'phone': '+79998765432',
|
||||
'email': 'petrov@mail.ru',
|
||||
'rating': 72, # INTEGER значение от 0 до 100
|
||||
'sales_locations': 'Санкт-Петербург'
|
||||
}
|
||||
]
|
||||
|
||||
for partner in test_partners:
|
||||
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)
|
||||
""", (
|
||||
partner['partner_type'], partner['company_name'],
|
||||
partner['legal_address'], partner['inn'],
|
||||
partner['director_name'], partner['phone'],
|
||||
partner['email'], partner['rating'],
|
||||
partner['sales_locations']
|
||||
))
|
||||
|
||||
print("✅ Тестовые партнеры добавлены")
|
||||
|
||||
# Добавление тестовых продаж
|
||||
test_sales = [
|
||||
(1, 'Продукт А', 150.50, '2024-01-15'),
|
||||
(1, 'Продукт Б', 75.25, '2024-01-16'),
|
||||
(2, 'Продукт В', 200.00, '2024-01-17'),
|
||||
(1, 'Продукт А', 100.00, '2024-01-18')
|
||||
]
|
||||
|
||||
for sale in test_sales:
|
||||
db.execute_query("""
|
||||
INSERT INTO sales (partner_id, product_name, quantity, sale_date)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
""", sale)
|
||||
|
||||
print("✅ Тестовые продажи добавлены")
|
||||
|
||||
# Проверяем, что данные корректно добавлены
|
||||
partners_count = db.execute_query("SELECT COUNT(*) as count FROM partners")[0]['count']
|
||||
sales_count = db.execute_query("SELECT COUNT(*) as count FROM sales")[0]['count']
|
||||
managers_count = db.execute_query("SELECT COUNT(*) as count FROM managers")[0]['count']
|
||||
|
||||
print(f"📊 Статистика базы данных:")
|
||||
print(f" - Партнеров: {partners_count}")
|
||||
print(f" - Продаж: {sales_count}")
|
||||
print(f" - Менеджеров: {managers_count}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка инициализации базы данных: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Основная функция"""
|
||||
args = parse_arguments()
|
||||
|
||||
# Формируем URL подключения
|
||||
db_url = f"postgresql://{args.username}:{args.password}@{args.host}:{args.port}/{args.database}"
|
||||
|
||||
print(f"🔄 Подключение к базе данных: {args.database} на {args.host}:{args.port}")
|
||||
|
||||
success = initialize_database(db_url)
|
||||
|
||||
if success:
|
||||
print("🎉 Инициализация базы данных завершена успешно!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("💥 Инициализация базы данных завершена с ошибками!")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
9
ressult/gui/__init__.py
Normal file
9
ressult/gui/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# gui/__init__.py
|
||||
"""
|
||||
Пакет графического интерфейса с авторизацией
|
||||
"""
|
||||
from .login_window import LoginWindow
|
||||
from .main_window import MainWindow
|
||||
from .partner_form import PartnerForm
|
||||
from .sales_history import SalesHistoryWindow
|
||||
from .material_calculator import MaterialCalculatorWindow
|
||||
BIN
ressult/gui/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/login_window.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/login_window.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/main_window.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/main_window.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/material_calculator.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/material_calculator.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/partner_form.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/partner_form.cpython-314.pyc
Normal file
Binary file not shown.
BIN
ressult/gui/__pycache__/sales_history.cpython-314.pyc
Normal file
BIN
ressult/gui/__pycache__/sales_history.cpython-314.pyc
Normal file
Binary file not shown.
253
ressult/gui/login_window.py
Normal file
253
ressult/gui/login_window.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
# gui/login_window.py
|
||||
"""
|
||||
Окно авторизации менеджера
|
||||
Соответствует требованиям ТЗ по аутентификации
|
||||
"""
|
||||
import sys
|
||||
from PyQt6.QtWidgets import (QApplication, QDialog, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QPushButton, QMessageBox,
|
||||
QFrame, QCheckBox)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QFont, QPixmap, QIcon
|
||||
import requests
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
class LoginWindow(QDialog):
|
||||
"""Окно авторизации системы MasterPol"""
|
||||
|
||||
login_success = pyqtSignal(dict) # Сигнал об успешной авторизации
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setup_ui()
|
||||
self.load_settings()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Настройка интерфейса окна авторизации"""
|
||||
self.setWindowTitle("MasterPol - Авторизация")
|
||||
self.setFixedSize(400, 500)
|
||||
self.setModal(True)
|
||||
|
||||
# Установка иконки приложения
|
||||
try:
|
||||
self.setWindowIcon(QIcon("resources/icon.png"))
|
||||
except:
|
||||
pass
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(30, 30, 30, 30)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# Заголовок
|
||||
title_label = QLabel("MasterPol")
|
||||
title_label.setFont(QFont("Arial", 24, QFont.Weight.Bold))
|
||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title_label.setStyleSheet("color: #007acc; margin-bottom: 20px;")
|
||||
|
||||
subtitle_label = QLabel("Система управления партнерами")
|
||||
subtitle_label.setFont(QFont("Arial", 12))
|
||||
subtitle_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
subtitle_label.setStyleSheet("color: #666; margin-bottom: 30px;")
|
||||
|
||||
layout.addWidget(title_label)
|
||||
layout.addWidget(subtitle_label)
|
||||
|
||||
# Форма авторизаци
|
||||
form_frame = QFrame()
|
||||
form_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: white;
|
||||
border: 0px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
""")
|
||||
|
||||
form_layout = QVBoxLayout()
|
||||
form_layout.setSpacing(15)
|
||||
|
||||
# Поле логина
|
||||
username_layout = QVBoxLayout()
|
||||
username_label = QLabel("Имя пользователя:")
|
||||
username_label.setStyleSheet("font-weight: bold; color: #333;")
|
||||
|
||||
self.username_input = QLineEdit()
|
||||
self.username_input.setPlaceholderText("Введите имя пользователя")
|
||||
self.username_input.setStyleSheet("""
|
||||
QLineEdit {
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border-color: #007acc;
|
||||
}
|
||||
""")
|
||||
|
||||
username_layout.addWidget(username_label)
|
||||
username_layout.addWidget(self.username_input)
|
||||
|
||||
# Поле пароля
|
||||
password_layout = QVBoxLayout()
|
||||
password_label = QLabel("Пароль:")
|
||||
password_label.setStyleSheet("font-weight: bold; color: #333;")
|
||||
|
||||
self.password_input = QLineEdit()
|
||||
self.password_input.setPlaceholderText("Введите пароль")
|
||||
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
self.password_input.setStyleSheet("""
|
||||
QLineEdit {
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border-color: #007acc;
|
||||
}
|
||||
""")
|
||||
|
||||
password_layout.addWidget(password_label)
|
||||
password_layout.addWidget(self.password_input)
|
||||
|
||||
# Запомнить меня
|
||||
self.remember_checkbox = QCheckBox("Запомнить меня")
|
||||
self.remember_checkbox.setStyleSheet("color: #333;")
|
||||
|
||||
# Кнопка входа
|
||||
self.login_button = QPushButton("Войти в систему")
|
||||
self.login_button.clicked.connect(self.authenticate)
|
||||
self.login_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #ccc;
|
||||
color: #666;
|
||||
}
|
||||
""")
|
||||
|
||||
# Подсказка
|
||||
hint_label = QLabel("Используйте логин: manager, пароль: pass123")
|
||||
hint_label.setStyleSheet("color: #666; font-size: 12px; margin-top: 10px;")
|
||||
hint_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
form_layout.addLayout(username_layout)
|
||||
form_layout.addLayout(password_layout)
|
||||
form_layout.addWidget(self.remember_checkbox)
|
||||
form_layout.addWidget(self.login_button)
|
||||
form_layout.addWidget(hint_label)
|
||||
|
||||
form_frame.setLayout(form_layout)
|
||||
layout.addWidget(form_frame)
|
||||
|
||||
# Информация о системе
|
||||
info_label = QLabel("MasterPol v1.0.0\nСистема управления партнерами и продажами")
|
||||
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
info_label.setStyleSheet("color: #999; font-size: 11px; margin-top: 20px;")
|
||||
layout.addWidget(info_label)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
# Подключаем обработчики событий
|
||||
self.username_input.returnPressed.connect(self.authenticate)
|
||||
self.password_input.returnPressed.connect(self.authenticate)
|
||||
|
||||
def load_settings(self):
|
||||
"""Загрузка сохраненных настроек авторизации"""
|
||||
try:
|
||||
# Здесь можно добавить загрузку из файла настроек
|
||||
# Пока просто устанавливаем значения по умолчанию
|
||||
self.username_input.setText("manager")
|
||||
except:
|
||||
pass
|
||||
|
||||
def save_settings(self):
|
||||
"""Сохранение настроек авторизации"""
|
||||
if self.remember_checkbox.isChecked():
|
||||
# Здесь можно добавить сохранение в файл настроек
|
||||
pass
|
||||
|
||||
def authenticate(self):
|
||||
"""Аутентификация пользователя"""
|
||||
username = self.username_input.text().strip()
|
||||
password = self.password_input.text().strip()
|
||||
|
||||
if not username or not password:
|
||||
QMessageBox.warning(self, "Ошибка", "Заполните все поля")
|
||||
return
|
||||
|
||||
# Блокируем кнопку во время аутентификации
|
||||
self.login_button.setEnabled(False)
|
||||
self.login_button.setText("Проверка...")
|
||||
|
||||
try:
|
||||
# Выполняем аутентификацию через API
|
||||
response = requests.post(
|
||||
"http://localhost:8000/api/v1/auth/login",
|
||||
auth=HTTPBasicAuth(username, password),
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
user_data = response.json()
|
||||
|
||||
# Сохраняем настройки
|
||||
self.save_settings()
|
||||
|
||||
# Сохраняем учетные данные для будущих запросов
|
||||
user_data['auth'] = HTTPBasicAuth(username, password)
|
||||
|
||||
# Отправляем сигнал об успешной авторизации
|
||||
self.login_success.emit(user_data)
|
||||
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"Ошибка авторизации",
|
||||
"Неверное имя пользователя или пароль"
|
||||
)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Ошибка подключения",
|
||||
"Не удалось подключиться к серверу.\n"
|
||||
"Убедитесь, что сервер запущен на localhost:8000"
|
||||
)
|
||||
except requests.exceptions.Timeout:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Ошибка подключения",
|
||||
"Превышено время ожидания ответа от сервера"
|
||||
)
|
||||
except Exception as e:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Ошибка",
|
||||
f"Произошла непредвиденная ошибка:\n{str(e)}"
|
||||
)
|
||||
finally:
|
||||
# Разблокируем кнопку
|
||||
self.login_button.setEnabled(True)
|
||||
self.login_button.setText("Войти в систему")
|
||||
|
||||
def main():
|
||||
"""Точка входа для тестирования окна авторизации"""
|
||||
app = QApplication(sys.argv)
|
||||
window = LoginWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
574
ressult/gui/main_window
Normal file
574
ressult/gui/main_window
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
# gui/main_window.py
|
||||
"""
|
||||
Главное окно приложения PyQt6 с поддержкой авторизации
|
||||
"""
|
||||
import sys
|
||||
import requests
|
||||
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QLabel, QPushButton, QListWidget,
|
||||
QListWidgetItem, QMessageBox, QFrame, QStackedWidget,
|
||||
QMenuBar, QMenu, QStatusBar, QToolBar)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction
|
||||
from .partner_form import PartnerForm
|
||||
from .sales_history import SalesHistoryWindow
|
||||
from .material_calculator import MaterialCalculatorWindow
|
||||
|
||||
class PartnerCard(QFrame):
|
||||
"""Карточка партнера для отображения в списке"""
|
||||
partner_clicked = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, partner_data):
|
||||
super().__init__()
|
||||
self.partner_data = partner_data
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
self.setStyleSheet("""
|
||||
PartnerCard {
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 4px;
|
||||
}
|
||||
PartnerCard:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #007acc;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# Заголовок с типом и названием
|
||||
header_layout = QHBoxLayout()
|
||||
header_layout.setSpacing(4)
|
||||
|
||||
type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |")
|
||||
type_label.setStyleSheet("color: #666; font-weight: bold;")
|
||||
|
||||
name_label = QLabel(self.partner_data['company_name'])
|
||||
name_label.setStyleSheet("font-weight: bold; font-size: 14px;")
|
||||
name_label.setWordWrap(True)
|
||||
|
||||
# Безопасное преобразование рейтинга
|
||||
rating_value = self.partner_data.get('rating', 0)
|
||||
if isinstance(rating_value, float):
|
||||
rating_value = int(rating_value)
|
||||
|
||||
rating_label = QLabel(f"{rating_value}%")
|
||||
rating_label.setStyleSheet("color: #007acc; font-weight: bold;")
|
||||
|
||||
header_layout.addWidget(type_label)
|
||||
header_layout.addWidget(name_label)
|
||||
header_layout.addStretch()
|
||||
header_layout.addWidget(rating_label)
|
||||
|
||||
# Информация о директоре
|
||||
director_label = QLabel(self.partner_data.get('director_name', 'Директор не указан'))
|
||||
director_label.setStyleSheet("color: #444;")
|
||||
|
||||
# Контактная информация
|
||||
phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан'))
|
||||
phone_label.setStyleSheet("color: #666;")
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
layout.addWidget(director_label)
|
||||
layout.addWidget(phone_label)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Обработка клика на карточке"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.partner_clicked.emit(self.partner_data)
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Главное окно приложения с поддержкой авторизации"""
|
||||
|
||||
def __init__(self, user_data):
|
||||
super().__init__()
|
||||
self.user_data = user_data
|
||||
self.current_partner = None
|
||||
self.auth = user_data.get('auth')
|
||||
self.setup_ui()
|
||||
self.load_partners()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Настройка интерфейса главного окна"""
|
||||
self.setWindowTitle(f"MasterPol - Система управления партнерами")
|
||||
self.setGeometry(100, 100, 1200, 700)
|
||||
|
||||
# Установка иконки приложения
|
||||
try:
|
||||
self.setWindowIcon(QIcon("resources/icon.png"))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Создание меню
|
||||
self.create_menu()
|
||||
|
||||
# Создание тулбара
|
||||
self.create_toolbar()
|
||||
|
||||
# Создание статусной строки
|
||||
self.create_statusbar()
|
||||
|
||||
# Центральный виджет
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
main_layout = QHBoxLayout()
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Левая панель - список партнеров
|
||||
left_panel = self.create_partners_panel()
|
||||
main_layout.addWidget(left_panel, 1)
|
||||
|
||||
# Правая панель - детальная информация
|
||||
self.right_panel = self.create_details_panel()
|
||||
main_layout.addWidget(self.right_panel, 2)
|
||||
|
||||
central_widget.setLayout(main_layout)
|
||||
|
||||
def create_menu(self):
|
||||
"""Создание меню приложения"""
|
||||
menubar = self.menuBar()
|
||||
|
||||
# Меню Файл
|
||||
file_menu = menubar.addMenu('Файл')
|
||||
|
||||
refresh_action = QAction('Обновить', self)
|
||||
refresh_action.setShortcut('F5')
|
||||
refresh_action.triggered.connect(self.load_partners)
|
||||
file_menu.addAction(refresh_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
logout_action = QAction('Выход', self)
|
||||
logout_action.setShortcut('Ctrl+Q')
|
||||
logout_action.triggered.connect(self.logout)
|
||||
file_menu.addAction(logout_action)
|
||||
|
||||
# Меню Сервис
|
||||
service_menu = menubar.addMenu('Сервис')
|
||||
|
||||
calc_action = QAction('Калькулятор материалов', self)
|
||||
calc_action.triggered.connect(self.show_material_calculator)
|
||||
service_menu.addAction(calc_action)
|
||||
|
||||
# Меню Справка
|
||||
help_menu = menubar.addMenu('Справка')
|
||||
|
||||
about_action = QAction('О программе', self)
|
||||
about_action.triggered.connect(self.show_about)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
def create_toolbar(self):
|
||||
"""Создание панели инструментов"""
|
||||
toolbar = QToolBar("Основные инструменты")
|
||||
self.addToolBar(toolbar)
|
||||
|
||||
refresh_action = QAction('Обновить', self)
|
||||
refresh_action.triggered.connect(self.load_partners)
|
||||
toolbar.addAction(refresh_action)
|
||||
|
||||
toolbar.addSeparator()
|
||||
|
||||
add_partner_action = QAction('Добавить партнера', self)
|
||||
add_partner_action.triggered.connect(self.show_add_partner_form)
|
||||
toolbar.addAction(add_partner_action)
|
||||
|
||||
def create_statusbar(self):
|
||||
"""Создание статусной строки"""
|
||||
statusbar = self.statusBar()
|
||||
user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}"
|
||||
statusbar.showMessage(user_info)
|
||||
|
||||
def create_partners_panel(self):
|
||||
"""Создание панели списка партнеров"""
|
||||
panel = QWidget()
|
||||
panel.setMaximumWidth(400)
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Заголовок
|
||||
title = QLabel("Партнеры")
|
||||
title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title.setStyleSheet("padding: 10px;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Панель управления
|
||||
control_layout = QHBoxLayout()
|
||||
control_layout.setSpacing(10)
|
||||
|
||||
self.add_button = QPushButton("Добавить партнера")
|
||||
self.add_button.clicked.connect(self.show_add_partner_form)
|
||||
self.add_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
""")
|
||||
|
||||
self.refresh_button = QPushButton("Обновить")
|
||||
self.refresh_button.clicked.connect(self.load_partners)
|
||||
self.refresh_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
""")
|
||||
|
||||
control_layout.addWidget(self.add_button)
|
||||
control_layout.addWidget(self.refresh_button)
|
||||
control_layout.addStretch()
|
||||
|
||||
layout.addLayout(control_layout)
|
||||
|
||||
# Список партнеров
|
||||
self.partners_list = QListWidget()
|
||||
self.partners_list.setStyleSheet("""
|
||||
QListWidget {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
outline: none;
|
||||
}
|
||||
QListWidget::item {
|
||||
border: none;
|
||||
padding: 0px;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.partners_list)
|
||||
|
||||
# Кнопка расчета материалов
|
||||
self.calc_button = QPushButton("Калькулятор материалов")
|
||||
self.calc_button.clicked.connect(self.show_material_calculator)
|
||||
self.calc_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.calc_button)
|
||||
|
||||
panel.setLayout(layout)
|
||||
return panel
|
||||
|
||||
def create_details_panel(self):
|
||||
"""Создание панели детальной информации"""
|
||||
panel = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Заголовок детальной информации
|
||||
self.details_title = QLabel("Выберите партнера")
|
||||
self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold))
|
||||
self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.details_title.setStyleSheet("padding: 10px;")
|
||||
layout.addWidget(self.details_title)
|
||||
|
||||
# Детальная информация о партнере - создаем пустой frame
|
||||
self.details_frame = QFrame()
|
||||
self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
self.details_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
self.details_layout = QVBoxLayout()
|
||||
self.details_layout.setSpacing(8)
|
||||
self.details_frame.setLayout(self.details_layout)
|
||||
self.details_frame.hide()
|
||||
|
||||
layout.addWidget(self.details_frame)
|
||||
|
||||
# Кнопки управления выбранным партнером
|
||||
self.control_buttons = QWidget()
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout.setSpacing(10)
|
||||
|
||||
self.edit_button = QPushButton("Редактировать")
|
||||
self.edit_button.clicked.connect(self.edit_partner)
|
||||
self.edit_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
""")
|
||||
self.edit_button.hide()
|
||||
|
||||
self.sales_button = QPushButton("История продаж")
|
||||
self.sales_button.clicked.connect(self.show_sales_history)
|
||||
self.sales_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
""")
|
||||
self.sales_button.hide()
|
||||
|
||||
self.discount_button = QPushButton("Расчет скидки")
|
||||
self.discount_button.clicked.connect(self.calculate_discount)
|
||||
self.discount_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #ffc107;
|
||||
color: black;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
""")
|
||||
self.discount_button.hide()
|
||||
|
||||
buttons_layout.addWidget(self.edit_button)
|
||||
buttons_layout.addWidget(self.sales_button)
|
||||
buttons_layout.addWidget(self.discount_button)
|
||||
buttons_layout.addStretch()
|
||||
|
||||
self.control_buttons.setLayout(buttons_layout)
|
||||
layout.addWidget(self.control_buttons)
|
||||
|
||||
# Добавляем растягивающийся элемент в конец
|
||||
layout.addStretch()
|
||||
|
||||
panel.setLayout(layout)
|
||||
return panel
|
||||
|
||||
def load_partners(self):
|
||||
"""Загрузка списка партнеров из API с авторизацией"""
|
||||
try:
|
||||
response = requests.get(
|
||||
"http://localhost:8000/api/v1/partners",
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.partners_list.clear()
|
||||
partners = response.json()
|
||||
|
||||
for partner in partners:
|
||||
item = QListWidgetItem()
|
||||
card = PartnerCard(partner)
|
||||
card.partner_clicked.connect(self.show_partner_details)
|
||||
|
||||
# Устанавливаем фиксированный размер для элемента
|
||||
item.setSizeHint(card.sizeHint())
|
||||
self.partners_list.addItem(item)
|
||||
self.partners_list.setItemWidget(item, card)
|
||||
|
||||
# Сбрасываем выделение
|
||||
self.partners_list.clearSelection()
|
||||
self.current_partner = None
|
||||
self.details_title.setText("Выберите партнера")
|
||||
self.details_frame.hide()
|
||||
self.edit_button.hide()
|
||||
self.sales_button.hide()
|
||||
self.discount_button.hide()
|
||||
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
|
||||
self.logout()
|
||||
else:
|
||||
QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}")
|
||||
|
||||
def show_partner_details(self, partner_data):
|
||||
"""Отображение детальной информации о партнере"""
|
||||
self.current_partner = partner_data
|
||||
self.details_title.setText(partner_data['company_name'])
|
||||
|
||||
# Создаем новый виджет для деталей вместо очистки layout
|
||||
new_details_frame = QFrame()
|
||||
new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
new_details_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
new_details_layout = QVBoxLayout()
|
||||
new_details_layout.setSpacing(8)
|
||||
|
||||
# Добавляем новую информацию
|
||||
details = [
|
||||
("Тип:", partner_data.get('partner_type', 'Не указан')),
|
||||
("ИНН:", partner_data.get('inn', 'Не указан')),
|
||||
("Директор:", partner_data.get('director_name', 'Не указан')),
|
||||
("Телефон:", partner_data.get('phone', 'Не указан')),
|
||||
("Email:", partner_data.get('email', 'Не указан')),
|
||||
("Рейтинг:", str(partner_data.get('rating', 0))),
|
||||
("Адрес:", partner_data.get('legal_address', 'Не указан')),
|
||||
("Регионы:", partner_data.get('sales_locations', 'Не указан'))
|
||||
]
|
||||
|
||||
for label, value in details:
|
||||
row_widget = QWidget()
|
||||
row_layout = QHBoxLayout(row_widget)
|
||||
row_layout.setContentsMargins(0, 2, 0, 2)
|
||||
|
||||
label_widget = QLabel(label)
|
||||
label_widget.setStyleSheet("font-weight: bold; min-width: 100px;")
|
||||
label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
value_widget = QLabel(str(value))
|
||||
value_widget.setWordWrap(True)
|
||||
value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
row_layout.addWidget(label_widget)
|
||||
row_layout.addWidget(value_widget)
|
||||
row_layout.addStretch()
|
||||
|
||||
new_details_layout.addWidget(row_widget)
|
||||
|
||||
new_details_frame.setLayout(new_details_layout)
|
||||
|
||||
# Заменяем старый details_frame на новый
|
||||
old_frame = self.details_frame
|
||||
layout = self.right_panel.layout()
|
||||
layout.replaceWidget(old_frame, new_details_frame)
|
||||
old_frame.deleteLater()
|
||||
|
||||
self.details_frame = new_details_frame
|
||||
self.details_layout = new_details_layout
|
||||
|
||||
self.details_frame.show()
|
||||
self.edit_button.show()
|
||||
self.sales_button.show()
|
||||
self.discount_button.show()
|
||||
|
||||
def show_add_partner_form(self):
|
||||
"""Открытие формы добавления партнера"""
|
||||
form = PartnerForm(self, auth=self.auth)
|
||||
form.partner_saved.connect(self.load_partners)
|
||||
form.exec()
|
||||
|
||||
def edit_partner(self):
|
||||
"""Редактирование выбранного партнера"""
|
||||
if self.current_partner:
|
||||
form = PartnerForm(self, self.current_partner, auth=self.auth)
|
||||
form.partner_saved.connect(self.load_partners)
|
||||
form.exec()
|
||||
|
||||
def show_sales_history(self):
|
||||
"""Открытие истории продаж партнера"""
|
||||
if self.current_partner:
|
||||
sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth)
|
||||
sales_window.exec()
|
||||
|
||||
def calculate_discount(self):
|
||||
"""Расчет скидки для партнера с авторизацией"""
|
||||
if self.current_partner:
|
||||
try:
|
||||
response = requests.get(
|
||||
f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount",
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
discount_data = response.json()
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Расчет скидки",
|
||||
f"Партнер: {self.current_partner['company_name']}\n"
|
||||
f"Общие продажи: {discount_data['total_sales']}\n"
|
||||
f"Скидка: {discount_data['discount_percent']}%"
|
||||
)
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
|
||||
self.logout()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}")
|
||||
|
||||
def show_material_calculator(self):
|
||||
"""Открытие калькулятора материалов"""
|
||||
calculator = MaterialCalculatorWindow(self, auth=self.auth)
|
||||
calculator.exec()
|
||||
|
||||
def logout(self):
|
||||
"""Выход из системы"""
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Подтверждение выхода",
|
||||
"Вы уверены, что хотите выйти из системы?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.close()
|
||||
# Здесь можно добавить вызов окна авторизации
|
||||
# или перезапуск приложения
|
||||
|
||||
def show_about(self):
|
||||
"""Показать информацию о программе"""
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"О программе MasterPol",
|
||||
"MasterPol - Система управления партнерами\n\n"
|
||||
"Версия: 1.0.0\n"
|
||||
"Разработчик: Команда MasterPol\n\n"
|
||||
"Система предназначена для управления партнерами,\n"
|
||||
"учета продаж и расчета бизнес-показателей."
|
||||
)
|
||||
574
ressult/gui/main_window.py
Normal file
574
ressult/gui/main_window.py
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
# gui/main_window.py
|
||||
"""
|
||||
Главное окно приложения PyQt6 с поддержкой авторизации
|
||||
"""
|
||||
import sys
|
||||
import requests
|
||||
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QLabel, QPushButton, QListWidget,
|
||||
QListWidgetItem, QMessageBox, QFrame, QStackedWidget,
|
||||
QMenuBar, QMenu, QStatusBar, QToolBar)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction
|
||||
from .partner_form import PartnerForm
|
||||
from .sales_history import SalesHistoryWindow
|
||||
from .material_calculator import MaterialCalculatorWindow
|
||||
|
||||
class PartnerCard(QFrame):
|
||||
"""Карточка партнера для отображения в списке"""
|
||||
partner_clicked = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, partner_data):
|
||||
super().__init__()
|
||||
self.partner_data = partner_data
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
self.setStyleSheet("""
|
||||
PartnerCard {
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 4px;
|
||||
}
|
||||
PartnerCard:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #007acc;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# Заголовок с типом и названием
|
||||
header_layout = QHBoxLayout()
|
||||
header_layout.setSpacing(4)
|
||||
|
||||
type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |")
|
||||
type_label.setStyleSheet("color: #666; font-weight: bold;")
|
||||
|
||||
name_label = QLabel(self.partner_data['company_name'])
|
||||
name_label.setStyleSheet("font-weight: bold; font-size: 14px;")
|
||||
name_label.setWordWrap(True)
|
||||
|
||||
# Безопасное преобразование рейтинга
|
||||
rating_value = self.partner_data.get('rating', 0)
|
||||
if isinstance(rating_value, float):
|
||||
rating_value = int(rating_value)
|
||||
|
||||
rating_label = QLabel(f"{rating_value}%")
|
||||
rating_label.setStyleSheet("color: #007acc; font-weight: bold;")
|
||||
|
||||
header_layout.addWidget(type_label)
|
||||
header_layout.addWidget(name_label)
|
||||
header_layout.addStretch()
|
||||
header_layout.addWidget(rating_label)
|
||||
|
||||
# Информация о директоре
|
||||
director_label = QLabel(self.partner_data.get('director_name', 'Директор не указан'))
|
||||
director_label.setStyleSheet("color: #444;")
|
||||
|
||||
# Контактная информация
|
||||
phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан'))
|
||||
phone_label.setStyleSheet("color: #666;")
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
layout.addWidget(director_label)
|
||||
layout.addWidget(phone_label)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Обработка клика на карточке"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.partner_clicked.emit(self.partner_data)
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Главное окно приложения с поддержкой авторизации"""
|
||||
|
||||
def __init__(self, user_data):
|
||||
super().__init__()
|
||||
self.user_data = user_data
|
||||
self.current_partner = None
|
||||
self.auth = user_data.get('auth')
|
||||
self.setup_ui()
|
||||
self.load_partners()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Настройка интерфейса главного окна"""
|
||||
self.setWindowTitle(f"MasterPol - Система управления партнерами")
|
||||
self.setGeometry(100, 100, 1200, 700)
|
||||
|
||||
# Установка иконки приложения
|
||||
try:
|
||||
self.setWindowIcon(QIcon("resources/icon.png"))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Создание меню
|
||||
self.create_menu()
|
||||
|
||||
# Создание тулбара
|
||||
self.create_toolbar()
|
||||
|
||||
# Создание статусной строки
|
||||
self.create_statusbar()
|
||||
|
||||
# Центральный виджет
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
main_layout = QHBoxLayout()
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Левая панель - список партнеров
|
||||
left_panel = self.create_partners_panel()
|
||||
main_layout.addWidget(left_panel, 1)
|
||||
|
||||
# Правая панель - детальная информация
|
||||
self.right_panel = self.create_details_panel()
|
||||
main_layout.addWidget(self.right_panel, 2)
|
||||
|
||||
central_widget.setLayout(main_layout)
|
||||
|
||||
def create_menu(self):
|
||||
"""Создание меню приложения"""
|
||||
menubar = self.menuBar()
|
||||
|
||||
# Меню Файл
|
||||
file_menu = menubar.addMenu('Файл')
|
||||
|
||||
refresh_action = QAction('Обновить', self)
|
||||
refresh_action.setShortcut('F5')
|
||||
refresh_action.triggered.connect(self.load_partners)
|
||||
file_menu.addAction(refresh_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
logout_action = QAction('Выход', self)
|
||||
logout_action.setShortcut('Ctrl+Q')
|
||||
logout_action.triggered.connect(self.logout)
|
||||
file_menu.addAction(logout_action)
|
||||
|
||||
# Меню Сервис
|
||||
service_menu = menubar.addMenu('Сервис')
|
||||
|
||||
calc_action = QAction('Калькулятор материалов', self)
|
||||
calc_action.triggered.connect(self.show_material_calculator)
|
||||
service_menu.addAction(calc_action)
|
||||
|
||||
# Меню Справка
|
||||
help_menu = menubar.addMenu('Справка')
|
||||
|
||||
about_action = QAction('О программе', self)
|
||||
about_action.triggered.connect(self.show_about)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
def create_toolbar(self):
|
||||
"""Создание панели инструментов"""
|
||||
toolbar = QToolBar("Основные инструменты")
|
||||
self.addToolBar(toolbar)
|
||||
|
||||
refresh_action = QAction('Обновить', self)
|
||||
refresh_action.triggered.connect(self.load_partners)
|
||||
toolbar.addAction(refresh_action)
|
||||
|
||||
toolbar.addSeparator()
|
||||
|
||||
add_partner_action = QAction('Добавить партнера', self)
|
||||
add_partner_action.triggered.connect(self.show_add_partner_form)
|
||||
toolbar.addAction(add_partner_action)
|
||||
|
||||
def create_statusbar(self):
|
||||
"""Создание статусной строки"""
|
||||
statusbar = self.statusBar()
|
||||
user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}"
|
||||
statusbar.showMessage(user_info)
|
||||
|
||||
def create_partners_panel(self):
|
||||
"""Создание панели списка партнеров"""
|
||||
panel = QWidget()
|
||||
panel.setMaximumWidth(400)
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Заголовок
|
||||
title = QLabel("Партнеры")
|
||||
title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title.setStyleSheet("padding: 10px;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Панель управления
|
||||
control_layout = QHBoxLayout()
|
||||
control_layout.setSpacing(10)
|
||||
|
||||
self.add_button = QPushButton("Добавить партнера")
|
||||
self.add_button.clicked.connect(self.show_add_partner_form)
|
||||
self.add_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
""")
|
||||
|
||||
self.refresh_button = QPushButton("Обновить")
|
||||
self.refresh_button.clicked.connect(self.load_partners)
|
||||
self.refresh_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
""")
|
||||
|
||||
control_layout.addWidget(self.add_button)
|
||||
control_layout.addWidget(self.refresh_button)
|
||||
control_layout.addStretch()
|
||||
|
||||
layout.addLayout(control_layout)
|
||||
|
||||
# Список партнеров
|
||||
self.partners_list = QListWidget()
|
||||
self.partners_list.setStyleSheet("""
|
||||
QListWidget {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
outline: none;
|
||||
}
|
||||
QListWidget::item {
|
||||
border: none;
|
||||
padding: 0px;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.partners_list)
|
||||
|
||||
# Кнопка расчета материалов
|
||||
self.calc_button = QPushButton("Калькулятор материалов")
|
||||
self.calc_button.clicked.connect(self.show_material_calculator)
|
||||
self.calc_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.calc_button)
|
||||
|
||||
panel.setLayout(layout)
|
||||
return panel
|
||||
|
||||
def create_details_panel(self):
|
||||
"""Создание панели детальной информации"""
|
||||
panel = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Заголовок детальной информации
|
||||
self.details_title = QLabel("Выберите партнера")
|
||||
self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold))
|
||||
self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.details_title.setStyleSheet("padding: 10px;")
|
||||
layout.addWidget(self.details_title)
|
||||
|
||||
# Детальная информация о партнере - создаем пустой frame
|
||||
self.details_frame = QFrame()
|
||||
self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
self.details_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
self.details_layout = QVBoxLayout()
|
||||
self.details_layout.setSpacing(8)
|
||||
self.details_frame.setLayout(self.details_layout)
|
||||
self.details_frame.hide()
|
||||
|
||||
layout.addWidget(self.details_frame)
|
||||
|
||||
# Кнопки управления выбранным партнером
|
||||
self.control_buttons = QWidget()
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout.setSpacing(10)
|
||||
|
||||
self.edit_button = QPushButton("Редактировать")
|
||||
self.edit_button.clicked.connect(self.edit_partner)
|
||||
self.edit_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
""")
|
||||
self.edit_button.hide()
|
||||
|
||||
self.sales_button = QPushButton("История продаж")
|
||||
self.sales_button.clicked.connect(self.show_sales_history)
|
||||
self.sales_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
""")
|
||||
self.sales_button.hide()
|
||||
|
||||
self.discount_button = QPushButton("Расчет скидки")
|
||||
self.discount_button.clicked.connect(self.calculate_discount)
|
||||
self.discount_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #ffc107;
|
||||
color: black;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
""")
|
||||
self.discount_button.hide()
|
||||
|
||||
buttons_layout.addWidget(self.edit_button)
|
||||
buttons_layout.addWidget(self.sales_button)
|
||||
buttons_layout.addWidget(self.discount_button)
|
||||
buttons_layout.addStretch()
|
||||
|
||||
self.control_buttons.setLayout(buttons_layout)
|
||||
layout.addWidget(self.control_buttons)
|
||||
|
||||
# Добавляем растягивающийся элемент в конец
|
||||
layout.addStretch()
|
||||
|
||||
panel.setLayout(layout)
|
||||
return panel
|
||||
|
||||
def load_partners(self):
|
||||
"""Загрузка списка партнеров из API с авторизацией"""
|
||||
try:
|
||||
response = requests.get(
|
||||
"http://localhost:8000/api/v1/partners",
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.partners_list.clear()
|
||||
partners = response.json()
|
||||
|
||||
for partner in partners:
|
||||
item = QListWidgetItem()
|
||||
card = PartnerCard(partner)
|
||||
card.partner_clicked.connect(self.show_partner_details)
|
||||
|
||||
# Устанавливаем фиксированный размер для элемента
|
||||
item.setSizeHint(card.sizeHint())
|
||||
self.partners_list.addItem(item)
|
||||
self.partners_list.setItemWidget(item, card)
|
||||
|
||||
# Сбрасываем выделение
|
||||
self.partners_list.clearSelection()
|
||||
self.current_partner = None
|
||||
self.details_title.setText("Выберите партнера")
|
||||
self.details_frame.hide()
|
||||
self.edit_button.hide()
|
||||
self.sales_button.hide()
|
||||
self.discount_button.hide()
|
||||
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
|
||||
self.logout()
|
||||
else:
|
||||
QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}")
|
||||
|
||||
def show_partner_details(self, partner_data):
|
||||
"""Отображение детальной информации о партнере"""
|
||||
self.current_partner = partner_data
|
||||
self.details_title.setText(partner_data['company_name'])
|
||||
|
||||
# Создаем новый виджет для деталей вместо очистки layout
|
||||
new_details_frame = QFrame()
|
||||
new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
new_details_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
new_details_layout = QVBoxLayout()
|
||||
new_details_layout.setSpacing(8)
|
||||
|
||||
# Добавляем новую информацию
|
||||
details = [
|
||||
("Тип:", partner_data.get('partner_type', 'Не указан')),
|
||||
("ИНН:", partner_data.get('inn', 'Не указан')),
|
||||
("Директор:", partner_data.get('director_name', 'Не указан')),
|
||||
("Телефон:", partner_data.get('phone', 'Не указан')),
|
||||
("Email:", partner_data.get('email', 'Не указан')),
|
||||
("Рейтинг:", str(partner_data.get('rating', 0))),
|
||||
("Адрес:", partner_data.get('legal_address', 'Не указан')),
|
||||
("Регионы:", partner_data.get('sales_locations', 'Не указан'))
|
||||
]
|
||||
|
||||
for label, value in details:
|
||||
row_widget = QWidget()
|
||||
row_layout = QHBoxLayout(row_widget)
|
||||
row_layout.setContentsMargins(0, 2, 0, 2)
|
||||
|
||||
label_widget = QLabel(label)
|
||||
label_widget.setStyleSheet("font-weight: bold; min-width: 100px;")
|
||||
label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
value_widget = QLabel(str(value))
|
||||
value_widget.setWordWrap(True)
|
||||
value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
row_layout.addWidget(label_widget)
|
||||
row_layout.addWidget(value_widget)
|
||||
row_layout.addStretch()
|
||||
|
||||
new_details_layout.addWidget(row_widget)
|
||||
|
||||
new_details_frame.setLayout(new_details_layout)
|
||||
|
||||
# Заменяем старый details_frame на новый
|
||||
old_frame = self.details_frame
|
||||
layout = self.right_panel.layout()
|
||||
layout.replaceWidget(old_frame, new_details_frame)
|
||||
old_frame.deleteLater()
|
||||
|
||||
self.details_frame = new_details_frame
|
||||
self.details_layout = new_details_layout
|
||||
|
||||
self.details_frame.show()
|
||||
self.edit_button.show()
|
||||
self.sales_button.show()
|
||||
self.discount_button.show()
|
||||
|
||||
def show_add_partner_form(self):
|
||||
"""Открытие формы добавления партнера"""
|
||||
form = PartnerForm(self, auth=self.auth)
|
||||
form.partner_saved.connect(self.load_partners)
|
||||
form.exec()
|
||||
|
||||
def edit_partner(self):
|
||||
"""Редактирование выбранного партнера"""
|
||||
if self.current_partner:
|
||||
form = PartnerForm(self, self.current_partner, auth=self.auth)
|
||||
form.partner_saved.connect(self.load_partners)
|
||||
form.exec()
|
||||
|
||||
def show_sales_history(self):
|
||||
"""Открытие истории продаж партнера"""
|
||||
if self.current_partner:
|
||||
sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth)
|
||||
sales_window.exec()
|
||||
|
||||
def calculate_discount(self):
|
||||
"""Расчет скидки для партнера с авторизацией"""
|
||||
if self.current_partner:
|
||||
try:
|
||||
response = requests.get(
|
||||
f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount",
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
discount_data = response.json()
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Расчет скидки",
|
||||
f"Партнер: {self.current_partner['company_name']}\n"
|
||||
f"Общие продажи: {discount_data['total_sales']}\n"
|
||||
f"Скидка: {discount_data['discount_percent']}%"
|
||||
)
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
|
||||
self.logout()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}")
|
||||
|
||||
def show_material_calculator(self):
|
||||
"""Открытие калькулятора материалов"""
|
||||
calculator = MaterialCalculatorWindow(self, auth=self.auth)
|
||||
calculator.exec()
|
||||
|
||||
def logout(self):
|
||||
"""Выход из системы"""
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Подтверждение выхода",
|
||||
"Вы уверены, что хотите выйти из системы?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.close()
|
||||
# Здесь можно добавить вызов окна авторизации
|
||||
# или перезапуск приложения
|
||||
|
||||
def show_about(self):
|
||||
"""Показать информацию о программе"""
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"О программе MasterPol",
|
||||
"MasterPol - Система управления партнерами\n\n"
|
||||
"Версия: 1.0.0\n"
|
||||
"Разработчик: Команда MasterPol\n\n"
|
||||
"Система предназначена для управления партнерами,\n"
|
||||
"учета продаж и расчета бизнес-показателей."
|
||||
)
|
||||
616
ressult/gui/main_window.py.bak
Normal file
616
ressult/gui/main_window.py.bak
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
# gui/main_wind/w.py
|
||||
"""
|
||||
Главное окно приложения PyQt6 с поддержкой авторизации
|
||||
"""
|
||||
import sys
|
||||
import requests
|
||||
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QLabel, QPushButton, QListWidget,
|
||||
QListWidgetItem, QMessageBox, QFrame, QStackedWidget,
|
||||
QMenuBar, QMenu, QStatusBar, QToolBar)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtGui import QFont, QPixmap, QIcon, QAction
|
||||
from .partner_form import PartnerForm
|
||||
from .sales_history import SalesHistoryWindow
|
||||
from .material_calculator import MaterialCalculatorWindow
|
||||
|
||||
class PartnerCard(QFrame):
|
||||
"""Карточка партнера для отображения в списке"""
|
||||
partner_clicked = pyqtSignal(dict)
|
||||
|
||||
def __init__(self, partner_data):
|
||||
super().__init__()
|
||||
self.partner_data = partner_data
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
self.setStyleSheet("""
|
||||
PartnerCard {
|
||||
background-color: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 4px;
|
||||
}
|
||||
PartnerCard:hover {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #007acc;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(8, 8, 8, 8)
|
||||
layout.setSpacing(4)
|
||||
|
||||
# Заголовок с типом и названием
|
||||
header_layout = QHBoxLayout()
|
||||
header_layout.setSpacing(4)
|
||||
|
||||
type_label = QLabel(f"{self.partner_data.get('partner_type', 'Тип не указан')} |")
|
||||
type_label.setStyleSheet("color: #666; font-weight: bold;")
|
||||
|
||||
name_label = QLabel(self.partner_data['company_name'])
|
||||
name_label.setStyleSheet("font-weight: bold; font-size: 14px;")
|
||||
name_label.setWordWrap(True)
|
||||
|
||||
# Безопасное преобразование рейтинга
|
||||
rating_value = self.partner_data.get('rating', 0)
|
||||
if isinstance(rating_value, float):
|
||||
rating_value = int(rating_value)
|
||||
|
||||
rating_label = QLabel(f"{rating_value}%")
|
||||
rating_label.setStyleSheet("color: #007acc; font-weight: bold;")
|
||||
|
||||
header_layout.addWidget(type_label)
|
||||
header_layout.addWidget(name_label)
|
||||
header_layout.addStretch()
|
||||
header_layout.addWidget(rating_label)
|
||||
|
||||
# Информация о директоре
|
||||
QLabel(self.partner_data.get('director_name', 'Директор не указан'))
|
||||
director_label.setStyleSheet("color: #444;")
|
||||
|
||||
# Контактная информация
|
||||
phone_label = QLabel(self.partner_data.get('phone', 'Телефон не указан'))
|
||||
phone_label.setStyleSheet("color: #666;")
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
layout.addWidget(director_label)
|
||||
layout.addWidget(phone_label)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Обработка клика на карточке"""
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
self.partner_clicked.emit(self.partner_data)
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Главное окно приложения с поддержкой авторизации"""
|
||||
|
||||
def __init__(self, user_data):
|
||||
super().__init__()
|
||||
self.user_data = user_data
|
||||
self.current_partner = None
|
||||
self.orders_panel = None
|
||||
self.auth = user_data.get('auth')
|
||||
self.setup_ui()
|
||||
self.load_partners()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Настройка интерфейса главного окна"""
|
||||
self.setWindowTitle(f"MasterPol - Система управления партнерами")
|
||||
self.setGeometry(100, 100, 1200, 700)
|
||||
|
||||
# Установка иконки приложения
|
||||
try:
|
||||
self.setWindowIcon(QIcon("resources/icon.png"))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Создание меню
|
||||
self.create_menu()
|
||||
|
||||
# Создание тулбара
|
||||
self.create_toolbar()
|
||||
|
||||
# Создание статусной строки
|
||||
self.create_statusbar()
|
||||
|
||||
# Центральный виджет
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
main_layout = QHBoxLayout()
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Левая панель - список партнеров
|
||||
left_panel = self.create_partners_panel()
|
||||
main_layout.addWidget(left_panel, 1)
|
||||
|
||||
# Правая панель - детальная информация
|
||||
self.right_panel = self.create_details_panel()
|
||||
main_layout.addWidget(self.right_panel, 2)
|
||||
|
||||
central_widget.setLayout(main_layout)
|
||||
|
||||
def create_menu(self):
|
||||
"""Создание меню приложения"""
|
||||
menubar = self.menuBar()
|
||||
|
||||
# Меню Файл
|
||||
file_menu = menubar.addMenu('Файл')
|
||||
|
||||
refresh_action = QAction('Обновить', self)
|
||||
refresh_action.setShortcut('F5')
|
||||
refresh_action.triggered.connect(self.load_partners)
|
||||
file_menu.addAction(refresh_action)
|
||||
|
||||
file_menu.addSeparator()
|
||||
|
||||
logout_action = QAction('Выход', self)
|
||||
logout_action.setShortcut('Ctrl+Q')
|
||||
logout_action.triggered.connect(self.logout)
|
||||
file_menu.addAction(logout_action)
|
||||
|
||||
# Меню Сервис
|
||||
service_menu = menubar.addMenu('Сервис')
|
||||
|
||||
calc_action = QAction('Калькулятор материалов', self)
|
||||
calc_action.triggered.connect(self.show_material_calculator)
|
||||
service_menu.addAction(calc_action)
|
||||
|
||||
# Меню Справка
|
||||
help_menu = menubar.addMenu('Справка')
|
||||
|
||||
about_action = QAction('О программе', self)
|
||||
about_action.triggered.connect(self.show_about)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
def create_toolbar(self):
|
||||
"""Создание панели инструментов"""
|
||||
toolbar = QToolBar("Основные инструменты")
|
||||
self.addToolBar(toolbar)
|
||||
|
||||
refresh_action = QAction('Обновить', self)
|
||||
refresh_action.triggered.connect(self.load_partners)
|
||||
toolbar.addAction(refresh_action)
|
||||
|
||||
toolbar.addSeparator()
|
||||
|
||||
add_partner_action = QAction('Добавить партнера', self)
|
||||
add_partner_action.triggered.connect(self.show_add_partner_form)
|
||||
toolbar.addAction(add_partner_action)
|
||||
|
||||
def create_statusbar(self):
|
||||
"""Создание статусной строки"""
|
||||
statusbar = self.statusBar()
|
||||
user_info = f"Пользователь: {self.user_data.get('full_name', 'Неизвестно')}"
|
||||
statusbar.showMessage(user_info)
|
||||
|
||||
def create_partners_panel(self):
|
||||
"""Создание панели списка партнеров"""
|
||||
panel = QWidget()
|
||||
panel.setMaximumWidth(400)
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Заголовок
|
||||
title = QLabel("Партнеры")
|
||||
title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title.setStyleSheet("padding: 10px;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Панель управления
|
||||
control_layout = QHBoxLayout()
|
||||
control_layout.setSpacing(10)
|
||||
|
||||
self.add_button = QPushButton("Добавить партнера")
|
||||
self.add_button.clicked.connect(self.show_add_partner_form)
|
||||
self.add_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
""")
|
||||
|
||||
self.refresh_button = QPushButton("Обновить")
|
||||
self.refresh_button.clicked.connect(self.load_partners)
|
||||
self.refresh_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
""")
|
||||
|
||||
control_layout.addWidget(self.add_button)
|
||||
control_layout.addWidget(self.refresh_button)
|
||||
control_layout.addStretch()
|
||||
|
||||
layout.addLayout(control_layout)
|
||||
|
||||
# Список партнеров
|
||||
self.partners_list = QListWidget()
|
||||
self.partners_list.setStyleSheet("""
|
||||
QListWidget {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
outline: none;
|
||||
}
|
||||
QListWidget::item {
|
||||
border: none;
|
||||
padding: 0px;
|
||||
}
|
||||
QListWidget::item:selected {
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.partners_list)
|
||||
|
||||
# Кнопка расчета материалов
|
||||
self.calc_button = QPushButton("Калькулятор материалов")
|
||||
self.calc_button.clicked.connect(self.show_material_calculator)
|
||||
self.calc_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
""")
|
||||
layout.addWidget(self.calc_button)
|
||||
|
||||
panel.setLayout(layout)
|
||||
return panel
|
||||
|
||||
def create_details_panel(self):
|
||||
"""Создание панели детальной информации"""
|
||||
panel = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Заголовок детальной информации
|
||||
self.details_title = QLabel("Выберите партнера")
|
||||
self.details_title.setFont(QFont("Arial", 14, QFont.Weight.Bold))
|
||||
self.details_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.details_title.setStyleSheet("padding: 10px;")
|
||||
layout.addWidget(self.details_title)
|
||||
|
||||
# Детальная информация о партнере - создаем пустой frame
|
||||
self.details_frame = QFrame()
|
||||
self.details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
self.details_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
self.details_layout = QVBoxLayout()
|
||||
self.details_layout.setSpacing(8)
|
||||
self.details_frame.setLayout(self.details_layout)
|
||||
self.details_frame.hide()
|
||||
|
||||
layout.addWidget(self.details_frame)
|
||||
|
||||
# Кнопки управления выбранным партнером
|
||||
self.control_buttons = QWidget()
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout.setSpacing(10)
|
||||
|
||||
self.edit_button = QPushButton("Редактировать")
|
||||
self.edit_button.clicked.connect(self.edit_partner)
|
||||
self.edit_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
""")
|
||||
self.edit_button.hide()
|
||||
|
||||
self.sales_button = QPushButton("История продаж")
|
||||
self.sales_button.clicked.connect(self.show_sales_history)
|
||||
self.sales_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
""")
|
||||
self.sales_button.hide()
|
||||
|
||||
self.discount_button = QPushButton("Расчет скидки")
|
||||
self.discount_button.clicked.connect(self.calculate_discount)
|
||||
self.discount_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #ffc107;
|
||||
color: black;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
""")
|
||||
self.discount_button.hide()
|
||||
|
||||
buttons_layout.addWidget(self.edit_button)
|
||||
buttons_layout.addWidget(self.sales_button)
|
||||
buttons_layout.addWidget(self.discount_button)
|
||||
buttons_layout.addStretch()
|
||||
|
||||
self.control_buttons.setLayout(buttons_layout)
|
||||
layout.addWidget(self.control_buttons)
|
||||
|
||||
# Добавляем растягивающийся элемент в конец
|
||||
layout.addStretch()
|
||||
|
||||
panel.setLayout(layout)
|
||||
return panel
|
||||
|
||||
def load_partners(self):
|
||||
"""Загрузка списка партнеров из API с авторизацией"""
|
||||
try:
|
||||
response = requests.get(
|
||||
"http://localhost:8000/api/v1/partners",
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.partners_list.clear()
|
||||
partners = response.json()
|
||||
|
||||
for partner in partners:
|
||||
item = QListWidgetItem()
|
||||
card = PartnerCard(partner)
|
||||
card.partner_clicked.connect(self.show_partner_details)
|
||||
|
||||
# Устанавливаем фиксированный размер для элемента
|
||||
item.setSizeHint(card.sizeHint())
|
||||
self.partners_list.addItem(item)
|
||||
self.partners_list.setItemWidget(item, card)
|
||||
|
||||
# Сбрасываем выделение
|
||||
self.partners_list.clearSelection()
|
||||
self.current_partner = None
|
||||
self.details_title.setText("Выберите партнера")
|
||||
self.details_frame.hide()
|
||||
self.edit_button.hide()
|
||||
self.sales_button.hide()
|
||||
self.discount_button.hide()
|
||||
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
|
||||
self.logout()
|
||||
else:
|
||||
QMessageBox.warning(self, "Ошибка", "Не удалось загрузить партнеров")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить партнеров: {str(e)}")
|
||||
|
||||
def show_partner_details(self, partner_data):
|
||||
"""Отображение детальной информации о партнере"""
|
||||
self.current_partner = partner_data
|
||||
self.details_title.setText(partner_data['company_name'])
|
||||
|
||||
# Создаем новый виджет для деталей вместо очистки layout
|
||||
new_details_frame = QFrame()
|
||||
new_details_frame.setFrameStyle(QFrame.Shape.StyledPanel)
|
||||
new_details_frame.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
}
|
||||
""")
|
||||
new_details_layout = QVBoxLayout()
|
||||
new_details_layout.setSpacing(8)
|
||||
|
||||
# Добавляем новую информацию
|
||||
details = [
|
||||
("Тип:", partner_data.get('partner_type', 'Не указан')),
|
||||
("ИНН:", partner_data.get('inn', 'Не указан')),
|
||||
("Директор:", partner_data.get('director_name', 'Не указан')),
|
||||
("Телефон:", partner_data.get('phone', 'Не указан')),
|
||||
("Email:", partner_data.get('email', 'Не указан')),
|
||||
("Рейтинг:", str(partner_data.get('rating', 0))),
|
||||
("Адрес:", partner_data.get('legal_address', 'Не указан')),
|
||||
("Регионы:", partner_data.get('sales_locations', 'Не указан'))
|
||||
]
|
||||
|
||||
# ЗАМЕНИТЕ этот блок кода в методе show_partner_details:
|
||||
for label, value in details:
|
||||
row_widget = QWidget()
|
||||
row_layout = QHBoxLayout(row_widget)
|
||||
row_layout.setContentsMargins(0, 2, 0, 2)
|
||||
|
||||
label_widget = QLabel(label)
|
||||
label_widget.setStyleSheet("font-weight: bold; min-width: 100px;")
|
||||
label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
value_widget = QLabel(str(value))
|
||||
value_widget.setWordWrap(True)
|
||||
value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
row_layout.addWidget(label_widget)
|
||||
row_layout.addWidget(value_widget)
|
||||
row_layout.addStretch()
|
||||
|
||||
new_details_layout.addWidget(row_widget)
|
||||
|
||||
# НА этот исправленный вариант:
|
||||
for label, value in details:
|
||||
# Создаем контейнер для строки
|
||||
row_container = QWidget()
|
||||
row_container.setFixedHeight(30) # Фиксированная высота для каждой строки
|
||||
row_layout = QHBoxLayout(row_container)
|
||||
row_layout.setContentsMargins(5, 0, 5, 0)
|
||||
row_layout.setSpacing(10)
|
||||
|
||||
# Лейбл (название поля)
|
||||
label_widget = QLabel(label)
|
||||
label_widget.setStyleSheet("""
|
||||
QLabel {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
min-width: 120px;
|
||||
max-width: 120px;
|
||||
background-color: transparent;
|
||||
}
|
||||
""")
|
||||
label_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
# Значение
|
||||
value_widget = QLabel(str(value))
|
||||
value_widget.setStyleSheet("""
|
||||
QLabel {
|
||||
color: #555;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
value_widget.setWordWrap(True)
|
||||
value_widget.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
row_layout.addWidget(label_widget)
|
||||
row_layout.addWidget(value_widget)
|
||||
row_layout.addStretch()
|
||||
|
||||
new_details_layout.addWidget(row_container)
|
||||
|
||||
new_details_frame.setLayout(new_details_layout)
|
||||
|
||||
# Заменяем старый details_frame на новый
|
||||
old_frame = self.details_frame
|
||||
layout = self.right_panel.layout()
|
||||
layout.replaceWidget(old_frame, new_details_frame)
|
||||
old_frame.deleteLater()
|
||||
|
||||
self.details_frame = new_details_frame
|
||||
self.details_layout = new_details_layout
|
||||
|
||||
self.details_frame.show()
|
||||
self.edit_button.show()
|
||||
self.sales_button.show()
|
||||
self.discount_button.show()
|
||||
|
||||
def show_add_partner_form(self):
|
||||
"""Открытие формы добавления партнера"""
|
||||
form = PartnerForm(self, auth=self.auth)
|
||||
form.partner_saved.connect(self.load_partners)
|
||||
form.exec()
|
||||
|
||||
def edit_partner(self):
|
||||
"""Редактирование выбранного партнера"""
|
||||
if self.current_partner:
|
||||
form = PartnerForm(self, self.current_partner, auth=self.auth)
|
||||
form.partner_saved.connect(self.load_partners)
|
||||
form.exec()
|
||||
|
||||
def show_sales_history(self):
|
||||
"""Открытие истории продаж партнера"""
|
||||
if self.current_partner:
|
||||
sales_window = SalesHistoryWindow(self.current_partner, self, auth=self.auth)
|
||||
sales_window.exec()
|
||||
|
||||
def calculate_discount(self):
|
||||
"""Расчет скидки для партнера с авторизацией"""
|
||||
if self.current_partner:
|
||||
try:
|
||||
response = requests.get(
|
||||
f"http://localhost:8000/api/v1/partners/{self.current_partner['partner_id']}/discount",
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
discount_data = response.json()
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Расчет скидки",
|
||||
f"Партнер: {self.current_partner['company_name']}\n"
|
||||
f"Общие продажи: {discount_data['total_sales']}\n"
|
||||
f"Скидка: {discount_data['discount_percent']}%"
|
||||
)
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
|
||||
self.logout()
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось рассчитать скидку: {str(e)}")
|
||||
|
||||
def show_material_calculator(self):
|
||||
"""Открытие калькулятора материалов"""
|
||||
calculator = MaterialCalculatorWindow(self, auth=self.auth)
|
||||
calculator.exec()
|
||||
|
||||
def logout(self):
|
||||
"""Выход из системы"""
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Подтверждение выхода",
|
||||
"Вы уверены, что хотите выйти из системы?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
self.close()
|
||||
# Здесь можно добавить вызов окна авторизации
|
||||
# или перезапуск приложения
|
||||
|
||||
def show_about(self):
|
||||
"""Показать информацию о программе"""
|
||||
QMessageBox.about(
|
||||
self,
|
||||
"О программе MasterPol",
|
||||
"MasterPol - Система управления партнерами\n\n"
|
||||
"Версия: 1.0.0\n"
|
||||
"Разработчик: Команда MasterPol\n\n"
|
||||
"Система предназначена для управления партнерами,\n"
|
||||
"учета продаж и расчета бизнес-показателей."
|
||||
)
|
||||
160
ressult/gui/material_calculator.py
Normal file
160
ressult/gui/material_calculator.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# gui/material_calculator.py
|
||||
"""
|
||||
Калькулятор материалов для производства
|
||||
Соответствует модулю 4 ТЗ
|
||||
"""
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QLineEdit, QPushButton, QMessageBox, QFormLayout,
|
||||
QDoubleSpinBox, QSpinBox)
|
||||
from PyQt6.QtCore import Qt
|
||||
import requests
|
||||
import math
|
||||
|
||||
class MaterialCalculatorWindow(QDialog):
|
||||
def __init__(self, parent=None, auth=None):
|
||||
super().__init__(parent)
|
||||
self.auth = auth # Сохраняем auth, даже если не используется
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle("Калькулятор материалов для производства")
|
||||
self.setModal(True)
|
||||
self.resize(400, 300)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Заголовок
|
||||
title = QLabel("Расчет количества материала")
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Форма ввода параметров
|
||||
form_layout = QFormLayout()
|
||||
|
||||
self.product_type_id = QSpinBox()
|
||||
self.product_type_id.setRange(1, 100)
|
||||
form_layout.addRow("ID типа продукции:", self.product_type_id)
|
||||
|
||||
self.material_type_id = QSpinBox()
|
||||
self.material_type_id.setRange(1, 100)
|
||||
form_layout.addRow("ID типа материала:", self.material_type_id)
|
||||
|
||||
self.quantity = QSpinBox()
|
||||
self.quantity.setRange(1, 1000000)
|
||||
form_layout.addRow("Количество продукции:", self.quantity)
|
||||
|
||||
self.param1 = QDoubleSpinBox()
|
||||
self.param1.setRange(0.1, 1000.0)
|
||||
self.param1.setDecimals(2)
|
||||
form_layout.addRow("Параметр продукции 1:", self.param1)
|
||||
|
||||
self.param2 = QDoubleSpinBox()
|
||||
self.param2.setRange(0.1, 1000.0)
|
||||
self.param2.setDecimals(2)
|
||||
form_layout.addRow("Параметр продукции 2:", self.param2)
|
||||
|
||||
self.product_coeff = QDoubleSpinBox()
|
||||
self.product_coeff.setRange(0.1, 10.0)
|
||||
self.product_coeff.setDecimals(3)
|
||||
self.product_coeff.setValue(1.0)
|
||||
form_layout.addRow("Коэффициент типа продукции:", self.product_coeff)
|
||||
|
||||
self.defect_percent = QDoubleSpinBox()
|
||||
self.defect_percent.setRange(0.0, 50.0)
|
||||
self.defect_percent.setDecimals(1)
|
||||
self.defect_percent.setSuffix("%")
|
||||
form_layout.addRow("Процент брака материала:", self.defect_percent)
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
|
||||
# Результат
|
||||
self.result_label = QLabel()
|
||||
self.result_label.setStyleSheet("font-weight: bold; color: #007acc; margin: 10px;")
|
||||
self.result_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.result_label)
|
||||
|
||||
# Кнопки
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
self.calculate_button = QPushButton("Рассчитать")
|
||||
self.calculate_button.clicked.connect(self.calculate_material)
|
||||
self.calculate_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
""")
|
||||
|
||||
self.close_button = QPushButton("Закрыть")
|
||||
self.close_button.clicked.connect(self.accept)
|
||||
|
||||
buttons_layout.addWidget(self.calculate_button)
|
||||
buttons_layout.addStretch()
|
||||
buttons_layout.addWidget(self.close_button)
|
||||
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def calculate_material(self):
|
||||
"""Расчет количества материала с обработкой ошибок"""
|
||||
try:
|
||||
# Проверяем валидность входных данных
|
||||
if (self.param1.value() <= 0 or self.param2.value() <= 0 or
|
||||
self.product_coeff.value() <= 0 or self.defect_percent.value() < 0):
|
||||
self.result_label.setText("Ошибка: неверные параметры")
|
||||
self.result_label.setStyleSheet("color: red; font-weight: bold;")
|
||||
return
|
||||
|
||||
# Создаем данные для расчета
|
||||
calculation_data = {
|
||||
'product_type_id': self.product_type_id.value(),
|
||||
'material_type_id': self.material_type_id.value(),
|
||||
'quantity': self.quantity.value(),
|
||||
'param1': self.param1.value(),
|
||||
'param2': self.param2.value(),
|
||||
'product_coeff': self.product_coeff.value(),
|
||||
'defect_percent': self.defect_percent.value()
|
||||
}
|
||||
|
||||
# Локальный расчет (без API)
|
||||
material_quantity = self.calculate_locally(calculation_data)
|
||||
|
||||
if material_quantity >= 0:
|
||||
self.result_label.setText(
|
||||
f"Необходимое количество материала: {material_quantity} единиц"
|
||||
)
|
||||
self.result_label.setStyleSheet("color: #007acc; font-weight: bold;")
|
||||
else:
|
||||
self.result_label.setText("Ошибка: неверные параметры расчета")
|
||||
self.result_label.setStyleSheet("color: red; font-weight: bold;")
|
||||
|
||||
except Exception as e:
|
||||
self.result_label.setText(f"Ошибка расчета: {str(e)}")
|
||||
self.result_label.setStyleSheet("color: red; font-weight: bold;")
|
||||
|
||||
def calculate_locally(self, data):
|
||||
"""Локальный расчет материалов"""
|
||||
try:
|
||||
import math
|
||||
|
||||
# Расчет количества материала на одну единицу продукции
|
||||
material_per_unit = data['param1'] * data['param2'] * data['product_coeff']
|
||||
|
||||
# Расчет общего количества материала с учетом брака
|
||||
total_material = material_per_unit * data['quantity']
|
||||
total_material_with_defect = total_material * (1 + data['defect_percent'] / 100)
|
||||
|
||||
# Округление до целого числа в большую сторону
|
||||
return math.ceil(total_material_with_defect)
|
||||
|
||||
except:
|
||||
return -1
|
||||
344
ressult/gui/orders_panel.py
Normal file
344
ressult/gui/orders_panel.py
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
# gui/orders_panel.py
|
||||
"""
|
||||
Панель управления заказами и продажами
|
||||
"""
|
||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QTableWidget, QTableWidgetItem, QPushButton,
|
||||
QHeaderView, QMessageBox, QDateEdit, QComboBox,
|
||||
QLineEdit, QFormLayout, QDialog, QDoubleSpinBox)
|
||||
from PyQt6.QtCore import Qt, QDate
|
||||
from PyQt6.QtGui import QFont
|
||||
import requests
|
||||
|
||||
class OrderForm(QDialog):
|
||||
"""Форма для добавления/редактирования заказа"""
|
||||
|
||||
order_saved = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None, order_data=None, auth=None, partners=None):
|
||||
super().__init__(parent)
|
||||
self.order_data = order_data
|
||||
self.auth = auth
|
||||
self.partners = partners or []
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle("Добавить заказ" if not self.order_data else "Редактировать заказ")
|
||||
self.setModal(True)
|
||||
self.resize(400, 300)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Форма ввода данных
|
||||
form_layout = QFormLayout()
|
||||
|
||||
# Выбор партнера
|
||||
self.partner_combo = QComboBox()
|
||||
self.partner_combo.addItem("Выберите партнера", None)
|
||||
for partner in self.partners:
|
||||
self.partner_combo.addItem(partner['company_name'], partner['partner_id'])
|
||||
form_layout.addRow("Партнер*:", self.partner_combo)
|
||||
|
||||
# Название продукта
|
||||
self.product_name = QLineEdit()
|
||||
self.product_name.setPlaceholderText("Введите название продукта")
|
||||
form_layout.addRow("Продукт*:", self.product_name)
|
||||
|
||||
# Количество
|
||||
self.quantity = QDoubleSpinBox()
|
||||
self.quantity.setRange(0.01, 100000.0)
|
||||
self.quantity.setDecimals(2)
|
||||
form_layout.addRow("Количество*:", self.quantity)
|
||||
|
||||
# Дата продажи
|
||||
self.sale_date = QDateEdit()
|
||||
self.sale_date.setDate(QDate.currentDate())
|
||||
self.sale_date.setCalendarPopup(True)
|
||||
form_layout.addRow("Дата продажи*:", self.sale_date)
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
|
||||
# Кнопки
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
self.save_button = QPushButton("Сохранить")
|
||||
self.save_button.clicked.connect(self.save_order)
|
||||
self.save_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
""")
|
||||
|
||||
self.cancel_button = QPushButton("Отмена")
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
|
||||
buttons_layout.addWidget(self.save_button)
|
||||
buttons_layout.addWidget(self.cancel_button)
|
||||
buttons_layout.addStretch()
|
||||
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
# Если редактирование, заполняем форму
|
||||
if self.order_data:
|
||||
self.fill_form()
|
||||
|
||||
def fill_form(self):
|
||||
"""Заполнение формы данными заказа"""
|
||||
data = self.order_data
|
||||
|
||||
# Устанавливаем партнера
|
||||
partner_index = self.partner_combo.findData(data.get('partner_id'))
|
||||
if partner_index >= 0:
|
||||
self.partner_combo.setCurrentIndex(partner_index)
|
||||
|
||||
self.product_name.setText(data.get('product_name', ''))
|
||||
self.quantity.setValue(float(data.get('quantity', 0)))
|
||||
|
||||
# Устанавливаем дату
|
||||
sale_date = data.get('sale_date')
|
||||
if sale_date:
|
||||
date = QDate.fromString(sale_date, 'yyyy-MM-dd')
|
||||
if date.isValid():
|
||||
self.sale_date.setDate(date)
|
||||
|
||||
def validate_form(self):
|
||||
"""Валидация данных формы"""
|
||||
errors = []
|
||||
|
||||
if not self.partner_combo.currentData():
|
||||
errors.append("Выберите партнера")
|
||||
|
||||
if not self.product_name.text().strip():
|
||||
errors.append("Введите название продукта")
|
||||
|
||||
if self.quantity.value() <= 0:
|
||||
errors.append("Количество должно быть больше 0")
|
||||
|
||||
return errors
|
||||
|
||||
def save_order(self):
|
||||
"""Сохранение заказа"""
|
||||
errors = self.validate_form()
|
||||
if errors:
|
||||
QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors))
|
||||
return
|
||||
|
||||
order_data = {
|
||||
'partner_id': self.partner_combo.currentData(),
|
||||
'product_name': self.product_name.text().strip(),
|
||||
'quantity': self.quantity.value(),
|
||||
'sale_date': self.sale_date.date().toString('yyyy-MM-dd')
|
||||
}
|
||||
|
||||
try:
|
||||
if self.order_data:
|
||||
# Обновление существующего заказа
|
||||
response = requests.put(
|
||||
f"http://localhost:8000/api/v1/sales/{self.order_data['sale_id']}",
|
||||
json=order_data,
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
else:
|
||||
# Создание нового заказа
|
||||
response = requests.post(
|
||||
"http://localhost:8000/api/v1/sales",
|
||||
json=order_data,
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.order_saved.emit()
|
||||
QMessageBox.information(self, "Успех", "Заказ успешно сохранен")
|
||||
self.accept()
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
|
||||
else:
|
||||
error_msg = response.json().get('detail', 'Неизвестная ошибка')
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить заказ: {error_msg}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}")
|
||||
|
||||
class OrdersPanel(QWidget):
|
||||
"""Панель управления заказами"""
|
||||
|
||||
def __init__(self, auth=None):
|
||||
super().__init__()
|
||||
self.auth = auth
|
||||
self.partners = []
|
||||
self.setup_ui()
|
||||
self.load_partners()
|
||||
self.load_orders()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Настройка интерфейса панели заказов"""
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(10, 10, 10, 10)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Заголовок
|
||||
title = QLabel("Управление заказами")
|
||||
title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
# Панель управления
|
||||
control_layout = QHBoxLayout()
|
||||
|
||||
self.add_button = QPushButton("Добавить заказ")
|
||||
self.add_button.clicked.connect(self.show_add_order_form)
|
||||
self.add_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
}
|
||||
""")
|
||||
|
||||
self.refresh_button = QPushButton("Обновить")
|
||||
self.refresh_button.clicked.connect(self.load_orders)
|
||||
|
||||
control_layout.addWidget(self.add_button)
|
||||
control_layout.addWidget(self.refresh_button)
|
||||
control_layout.addStretch()
|
||||
|
||||
layout.addLayout(control_layout)
|
||||
|
||||
# Таблица заказов
|
||||
self.orders_table = QTableWidget()
|
||||
self.orders_table.setColumnCount(6)
|
||||
self.orders_table.setHorizontalHeaderLabels([
|
||||
"ID", "Партнер", "Продукт", "Количество", "Дата", "Действия"
|
||||
])
|
||||
self.orders_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
self.orders_table.setStyleSheet("""
|
||||
QTableWidget {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
QTableWidget::item {
|
||||
padding: 8px;
|
||||
}
|
||||
""")
|
||||
|
||||
layout.addWidget(self.orders_table)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def load_partners(self):
|
||||
"""Загрузка списка партнеров"""
|
||||
try:
|
||||
response = requests.get(
|
||||
"http://localhost:8000/api/v1/partners",
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
if response.status_code == 200:
|
||||
self.partners = response.json()
|
||||
except:
|
||||
self.partners = []
|
||||
|
||||
def load_orders(self):
|
||||
"""Загрузка списка заказов"""
|
||||
try:
|
||||
response = requests.get(
|
||||
"http://localhost:8000/api/v1/sales",
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
if response.status_code == 200:
|
||||
orders = response.json()
|
||||
self.display_orders(orders)
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось загрузить заказы: {str(e)}")
|
||||
|
||||
def display_orders(self, orders):
|
||||
"""Отображение заказов в таблице"""
|
||||
self.orders_table.setRowCount(len(orders))
|
||||
|
||||
for row, order in enumerate(orders):
|
||||
self.orders_table.setItem(row, 0, QTableWidgetItem(str(order.get('sale_id', ''))))
|
||||
self.orders_table.setItem(row, 1, QTableWidgetItem(order.get('company_name', 'Неизвестно')))
|
||||
self.orders_table.setItem(row, 2, QTableWidgetItem(order.get('product_name', '')))
|
||||
self.orders_table.setItem(row, 3, QTableWidgetItem(str(order.get('quantity', ''))))
|
||||
self.orders_table.setItem(row, 4, QTableWidgetItem(order.get('sale_date', '')))
|
||||
|
||||
# Кнопки действий
|
||||
actions_widget = QWidget()
|
||||
actions_layout = QHBoxLayout(actions_widget)
|
||||
actions_layout.setContentsMargins(4, 4, 4, 4)
|
||||
actions_layout.setSpacing(4)
|
||||
|
||||
delete_button = QPushButton("Удалить")
|
||||
delete_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
""")
|
||||
delete_button.clicked.connect(lambda checked, o=order: self.delete_order(o))
|
||||
|
||||
actions_layout.addWidget(delete_button)
|
||||
actions_layout.addStretch()
|
||||
|
||||
self.orders_table.setCellWidget(row, 5, actions_widget)
|
||||
|
||||
def show_add_order_form(self):
|
||||
"""Открытие формы добавления заказа"""
|
||||
form = OrderForm(self, auth=self.auth, partners=self.partners)
|
||||
form.order_saved.connect(self.load_orders)
|
||||
form.exec()
|
||||
|
||||
def delete_order(self, order):
|
||||
"""Удаление заказа"""
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Подтверждение удаления",
|
||||
f"Вы уверены, что хотите удалить заказ #{order.get('sale_id')}?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
try:
|
||||
response = requests.delete(
|
||||
f"http://localhost:8000/api/v1/sales/{order['sale_id']}",
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
if response.status_code == 200:
|
||||
self.load_orders()
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось удалить заказ: {str(e)}")
|
||||
193
ressult/gui/partner_form.py
Normal file
193
ressult/gui/partner_form.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# gui/partner_form.py (обновленный)
|
||||
"""
|
||||
Форма для добавления/редактирования партнера с поддержкой авторизации
|
||||
"""
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QLineEdit, QComboBox, QPushButton, QMessageBox,
|
||||
QFormLayout, QSpinBox)
|
||||
from PyQt6.QtCore import pyqtSignal
|
||||
import requests
|
||||
|
||||
class PartnerForm(QDialog):
|
||||
partner_saved = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None, partner_data=None, auth=None):
|
||||
super().__init__(parent)
|
||||
self.partner_data = partner_data
|
||||
self.auth = auth
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle("Добавить партнера" if not self.partner_data else "Редактировать партнера")
|
||||
self.setModal(True)
|
||||
self.resize(500, 400)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Форма ввода данных
|
||||
form_layout = QFormLayout()
|
||||
|
||||
self.company_name = QLineEdit()
|
||||
self.company_name.setPlaceholderText("Введите наименование компании")
|
||||
form_layout.addRow("Наименование компании*:", self.company_name)
|
||||
|
||||
self.inn = QLineEdit()
|
||||
self.inn.setPlaceholderText("Введите ИНН")
|
||||
form_layout.addRow("ИНН*:", self.inn)
|
||||
|
||||
self.partner_type = QComboBox()
|
||||
self.partner_type.addItems(["", "distributor", "retail", "wholesale", "dealer"])
|
||||
self.partner_type.setPlaceholderText("Выберите тип партнера")
|
||||
form_layout.addRow("Тип партнера:", self.partner_type)
|
||||
|
||||
self.rating = QSpinBox()
|
||||
self.rating.setRange(0, 100)
|
||||
self.rating.setSuffix("%")
|
||||
form_layout.addRow("Рейтинг:", self.rating)
|
||||
|
||||
self.legal_address = QLineEdit()
|
||||
self.legal_address.setPlaceholderText("Введите юридический адрес")
|
||||
form_layout.addRow("Юридический адрес:", self.legal_address)
|
||||
|
||||
self.director_name = QLineEdit()
|
||||
self.director_name.setPlaceholderText("Введите ФИО директора")
|
||||
form_layout.addRow("ФИО директора:", self.director_name)
|
||||
|
||||
self.phone = QLineEdit()
|
||||
self.phone.setPlaceholderText("+7XXXXXXXXXX")
|
||||
form_layout.addRow("Телефон:", self.phone)
|
||||
|
||||
self.email = QLineEdit()
|
||||
self.email.setPlaceholderText("email@example.com")
|
||||
form_layout.addRow("Email:", self.email)
|
||||
|
||||
self.sales_locations = QLineEdit()
|
||||
self.sales_locations.setPlaceholderText("Москва, Санкт-Петербург...")
|
||||
form_layout.addRow("Регионы продаж:", self.sales_locations)
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
|
||||
# Кнопки
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
self.save_button = QPushButton("Сохранить")
|
||||
self.save_button.clicked.connect(self.save_partner)
|
||||
self.save_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
""")
|
||||
|
||||
self.cancel_button = QPushButton("Отмена")
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
|
||||
buttons_layout.addWidget(self.save_button)
|
||||
buttons_layout.addWidget(self.cancel_button)
|
||||
buttons_layout.addStretch()
|
||||
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
# Если редактирование, заполняем форму
|
||||
if self.partner_data:
|
||||
self.fill_form()
|
||||
|
||||
def fill_form(self):
|
||||
"""Заполнение формы данными партнера"""
|
||||
data = self.partner_data
|
||||
self.company_name.setText(data.get('company_name', ''))
|
||||
self.inn.setText(data.get('inn', ''))
|
||||
|
||||
partner_type = data.get('partner_type', '')
|
||||
if partner_type:
|
||||
index = self.partner_type.findText(partner_type)
|
||||
if index >= 0:
|
||||
self.partner_type.setCurrentIndex(index)
|
||||
|
||||
# Безопасное преобразование рейтинга
|
||||
rating = data.get('rating', 0)
|
||||
if isinstance(rating, float):
|
||||
rating = int(rating)
|
||||
self.rating.setValue(rating)
|
||||
|
||||
self.legal_address.setText(data.get('legal_address', ''))
|
||||
self.director_name.setText(data.get('director_name', ''))
|
||||
self.phone.setText(data.get('phone', ''))
|
||||
self.email.setText(data.get('email', ''))
|
||||
self.sales_locations.setText(data.get('sales_locations', ''))
|
||||
|
||||
def validate_form(self):
|
||||
"""Валидация данных формы"""
|
||||
errors = []
|
||||
|
||||
if not self.company_name.text().strip():
|
||||
errors.append("Наименование компании обязательно")
|
||||
|
||||
if not self.inn.text().strip():
|
||||
errors.append("ИНН обязателен")
|
||||
|
||||
if self.phone.text() and not self.phone.text().startswith('+'):
|
||||
errors.append("Телефон должен начинаться с '+'")
|
||||
|
||||
return errors
|
||||
|
||||
def save_partner(self):
|
||||
"""Сохранение партнера с авторизацией"""
|
||||
errors = self.validate_form()
|
||||
if errors:
|
||||
QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors))
|
||||
return
|
||||
|
||||
partner_data = {
|
||||
'company_name': self.company_name.text().strip(),
|
||||
'inn': self.inn.text().strip(),
|
||||
'partner_type': self.partner_type.currentText() or None,
|
||||
'rating': self.rating.value(),
|
||||
'legal_address': self.legal_address.text().strip() or None,
|
||||
'director_name': self.director_name.text().strip() or None,
|
||||
'phone': self.phone.text().strip() or None,
|
||||
'email': self.email.text().strip() or None,
|
||||
'sales_locations': self.sales_locations.text().strip() or None
|
||||
}
|
||||
|
||||
try:
|
||||
if self.partner_data:
|
||||
# Обновление существующего партнера
|
||||
response = requests.put(
|
||||
f"http://localhost:8000/api/v1/partners/{self.partner_data['partner_id']}",
|
||||
json=partner_data,
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
else:
|
||||
# Создание нового партнера
|
||||
response = requests.post(
|
||||
"http://localhost:8000/api/v1/partners",
|
||||
json=partner_data,
|
||||
auth=self.auth,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.partner_saved.emit()
|
||||
QMessageBox.information(self, "Успех", "Партнер успешно сохранен")
|
||||
self.accept()
|
||||
elif response.status_code == 401:
|
||||
QMessageBox.warning(self, "Ошибка авторизации", "Сессия истекла. Пожалуйста, войдите снова.")
|
||||
else:
|
||||
error_msg = response.json().get('detail', 'Неизвестная ошибка')
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить партнера: {error_msg}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
QMessageBox.critical(self, "Ошибка", "Не удалось подключиться к серверу")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}")
|
||||
186
ressult/gui/partner_form.py.bak
Normal file
186
ressult/gui/partner_form.py.bak
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# gui/partner_form.py
|
||||
"""
|
||||
Форма для добавления/редактирования партнера
|
||||
Соответствует модулю 3 ТЗ
|
||||
"""
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QLineEdit, QComboBox, QPushButton, QMessageBox,
|
||||
QFormLayout, QSpinBox)
|
||||
from PyQt6.QtCore import pyqtSignal
|
||||
import requests
|
||||
|
||||
class PartnerForm(QDialog):
|
||||
partner_saved = pyqtSignal()
|
||||
|
||||
def __init__(self, parent=None, partner_data=None):
|
||||
super().__init__(parent)
|
||||
self.partner_data = partner_data
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle("Добавить партнера" if not self.partner_data else "Редактировать партнера")
|
||||
self.setModal(True)
|
||||
self.resize(500, 400)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Форма ввода данных
|
||||
form_layout = QFormLayout()
|
||||
|
||||
self.company_name = QLineEdit()
|
||||
self.company_name.setPlaceholderText("Введите наименование компании")
|
||||
form_layout.addRow("Наименование компании*:", self.company_name)
|
||||
|
||||
self.inn = QLineEdit()
|
||||
self.inn.setPlaceholderText("Введите ИНН")
|
||||
form_layout.addRow("ИНН*:", self.inn)
|
||||
|
||||
self.partner_type = QComboBox()
|
||||
self.partner_type.addItems(["", "distributor", "retail", "wholesale", "dealer"])
|
||||
self.partner_type.setPlaceholderText("Выберите тип партнера")
|
||||
form_layout.addRow("Тип партнера:", self.partner_type)
|
||||
|
||||
self.rating = QSpinBox()
|
||||
self.rating.setRange(0, 100)
|
||||
self.rating.setSuffix("%")
|
||||
form_layout.addRow("Рейтинг:", self.rating)
|
||||
|
||||
self.legal_address = QLineEdit()
|
||||
self.legal_address.setPlaceholderText("Введите юридический адрес")
|
||||
form_layout.addRow("Юридический адрес:", self.legal_address)
|
||||
|
||||
self.director_name = QLineEdit()
|
||||
self.director_name.setPlaceholderText("Введите ФИО директора")
|
||||
form_layout.addRow("ФИО директора:", self.director_name)
|
||||
|
||||
self.phone = QLineEdit()
|
||||
self.phone.setPlaceholderText("+7XXXXXXXXXX")
|
||||
form_layout.addRow("Телефон:", self.phone)
|
||||
|
||||
self.email = QLineEdit()
|
||||
self.email.setPlaceholderText("email@example.com")
|
||||
form_layout.addRow("Email:", self.email)
|
||||
|
||||
self.sales_locations = QLineEdit()
|
||||
self.sales_locations.setPlaceholderText("Москва, Санкт-Петербург...")
|
||||
form_layout.addRow("Регионы продаж:", self.sales_locations)
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
|
||||
# Кнопки
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
self.save_button = QPushButton("Сохранить")
|
||||
self.save_button.clicked.connect(self.save_partner)
|
||||
self.save_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
""")
|
||||
|
||||
self.cancel_button = QPushButton("Отмена")
|
||||
self.cancel_button.clicked.connect(self.reject)
|
||||
|
||||
buttons_layout.addWidget(self.save_button)
|
||||
buttons_layout.addWidget(self.cancel_button)
|
||||
buttons_layout.addStretch()
|
||||
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
# Если редактирование, заполняем форму
|
||||
if self.partner_data:
|
||||
self.fill_form()
|
||||
|
||||
# gui/partner_form.py (исправленный метод fill_form)
|
||||
def fill_form(self):
|
||||
"""Заполнение формы данными партнера"""
|
||||
data = self.partner_data
|
||||
self.company_name.setText(data.get('company_name', ''))
|
||||
self.inn.setText(data.get('inn', ''))
|
||||
|
||||
partner_type = data.get('partner_type', '')
|
||||
if partner_type:
|
||||
index = self.partner_type.findText(partner_type)
|
||||
if index >= 0:
|
||||
self.partner_type.setCurrentIndex(index)
|
||||
|
||||
# Безопасное преобразование рейтинга к int
|
||||
rating = data.get('rating', 0)
|
||||
if isinstance(rating, float):
|
||||
rating = int(rating)
|
||||
self.rating.setValue(rating)
|
||||
|
||||
self.legal_address.setText(data.get('legal_address', ''))
|
||||
self.director_name.setText(data.get('director_name', ''))
|
||||
self.phone.setText(data.get('phone', ''))
|
||||
self.email.setText(data.get('email', ''))
|
||||
self.sales_locations.setText(data.get('sales_locations', ''))
|
||||
|
||||
def validate_form(self):
|
||||
"""Валидация данных формы"""
|
||||
errors = []
|
||||
|
||||
if not self.company_name.text().strip():
|
||||
errors.append("Наименование компании обязательно")
|
||||
|
||||
if not self.inn.text().strip():
|
||||
errors.append("ИНН обязателен")
|
||||
|
||||
if self.phone.text() and not self.phone.text().startswith('+'):
|
||||
errors.append("Телефон должен начинаться с '+'")
|
||||
|
||||
return errors
|
||||
|
||||
def save_partner(self):
|
||||
"""Сохранение партнера"""
|
||||
errors = self.validate_form()
|
||||
if errors:
|
||||
QMessageBox.warning(self, "Ошибка валидации", "\n".join(errors))
|
||||
return
|
||||
|
||||
partner_data = {
|
||||
'company_name': self.company_name.text().strip(),
|
||||
'inn': self.inn.text().strip(),
|
||||
'partner_type': self.partner_type.currentText() or None,
|
||||
'rating': self.rating.value(),
|
||||
'legal_address': self.legal_address.text().strip() or None,
|
||||
'director_name': self.director_name.text().strip() or None,
|
||||
'phone': self.phone.text().strip() or None,
|
||||
'email': self.email.text().strip() or None,
|
||||
'sales_locations': self.sales_locations.text().strip() or None
|
||||
}
|
||||
|
||||
try:
|
||||
if self.partner_data:
|
||||
# Обновление существующего партнера
|
||||
response = requests.put(
|
||||
f"http://localhost:8000/api/v1/partners/{self.partner_data['partner_id']}",
|
||||
json=partner_data
|
||||
)
|
||||
else:
|
||||
# Создание нового партнера
|
||||
response = requests.post(
|
||||
"http://localhost:8000/api/v1/partners",
|
||||
json=partner_data
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.partner_saved.emit()
|
||||
QMessageBox.information(self, "Успех", "Партнер успешно сохранен")
|
||||
self.accept()
|
||||
else:
|
||||
error_msg = response.json().get('detail', 'Неизвестная ошибка')
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось сохранить партнера: {error_msg}")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}")
|
||||
91
ressult/gui/sales_history.py
Normal file
91
ressult/gui/sales_history.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# gui/sales_history.py
|
||||
"""
|
||||
Окно истории продаж партнера
|
||||
Соответствует модулю 4 ТЗ
|
||||
"""
|
||||
from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QTableWidget, QTableWidgetItem, QPushButton,
|
||||
QHeaderView, QMessageBox)
|
||||
from PyQt6.QtCore import Qt
|
||||
import requests
|
||||
|
||||
class SalesHistoryWindow(QDialog):
|
||||
def __init__(self, partner_data, parent=None):
|
||||
super().__init__(parent)
|
||||
self.partner_data = partner_data
|
||||
self.setup_ui()
|
||||
self.load_sales_history()
|
||||
|
||||
def setup_ui(self):
|
||||
self.setWindowTitle(f"История продаж - {self.partner_data['company_name']}")
|
||||
self.setModal(True)
|
||||
self.resize(800, 400)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# Заголовок
|
||||
title = QLabel(f"История реализации продукции\n{self.partner_data['company_name']}")
|
||||
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title.setStyleSheet("font-size: 16px; font-weight: bold; margin: 10px;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Таблица продаж
|
||||
self.sales_table = QTableWidget()
|
||||
self.sales_table.setColumnCount(4)
|
||||
self.sales_table.setHorizontalHeaderLabels([
|
||||
"ID", "Наименование продукции", "Количество", "Дата продажи"
|
||||
])
|
||||
self.sales_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
layout.addWidget(self.sales_table)
|
||||
|
||||
# Статистика
|
||||
self.stats_label = QLabel()
|
||||
self.stats_label.setStyleSheet("font-weight: bold; margin: 10px;")
|
||||
layout.addWidget(self.stats_label)
|
||||
|
||||
# Кнопки
|
||||
buttons_layout = QHBoxLayout()
|
||||
|
||||
self.close_button = QPushButton("Закрыть")
|
||||
self.close_button.clicked.connect(self.accept)
|
||||
|
||||
buttons_layout.addStretch()
|
||||
buttons_layout.addWidget(self.close_button)
|
||||
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def load_sales_history(self):
|
||||
"""Загрузка истории продаж партнера"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f"http://localhost:8000/api/v1/sales/partner/{self.partner_data['partner_id']}"
|
||||
)
|
||||
if response.status_code == 200:
|
||||
sales_data = response.json()
|
||||
self.display_sales_data(sales_data)
|
||||
else:
|
||||
QMessageBox.warning(self, "Ошибка", "Не удалось загрузить историю продаж")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Ошибка", f"Ошибка подключения: {str(e)}")
|
||||
|
||||
def display_sales_data(self, sales_data):
|
||||
"""Отображение данных о продажах в таблице"""
|
||||
self.sales_table.setRowCount(len(sales_data))
|
||||
|
||||
total_quantity = 0
|
||||
for row, sale in enumerate(sales_data):
|
||||
self.sales_table.setItem(row, 0, QTableWidgetItem(str(sale['sale_id'])))
|
||||
self.sales_table.setItem(row, 1, QTableWidgetItem(sale['product_name']))
|
||||
self.sales_table.setItem(row, 2, QTableWidgetItem(str(sale['quantity'])))
|
||||
self.sales_table.setItem(row, 3, QTableWidgetItem(sale['sale_date']))
|
||||
|
||||
total_quantity += float(sale['quantity'])
|
||||
|
||||
# Обновление статистики
|
||||
self.stats_label.setText(
|
||||
f"Общее количество проданной продукции: {total_quantity}\n"
|
||||
f"Всего продаж: {len(sales_data)}"
|
||||
)
|
||||
11
ressult/requirements.txt
Normal file
11
ressult/requirements.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# requirements.txt
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
psycopg2-binary==2.9.9
|
||||
python-dotenv==1.0.0
|
||||
python-multipart==0.0.6
|
||||
pandas==2.1.3
|
||||
openpyxl==3.1.2
|
||||
aiofiles==23.2.1
|
||||
pydantic[email]==2.5.0
|
||||
bcrypt==4.1.1
|
||||
17
ressult/run.py
Normal file
17
ressult/run.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# run.py
|
||||
"""
|
||||
Точка входа для запуска сервера
|
||||
"""
|
||||
import uvicorn
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host=os.getenv('HOST', '0.0.0.0'),
|
||||
port=int(os.getenv('PORT', 8000)),
|
||||
reload=os.getenv('DEBUG', 'False').lower() == 'true'
|
||||
)
|
||||
51
ressult/run_gui.py
Normal file
51
ressult/run_gui.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# run_gui.py
|
||||
"""
|
||||
Главный модуль запуска GUI приложения с авторизацией
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from gui.login_window import LoginWindow
|
||||
from gui.main_window import MainWindow
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from PyQt6.QtCore import QTimer
|
||||
|
||||
class ApplicationController:
|
||||
"""Контроллер приложения, управляющий авторизацией и главным окном"""
|
||||
|
||||
def __init__(self):
|
||||
self.app = QApplication(sys.argv)
|
||||
self.login_window = None
|
||||
self.main_window = None
|
||||
self.current_user = None
|
||||
|
||||
def show_login(self):
|
||||
"""Показать окно авторизации"""
|
||||
self.login_window = LoginWindow()
|
||||
self.login_window.login_success.connect(self.on_login_success)
|
||||
self.login_window.show()
|
||||
|
||||
def on_login_success(self, user_data):
|
||||
"""Обработка успешной авторизации"""
|
||||
self.current_user = user_data
|
||||
self.login_window.close()
|
||||
self.show_main_window()
|
||||
|
||||
def show_main_window(self):
|
||||
"""Показать главное окно приложения"""
|
||||
self.main_window = MainWindow(self.current_user)
|
||||
self.main_window.show()
|
||||
|
||||
def run(self):
|
||||
"""Запуск приложения"""
|
||||
self.show_login()
|
||||
return self.app.exec()
|
||||
|
||||
def main():
|
||||
"""Точка входа приложения"""
|
||||
controller = ApplicationController()
|
||||
sys.exit(controller.run())
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue