This commit is contained in:
2024-03-19 09:01:46 +03:00
parent aafa1050a7
commit 6ba041a839
23 changed files with 369 additions and 39 deletions

View File

@@ -1,3 +1,5 @@
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@@ -10,10 +12,10 @@ session_maker = sessionmaker(
class_=AsyncSession, class_=AsyncSession,
expire_on_commit=False, expire_on_commit=False,
autocommit=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: async with session_maker() as session:
yield session yield session

View File

@@ -19,7 +19,8 @@ app.add_middleware(
routers_list = [ routers_list = [
routers.auth_router, routers.auth_router,
routers.deal_router, routers.deal_router,
routers.client_router routers.client_router,
routers.service_router
] ]
for router in routers_list: for router in routers_list:
app.include_router(router) app.include_router(router)

View File

@@ -1,3 +1,9 @@
from sqlalchemy.orm import configure_mappers
from .auth import * from .auth import *
from .deal import * from .deal import *
from .client import * from .client import *
from .service import *
from .secondary import *
configure_mappers()

View File

@@ -1,3 +1,4 @@
from sqlalchemy.orm import declarative_base from sqlalchemy.orm import declarative_base
BaseModel = declarative_base() BaseModel = declarative_base()
metadata = BaseModel.metadata

View File

@@ -1,5 +1,5 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, BigInteger from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, BigInteger
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship, backref
from models import BaseModel from models import BaseModel
@@ -17,7 +17,7 @@ class ClientDetails(BaseModel):
id = Column(Integer, autoincrement=True, primary_key=True, index=True) id = Column(Integer, autoincrement=True, primary_key=True, index=True)
client_id = Column(Integer, ForeignKey('clients.id'), unique=True, nullable=False, comment='ID клиента') 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) address = Column(String)
phone_number = Column(String) phone_number = Column(String)

View File

@@ -1,9 +1,10 @@
from enum import IntEnum, unique 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 sqlalchemy.orm import relationship
from models.base import BaseModel from models.base import BaseModel
from models.secondary import deal_services
@unique @unique
@@ -29,6 +30,11 @@ class Deal(BaseModel):
status_history = relationship('DealStatusHistory', back_populates='deal', cascade="all, delete-orphan") 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): class DealStatusHistory(BaseModel):
__tablename__ = 'deals_status_history' __tablename__ = 'deals_status_history'

10
models/secondary.py Normal file
View 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
View 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)

View File

@@ -16,4 +16,5 @@ alembic
# Other # Other
python-dotenv python-dotenv
aiohttp aiohttp
aiohttp[speedups] aiohttp[speedups]
openpyxl

View File

@@ -1,4 +1,5 @@
from .auth import auth_router from .auth import auth_router
from .deal import deal_router from .deal import deal_router
from .client import client_router from .client import client_router
from .service import service_router

View File

@@ -39,7 +39,11 @@ async def update_client_details(
return ClientUpdateDetailsResponse(ok=True) 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( async def get_all_clients(
session: Annotated[AsyncSession, Depends(get_session)], session: Annotated[AsyncSession, Depends(get_session)],
): ):

View File

@@ -40,3 +40,13 @@ async def change_status(
user: Annotated[User, Depends(get_current_user)] user: Annotated[User, Depends(get_current_user)]
): ):
return await DealService(session).change_status(request, 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
View 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)

View File

@@ -10,3 +10,7 @@ class CustomModel(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
class OkMessageSchema(BaseModel):
ok: bool
message: str

View File

@@ -3,18 +3,17 @@ from typing import List
from schemas.base import CustomModel from schemas.base import CustomModel
class ClientSchema(CustomModel):
id: int
name: str
class ClientDetailsSchema(CustomModel): class ClientDetailsSchema(CustomModel):
address: str | None = None address: str | None = None
phone_number: str | None = None phone_number: str | None = None
inn: int | None = None inn: int | None = None
email: str | None = None email: str | None = None
# TODO add email validation
class ClientSchema(CustomModel):
id: int
name: str
details: ClientDetailsSchema | None = None
class ClientSearchRequest(CustomModel): class ClientSearchRequest(CustomModel):

View File

@@ -1,18 +1,34 @@
import datetime import datetime
from typing import List
from schemas.base import CustomModel from schemas.base import CustomModel
from schemas.client import ClientSchema 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): class DealChangeStatusRequest(CustomModel):
deal_id: int deal_id: int
new_status: int new_status: int
class DealChangeStatusResponse(CustomModel):
ok: bool
class DealCreateRequest(CustomModel): class DealCreateRequest(CustomModel):
name: str name: str
@@ -25,16 +41,27 @@ class DealQuickCreateRequest(CustomModel):
acceptance_date: datetime.datetime acceptance_date: datetime.datetime
class DealQuickCreateResponse(CustomModel): class DealSummaryRequest(CustomModel):
deal_id: int pass
# endregion Requests
# region Responses
class DealChangeStatusResponse(CustomModel):
ok: bool
class DealCreateResponse(CustomModel): class DealCreateResponse(CustomModel):
ok: bool ok: bool
class FastDeal(CustomModel): class DealQuickCreateResponse(CustomModel):
name: str deal_id: int
client: ClientSchema
comment: str
acceptance_date: datetime.datetime class DealSummaryResponse(CustomModel):
summaries: List[DealSummary]
# endregion Responses

49
schemas/services.py Normal file
View 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

View File

@@ -3,6 +3,7 @@ from typing import Union, Annotated
from fastapi import Depends from fastapi import Depends
from sqlalchemy import select, update from sqlalchemy import select, update
from sqlalchemy.orm import joinedload
from models import Client, ClientDetails, User from models import Client, ClientDetails, User
from services.auth import get_current_user from services.auth import get_current_user
@@ -24,7 +25,7 @@ class ClientService(BaseService):
return details return details
async def get_all(self) -> ClientGetAllResponse: 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() clients = clients_query.all()
result = [] result = []
for client in clients: for client in clients:
@@ -65,7 +66,8 @@ class ClientService(BaseService):
async def search_clients(self, request: ClientSearchRequest) -> ClientSearchResponse: async def search_clients(self, request: ClientSearchRequest) -> ClientSearchResponse:
query = await self.session.scalars(select(Client) 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 = [] clients = []
for client in query.all(): for client in query.all():
clients.append(ClientSchema.model_validate(client)) clients.append(ClientSchema.model_validate(client))

View File

@@ -2,6 +2,7 @@ import datetime
from typing import Type, Union from typing import Type, Union
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import joinedload, selectinload
from models import User, Deal from models import User, Deal
from models.deal import * from models.deal import *
@@ -16,7 +17,9 @@ class DealService(BaseService):
async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]: async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]:
return await self.session.get(Deal, deal_id) 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: datetime.datetime = None) -> DealStatusHistory:
deadline = deadline deadline = deadline
status_change = DealStatusHistory( status_change = DealStatusHistory(
@@ -75,3 +78,24 @@ class DealService(BaseService):
await self.change_status(deal, DealStatus(request.new_status), user) await self.change_status(deal, DealStatus(request.new_status), user)
await self.session.commit() await self.session.commit()
return DealChangeStatusResponse(ok=True) 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
View 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)

View File

@@ -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
View 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

Binary file not shown.