diff --git a/main.py b/main.py index dfa54ef..660a5b9 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/models/__init__.py b/models/__init__.py index 7827e82..3fcbaca 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -4,6 +4,7 @@ from .auth import * from .deal import * from .client import * from .service import * +from .product import * from .secondary import * configure_mappers() diff --git a/models/client.py b/models/client.py index deb0fe7..7b54fb3 100644 --- a/models/client.py +++ b/models/client.py @@ -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' diff --git a/models/deal.py b/models/deal.py index b231482..82296a1 100644 --- a/models/deal.py +++ b/models/deal.py @@ -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): diff --git a/models/product.py b/models/product.py index 4262b2a..e4439da 100644 --- a/models/product.py +++ b/models/product.py @@ -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') diff --git a/models/secondary.py b/models/secondary.py index b5a7c68..fc73fc5 100644 --- a/models/secondary.py +++ b/models/secondary.py @@ -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='Кол-во услуги') diff --git a/models/service.py b/models/service.py index 3a81d37..c23e59a 100644 --- a/models/service.py +++ b/models/service.py @@ -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 категории услуги') diff --git a/routers/__init__.py b/routers/__init__.py index 0788b9a..e220ddc 100644 --- a/routers/__init__.py +++ b/routers/__init__.py @@ -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 diff --git a/routers/deal.py b/routers/deal.py index b34154c..672d7e6 100644 --- a/routers/deal.py +++ b/routers/deal.py @@ -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) diff --git a/routers/product.py b/routers/product.py new file mode 100644 index 0000000..70b6ed9 --- /dev/null +++ b/routers/product.py @@ -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) diff --git a/routers/service.py b/routers/service.py index 8f4b86a..64c0823 100644 --- a/routers/service.py +++ b/routers/service.py @@ -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( diff --git a/schemas/base.py b/schemas/base.py index 4e5be92..9a8b0ed 100644 --- a/schemas/base.py +++ b/schemas/base.py @@ -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 diff --git a/schemas/deal.py b/schemas/deal.py index 6fe7faa..31bd0da 100644 --- a/schemas/deal.py +++ b/schemas/deal.py @@ -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 diff --git a/schemas/product.py b/schemas/product.py new file mode 100644 index 0000000..615df80 --- /dev/null +++ b/schemas/product.py @@ -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 diff --git a/schemas/services.py b/schemas/service.py similarity index 100% rename from schemas/services.py rename to schemas/service.py diff --git a/services/deal.py b/services/deal.py index 20b3caf..95b4a4c 100644 --- a/services/deal.py +++ b/services/deal.py @@ -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) - .options(selectinload(Deal.status_history), - joinedload(Deal.client)) - .where(Deal.is_deleted == False, - Deal.is_completed == False)) + 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) \ No newline at end of file + 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='Услуги успешно добавлены') diff --git a/services/product.py b/services/product.py new file mode 100644 index 0000000..e1f1e19 --- /dev/null +++ b/services/product.py @@ -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) diff --git a/services/service.py b/services/service.py index 5a7cf92..41a6890 100644 --- a/services/service.py +++ b/services/service.py @@ -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 diff --git a/utils/dependecies.py b/utils/dependecies.py new file mode 100644 index 0000000..74876dd --- /dev/null +++ b/utils/dependecies.py @@ -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)