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