After Graduate Update
This commit is contained in:
parent
b92a91ab37
commit
c6917dd85e
69 changed files with 7540 additions and 0 deletions
BIN
fitness.db
BIN
fitness.db
Binary file not shown.
BIN
masterpol.db
BIN
masterpol.db
Binary file not shown.
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()
|
||||||
2
robbery/master_pol-module_1_2/.gitignore
vendored
Normal file
2
robbery/master_pol-module_1_2/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
**/__pycache__/
|
||||||
|
.venv/
|
||||||
3
robbery/master_pol-module_1_2/.idea/.gitignore
generated
vendored
Normal file
3
robbery/master_pol-module_1_2/.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
1
robbery/master_pol-module_1_2/.idea/.name
generated
Normal file
1
robbery/master_pol-module_1_2/.idea/.name
generated
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
main.py
|
||||||
6
robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
robbery/master_pol-module_1_2/.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
10
robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml
generated
Normal file
10
robbery/master_pol-module_1_2/.idea/master_pol-module_1_2.iml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.11 virtualenv at C:\Users\student\Desktop\master_pol-module_1_2\.venv" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
7
robbery/master_pol-module_1_2/.idea/misc.xml
generated
Normal file
7
robbery/master_pol-module_1_2/.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.11 virtualenv at C:\Users\student\Desktop\master_pol-module_1_2\.venv" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 virtualenv at C:\Users\student\Desktop\master_pol-module_1_2\.venv" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
robbery/master_pol-module_1_2/.idea/modules.xml
generated
Normal file
8
robbery/master_pol-module_1_2/.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/master_pol-module_1_2.iml" filepath="$PROJECT_DIR$/.idea/master_pol-module_1_2.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
55
robbery/master_pol-module_1_2/README.md
Normal file
55
robbery/master_pol-module_1_2/README.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# MasterPol
|
||||||
|
|
||||||
|
Графическое приложение на PyQt6 для работы с базой данных MySQL.
|
||||||
|
|
||||||
|
## Подготовка проекта
|
||||||
|
|
||||||
|
1. **Клонируйте репозиторий и перейдите в папку проекта:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone <адрес-репозитория>
|
||||||
|
cd master_pol
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Создайте и активируйте виртуальное окружение:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\activate # Windows
|
||||||
|
# source .venv/bin/activate # Linux/MacOS
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Установите зависимости:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Создайте базу данных и выполните SQL-скрипт:**
|
||||||
|
|
||||||
|
- Запустите MySQL и выполните скрипт `app/database/script.sql` для создания необходимых таблиц и данных:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mysql -u <user> -p <db_name> < app/database/script.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
- Замените `<user>` и `<db_name>` на свои значения.
|
||||||
|
|
||||||
|
5. **Проверьте параметры подключения к базе данных:**
|
||||||
|
- Откройте файл `app/database/db.py` и убедитесь, что значения для подключения (host, user, password, database) указаны верно.
|
||||||
|
|
||||||
|
## Запуск приложения
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python app/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
- `app/main.py` — точка входа, запуск приложения
|
||||||
|
- `app/components/` — компоненты интерфейса
|
||||||
|
- `app/database/` — работа с БД, скрипты и настройки
|
||||||
|
- `app/pages/` — страницы приложения
|
||||||
|
- `app/res/` — ресурсы (цвета, шрифты)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QDialog,
|
||||||
|
QVBoxLayout,
|
||||||
|
QFormLayout,
|
||||||
|
QLineEdit,
|
||||||
|
QPushButton,
|
||||||
|
QComboBox,
|
||||||
|
QSpinBox,
|
||||||
|
QMessageBox,
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from res.colors import ACCENT_COLOR
|
||||||
|
from dto.partners_dto import PartnerUpdateDto, PartnersInfo
|
||||||
|
|
||||||
|
|
||||||
|
class EditPartnerDialog(QDialog):
|
||||||
|
def __init__(self, partner_data: PartnersInfo, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.partner_data = partner_data
|
||||||
|
self.setup_ui()
|
||||||
|
self.load_partner_types()
|
||||||
|
self.fill_form()
|
||||||
|
self.result = None
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
self.setWindowTitle("Редактирование партнера")
|
||||||
|
self.setFixedSize(500, 400)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
form_layout = QFormLayout()
|
||||||
|
|
||||||
|
# Создаем поля формы
|
||||||
|
self.partner_type = QComboBox()
|
||||||
|
self.partner_name = QLineEdit()
|
||||||
|
self.first_name = QLineEdit()
|
||||||
|
self.last_name = QLineEdit()
|
||||||
|
self.middle_name = QLineEdit()
|
||||||
|
self.email = QLineEdit()
|
||||||
|
self.phone = QLineEdit()
|
||||||
|
self.address = QLineEdit()
|
||||||
|
self.inn = QLineEdit()
|
||||||
|
self.rating = QSpinBox()
|
||||||
|
self.rating.setRange(0, 10)
|
||||||
|
|
||||||
|
# Добавляем поля в форму
|
||||||
|
form_layout.addRow("Тип партнера:", self.partner_type)
|
||||||
|
form_layout.addRow("Название:", self.partner_name)
|
||||||
|
form_layout.addRow("Имя директора:", self.first_name)
|
||||||
|
form_layout.addRow("Фамилия директора:", self.last_name)
|
||||||
|
form_layout.addRow("Отчество директора:", self.middle_name)
|
||||||
|
form_layout.addRow("Email:", self.email)
|
||||||
|
form_layout.addRow("Телефон:", self.phone)
|
||||||
|
form_layout.addRow("Адрес:", self.address)
|
||||||
|
form_layout.addRow("ИНН:", self.inn)
|
||||||
|
form_layout.addRow("Рейтинг:", self.rating)
|
||||||
|
|
||||||
|
# Кнопки
|
||||||
|
self.save_button = QPushButton("Сохранить")
|
||||||
|
self.cancel_button = QPushButton("Отмена")
|
||||||
|
|
||||||
|
self.save_button.clicked.connect(self.save_changes)
|
||||||
|
self.cancel_button.clicked.connect(self.reject)
|
||||||
|
|
||||||
|
layout.addLayout(form_layout)
|
||||||
|
layout.addWidget(self.save_button)
|
||||||
|
layout.addWidget(self.cancel_button)
|
||||||
|
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# Стили
|
||||||
|
self.setStyleSheet(
|
||||||
|
f"""
|
||||||
|
QPushButton {{
|
||||||
|
background-color: {ACCENT_COLOR};
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_partner_types(self):
|
||||||
|
types = ['ООО', "ЗАО"]
|
||||||
|
for i, val in enumerate(types):
|
||||||
|
self.partner_type.addItem(val, i + 1)
|
||||||
|
|
||||||
|
def fill_form(self):
|
||||||
|
pass
|
||||||
|
def save_changes(self):
|
||||||
|
try:
|
||||||
|
partner_data = PartnerUpdateDto(
|
||||||
|
id=self.partner_data.id,
|
||||||
|
partner_type_id=self.partner_type.currentData(),
|
||||||
|
partner_name=self.partner_name.text(),
|
||||||
|
first_name=self.first_name.text(),
|
||||||
|
last_name=self.last_name.text(),
|
||||||
|
middle_name=self.middle_name.text(),
|
||||||
|
email=self.email.text(),
|
||||||
|
phone=self.phone.text(),
|
||||||
|
address=self.address.text(),
|
||||||
|
inn=self.inn.text(),
|
||||||
|
rating=self.rating.value(),
|
||||||
|
)
|
||||||
|
db.update_partner(partner_data)
|
||||||
|
self.accept()
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(
|
||||||
|
self, "Ошибка", f"Не удалось сохранить изменения: {str(e)}"
|
||||||
|
)
|
||||||
94
robbery/master_pol-module_1_2/app/components/partner_card.py
Normal file
94
robbery/master_pol-module_1_2/app/components/partner_card.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QFrame
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal
|
||||||
|
from res.colors import ACCENT_COLOR, SECONDARY_COLOR
|
||||||
|
from res.fonts import MAIN_FONT
|
||||||
|
from dto.partners_dto import PartnersInfo
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PartnerCard(QFrame):
|
||||||
|
doubleClicked = pyqtSignal(PartnersInfo)
|
||||||
|
|
||||||
|
def __init__(self, info: PartnersInfo):
|
||||||
|
super().__init__()
|
||||||
|
self.info = info
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
self.set_styles()
|
||||||
|
|
||||||
|
def mouseDoubleClickEvent(self, a0):
|
||||||
|
self.doubleClicked.emit(self.info)
|
||||||
|
return super().mouseDoubleClickEvent(a0)
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
# Верхняя строка: Тип | Наименование и скидка
|
||||||
|
header_layout = QHBoxLayout()
|
||||||
|
header_text = QLabel(f"{self.info.type_name} | {self.info.partner_name}")
|
||||||
|
header_text.setObjectName("partnerHeader")
|
||||||
|
discount_text = QLabel(f"{self.info.discount}%")
|
||||||
|
discount_text.setObjectName("partnerDiscount")
|
||||||
|
|
||||||
|
header_layout.addWidget(header_text)
|
||||||
|
header_layout.addWidget(discount_text, alignment=Qt.AlignmentFlag.AlignRight)
|
||||||
|
|
||||||
|
# Информация о директоре
|
||||||
|
director_text = QLabel(f"Директор")
|
||||||
|
director_text.setObjectName("fieldLabel")
|
||||||
|
director_name = QLabel(
|
||||||
|
f"{self.info.last_name_director} {self.info.first_name_director} {self.info.middle_name_director}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Контактная информация
|
||||||
|
phone_text = QLabel(f"+{self.info.phone_partner}")
|
||||||
|
|
||||||
|
# Рейтинг
|
||||||
|
rating_layout = QHBoxLayout()
|
||||||
|
rating_label = QLabel("Рейтинг:")
|
||||||
|
rating_label.setObjectName("fieldLabel")
|
||||||
|
rating_value = QLabel(str(self.info.rating))
|
||||||
|
rating_layout.addWidget(rating_label)
|
||||||
|
rating_layout.addWidget(rating_value)
|
||||||
|
rating_layout.addStretch()
|
||||||
|
|
||||||
|
# Добавляем все элементы в главный layout
|
||||||
|
main_layout.addLayout(header_layout)
|
||||||
|
main_layout.addWidget(director_text)
|
||||||
|
main_layout.addWidget(director_name)
|
||||||
|
main_layout.addWidget(phone_text)
|
||||||
|
main_layout.addLayout(rating_layout)
|
||||||
|
|
||||||
|
def set_styles(self):
|
||||||
|
self.setStyleSheet(
|
||||||
|
"""
|
||||||
|
PartnerCard {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
QLabel {
|
||||||
|
font-family: %s;
|
||||||
|
}
|
||||||
|
#partnerHeader {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: %s;
|
||||||
|
}
|
||||||
|
#partnerDiscount {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: %s;
|
||||||
|
}
|
||||||
|
#fieldLabel {
|
||||||
|
color: gray;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
% (MAIN_FONT, ACCENT_COLOR, SECONDARY_COLOR)
|
||||||
|
)
|
||||||
84
robbery/master_pol-module_1_2/app/database/db.py
Normal file
84
robbery/master_pol-module_1_2/app/database/db.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import pymysql as psql
|
||||||
|
from dto.partners_dto import PartnerUpdateDto
|
||||||
|
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self, host, user, password, db):
|
||||||
|
self.connection = psql.connect(
|
||||||
|
host=host,
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
database=db,
|
||||||
|
cursorclass=psql.cursors.DictCursor,
|
||||||
|
)
|
||||||
|
|
||||||
|
def authorize_user(self, username, password):
|
||||||
|
query = "SELECT * FROM users WHERE username=%s AND password=%s"
|
||||||
|
with self.connection.cursor() as cur:
|
||||||
|
cur.execute(query, (username, password))
|
||||||
|
result = cur.fetchone()
|
||||||
|
return result is not None
|
||||||
|
|
||||||
|
def execute_select(self, query, params=None):
|
||||||
|
"""Выполняет SELECT запрос и возвращает результаты"""
|
||||||
|
with self.connection.cursor() as cur:
|
||||||
|
if params:
|
||||||
|
cur.execute(query, params)
|
||||||
|
else:
|
||||||
|
cur.execute(query)
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
def get_partner_types(self):
|
||||||
|
"""Получает все типы партнеров из таблицы partner_types"""
|
||||||
|
query = "SELECT * FROM partners_type"
|
||||||
|
with self.connection.cursor() as cur:
|
||||||
|
cur.execute(query)
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
def update_partner(self, partners_info: PartnerUpdateDto):
|
||||||
|
with self.connection.cursor() as cur:
|
||||||
|
cur.callproc(
|
||||||
|
"upd_partner",
|
||||||
|
(
|
||||||
|
partners_info.partner_type_id,
|
||||||
|
partners_info.id,
|
||||||
|
partners_info.partner_name,
|
||||||
|
partners_info.first_name,
|
||||||
|
partners_info.last_name,
|
||||||
|
partners_info.middle_name,
|
||||||
|
partners_info.email,
|
||||||
|
partners_info.phone,
|
||||||
|
partners_info.address,
|
||||||
|
partners_info.inn,
|
||||||
|
partners_info.rating,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.connection.commit()
|
||||||
|
|
||||||
|
def get_disc(self, partner_name):
|
||||||
|
"""
|
||||||
|
Получает скидку для партнера, вызывая функцию get_disc из БД
|
||||||
|
"""
|
||||||
|
# Сначала получим ID партнера по его имени
|
||||||
|
query = "SELECT id FROM partners WHERE partner_name = %s"
|
||||||
|
with self.connection.cursor() as cur:
|
||||||
|
cur.execute(query, (partner_name,))
|
||||||
|
result = cur.fetchone()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Вызываем функцию get_disc из БД
|
||||||
|
query = "SELECT get_disc(%s) as discount"
|
||||||
|
cur.execute(query, (result["id"],))
|
||||||
|
discount_result = cur.fetchone()
|
||||||
|
|
||||||
|
return discount_result["discount"] if discount_result else 0
|
||||||
|
|
||||||
|
|
||||||
|
db = None
|
||||||
|
try:
|
||||||
|
db = Database(host="localhost", user="root", password="", db="master_pol")
|
||||||
|
print("Database connection established.")
|
||||||
|
except psql.MySQLError as e:
|
||||||
|
print(f"Error connecting to database: {e}")
|
||||||
460
robbery/master_pol-module_1_2/app/database/script.sql
Normal file
460
robbery/master_pol-module_1_2/app/database/script.sql
Normal file
|
|
@ -0,0 +1,460 @@
|
||||||
|
CREATE DATABASE master_pol;
|
||||||
|
use master_pol;
|
||||||
|
|
||||||
|
CREATE TABLE `partners` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`partner_type_id` INTEGER NOT NULL,
|
||||||
|
`partner_name` VARCHAR(255) NOT NULL,
|
||||||
|
`first_name_director` VARCHAR(50) NOT NULL,
|
||||||
|
`last_name_director` VARCHAR(50) NOT NULL,
|
||||||
|
`middle_name_director` VARCHAR(255),
|
||||||
|
`email_partner` VARCHAR(100) NOT NULL,
|
||||||
|
`phone_partner` VARCHAR(15) NOT NULL,
|
||||||
|
`address` VARCHAR(255) NOT NULL,
|
||||||
|
`INN` VARCHAR(10) NOT NULL,
|
||||||
|
`rating` INTEGER NOT NULL,
|
||||||
|
`logo` LONGBLOB,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `partners_type` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`name` VARCHAR(255),
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `products` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`article` VARCHAR(10) NOT NULL,
|
||||||
|
`name` VARCHAR(100) NOT NULL,
|
||||||
|
`product_type_id` INTEGER NOT NULL,
|
||||||
|
`description` VARCHAR(255),
|
||||||
|
`picture` LONGBLOB,
|
||||||
|
`min_price_partners` DECIMAL(10,2) NOT NULL,
|
||||||
|
`cert_quality` LONGBLOB,
|
||||||
|
`standard_number` VARCHAR(255),
|
||||||
|
`selfcost` DECIMAL(10,2),
|
||||||
|
`length` DECIMAL(10,2),
|
||||||
|
`width` DECIMAL(10,2),
|
||||||
|
`height` DECIMAL(10,2),
|
||||||
|
`weight_no_package` DECIMAL(10,2),
|
||||||
|
`weight_with_package` DECIMAL(10,2),
|
||||||
|
`time_to_create_min` INTEGER,
|
||||||
|
`workshop_number` INTEGER,
|
||||||
|
`people_count_production` INTEGER,
|
||||||
|
`product_current_stock` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `products_types` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`name` VARCHAR(70) NOT NULL,
|
||||||
|
`coefficent` DECIMAL(3,2) NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `product_partners` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`product_id` INTEGER NOT NULL,
|
||||||
|
`partner_id` INTEGER NOT NULL,
|
||||||
|
`amount` INTEGER NOT NULL,
|
||||||
|
`sale_date` DATE NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `employees` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`employee_type_id` INTEGER NOT NULL,
|
||||||
|
`first_name` VARCHAR(50) NOT NULL,
|
||||||
|
`last_name` VARCHAR(50) NOT NULL,
|
||||||
|
`middle_name` VARCHAR(60) NULL,
|
||||||
|
`birth_date` DATE NOT NULL,
|
||||||
|
`passport_data` VARCHAR(11) NOT NULL,
|
||||||
|
`bank_details` VARCHAR(100) NOT NULL,
|
||||||
|
`has_family` BOOLEAN,
|
||||||
|
`health_status` VARCHAR(25),
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `employees_types` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`name` VARCHAR(50) NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`username` VARCHAR(30) NOT NULL,
|
||||||
|
`password` VARCHAR(80) NOT NULL,
|
||||||
|
`employee_id` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `materials` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`material_type_id` INTEGER NOT NULL,
|
||||||
|
`supplier_id` INTEGER NOT NULL,
|
||||||
|
`name` VARCHAR(60) NOT NULL,
|
||||||
|
`package_quantity` INTEGER NOT NULL,
|
||||||
|
`unit` VARCHAR(20) NOT NULL,
|
||||||
|
`cost` DECIMAL(8,2) NOT NULL,
|
||||||
|
`image` LONGBLOB,
|
||||||
|
`min_stock` INTEGER,
|
||||||
|
`material_current_stock` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `materials_type` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`name` VARCHAR(50) NOT NULL,
|
||||||
|
`defect_percent` DECIMAL(10,2) NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `products_recipes` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`product_id` INTEGER NOT NULL,
|
||||||
|
`material_id` INTEGER NOT NULL,
|
||||||
|
`material_count` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `partners_rating_history` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`partner_id` INTEGER NOT NULL,
|
||||||
|
`new_rating` INTEGER NOT NULL,
|
||||||
|
`changed` DATETIME NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `orders` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`partner_id` INTEGER NOT NULL,
|
||||||
|
`manager_id` INTEGER NOT NULL,
|
||||||
|
`total_price` DECIMAL(10,2) NOT NULL,
|
||||||
|
`order_payment` DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||||
|
`created` DATETIME NOT NULL,
|
||||||
|
`status` ENUM('created', 'waiting prepayment', 'prepayment received', 'completed', 'canceled', 'ready for shipment', 'pending', 'in production') NOT NULL,
|
||||||
|
`prepayment_date` DATETIME,
|
||||||
|
`payment_date` DATETIME,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `products_orders` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`order_id` INTEGER NOT NULL,
|
||||||
|
`product_id` INTEGER NOT NULL,
|
||||||
|
`quantity` INTEGER NOT NULL,
|
||||||
|
`agreed_price_per` DECIMAL(8,2),
|
||||||
|
`production_date` DATE,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `suppliers` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`name` VARCHAR(50) NOT NULL,
|
||||||
|
`INN` VARCHAR(10) NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `materials_supply_history` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`material_id` INTEGER NOT NULL,
|
||||||
|
`supplier_id` INTEGER NOT NULL,
|
||||||
|
`quantity` INTEGER NOT NULL,
|
||||||
|
`delivery_date` DATE NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `materials_movement` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`material_id` INTEGER NOT NULL,
|
||||||
|
`amount` INTEGER NOT NULL,
|
||||||
|
`movement_type` ENUM('incoming', 'reserve', 'write off') NOT NULL DEFAULT 'incoming',
|
||||||
|
`movement_date` DATETIME NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `employees_access` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
`employee_id` INTEGER NOT NULL,
|
||||||
|
`door_id` INTEGER NOT NULL,
|
||||||
|
`access_date` DATETIME NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE `partners`
|
||||||
|
ADD FOREIGN KEY(`partner_type_id`) REFERENCES `partners_type`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `products`
|
||||||
|
ADD FOREIGN KEY(`product_type_id`) REFERENCES `products_types`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `product_partners`
|
||||||
|
ADD FOREIGN KEY(`product_id`) REFERENCES `products`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `product_partners`
|
||||||
|
ADD FOREIGN KEY(`partner_id`) REFERENCES `partners`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `employees`
|
||||||
|
ADD FOREIGN KEY(`employee_type_id`) REFERENCES `employees_types`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD FOREIGN KEY(`employee_id`) REFERENCES `employees`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `materials`
|
||||||
|
ADD FOREIGN KEY(`material_type_id`) REFERENCES `materials_type`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `products_recipes`
|
||||||
|
ADD FOREIGN KEY(`product_id`) REFERENCES `products`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `products_recipes`
|
||||||
|
ADD FOREIGN KEY(`material_id`) REFERENCES `materials`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `partners_rating_history`
|
||||||
|
ADD FOREIGN KEY(`partner_id`) REFERENCES `partners`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `orders`
|
||||||
|
ADD FOREIGN KEY(`partner_id`) REFERENCES `partners`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `orders`
|
||||||
|
ADD FOREIGN KEY(`manager_id`) REFERENCES `employees`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `products_orders`
|
||||||
|
ADD FOREIGN KEY(`order_id`) REFERENCES `orders`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `products_orders`
|
||||||
|
ADD FOREIGN KEY(`product_id`) REFERENCES `products`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `materials`
|
||||||
|
ADD FOREIGN KEY(`supplier_id`) REFERENCES `suppliers`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `materials_supply_history`
|
||||||
|
ADD FOREIGN KEY(`material_id`) REFERENCES `materials`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `materials_supply_history`
|
||||||
|
ADD FOREIGN KEY(`supplier_id`) REFERENCES `suppliers`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `materials_movement`
|
||||||
|
ADD FOREIGN KEY(`material_id`) REFERENCES `materials`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
ALTER TABLE `employees_access`
|
||||||
|
ADD FOREIGN KEY(`employee_id`) REFERENCES `employees`(`id`)
|
||||||
|
ON UPDATE NO ACTION ON DELETE NO ACTION;
|
||||||
|
|
||||||
|
INSERT INTO materials_type (name, defect_percent) VALUES
|
||||||
|
('Тип материала 1', 0.001),
|
||||||
|
('Тип материала 2', 0.0095),
|
||||||
|
('Тип материала 3', 0.0028),
|
||||||
|
('Тип материала 4', 0.0055),
|
||||||
|
('Тип материала 5', 0.0034);
|
||||||
|
|
||||||
|
INSERT INTO products_types (name, coefficent) VALUES
|
||||||
|
('Ламинат', 2.35),
|
||||||
|
('Массивная доска', 5.15),
|
||||||
|
('Паркетная доска', 4.34),
|
||||||
|
('Пробковое покрытие', 1.5);
|
||||||
|
|
||||||
|
INSERT INTO partners_type (name) VALUES
|
||||||
|
('ЗАО'),
|
||||||
|
('ООО'),
|
||||||
|
('ПАО'),
|
||||||
|
('ОАО');
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO partners (partner_type_id, partner_name, first_name_director, last_name_director, middle_name_director, email_partner, phone_partner, address, INN, rating) VALUES
|
||||||
|
(1, 'База Строитель', 'Александра', 'Иванова', 'Ивановна', 'aleksandraivanova@ml.ru', '4931234567', '652050, Кемеровская область, город Юрга, ул. Лесная, 15', '2222455179', 7),
|
||||||
|
(2, 'Паркет 29', 'Василий', 'Петров', 'Петрович', 'vppetrov@vl.ru', '9871235678', '164500, Архангельская область, город Северодвинск, ул. Строителей, 18', '3333888520', 7),
|
||||||
|
(3, 'Стройсервис', 'Андрей', 'Соловьев', 'Николаевич', 'ansolovev@st.ru', '8122233200', '188910, Ленинградская область, город Приморск, ул. Парковая, 21', '4440391035', 7),
|
||||||
|
(4, 'Ремонт и отделка', 'Екатерина', 'Воробьева', 'Валерьевна', 'ekaterina.vorobeva@ml.ru', '4442223311', '143960, Московская область, город Реутов, ул. Свободы, 51', '1111520857', 5),
|
||||||
|
(1, 'МонтажПро', 'Степан', 'Степанов', 'Сергеевич', 'stepanov@stepan.ru', '9128883333', '309500, Белгородская область, город Старый Оскол, ул. Рабочая, 122', '5552431140', 10);
|
||||||
|
|
||||||
|
INSERT INTO products (article, name, product_type_id, min_price_partners) VALUES
|
||||||
|
('8758385', 'Паркетная доска Ясень темный однополосная 14 мм', 3, 4456.90),
|
||||||
|
('8858958', 'Инженерная доска Дуб Французская елка однополосная 12 мм', 3, 7330.99),
|
||||||
|
('7750282', 'Ламинат Дуб дымчато-белый 33 класс 12 мм', 1, 1799.33),
|
||||||
|
('7028748', 'Ламинат Дуб серый 32 класс 8 мм с фаской', 1, 3890.41),
|
||||||
|
('5012543', 'Пробковое напольное клеевое покрытие 32 класс 4 мм', 4, 5450.59);
|
||||||
|
|
||||||
|
INSERT INTO product_partners (product_id, partner_id, amount, sale_date) VALUES
|
||||||
|
(1, 1, 15500, '2023-03-23'),
|
||||||
|
(3, 1, 12350, '2023-12-18'),
|
||||||
|
(4, 1, 37400, '2024-06-07'),
|
||||||
|
(2, 2, 35000, '2022-12-02'),
|
||||||
|
(5, 2, 1250, '2023-05-17'),
|
||||||
|
(3, 2, 1000, '2024-06-07'),
|
||||||
|
(1, 2, 7550, '2024-07-01'),
|
||||||
|
(1, 3, 7250, '2023-01-22'),
|
||||||
|
(2, 3, 2500, '2024-07-05'),
|
||||||
|
(4, 4, 59050, '2023-03-20'),
|
||||||
|
(3, 4, 37200, '2024-03-12'),
|
||||||
|
(5, 4, 4500, '2024-05-14'),
|
||||||
|
(3, 5, 50000, '2023-09-19'),
|
||||||
|
(4, 5, 670000, '2023-11-10'),
|
||||||
|
(1, 5, 35000, '2024-04-15'),
|
||||||
|
(2, 5, 25000, '2024-06-12');
|
||||||
|
|
||||||
|
-- === 1. Типы сотрудников ===
|
||||||
|
INSERT INTO employees_types (name)
|
||||||
|
VALUES
|
||||||
|
('Менеджер'),
|
||||||
|
('Бухгалтер'),
|
||||||
|
('Программист'),
|
||||||
|
('Охранник'),
|
||||||
|
('Уборщик');
|
||||||
|
|
||||||
|
-- === 2. Сотрудники ===
|
||||||
|
INSERT INTO employees (
|
||||||
|
employee_type_id, first_name, last_name, middle_name, birth_date,
|
||||||
|
passport_data, bank_details, has_family, health_status
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
-- Менеджеры
|
||||||
|
(1, 'Иван', 'Петров', 'Сергеевич', '1988-03-15', '40051234567', '123456789', TRUE, 'Хорошее'),
|
||||||
|
(1, 'Мария', 'Сидорова', 'Игоревна', '1990-11-02', '40057891234', '987654321', FALSE, 'Отличное'),
|
||||||
|
|
||||||
|
-- Программист
|
||||||
|
(3, 'Андрей', 'Кузнецов', 'Алексеевич', '1995-07-21', '40101234567', '111122223333', TRUE, 'Хорошее'),
|
||||||
|
|
||||||
|
-- Бухгалтер
|
||||||
|
(2, 'Елена', 'Морозова', 'Павловна', '1982-05-08', '40104561234', '444455556666', TRUE, 'Удовлетворительное'),
|
||||||
|
|
||||||
|
-- Охранник
|
||||||
|
(4, 'Сергей', 'Волков', 'Владимирович', '1979-09-10', '40205678901', '555566667777', FALSE, 'Хорошее'),
|
||||||
|
|
||||||
|
-- Уборщик
|
||||||
|
(5, 'Наталья', 'Орлова', 'Геннадьевна', '1975-12-25', '40307891234', '888899990000', TRUE, 'Хорошее');
|
||||||
|
|
||||||
|
-- === 3. Пользователи ===
|
||||||
|
-- Пользователи, связанные с менеджерами
|
||||||
|
INSERT INTO users (username, password, employee_id)
|
||||||
|
VALUES
|
||||||
|
('ivan', 'test', 1),
|
||||||
|
('manager_maria', 'hashed_password_456', 2);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE VIEW show_partners
|
||||||
|
AS
|
||||||
|
SELECT p.id, pt.name AS type_name, p.partner_name, p.first_name_director, p.last_name_director, p.middle_name_director, p.phone_partner, p.rating
|
||||||
|
FROM partners p JOIN partners_type pt
|
||||||
|
ON
|
||||||
|
p.partner_type_id = pt.id;
|
||||||
|
|
||||||
|
|
||||||
|
DELIMITER //
|
||||||
|
CREATE PROCEDURE add_parther (IN p_partner_type_id INT, IN p_partner_name VARCHAR(255),
|
||||||
|
IN p_first_name_director VARCHAR(50), IN p_last_name_director VARCHAR(50), IN p_middle_name_director VARCHAR(255),
|
||||||
|
IN p_email_partner VARCHAR(100), IN p_phone_partner VARCHAR(15), IN p_address VARCHAR(255), IN p_INN VARCHAR(10), IN p_rating INT)
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO partners (
|
||||||
|
partner_type_id,
|
||||||
|
partner_name,
|
||||||
|
first_name_director,
|
||||||
|
last_name_director,
|
||||||
|
middle_name_director,
|
||||||
|
email_partner,
|
||||||
|
phone_partner,
|
||||||
|
address,
|
||||||
|
INN,
|
||||||
|
rating
|
||||||
|
) VALUES (
|
||||||
|
p_partner_type_id,
|
||||||
|
p_partner_name,
|
||||||
|
p_first_name_director,
|
||||||
|
p_last_name_director,
|
||||||
|
p_middle_name_director,
|
||||||
|
p_email_partner,
|
||||||
|
p_phone_partner,
|
||||||
|
p_address,
|
||||||
|
p_INN,
|
||||||
|
p_rating
|
||||||
|
);
|
||||||
|
END //
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
|
||||||
|
DELIMITER //
|
||||||
|
|
||||||
|
CREATE PROCEDURE upd_partner (IN p_partner_type_id INT, IN p_id INT, IN p_partner_name VARCHAR(255),
|
||||||
|
IN p_first_name_director VARCHAR(50), IN p_last_name_director VARCHAR(50), IN p_middle_name_director VARCHAR(255),
|
||||||
|
IN p_email_partner VARCHAR(100), IN p_phone_partner VARCHAR(15), IN p_address VARCHAR(255), IN p_INN VARCHAR(10), IN p_rating INT)
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
UPDATE partners
|
||||||
|
SET
|
||||||
|
partner_type_id = p_partner_type_id,
|
||||||
|
partner_name = p_partner_name,
|
||||||
|
first_name_director = p_first_name_director,
|
||||||
|
last_name_director = p_last_name_director,
|
||||||
|
middle_name_director = p_middle_name_director,
|
||||||
|
email_partner = p_email_partner,
|
||||||
|
phone_partner = p_phone_partner,
|
||||||
|
address = p_address,
|
||||||
|
INN = p_INN,
|
||||||
|
rating = p_rating
|
||||||
|
WHERE id = p_id;
|
||||||
|
|
||||||
|
END //
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
|
||||||
|
DELIMITER //
|
||||||
|
|
||||||
|
CREATE FUNCTION get_disc(partner_id INT)
|
||||||
|
RETURNS INT
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
DECLARE total_amount INT;
|
||||||
|
|
||||||
|
SELECT SUM(amount) INTO total_amount
|
||||||
|
FROM product_partners
|
||||||
|
WHERE partner_id = partner_id;
|
||||||
|
|
||||||
|
IF total_amount >= 300000 THEN RETURN 15;
|
||||||
|
ELSEIF total_amount >= 50000 THEN RETURN 10;
|
||||||
|
ELSEIF total_amount >= 10000 THEN RETURN 5;
|
||||||
|
ELSE RETURN 0;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END //
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
DELIMITER //
|
||||||
|
|
||||||
|
CREATE PROCEDURE partner_history(IN p_partner_id INT)
|
||||||
|
BEGIN
|
||||||
|
SELECT
|
||||||
|
pr.name AS product_name,
|
||||||
|
pp.amount AS quantity,
|
||||||
|
pp.sale_date AS sale_date
|
||||||
|
FROM product_partners pp JOIN products pr
|
||||||
|
ON
|
||||||
|
pp.product_id = pr.id
|
||||||
|
WHERE pp.partner_id = p_partner_id
|
||||||
|
ORDER BY pp.sale_date DESC;
|
||||||
|
END//
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
29
robbery/master_pol-module_1_2/app/dto/partners_dto.py
Normal file
29
robbery/master_pol-module_1_2/app/dto/partners_dto.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PartnersInfo:
|
||||||
|
id: int
|
||||||
|
type_name: str
|
||||||
|
partner_name: str
|
||||||
|
first_name_director: str
|
||||||
|
last_name_director: str
|
||||||
|
middle_name_director: str
|
||||||
|
phone_partner: str
|
||||||
|
rating: int
|
||||||
|
discount: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PartnerUpdateDto:
|
||||||
|
id: int
|
||||||
|
partner_type_id: int
|
||||||
|
partner_name: str
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
middle_name: str
|
||||||
|
email: str
|
||||||
|
phone: str
|
||||||
|
address: str
|
||||||
|
inn: str
|
||||||
|
rating: int
|
||||||
11
robbery/master_pol-module_1_2/app/main.py
Normal file
11
robbery/master_pol-module_1_2/app/main.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
from PyQt6.QtGui import QIcon
|
||||||
|
from pages.auth_page import AuthPage
|
||||||
|
|
||||||
|
app = QApplication([])
|
||||||
|
|
||||||
|
app.setWindowIcon(QIcon("app/res/imgs/master_pol.ico"))
|
||||||
|
start_page = AuthPage()
|
||||||
|
start_page.show()
|
||||||
|
|
||||||
|
app.exec()
|
||||||
94
robbery/master_pol-module_1_2/app/pages/auth_page.py
Normal file
94
robbery/master_pol-module_1_2/app/pages/auth_page.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget,
|
||||||
|
QLabel,
|
||||||
|
QFormLayout,
|
||||||
|
QPushButton,
|
||||||
|
QMessageBox,
|
||||||
|
QLineEdit,
|
||||||
|
QVBoxLayout,
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from res.colors import ACCENT_COLOR, SECONDARY_COLOR, ACCENT_COLOR_HOVER
|
||||||
|
from res.fonts import MAIN_FONT
|
||||||
|
|
||||||
|
|
||||||
|
class AuthPage(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setup_window()
|
||||||
|
self.init_ui()
|
||||||
|
self.set_styles()
|
||||||
|
|
||||||
|
def setup_window(self):
|
||||||
|
self.setWindowTitle("Авторизация")
|
||||||
|
self.setFixedSize(400, 250)
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
self.main_layout = QVBoxLayout()
|
||||||
|
self.form_layout: QFormLayout = QFormLayout()
|
||||||
|
|
||||||
|
self.title = QLabel("Авторизация")
|
||||||
|
self.title.setObjectName("title")
|
||||||
|
|
||||||
|
self.username_label = QLabel("Логин:")
|
||||||
|
self.password_label = QLabel("Пароль:")
|
||||||
|
|
||||||
|
self.username_input = QLineEdit()
|
||||||
|
self.password_input = QLineEdit()
|
||||||
|
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
|
|
||||||
|
self.login_button = QPushButton("Войти")
|
||||||
|
|
||||||
|
self.form_layout.addRow(self.username_label, self.username_input)
|
||||||
|
self.form_layout.addRow(self.password_label, self.password_input)
|
||||||
|
self.form_layout.addRow(self.login_button)
|
||||||
|
|
||||||
|
self.setLayout(self.main_layout)
|
||||||
|
self.main_layout.addWidget(self.title, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
|
||||||
|
self.main_layout.addStretch()
|
||||||
|
self.main_layout.addLayout(self.form_layout)
|
||||||
|
self.main_layout.addStretch()
|
||||||
|
|
||||||
|
self.login_button.clicked.connect(self.handle_login)
|
||||||
|
|
||||||
|
def handle_login(self):
|
||||||
|
username = self.username_input.text()
|
||||||
|
password = self.password_input.text()
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
QMessageBox.warning(self, "Ошибка", "Пожалуйста, заполните все поля.")
|
||||||
|
return
|
||||||
|
|
||||||
|
from pages.partners_page import PartnersPage
|
||||||
|
|
||||||
|
self.partners_page = PartnersPage()
|
||||||
|
self.partners_page.show()
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def set_styles(self):
|
||||||
|
self.setStyleSheet(
|
||||||
|
"""QLabel { font-size: 16px; font-family: %(MAIN_FONT)s}
|
||||||
|
#title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: %(ACCENT_COLOR)s;
|
||||||
|
}
|
||||||
|
QPushButton {
|
||||||
|
background-color: %(ACCENT_COLOR)s;
|
||||||
|
border: 1px solid black;
|
||||||
|
color: %(SECONDARY_COLOR)s;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: %(ACCENT_COLOR_HOVER)s;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
% {
|
||||||
|
"ACCENT_COLOR": ACCENT_COLOR,
|
||||||
|
"SECONDARY_COLOR": SECONDARY_COLOR,
|
||||||
|
"MAIN_FONT": MAIN_FONT,
|
||||||
|
"ACCENT_COLOR_HOVER": ACCENT_COLOR_HOVER,
|
||||||
|
}
|
||||||
|
)
|
||||||
130
robbery/master_pol-module_1_2/app/pages/partners_page.py
Normal file
130
robbery/master_pol-module_1_2/app/pages/partners_page.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QScrollArea, QVBoxLayout
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
from components.partner_card import PartnerCard, PartnersInfo
|
||||||
|
from res.colors import ACCENT_COLOR
|
||||||
|
|
||||||
|
|
||||||
|
class PartnersPage(QWidget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setup_window()
|
||||||
|
self.init_ui()
|
||||||
|
self.load_partners()
|
||||||
|
|
||||||
|
def setup_window(self):
|
||||||
|
self.setWindowTitle("Партнеры")
|
||||||
|
self.resize(800, 600)
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
# Заголовок
|
||||||
|
title = QLabel("Партнеры")
|
||||||
|
title.setObjectName("title")
|
||||||
|
title.setStyleSheet(
|
||||||
|
f"""
|
||||||
|
#title {{
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: {ACCENT_COLOR};
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
main_layout.addWidget(title, alignment=Qt.AlignmentFlag.AlignHCenter)
|
||||||
|
|
||||||
|
# Создаем область прокрутки
|
||||||
|
scroll_area = QScrollArea()
|
||||||
|
scroll_area.setWidgetResizable(True)
|
||||||
|
scroll_content = QWidget()
|
||||||
|
self.partners_layout = QVBoxLayout(scroll_content)
|
||||||
|
scroll_area.setWidget(scroll_content)
|
||||||
|
main_layout.addWidget(scroll_area)
|
||||||
|
|
||||||
|
def handle_partner_double_click(self, partner_info: PartnersInfo):
|
||||||
|
from components.edit_partner_dialog import EditPartnerDialog
|
||||||
|
|
||||||
|
dialog = EditPartnerDialog(partner_info, self)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
|
def load_partners(self):
|
||||||
|
# Тестовые данные партнеров
|
||||||
|
test_partners = [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type_name": "Золотой партнер",
|
||||||
|
"partner_name": "ООО 'ТехноПрофи'",
|
||||||
|
"first_name_director": "Иван",
|
||||||
|
"last_name_director": "Петров",
|
||||||
|
"middle_name_director": "Сергеевич",
|
||||||
|
"phone_partner": "+7 (495) 123-45-67",
|
||||||
|
"rating": 4.8,
|
||||||
|
"discount": 15.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type_name": "Серебряный партнер",
|
||||||
|
"partner_name": "ИП Сидоров А.В.",
|
||||||
|
"first_name_director": "Алексей",
|
||||||
|
"last_name_director": "Сидоров",
|
||||||
|
"middle_name_director": "Викторович",
|
||||||
|
"phone_partner": "+7 (495) 234-56-78",
|
||||||
|
"rating": 4.2,
|
||||||
|
"discount": 10.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type_name": "Бронзовый партнер",
|
||||||
|
"partner_name": "ООО 'СтройМастер'",
|
||||||
|
"first_name_director": "Мария",
|
||||||
|
"last_name_director": "Иванова",
|
||||||
|
"middle_name_director": "Олеговна",
|
||||||
|
"phone_partner": "+7 (495) 345-67-89",
|
||||||
|
"rating": 3.9,
|
||||||
|
"discount": 7.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type_name": "Золотой партнер",
|
||||||
|
"partner_name": "АО 'ПромИнвест'",
|
||||||
|
"first_name_director": "Сергей",
|
||||||
|
"last_name_director": "Козлов",
|
||||||
|
"middle_name_director": "Анатольевич",
|
||||||
|
"phone_partner": "+7 (495) 456-78-90",
|
||||||
|
"rating": 4.9,
|
||||||
|
"discount": 18.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type_name": "Стандартный партнер",
|
||||||
|
"partner_name": "ООО 'ТоргСервис'",
|
||||||
|
"first_name_director": "Ольга",
|
||||||
|
"last_name_director": "Смирнова",
|
||||||
|
"middle_name_director": "Дмитриевна",
|
||||||
|
"phone_partner": "+7 (495) 567-89-01",
|
||||||
|
"rating": 3.5,
|
||||||
|
"discount": 5.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# Создаем карточки партнеров на основе тестовых данных
|
||||||
|
for partner in test_partners:
|
||||||
|
partner_info = PartnersInfo(
|
||||||
|
id=partner["id"],
|
||||||
|
type_name=partner["type_name"],
|
||||||
|
partner_name=partner["partner_name"],
|
||||||
|
first_name_director=partner["first_name_director"],
|
||||||
|
last_name_director=partner["last_name_director"],
|
||||||
|
middle_name_director=partner["middle_name_director"],
|
||||||
|
phone_partner=partner["phone_partner"],
|
||||||
|
rating=partner["rating"],
|
||||||
|
discount=partner["discount"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Создаем и добавляем карточку партнера
|
||||||
|
partner_card = PartnerCard(partner_info)
|
||||||
|
partner_card.doubleClicked.connect(self.handle_partner_double_click)
|
||||||
|
self.partners_layout.addWidget(partner_card)
|
||||||
|
|
||||||
|
self.partners_layout.addStretch()
|
||||||
4
robbery/master_pol-module_1_2/app/res/colors.py
Normal file
4
robbery/master_pol-module_1_2/app/res/colors.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
MAIN_COLOR = "#FFFFFF"
|
||||||
|
SECONDARY_COLOR = "#F4E8D3"
|
||||||
|
ACCENT_COLOR = "#67BA80"
|
||||||
|
ACCENT_COLOR_HOVER = "#529265"
|
||||||
1
robbery/master_pol-module_1_2/app/res/fonts.py
Normal file
1
robbery/master_pol-module_1_2/app/res/fonts.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
MAIN_FONT = "Segoe UI"
|
||||||
BIN
robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico
Normal file
BIN
robbery/master_pol-module_1_2/app/res/imgs/master_pol.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
robbery/master_pol-module_1_2/app/res/imgs/master_pol.png
Normal file
BIN
robbery/master_pol-module_1_2/app/res/imgs/master_pol.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
35
robbery/master_pol-module_1_2/app/res/styles.py
Normal file
35
robbery/master_pol-module_1_2/app/res/styles.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
from string import Template
|
||||||
|
from res.colors import MAIN_COLOR, SECONDARY_COLOR, ACCENT_COLOR
|
||||||
|
from res.fonts import MAIN_FONT
|
||||||
|
|
||||||
|
styles_template = Template(
|
||||||
|
"""
|
||||||
|
QWidget {
|
||||||
|
font-family: {MAIN_FONT};
|
||||||
|
background-color: {MAIN_COLOR}
|
||||||
|
color: {SECONDARY_COLOR};
|
||||||
|
}
|
||||||
|
QPushButton {
|
||||||
|
background-color: {ACCENT_COLOR};
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: {SECONDARY_COLOR};
|
||||||
|
}
|
||||||
|
QLineEdit {
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid {ACCENT_COLOR};
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
styles = styles_template.substitute(
|
||||||
|
MAIN_FONT=MAIN_FONT,
|
||||||
|
MAIN_COLOR=MAIN_COLOR,
|
||||||
|
SECONDARY_COLOR=SECONDARY_COLOR,
|
||||||
|
ACCENT_COLOR=ACCENT_COLOR,
|
||||||
|
)
|
||||||
BIN
robbery/master_pol-module_1_2/requirements.txt
Normal file
BIN
robbery/master_pol-module_1_2/requirements.txt
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue