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.deal_router,
routers.client_router,
routers.service_router
routers.service_router,
routers.product_router,
]
for router in routers_list:
app.include_router(router)

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ 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
@@ -33,7 +32,7 @@ class Deal(BaseModel):
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)
services = relationship('DealService', back_populates='deal')
class DealStatusHistory(BaseModel):

View File

@@ -11,5 +11,5 @@ class Product(BaseModel):
article = Column(String, nullable=False, index=True)
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.orm import relationship
from models.base import metadata
from models.base import metadata, BaseModel
deal_services = Table(
'deal_services', metadata,
Column('deal_id', Integer, ForeignKey('deals.id')),
Column('service_id', Integer, ForeignKey('services.id')),
Column('quantity', Integer)
)
class DealService(BaseModel):
__tablename__ = 'deal_services'
deal_id = Column(Integer, ForeignKey('deals.id'), nullable=False, comment='ID Сделки', primary_key=True)
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.orm import relationship
from sqlalchemy.orm import relationship, mapped_column, Mapped
from models import BaseModel
class Service(BaseModel):
__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='Название услуги')
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 .client import client_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)]
):
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 backend.session import get_session
from schemas.services import *
from schemas.service import *
from services.service import ServiceService
service_router = APIRouter(

View File

@@ -14,3 +14,13 @@ class CustomModel(BaseModel):
class OkMessageSchema(BaseModel):
ok: bool
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
changed_at: datetime.datetime
status: int
total_price: int
class DealServiceSchema(CustomModel):
id: int
quantity: int
# endregion Entities
@@ -45,6 +51,11 @@ class DealSummaryRequest(CustomModel):
pass
class DealAddServicesRequest(CustomModel):
deal_id: int
services: list[DealServiceSchema]
# endregion Requests
# region Responses
@@ -64,4 +75,8 @@ class DealQuickCreateResponse(CustomModel):
class DealSummaryResponse(CustomModel):
summaries: List[DealSummary]
class DealAddServicesResponse(CustomModel):
ok: bool
message: str
# 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
from typing import Type, Union
import models.secondary
from typing import Union
from sqlalchemy import select
from fastapi import HTTPException
from sqlalchemy import select, func
from sqlalchemy.orm import joinedload, selectinload
from models import User, Deal
from models import User, Service
from models.deal import *
from schemas.client import ClientDetailsSchema
from schemas.deal import *
@@ -80,13 +81,27 @@ class DealService(BaseService):
return DealChangeStatusResponse(ok=True)
async def get_summary(self) -> DealSummaryResponse:
deals_query = await self.session.scalars(select(Deal)
service_subquery = (
select(
models.secondary.DealService.deal_id,
func.sum(models.secondary.DealService.quantity * Service.price).label('total_price')
)
.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 = []
for deal in deals_query.all():
for deal, total_price in deals_query.all():
deal: Deal
last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at)
summaries.append(
@@ -95,7 +110,49 @@ class DealService(BaseService):
client_name=deal.client.name,
name=deal.name,
changed_at=last_status.changed_at,
status=last_status.to_status
status=last_status.to_status,
total_price=total_price
)
)
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 services.base import BaseService
from schemas.services import ServiceGetAllResponse, ServiceSchema, ServiceGetAllCategoriesResponse, \
from schemas.service import ServiceGetAllResponse, ServiceSchema, ServiceGetAllCategoriesResponse, \
ServiceCategorySchema, ServiceCreateRequest, ServiceCreateResponse, ServiceCreateCategoryRequest, \
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)