diff --git a/models/deal.py b/models/deal.py index 0704829..6c32557 100644 --- a/models/deal.py +++ b/models/deal.py @@ -33,7 +33,6 @@ class Deal(BaseModel): is_completed = Column(Boolean, nullable=False, server_default='0', default=False, comment='Завершена') services = relationship('DealService', back_populates='deal', cascade="all, delete-orphan") - # product_services = relationship('DealProductService', back_populates='deal', cascade="all, delete-orphan") products = relationship('DealProduct', back_populates='deal', cascade="all, delete-orphan") diff --git a/models/secondary.py b/models/secondary.py index 222d19a..8e5bd3f 100644 --- a/models/secondary.py +++ b/models/secondary.py @@ -21,11 +21,20 @@ class DealService(BaseModel): class DealProductService(BaseModel): __tablename__ = 'deal_product_services' - deal_id = Column(Integer, nullable=False, primary_key=True, comment='ID Сделки') - product_id = Column(Integer, nullable=False, primary_key=True, comment='ID Продукта') - service_id = Column(Integer, ForeignKey('services.id'), nullable=False, comment='ID Услуги') + deal_id = Column(Integer, primary_key=True, nullable=False, comment='ID Сделки') + product_id = Column(Integer, primary_key=True, nullable=False, comment='ID Продукта') + service_id = Column(Integer, ForeignKey('services.id'), primary_key=True, nullable=False, comment='ID Услуги') price = Column(Integer, nullable=False, comment='Цена услуги') + deal_product = relationship('DealProduct', + back_populates='services', + primaryjoin="and_(DealProductService.deal_id == DealProduct.deal_id, " + "DealProductService.product_id == DealProduct.product_id)", + foreign_keys=[deal_id, product_id]) + service = relationship('Service', + foreign_keys=[service_id], + lazy='joined') + __table_args__ = ( ForeignKeyConstraint( ['deal_id', 'product_id'], @@ -33,21 +42,28 @@ class DealProductService(BaseModel): ), ) - deal_product = relationship('DealProduct', back_populates='services') - service = relationship('Service', lazy='joined') - class DealProduct(BaseModel): __tablename__ = 'deal_products' - deal_id = Column(Integer, ForeignKey('deals.id'), nullable=False, comment='ID Сделки', primary_key=True) - product_id = Column(Integer, ForeignKey('products.id'), nullable=False, comment='ID Продукта', primary_key=True) + deal_id = Column(Integer, ForeignKey('deals.id'), primary_key=True, nullable=False, comment='ID Сделки') + product_id = Column(Integer, ForeignKey('products.id'), primary_key=True, nullable=False, comment='ID Продукта') quantity = Column(Integer, nullable=False, comment='Кол-во продукта') - deal = relationship('Deal', back_populates='products') - product = relationship('Product') + deal = relationship('Deal', + back_populates='products', + foreign_keys=[deal_id]) + product = relationship('Product', + lazy='joined', + foreign_keys=[product_id]) - services = relationship('DealProductService', back_populates='deal_product', lazy='joined', - cascade="all, delete-orphan") + services = relationship('DealProductService', + back_populates='deal_product', + cascade="all, delete-orphan", + primaryjoin="and_(DealProductService.deal_id == DealProduct.deal_id, " + "DealProductService.product_id == DealProduct.product_id)", + foreign_keys=[DealProductService.deal_id, DealProductService.product_id], + lazy='joined' + ) barcode_template_attribute_link = Table( diff --git a/routers/deal.py b/routers/deal.py index da11d16..48e1a9a 100644 --- a/routers/deal.py +++ b/routers/deal.py @@ -145,13 +145,25 @@ async def services_add( response_model=DealUpdateServiceQuantityResponse, operation_id='updateDealServiceQuantity' ) -async def services_update( +async def services_update_quantity( request: DealUpdateServiceQuantityRequest, session: Annotated[AsyncSession, Depends(get_session)] ): return await DealService(session).update_service_quantity(request) +@deal_router.post( + '/services/update', + response_model=DealUpdateServiceResponse, + operation_id='updateDealService' +) +async def services_update( + request: DealUpdateServiceRequest, + session: Annotated[AsyncSession, Depends(get_session)] +): + return await DealService(session).update_service(request) + + @deal_router.post( '/services/delete', response_model=DealDeleteServiceResponse, @@ -218,4 +230,15 @@ async def products_delete( session: Annotated[AsyncSession, Depends(get_session)]): return await DealService(session).delete_products(request) + +@deal_router.post( + '/product/update', + response_model=DealUpdateProductResponse, + operation_id='updateDealProduct' +) +async def products_update( + request: DealUpdateProductRequest, + session: Annotated[AsyncSession, Depends(get_session)] +): + return await DealService(session).update_product(request) # endregion diff --git a/schemas/deal.py b/schemas/deal.py index df152ab..f1c401b 100644 --- a/schemas/deal.py +++ b/schemas/deal.py @@ -109,10 +109,16 @@ class DealUpdateServiceQuantityRequest(CustomModelCamel): quantity: int +class DealUpdateServiceRequest(CustomModelCamel): + deal_id: int + service: DealServiceSchema + + class DealAddServiceRequest(CustomModelCamel): deal_id: int service_id: int quantity: int + price: int class DealDeleteServiceRequest(CustomModelCamel): @@ -163,6 +169,11 @@ class DealDeleteRequest(CustomModelCamel): deal_id: int +class DealUpdateProductRequest(CustomModelCamel): + deal_id: int + product: DealProductSchema + + # endregion Requests # region Responses @@ -204,6 +215,10 @@ class DealUpdateServiceQuantityResponse(CustomModelCamel): message: str +class DealUpdateServiceResponse(OkMessageSchema): + pass + + class DealAddServiceResponse(OkMessageSchema): pass @@ -234,4 +249,8 @@ class DealSummaryReorderResponse(OkMessageSchema): class DealDeleteResponse(OkMessageSchema): pass + + +class DealUpdateProductResponse(OkMessageSchema): + pass # endregion Responses diff --git a/services/deal.py b/services/deal.py index 6af1317..520fed4 100644 --- a/services/deal.py +++ b/services/deal.py @@ -4,7 +4,7 @@ import models.secondary from typing import Union import models.deal from fastapi import HTTPException -from sqlalchemy import select, func +from sqlalchemy import select, func, update from sqlalchemy.orm import joinedload, selectinload from models import User, Service, Client @@ -118,20 +118,42 @@ class DealService(BaseService): await self.session.commit() return DealChangeStatusResponse(ok=True) - async def get_summary(self) -> DealSummaryResponse: - service_subquery = ( + def _get_price_subquery(self): + deal_services_subquery = ( select( models.secondary.DealService.deal_id, - func.sum(models.secondary.DealService.quantity * Service.price).label('total_price') + func.sum(models.secondary.DealService.quantity * models.secondary.DealService.price).label( + 'total_price') ) .join(Service) .group_by(models.secondary.DealService.deal_id) + ) + product_services_subquery = select( + select( + models.secondary.DealProductService.deal_id, + func.sum(models.DealProduct.quantity * models.secondary.DealProductService.price).label('total_price') + ) + .join(models.secondary.DealProduct) + .group_by(models.secondary.DealProductService.deal_id) .subquery() ) + union_subqueries = deal_services_subquery.union(product_services_subquery).subquery() + final_subquery = ( + select( + union_subqueries.c.deal_id, + func.sum(union_subqueries.c.total_price).label('total_price') + ) + .group_by(union_subqueries.c.deal_id) + .subquery() + ) + return final_subquery + + async def get_summary(self) -> DealSummaryResponse: + price_subquery = self._get_price_subquery() q = ( select( Deal, - func.coalesce(service_subquery.c.total_price, 0), + func.coalesce(price_subquery.c.total_price, 0), func.row_number().over( partition_by=Deal.current_status, order_by=Deal.lexorank @@ -142,7 +164,7 @@ class DealService(BaseService): joinedload(Deal.client) ) .outerjoin( - service_subquery, Deal.id == service_subquery.c.deal_id) + price_subquery, Deal.id == price_subquery.c.deal_id) .where( Deal.is_deleted == False, Deal.is_completed == False @@ -177,6 +199,7 @@ class DealService(BaseService): return DealGetAllResponse(deals=result) async def get_by_id(self, deal_id: int) -> DealSchema: + deal = await self.session.scalar( select(Deal) .options( @@ -190,13 +213,17 @@ class DealService(BaseService): selectinload(Deal.products) .joinedload(models.secondary.DealProduct.product) .joinedload(models.Product.barcodes), + selectinload(Deal.products) + .joinedload(models.secondary.DealProduct.services) + .joinedload(models.secondary.DealProductService.service), selectinload(Deal.status_history) .joinedload(DealStatusHistory.user), selectinload(Deal.status_history) - .noload(DealStatusHistory.deal) + .noload(DealStatusHistory.deal), ) .where(Deal.id == deal_id) ) + if not deal: raise HTTPException(status_code=404, detail="Сделка не найдена") return DealSchema.model_validate(deal) @@ -328,7 +355,7 @@ class DealService(BaseService): deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id)) if not deal: raise HTTPException(status_code=404, detail="Сделка не найдена") - service = await self.session.scalar(select(Service).where(Service.id == request.service_id)) + service: models.Service = await self.session.scalar(select(Service).where(Service.id == request.service_id)) if not service: raise HTTPException(status_code=404, detail="Услуга не найдена") # Preventing duplicates @@ -342,7 +369,8 @@ class DealService(BaseService): deal_service = models.secondary.DealService( deal_id=request.deal_id, service_id=request.service_id, - quantity=request.quantity + quantity=request.quantity, + price=request.price ) self.session.add(deal_service) await self.session.commit() @@ -382,6 +410,30 @@ class DealService(BaseService): await self.session.rollback() return DealDeleteServicesResponse(ok=False, message=str(e)) + async def update_service(self, request: DealUpdateServiceRequest) -> DealUpdateServiceResponse: + try: + deal_service = await self.session.scalar( + select(models.secondary.DealService) + .where(models.secondary.DealService.deal_id == request.deal_id, + models.secondary.DealService.service_id == request.service.service.id) + ) + if not deal_service: + raise HTTPException(status_code=404, detail="Сделка не найдена") + service_dict = request.service.dict() + del service_dict['service'] + service_dict['service_id'] = request.service.service.id + await self.session.execute( + update(models.secondary.DealService) + .where(models.secondary.DealService.deal_id == request.deal_id, + models.secondary.DealService.service_id == request.service.service.id) + .values(**service_dict) + ) + await self.session.commit() + return DealUpdateServiceQuantityResponse(ok=True, message='Количество успешно обновлено') + except Exception as e: + await self.session.rollback() + return DealUpdateServiceQuantityResponse(ok=False, message=str(e)) + # endregion # region Deal products @@ -408,23 +460,33 @@ class DealService(BaseService): deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id)) if not deal: raise HTTPException(status_code=404, detail="Сделка не найдена") - product = await self.session.scalar(select(models.Product).where(models.Product.id == request.product_id)) + product = await self.session.scalar( + select(models.Product).where(models.Product.id == request.product.product.id)) if not product: raise HTTPException(status_code=404, detail="Товар не найден") # Preventing duplicates deal_product = await self.session.scalar( select(models.secondary.DealProduct) .where(models.secondary.DealProduct.deal_id == request.deal_id, - models.secondary.DealProduct.product_id == request.product_id) + models.secondary.DealProduct.product_id == request.product.product.id) ) if deal_product: raise HTTPException(status_code=400, detail="Товар уже добавлен") deal_product = models.secondary.DealProduct( deal_id=request.deal_id, - product_id=request.product_id, - quantity=request.quantity + product_id=request.product.product.id, + quantity=request.product.quantity ) self.session.add(deal_product) + await self.session.flush() + for service in request.product.services: + deal_product_service = models.secondary.DealProductService( + deal_id=request.deal_id, + product_id=request.product.product.id, + service_id=service.service.id, + price=service.price + ) + self.session.add(deal_product_service) await self.session.commit() return DealAddProductResponse(ok=True, message='Товар успешно добавлен') except Exception as e: @@ -461,4 +523,56 @@ class DealService(BaseService): except Exception as e: await self.session.rollback() return DealDeleteProductsResponse(ok=False, message=str(e)) + + async def update_product(self, request: DealUpdateProductRequest): + try: + deal_product: models.DealProduct = await self.session.scalar( + select(models.secondary.DealProduct) + .where(models.secondary.DealProduct.deal_id == request.deal_id, + models.secondary.DealProduct.product_id == request.product.product.id) + ) + if not deal_product: + raise HTTPException(status_code=404, detail="Указанный товар не найден") + # getting new services and deleted + database_services = set([service.service_id for service in deal_product.services]) + request_services = set([service.service.id for service in request.product.services]) + + new_services = request_services.difference(database_services) + deleted_services = database_services.difference(request_services) + services_dict = {service.service.id: service for service in request.product.services} + + # Deleting and updating existing services + for service in deal_product.services: + service: models.DealProductService + if service.service_id in deleted_services: + await self.session.delete(service) + await self.session.flush() + continue + request_service = services_dict[service.service_id] + service.price = request_service.price + await self.session.flush() + + # Creating services + for service in request.product.services: + if service.service.id not in new_services: + continue + deal_product_service = models.DealProductService( + deal_id=request.deal_id, + product_id=request.product.product.id, + service_id=service.service.id, + price=service.price + ) + self.session.add(deal_product_service) + await self.session.flush() + + # Updating product + deal_product.quantity = request.product.quantity + + await self.session.commit() + return DealUpdateProductResponse(ok=True, message='Товар успешно обновлен') + + except Exception as e: + await self.session.rollback() + return DealUpdateProductResponse(ok=False, message=str(e)) + # endregion diff --git a/test/test.py b/test/test.py index d165328..39417d1 100644 --- a/test/test.py +++ b/test/test.py @@ -1,14 +1,46 @@ import asyncio +from sqlalchemy import select, func, union from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload from backend.session import session_maker -from migrations.env import run_async_migrations -from models import Product, ProductBarcode +from models import Deal, DealProduct, Service + +import models +import models.secondary async def main(session: AsyncSession): - await run_async_migrations() + deal_services_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) + ) + product_services_subquery = select( + select( + models.secondary.DealProductService.deal_id, + func.sum(models.DealProduct.quantity * models.secondary.DealProductService.price).label('total_price') + ) + .join(models.secondary.DealProduct) + .group_by(models.secondary.DealProductService.deal_id) + .subquery() + ) + union_subqueries = deal_services_subquery.union(product_services_subquery).subquery() + final_subquery = ( + select( + union_subqueries.c.deal_id, + func.sum(union_subqueries.c.total_price).label('total_sum') + ) + .group_by(union_subqueries.c.deal_id) + .subquery() + ) + + print(final_subquery) + async def preload(): async with session_maker() as session: