crappy
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
@@ -10,10 +12,10 @@ session_maker = sessionmaker(
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False
|
||||
autoflush=False,
|
||||
)
|
||||
|
||||
|
||||
async def get_session() -> AsyncSession:
|
||||
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async with session_maker() as session:
|
||||
yield session
|
||||
|
||||
3
main.py
3
main.py
@@ -19,7 +19,8 @@ app.add_middleware(
|
||||
routers_list = [
|
||||
routers.auth_router,
|
||||
routers.deal_router,
|
||||
routers.client_router
|
||||
routers.client_router,
|
||||
routers.service_router
|
||||
]
|
||||
for router in routers_list:
|
||||
app.include_router(router)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
from sqlalchemy.orm import configure_mappers
|
||||
|
||||
from .auth import *
|
||||
from .deal import *
|
||||
from .client import *
|
||||
from .service import *
|
||||
from .secondary import *
|
||||
|
||||
configure_mappers()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
BaseModel = declarative_base()
|
||||
metadata = BaseModel.metadata
|
||||
@@ -1,5 +1,5 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, BigInteger
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
|
||||
from models import BaseModel
|
||||
|
||||
@@ -17,7 +17,7 @@ class ClientDetails(BaseModel):
|
||||
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
|
||||
|
||||
client_id = Column(Integer, ForeignKey('clients.id'), unique=True, nullable=False, comment='ID клиента')
|
||||
client = relationship('Client', backref='details', uselist=False)
|
||||
client = relationship('Client', backref=backref('details', uselist=False))
|
||||
|
||||
address = Column(String)
|
||||
phone_number = Column(String)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from enum import IntEnum, unique
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.base import BaseModel
|
||||
from models.secondary import deal_services
|
||||
|
||||
|
||||
@unique
|
||||
@@ -29,6 +30,11 @@ class Deal(BaseModel):
|
||||
|
||||
status_history = relationship('DealStatusHistory', back_populates='deal', cascade="all, delete-orphan")
|
||||
|
||||
is_deleted = Column(Boolean, nullable=False, server_default='0', default=False, comment='Удалена')
|
||||
is_completed = Column(Boolean, nullable=False, server_default='0', default=False, comment='Завершена')
|
||||
|
||||
services = relationship('Service', secondary=deal_services)
|
||||
|
||||
|
||||
class DealStatusHistory(BaseModel):
|
||||
__tablename__ = 'deals_status_history'
|
||||
|
||||
10
models/secondary.py
Normal file
10
models/secondary.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from sqlalchemy import Table, Column, Integer, ForeignKey
|
||||
|
||||
from models.base import metadata
|
||||
|
||||
deal_services = Table(
|
||||
'deal_services', metadata,
|
||||
Column('deal_id', Integer, ForeignKey('deals.id')),
|
||||
Column('service_id', Integer, ForeignKey('services.id')),
|
||||
Column('quantity', Integer)
|
||||
)
|
||||
21
models/service.py
Normal file
21
models/service.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Double
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models import BaseModel
|
||||
|
||||
|
||||
class Service(BaseModel):
|
||||
__tablename__ = 'services'
|
||||
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False, comment='Название услуги')
|
||||
|
||||
category_id = Column(Integer, ForeignKey('service_categories.id'), nullable=False, comment='ID категории услуги')
|
||||
category = relationship('ServiceCategory')
|
||||
|
||||
price = Column(Double, nullable=False, comment='Стоимость услуги')
|
||||
|
||||
|
||||
class ServiceCategory(BaseModel):
|
||||
__tablename__ = 'service_categories'
|
||||
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False)
|
||||
@@ -16,4 +16,5 @@ alembic
|
||||
# Other
|
||||
python-dotenv
|
||||
aiohttp
|
||||
aiohttp[speedups]
|
||||
aiohttp[speedups]
|
||||
openpyxl
|
||||
@@ -1,4 +1,5 @@
|
||||
from .auth import auth_router
|
||||
from .deal import deal_router
|
||||
from .client import client_router
|
||||
from .service import service_router
|
||||
|
||||
|
||||
@@ -39,7 +39,11 @@ async def update_client_details(
|
||||
return ClientUpdateDetailsResponse(ok=True)
|
||||
|
||||
|
||||
@client_router.get('/get-all', operation_id='get_all_clients', response_model=ClientGetAllResponse)
|
||||
@client_router.get(
|
||||
'/get-all',
|
||||
operation_id='get_all_clients',
|
||||
response_model=ClientGetAllResponse
|
||||
)
|
||||
async def get_all_clients(
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
):
|
||||
|
||||
@@ -40,3 +40,13 @@ async def change_status(
|
||||
user: Annotated[User, Depends(get_current_user)]
|
||||
):
|
||||
return await DealService(session).change_status(request, user)
|
||||
|
||||
|
||||
@deal_router.get('/summaries',
|
||||
response_model=DealSummaryResponse,
|
||||
operation_id='getDealSummaries'
|
||||
)
|
||||
async def get_summary(
|
||||
session: Annotated[AsyncSession, Depends(get_session)]
|
||||
):
|
||||
return await DealService(session).get_summary()
|
||||
|
||||
59
routers/service.py
Normal file
59
routers/service.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.session import get_session
|
||||
from schemas.services import *
|
||||
from services.service import ServiceService
|
||||
|
||||
service_router = APIRouter(
|
||||
prefix="/service",
|
||||
tags=['service']
|
||||
)
|
||||
|
||||
|
||||
@service_router.get(
|
||||
'/get-all',
|
||||
response_model=ServiceGetAllResponse,
|
||||
operation_id="get_all_services"
|
||||
)
|
||||
async def get_all(
|
||||
session: Annotated[AsyncSession, Depends(get_session)]
|
||||
):
|
||||
return await ServiceService(session).get_all()
|
||||
|
||||
|
||||
@service_router.post(
|
||||
'/create',
|
||||
response_model=ServiceCreateResponse,
|
||||
operation_id="create_service"
|
||||
)
|
||||
async def create(
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
request: ServiceCreateRequest
|
||||
):
|
||||
return await ServiceService(session).create(request)
|
||||
|
||||
|
||||
@service_router.get(
|
||||
'/categories/get-all',
|
||||
response_model=ServiceGetAllCategoriesResponse,
|
||||
operation_id="get_all_service_categories"
|
||||
)
|
||||
async def get_all_categories(
|
||||
session: Annotated[AsyncSession, Depends(get_session)]
|
||||
):
|
||||
return await ServiceService(session).get_all_categories()
|
||||
|
||||
|
||||
@service_router.post(
|
||||
'/categories/create',
|
||||
response_model=ServiceCreateCategoryResponse,
|
||||
operation_id="create_service_category"
|
||||
)
|
||||
async def create_category(
|
||||
session: Annotated[AsyncSession, Depends(get_session)],
|
||||
request: ServiceCreateCategoryRequest
|
||||
):
|
||||
return await ServiceService(session).create_category(request)
|
||||
@@ -10,3 +10,7 @@ class CustomModel(BaseModel):
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class OkMessageSchema(BaseModel):
|
||||
ok: bool
|
||||
message: str
|
||||
|
||||
@@ -3,18 +3,17 @@ from typing import List
|
||||
from schemas.base import CustomModel
|
||||
|
||||
|
||||
class ClientSchema(CustomModel):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
class ClientDetailsSchema(CustomModel):
|
||||
address: str | None = None
|
||||
phone_number: str | None = None
|
||||
inn: int | None = None
|
||||
email: str | None = None
|
||||
|
||||
# TODO add email validation
|
||||
|
||||
class ClientSchema(CustomModel):
|
||||
id: int
|
||||
name: str
|
||||
details: ClientDetailsSchema | None = None
|
||||
|
||||
|
||||
class ClientSearchRequest(CustomModel):
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
import datetime
|
||||
from typing import List
|
||||
|
||||
from schemas.base import CustomModel
|
||||
from schemas.client import ClientSchema
|
||||
|
||||
|
||||
# region Entities
|
||||
class FastDeal(CustomModel):
|
||||
name: str
|
||||
client: ClientSchema
|
||||
comment: str
|
||||
acceptance_date: datetime.datetime
|
||||
|
||||
|
||||
class DealSummary(CustomModel):
|
||||
id: int
|
||||
name: str
|
||||
client_name: str
|
||||
changed_at: datetime.datetime
|
||||
status: int
|
||||
|
||||
|
||||
# endregion Entities
|
||||
|
||||
# region Requests
|
||||
class DealChangeStatusRequest(CustomModel):
|
||||
deal_id: int
|
||||
new_status: int
|
||||
|
||||
|
||||
class DealChangeStatusResponse(CustomModel):
|
||||
ok: bool
|
||||
|
||||
|
||||
class DealCreateRequest(CustomModel):
|
||||
name: str
|
||||
|
||||
@@ -25,16 +41,27 @@ class DealQuickCreateRequest(CustomModel):
|
||||
acceptance_date: datetime.datetime
|
||||
|
||||
|
||||
class DealQuickCreateResponse(CustomModel):
|
||||
deal_id: int
|
||||
class DealSummaryRequest(CustomModel):
|
||||
pass
|
||||
|
||||
|
||||
# endregion Requests
|
||||
|
||||
# region Responses
|
||||
|
||||
class DealChangeStatusResponse(CustomModel):
|
||||
ok: bool
|
||||
|
||||
|
||||
class DealCreateResponse(CustomModel):
|
||||
ok: bool
|
||||
|
||||
|
||||
class FastDeal(CustomModel):
|
||||
name: str
|
||||
client: ClientSchema
|
||||
comment: str
|
||||
acceptance_date: datetime.datetime
|
||||
class DealQuickCreateResponse(CustomModel):
|
||||
deal_id: int
|
||||
|
||||
|
||||
class DealSummaryResponse(CustomModel):
|
||||
summaries: List[DealSummary]
|
||||
|
||||
# endregion Responses
|
||||
|
||||
49
schemas/services.py
Normal file
49
schemas/services.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from typing import List
|
||||
|
||||
from schemas.base import CustomModel, OkMessageSchema
|
||||
|
||||
|
||||
# region Entities
|
||||
class ServiceCategorySchema(CustomModel):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
|
||||
class ServiceSchema(CustomModel):
|
||||
id: int
|
||||
name: str
|
||||
category: ServiceCategorySchema
|
||||
price: float
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Requests
|
||||
class ServiceCreateRequest(CustomModel):
|
||||
service: ServiceSchema
|
||||
|
||||
|
||||
class ServiceCreateCategoryRequest(CustomModel):
|
||||
category: ServiceCategorySchema
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Responses
|
||||
class ServiceGetAllResponse(CustomModel):
|
||||
services: List[ServiceSchema]
|
||||
|
||||
|
||||
class ServiceGetAllCategoriesResponse(CustomModel):
|
||||
categories: List[ServiceCategorySchema]
|
||||
|
||||
|
||||
class ServiceCreateResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
|
||||
class ServiceCreateCategoryResponse(OkMessageSchema):
|
||||
pass
|
||||
# endregion
|
||||
@@ -3,6 +3,7 @@ from typing import Union, Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from models import Client, ClientDetails, User
|
||||
from services.auth import get_current_user
|
||||
@@ -24,7 +25,7 @@ class ClientService(BaseService):
|
||||
return details
|
||||
|
||||
async def get_all(self) -> ClientGetAllResponse:
|
||||
clients_query = await self.session.scalars(select(Client))
|
||||
clients_query = await self.session.scalars(select(Client).options(joinedload(Client.details)))
|
||||
clients = clients_query.all()
|
||||
result = []
|
||||
for client in clients:
|
||||
@@ -65,7 +66,8 @@ class ClientService(BaseService):
|
||||
|
||||
async def search_clients(self, request: ClientSearchRequest) -> ClientSearchResponse:
|
||||
query = await self.session.scalars(select(Client)
|
||||
.where(Client.name.ilike(f'%{request.name}%')))
|
||||
.where(Client.name.ilike(f'%{request.name}%'))
|
||||
.options(joinedload(Client.details)))
|
||||
clients = []
|
||||
for client in query.all():
|
||||
clients.append(ClientSchema.model_validate(client))
|
||||
|
||||
@@ -2,6 +2,7 @@ import datetime
|
||||
from typing import Type, Union
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
|
||||
from models import User, Deal
|
||||
from models.deal import *
|
||||
@@ -16,7 +17,9 @@ class DealService(BaseService):
|
||||
async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]:
|
||||
return await self.session.get(Deal, deal_id)
|
||||
|
||||
async def change_status(self, deal: Deal, status: DealStatus, user: User,
|
||||
async def change_status(self, deal: Deal,
|
||||
status: DealStatus,
|
||||
user: User,
|
||||
deadline: datetime.datetime = None) -> DealStatusHistory:
|
||||
deadline = deadline
|
||||
status_change = DealStatusHistory(
|
||||
@@ -75,3 +78,24 @@ class DealService(BaseService):
|
||||
await self.change_status(deal, DealStatus(request.new_status), user)
|
||||
await self.session.commit()
|
||||
return DealChangeStatusResponse(ok=True)
|
||||
|
||||
async def get_summary(self) -> DealSummaryResponse:
|
||||
deals_query = await self.session.scalars(select(Deal)
|
||||
.options(selectinload(Deal.status_history),
|
||||
joinedload(Deal.client))
|
||||
.where(Deal.is_deleted == False,
|
||||
Deal.is_completed == False))
|
||||
summaries = []
|
||||
for deal in deals_query.all():
|
||||
deal: Deal
|
||||
last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at)
|
||||
summaries.append(
|
||||
DealSummary(
|
||||
id=deal.id,
|
||||
client_name=deal.client.name,
|
||||
name=deal.name,
|
||||
changed_at=last_status.changed_at,
|
||||
status=last_status.to_status
|
||||
)
|
||||
)
|
||||
return DealSummaryResponse(summaries=summaries)
|
||||
56
services/service.py
Normal file
56
services/service.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from models import Service, ServiceCategory
|
||||
from services.base import BaseService
|
||||
from schemas.services import ServiceGetAllResponse, ServiceSchema, ServiceGetAllCategoriesResponse, \
|
||||
ServiceCategorySchema, ServiceCreateRequest, ServiceCreateResponse, ServiceCreateCategoryRequest, \
|
||||
ServiceCreateCategoryResponse
|
||||
|
||||
|
||||
class ServiceService(BaseService):
|
||||
async def get_all(self) -> ServiceGetAllResponse:
|
||||
query = await (self.session
|
||||
.scalars(select(Service)
|
||||
.options(joinedload(Service.category))
|
||||
.order_by(Service.category_id, Service.id)))
|
||||
services = []
|
||||
for service in query.all():
|
||||
services.append(ServiceSchema.model_validate(service))
|
||||
return ServiceGetAllResponse(services=services)
|
||||
|
||||
async def create(self, request: ServiceCreateRequest) -> ServiceCreateResponse:
|
||||
try:
|
||||
raw_service = request.service
|
||||
service_dict = raw_service.model_dump()
|
||||
service_dict['category_id'] = raw_service.category.id
|
||||
del service_dict['id']
|
||||
del service_dict['category']
|
||||
service = Service(**service_dict)
|
||||
self.session.add(service)
|
||||
await self.session.commit()
|
||||
return ServiceCreateResponse(ok=True, message="Услуга успешно создана")
|
||||
except Exception as e:
|
||||
return ServiceCreateResponse(ok=False, message=f"Неудалось создать услугу, ошибка: {e}")
|
||||
|
||||
async def create_category(self, request: ServiceCreateCategoryRequest) -> ServiceCreateCategoryResponse:
|
||||
try:
|
||||
raw_category = request.category
|
||||
category_dict = raw_category.model_dump()
|
||||
del category_dict['id']
|
||||
category = ServiceCategory(**category_dict)
|
||||
self.session.add(category)
|
||||
await self.session.commit()
|
||||
return ServiceCreateCategoryResponse(ok=True, message="Категория успешно создана")
|
||||
|
||||
except Exception as e:
|
||||
return ServiceCreateCategoryResponse(ok=False, message=f"Неудалось создать категорию, ошибка: {e}")
|
||||
|
||||
async def get_all_categories(self) -> ServiceGetAllCategoriesResponse:
|
||||
query = await (self.session
|
||||
.scalars(select(ServiceCategory)
|
||||
.order_by(ServiceCategory.id)))
|
||||
categories = []
|
||||
for category in query.all():
|
||||
categories.append(ServiceCategorySchema.model_validate(category))
|
||||
return ServiceGetAllCategoriesResponse(categories=categories)
|
||||
@@ -1,11 +0,0 @@
|
||||
# Test your FastAPI endpoints
|
||||
|
||||
GET http://127.0.0.1:8000/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
|
||||
GET http://127.0.0.1:8000/hello/User
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
58
utils/import_services.py
Normal file
58
utils/import_services.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import asyncio
|
||||
|
||||
import openpyxl
|
||||
from openpyxl.worksheet.worksheet import Worksheet
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.session import session_maker
|
||||
from models import ServiceCategory, Service
|
||||
|
||||
|
||||
async def write_services(services: dict):
|
||||
async with session_maker() as session:
|
||||
session: AsyncSession
|
||||
|
||||
for raw_category in services.keys():
|
||||
category = ServiceCategory(name=raw_category)
|
||||
session.add(category)
|
||||
await session.flush()
|
||||
print(category.id)
|
||||
for raw_service in services[raw_category]:
|
||||
service = Service(name=raw_service['name'],
|
||||
price=raw_service['price'],
|
||||
category_id=category.id)
|
||||
session.add(service)
|
||||
await session.flush()
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def main():
|
||||
workbook = openpyxl.load_workbook('services.xlsx')
|
||||
sheet: Worksheet = workbook.active
|
||||
START_ROW = 12
|
||||
current_category = None
|
||||
services = {}
|
||||
merged_cells = sheet.merged_cells.ranges
|
||||
for row in range(START_ROW, sheet.max_row):
|
||||
first_column = sheet.cell(row, 1)
|
||||
first_column_value = first_column.value.strip()
|
||||
|
||||
second_column = sheet.cell(row, 2)
|
||||
second_column_value = second_column.value
|
||||
|
||||
is_category = any([second_column.coordinate in merged_cell for merged_cell in merged_cells])
|
||||
if is_category:
|
||||
current_category = first_column_value
|
||||
services[current_category] = []
|
||||
continue
|
||||
price = second_column_value
|
||||
name = first_column_value
|
||||
services[current_category].append({
|
||||
'name': name,
|
||||
'price': price
|
||||
})
|
||||
await write_services(services)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
BIN
utils/services.xlsx
Normal file
BIN
utils/services.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user