This commit is contained in:
2024-03-28 08:22:14 +03:00
parent 6ba041a839
commit a72eb31e07
19 changed files with 243 additions and 28 deletions

View File

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

View File

@@ -4,6 +4,7 @@ from .auth import *
from .deal import * from .deal import *
from .client import * from .client import *
from .service import * from .service import *
from .product import *
from .secondary import * from .secondary import *
configure_mappers() configure_mappers()

View File

@@ -10,6 +10,8 @@ class Client(BaseModel):
name = Column(String, nullable=False, unique=True, comment='Название клиента') name = Column(String, nullable=False, unique=True, comment='Название клиента')
created_at = Column(DateTime, nullable=False, comment='Дата создания') created_at = Column(DateTime, nullable=False, comment='Дата создания')
products = relationship('Product', back_populates='client')
class ClientDetails(BaseModel): class ClientDetails(BaseModel):
__tablename__ = 'client_details' __tablename__ = 'client_details'

View File

@@ -4,7 +4,6 @@ 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
@@ -33,7 +32,7 @@ class Deal(BaseModel):
is_deleted = Column(Boolean, nullable=False, server_default='0', default=False, comment='Удалена') is_deleted = Column(Boolean, nullable=False, server_default='0', default=False, comment='Удалена')
is_completed = 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) services = relationship('DealService', back_populates='deal')
class DealStatusHistory(BaseModel): class DealStatusHistory(BaseModel):

View File

@@ -11,5 +11,5 @@ class Product(BaseModel):
article = Column(String, nullable=False, index=True) article = Column(String, nullable=False, index=True)
client_id = Column(Integer, ForeignKey('clients.id'), nullable=False, comment='ID сделки') client_id = Column(Integer, ForeignKey('clients.id'), nullable=False, comment='ID сделки')
client = relationship('Client', back_populates='status_history') client = relationship('Client', back_populates='products')

View File

@@ -1,10 +1,15 @@
from sqlalchemy import Table, Column, Integer, ForeignKey from sqlalchemy import Table, Column, Integer, ForeignKey
from sqlalchemy.orm import relationship
from models.base import metadata from models.base import metadata, BaseModel
deal_services = Table(
'deal_services', metadata, class DealService(BaseModel):
Column('deal_id', Integer, ForeignKey('deals.id')), __tablename__ = 'deal_services'
Column('service_id', Integer, ForeignKey('services.id')), deal_id = Column(Integer, ForeignKey('deals.id'), nullable=False, comment='ID Сделки', primary_key=True)
Column('quantity', Integer) deal = relationship('Deal', back_populates='services')
)
service_id = Column(Integer, ForeignKey('services.id'), nullable=False, comment='ID Услуги', primary_key=True)
service = relationship('Service')
quantity = Column(Integer, nullable=False, comment='Кол-во услуги')

View File

@@ -1,12 +1,12 @@
from sqlalchemy import Column, Integer, String, ForeignKey, Double from sqlalchemy import Column, Integer, String, ForeignKey, Double
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship, mapped_column, Mapped
from models import BaseModel from models import BaseModel
class Service(BaseModel): class Service(BaseModel):
__tablename__ = 'services' __tablename__ = 'services'
id = Column(Integer, autoincrement=True, primary_key=True, index=True) id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True, index=True)
name = Column(String, nullable=False, comment='Название услуги') name = Column(String, nullable=False, comment='Название услуги')
category_id = Column(Integer, ForeignKey('service_categories.id'), nullable=False, comment='ID категории услуги') category_id = Column(Integer, ForeignKey('service_categories.id'), nullable=False, comment='ID категории услуги')

View File

@@ -2,4 +2,4 @@ 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 from .service import service_router
from .product import product_router

View File

@@ -50,3 +50,14 @@ async def get_summary(
session: Annotated[AsyncSession, Depends(get_session)] session: Annotated[AsyncSession, Depends(get_session)]
): ):
return await DealService(session).get_summary() return await DealService(session).get_summary()
@deal_router.post(
'/services/add',
response_model=DealAddServicesResponse,
)
async def services_add(
request: DealAddServicesRequest,
session: Annotated[AsyncSession, Depends(get_session)]
):
return await DealService(session).add_services(request)

39
routers/product.py Normal file
View File

@@ -0,0 +1,39 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
import utils.dependecies
from backend.session import get_session
from schemas.base import PaginationSchema
from schemas.product import *
from services.product import ProductService
product_router = APIRouter(
prefix="/product",
tags=["product"]
)
@product_router.post(
'/create',
response_model=ProductCreateResponse
)
async def create_product(
request: ProductCreateRequest,
session: Annotated[AsyncSession, Depends(get_session)]
):
return await ProductService(session).create(request)
@product_router.get(
'/get',
response_model=ProductGetResponse,
operation_id='get_products_by_client_id'
)
async def get_product(
client_id: int,
pagination: Annotated[PaginationSchema, Depends(utils.dependecies.pagination_parameters)],
session: Annotated[AsyncSession, Depends(get_session)]
):
return await ProductService(session).get_by_client_id(client_id, pagination)

View File

@@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session from backend.session import get_session
from schemas.services import * from schemas.service import *
from services.service import ServiceService from services.service import ServiceService
service_router = APIRouter( service_router = APIRouter(

View File

@@ -14,3 +14,13 @@ class CustomModel(BaseModel):
class OkMessageSchema(BaseModel): class OkMessageSchema(BaseModel):
ok: bool ok: bool
message: str message: str
class PaginationSchema(CustomModel):
page: int
items_per_page: int
class PaginationInfoSchema(CustomModel):
total_pages: int
total_items: int

View File

@@ -19,6 +19,12 @@ class DealSummary(CustomModel):
client_name: str client_name: str
changed_at: datetime.datetime changed_at: datetime.datetime
status: int status: int
total_price: int
class DealServiceSchema(CustomModel):
id: int
quantity: int
# endregion Entities # endregion Entities
@@ -45,6 +51,11 @@ class DealSummaryRequest(CustomModel):
pass pass
class DealAddServicesRequest(CustomModel):
deal_id: int
services: list[DealServiceSchema]
# endregion Requests # endregion Requests
# region Responses # region Responses
@@ -64,4 +75,8 @@ class DealQuickCreateResponse(CustomModel):
class DealSummaryResponse(CustomModel): class DealSummaryResponse(CustomModel):
summaries: List[DealSummary] summaries: List[DealSummary]
class DealAddServicesResponse(CustomModel):
ok: bool
message: str
# endregion Responses # endregion Responses

33
schemas/product.py Normal file
View File

@@ -0,0 +1,33 @@
from typing import List
from schemas.base import CustomModel, PaginationInfoSchema
# region Entities
class ProductSchema(CustomModel):
id: int
name: str
article: str
client_id: int
# endregion
# region Requests
class ProductCreateRequest(CustomModel):
name: str
article: str
client_id: int
# endregion
# region Responses
class ProductCreateResponse(CustomModel):
product_id: int
class ProductGetResponse(CustomModel):
products: List[ProductSchema]
pagination_info: PaginationInfoSchema
# endregion

View File

@@ -1,10 +1,11 @@
import datetime import models.secondary
from typing import Type, Union from typing import Union
from sqlalchemy import select from fastapi import HTTPException
from sqlalchemy import select, func
from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm import joinedload, selectinload
from models import User, Deal from models import User, Service
from models.deal import * from models.deal import *
from schemas.client import ClientDetailsSchema from schemas.client import ClientDetailsSchema
from schemas.deal import * from schemas.deal import *
@@ -80,13 +81,27 @@ class DealService(BaseService):
return DealChangeStatusResponse(ok=True) return DealChangeStatusResponse(ok=True)
async def get_summary(self) -> DealSummaryResponse: async def get_summary(self) -> DealSummaryResponse:
deals_query = await self.session.scalars(select(Deal) service_subquery = (
.options(selectinload(Deal.status_history), select(
joinedload(Deal.client)) models.secondary.DealService.deal_id,
.where(Deal.is_deleted == False, func.sum(models.secondary.DealService.quantity * Service.price).label('total_price')
Deal.is_completed == False)) )
.join(Service)
.group_by(models.secondary.DealService.deal_id)
.subquery()
)
q = (select(
Deal,
func.coalesce(service_subquery.c.total_price, 0)
)
.options(selectinload(Deal.status_history),
joinedload(Deal.client))
.outerjoin(service_subquery, Deal.id == service_subquery.c.deal_id)
.where(Deal.is_deleted == False,
Deal.is_completed == False))
deals_query = await self.session.execute(q)
summaries = [] summaries = []
for deal in deals_query.all(): for deal, total_price in deals_query.all():
deal: Deal deal: Deal
last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at) last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at)
summaries.append( summaries.append(
@@ -95,7 +110,49 @@ class DealService(BaseService):
client_name=deal.client.name, client_name=deal.client.name,
name=deal.name, name=deal.name,
changed_at=last_status.changed_at, changed_at=last_status.changed_at,
status=last_status.to_status status=last_status.to_status,
total_price=total_price
) )
) )
return DealSummaryResponse(summaries=summaries) return DealSummaryResponse(summaries=summaries)
async def add_services(self, request: DealAddServicesRequest):
# TODO refactor
deal: Deal = await self.session.scalar(
select(Deal)
.options(selectinload(Deal.services))
.where(Deal.id == request.deal_id)
)
if not deal:
raise HTTPException(status_code=404, detail="Deal is not found")
services_ids = [service.id for service in request.services]
existing_service_ids = {service.service_id for service in deal.services}
request_services_dict = {service.id: service.quantity for service in request.services}
services_query = await self.session.scalars(select(Service).where(Service.id.in_(services_ids)))
services = services_query.all()
if len(services) != len(services_ids):
raise HTTPException(status_code=404, detail="Some of services is not found")
# Adding quantity
for deal_service in deal.services:
deal_service: models.secondary.DealService
if deal_service.service_id not in services_ids:
continue
deal_service.quantity += request_services_dict[deal_service.service_id]
# Adding new services
for service in services:
if service.id in existing_service_ids:
continue
quantity = request_services_dict[service.id]
deal.services.append(
models.secondary.DealService(
service=service,
deal=deal,
quantity=quantity
)
)
await self.session.commit()
return DealAddServicesResponse(ok=True, message='Услуги успешно добавлены')

37
services/product.py Normal file
View File

@@ -0,0 +1,37 @@
from fastapi import HTTPException
from sqlalchemy import select
from models.product import Product
from schemas.base import PaginationSchema
from services.base import BaseService
from schemas.product import *
class ProductService(BaseService):
async def create(self, request: ProductCreateRequest) -> ProductCreateResponse:
existing_product_query = await self.session.execute(
select(Product)
.where(Product.client_id == request.client_id,
Product.article == request.article)
)
existing_product = existing_product_query.first()
if existing_product:
raise HTTPException(status_code=403, detail="Product already exists")
product = Product(**request.dict())
self.session.add(product)
await self.session.commit()
return ProductCreateResponse(product_id=product.id)
async def get_by_client_id(self, client_id: int, pagination: PaginationSchema) -> ProductGetResponse:
query = await self.session.execute(
select(Product)
.where(Product.client_id == client_id)
.offset(pagination.page * pagination.items_per_page)
.limit(pagination.items_per_page)
)
products: list[ProductSchema] = []
for product in query.scalars().all():
products.append(
ProductSchema.model_validate(product)
)
return ProductGetResponse(products=products)

View File

@@ -3,7 +3,7 @@ from sqlalchemy.orm import joinedload
from models import Service, ServiceCategory from models import Service, ServiceCategory
from services.base import BaseService from services.base import BaseService
from schemas.services import ServiceGetAllResponse, ServiceSchema, ServiceGetAllCategoriesResponse, \ from schemas.service import ServiceGetAllResponse, ServiceSchema, ServiceGetAllCategoriesResponse, \
ServiceCategorySchema, ServiceCreateRequest, ServiceCreateResponse, ServiceCreateCategoryRequest, \ ServiceCategorySchema, ServiceCreateRequest, ServiceCreateResponse, ServiceCreateCategoryRequest, \
ServiceCreateCategoryResponse ServiceCreateCategoryResponse

5
utils/dependecies.py Normal file
View File

@@ -0,0 +1,5 @@
from schemas.base import PaginationSchema
async def pagination_parameters(page: int, items_per_page: int) -> PaginationSchema:
return PaginationSchema(page=page, items_per_page=items_per_page)