import lexorank from attr import dataclass from fastapi import HTTPException from sqlalchemy import select, func, update, delete, insert, and_ from sqlalchemy.orm import joinedload, selectinload from starlette import status import models.deal import models.secondary from models import User, Service, Client, DealProductService, deal_relations, GroupBillRequest from models.deal import * from models.deal_group import DealGroup from models.shipping import ShippingProduct from schemas.client import ClientDetailsSchema from schemas.deal import * from services.auth import AuthService from services.base import BaseService from services.client import ClientService from services.deal_group import DealGroupService from services.service import ServiceService from services.shipping_warehouse import ShippingWarehouseService class DealService(BaseService): # region Deal @staticmethod def grant_access(user: Union[models.User, dict], deal_id): if type(user) is models.User: return user_deal_id = user['deal_id'] if int(user_deal_id) != int(deal_id): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]: return await self.session.get(Deal, deal_id) async def _get_rank_for_deal(self, deal_status: DealStatus) -> str: deal_query = await self.session.execute( select(Deal).where(Deal.current_status == deal_status).order_by(Deal.lexorank.desc()).limit(1)) deal = deal_query.scalar_one_or_none() if not deal: prev = lexorank.middle(lexorank.Bucket.BUCEKT_0) return str(prev.next()) return str(lexorank.parse(deal.lexorank).next()) async def change_status(self, deal: Deal, status: DealStatus, user: User, deadline: datetime.datetime = None, rank=None, comment: str = ''): if not deal.current_status == status: deadline = deadline status_change = DealStatusHistory( deal_id=deal.id, user_id=user.id, changed_at=datetime.datetime.now(), from_status=deal.current_status, to_status=status, next_status_deadline=deadline, comment=comment ) self.session.add(status_change) deal.current_status = status if not rank: rank = await self._get_rank_for_deal(status) if rank: deal.lexorank = rank await self.session.flush() async def create(self, request: DealCreateRequest, user: User) -> DealCreateResponse: rank = self._get_rank_for_deal(DealStatus.CREATED) deal = Deal( name=request.name, created_at=datetime.datetime.now(), current_status=DealStatus.CREATED, lexorank=rank ) self.session.add(deal) await self.session.flush() # Append status history await self.change_status(deal, DealStatus.AWAITING_ACCEPTANCE, user) await self.session.commit() return DealCreateResponse(ok=True) async def delete(self, request: DealDeleteRequest) -> DealDeleteResponse: deal = await self._get_deal_by_id(request.deal_id) if not deal: return DealDeleteResponse(ok=False, message="Сделка не найдена") if deal.group: await DealGroupService(self.session).delete_group(deal.group.id) else: deal.is_deleted = True await self.session.commit() return DealDeleteResponse(ok=True, message="Сделка успешно удалена") async def quick_create(self, request: DealQuickCreateRequest, user: User) -> DealQuickCreateResponse: client_service = ClientService(self.session) client = await client_service.get_by_name(request.client_name) if not client: client = await client_service.create_client_raw( user, request.client_name, ClientDetailsSchema() ) shipping_warehouse_service = ShippingWarehouseService(self.session) shipping_warehouse = await shipping_warehouse_service.get_by_name(name=request.shipping_warehouse) if not shipping_warehouse: shipping_warehouse = await shipping_warehouse_service.create_by_name(name=request.shipping_warehouse) rank = await self._get_rank_for_deal(DealStatus.CREATED) deal = Deal( name=request.name, created_at=datetime.datetime.now(), client_id=client.id, current_status=DealStatus.CREATED, lexorank=rank, shipping_warehouse_id=shipping_warehouse.id, base_marketplace_key=request.base_marketplace.key ) self.session.add(deal) await self.session.flush() await self.change_status(deal, DealStatus.AWAITING_ACCEPTANCE, user, deadline=request.acceptance_date, comment=request.comment) # add category if specified if request.category: deal_category = DealPriceCategory( deal_id=deal.id, category_id=request.category.id ) self.session.add(deal_category) await self.session.commit() return DealQuickCreateResponse(deal_id=deal.id) async def change_status_manual(self, request: DealChangeStatusRequest, user: User) -> DealChangeStatusResponse: # Changing current status deal = await self._get_deal_by_id(request.deal_id) if not deal: return DealChangeStatusResponse(ok=False) await self.change_status(deal, DealStatus(request.new_status), user) await self.session.commit() return DealChangeStatusResponse(ok=True) def _get_price_subquery(self): deal_services_subquery = ( select( models.secondary.DealService.deal_id, 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_all(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 def _get_products_quantity_subquery(self): deal_products_subquery = ( select( models.secondary.DealProduct.deal_id, func.sum(models.secondary.DealProduct.quantity).label('total_quantity') ) .group_by(models.secondary.DealProduct.deal_id) .subquery() ) return deal_products_subquery async def get_summary(self, full: bool = False) -> DealSummaryResponse: price_subquery = self._get_price_subquery() products_quantity_subquery = self._get_products_quantity_subquery() q = ( select( Deal, func.coalesce(price_subquery.c.total_price, 0), func.row_number().over( partition_by=Deal.current_status, order_by=Deal.lexorank ).label('rank'), func.coalesce(products_quantity_subquery.c.total_quantity, 0) ) .options( selectinload(Deal.status_history), joinedload(Deal.client), joinedload(Deal.shipping_warehouse), joinedload(Deal.bill_request) ) .outerjoin( price_subquery, Deal.id == price_subquery.c.deal_id, ) .outerjoin( products_quantity_subquery, Deal.id == products_quantity_subquery.c.deal_id ) .where( Deal.is_deleted == False, ) ) if not full: q = q.where( Deal.is_completed == False, # Deal.current_status != DealStatus.COMPLETED ) else: q = q.order_by(Deal.created_at.desc()) deals_query = await self.session.execute(q) summaries = [] for deal, total_price, rank, products_count in deals_query.all(): deal: Deal last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at) deadline = last_status.next_status_deadline base_marketplace = None if deal.base_marketplace: base_marketplace = BaseMarketplaceSchema.model_validate(deal.base_marketplace) shipment_warehouse_name = deal.shipping_warehouse.name if deal.shipping_warehouse else None summaries.append( DealSummary( id=deal.id, client_name=deal.client.name, name=deal.name, changed_at=last_status.changed_at, deadline=deadline, status=last_status.to_status, total_price=total_price, rank=rank, base_marketplace=base_marketplace, created_at=deal.created_at, shipment_warehouse_id=deal.shipping_warehouse_id, shipment_warehouse_name=shipment_warehouse_name, total_products=products_count, delivery_date=deal.delivery_date, receiving_slot_date=deal.receiving_slot_date, bill_request=deal.bill_request, group=deal.group ) ) return DealSummaryResponse(summaries=summaries) async def get_all(self) -> DealGetAllResponse: deals_query = ( await self.session.scalars( select( Deal ) .options( joinedload(Deal.shipping_warehouse), joinedload(Deal.client) .joinedload(Client.details), selectinload(Deal.services) .options( joinedload(models.secondary.DealService.service).joinedload(Service.category), selectinload(models.secondary.DealService.employees) ), selectinload(Deal.products) .joinedload(models.secondary.DealProduct.product) .joinedload(models.Product.client), selectinload(Deal.products) .joinedload(models.secondary.DealProduct.product) .joinedload(models.Product.barcodes), selectinload(Deal.products) .joinedload(models.secondary.DealProduct.services) .options( joinedload(models.secondary.DealProductService.service), selectinload(models.secondary.DealProductService.employees) ), selectinload(Deal.status_history) .joinedload(DealStatusHistory.user), selectinload(Deal.status_history) .noload(DealStatusHistory.deal), ) ) ) deals = deals_query.all() result = [] for deal in deals: result.append(DealSchema.model_validate(deal)) return DealGetAllResponse(deals=result) async def get_by_id(self, user: Union[models.User, dict], deal_id: int, return_raw=False) -> Union[ DealSchema, models.Deal]: self.grant_access(user, deal_id) deal = await self.session.scalar( select(Deal) .options( joinedload(Deal.shipping_warehouse), joinedload(Deal.client) .joinedload(Client.details), selectinload(Deal.services) .options( joinedload(models.secondary.DealService.service).joinedload(Service.category), selectinload(models.secondary.DealService.employees) ), selectinload(Deal.products) .joinedload(models.secondary.DealProduct.product) .joinedload(models.Product.client), selectinload(Deal.products) .joinedload(models.secondary.DealProduct.product) .joinedload(models.Product.barcodes), selectinload(Deal.products) .joinedload(models.secondary.DealProduct.services) .options( joinedload(models.secondary.DealProductService.service), selectinload(models.secondary.DealProductService.employees) ), selectinload(Deal.status_history) .joinedload(DealStatusHistory.user), selectinload(Deal.status_history) .noload(DealStatusHistory.deal), selectinload(Deal.pallets) .selectinload(Pallet.shipping_products) .selectinload(ShippingProduct.product) .noload(Product.barcodes), selectinload(Deal.boxes) .selectinload(Box.product) .noload(Product.barcodes), selectinload(Deal.employees) .joinedload(DealEmployees.user), ) .where(Deal.id == deal_id) ) if return_raw: return deal if not deal: raise HTTPException(status_code=404, detail="Сделка не найдена") return DealSchema.model_validate(deal) async def update_general_info(self, request: DealUpdateGeneralInfoRequest) -> DealUpdateGeneralInfoResponse: try: deal: Deal = await self.session.scalar( select(Deal) .options( selectinload(Deal.group) .selectinload(DealGroup.deals) ) .where(Deal.id == request.deal_id) ) if not deal: raise HTTPException(status_code=404, detail="Сделка не найдена") deal.name = request.data.name deal.comment = request.data.comment deal.is_deleted = request.data.is_deleted deal.is_completed = request.data.is_completed deal.delivery_date = request.data.delivery_date deal.receiving_slot_date = request.data.receiving_slot_date if deal.group: for deal in deal.group.deals: deal.is_accounted = request.data.is_accounted else: deal.is_accounted = request.data.is_accounted # Updating shipping warehouse shipping_warehouse_service = ShippingWarehouseService(self.session) shipping_warehouse = await shipping_warehouse_service.get_by_name(request.data.shipping_warehouse) if not shipping_warehouse and request.data.shipping_warehouse: shipping_warehouse = await shipping_warehouse_service.create_by_name(request.data.shipping_warehouse) deal.shipping_warehouse = shipping_warehouse # Updating manager if request.data.manager: deal.manager_id = request.data.manager.id else: deal.manager = None await self.session.commit() return DealUpdateGeneralInfoResponse(ok=True, message='Данные о сделке успешно обновлены') except Exception as e: await self.session.rollback() return DealUpdateGeneralInfoResponse(ok=False, message=str(e)) async def reorder(self, request: DealSummaryReorderRequest, user: User) -> DealSummaryResponse: deal: Deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id)) if request.index == 1: request.index = 0 is_first = request.index == 0 stmt = ( select(Deal) .where( Deal.current_status == request.status, Deal.id != request.deal_id, Deal.is_deleted == False, Deal.is_completed == False ) .order_by(Deal.lexorank) .offset(max([request.index - 2, 0])) .limit(2 if not is_first else 1) ) query = await self.session.execute(stmt) boundaries = query.scalars().all() top_boundary: Union[Deal, None] = boundaries[0] if not is_first else None bottom_boundary: Union[Deal, None] = boundaries[1] if len(boundaries) == 2 else None # working when between two elements if top_boundary and bottom_boundary: top_lexorank = lexorank.parse(top_boundary.lexorank) bottom_lexorank = lexorank.parse(bottom_boundary.lexorank) new_rank = lexorank.between(top_lexorank, bottom_lexorank) # working when at the bottom elif top_boundary and not bottom_boundary: new_rank = lexorank.parse(top_boundary.lexorank).next() # working when at the top elif bottom_boundary and not top_boundary: new_rank = lexorank.parse(bottom_boundary.lexorank).prev() elif not top_boundary and not bottom_boundary and len(boundaries) > 0: new_rank = lexorank.parse(boundaries[0].lexorank).prev() else: new_rank = lexorank.middle(lexorank.Bucket.BUCEKT_0) await self.change_status(deal, request.status, user, deadline=request.deadline, comment=request.comment, rank=str(new_rank)) await self.session.commit() return await self.get_summary() async def add_kit_to_deal(self, request: DealAddKitRequest) -> DealAddKitResponse: try: deal = await self._get_deal_by_id(request.deal_id) if not deal: return DealAddKitResponse(ok=False, message="Указанная сделка не найдена") kit = await ServiceService(self.session).get_kit_by_id(request.kit_id) if not kit: return DealAddKitResponse(ok=False, message="Указанный набор услуг не найден") services: list[models.Service] = kit.services insert_data = [] for service in services: price = self.get_service_price(service, 1) insert_data.append({ 'deal_id': deal.id, 'service_id': service.id, 'quantity': 1, 'price': price }) if not insert_data: return DealAddKitResponse(ok=True, message="Набор услуг успешно добавлен к сделке") # Deleting previous services delete_stmt = ( delete( models.DealService ) .where( models.DealService.deal_id == request.deal_id ) ) await self.session.execute(delete_stmt) await self.session.flush() await self.session.execute( insert(models.DealService), insert_data ) await self.session.flush() await self.session.commit() return DealAddKitResponse(ok=True, message="Набор услуг успешно добавлен к сделке") except Exception as e: return DealAddKitResponse(ok=False, message=str(e)) def create_guest_url(self, user: models.User, request: DealCreateGuestUrlRequest) -> DealCreateGuestUrlResponse: # if not user.is_admin: # return DealCreateGuestUrlResponse(ok=False, message='Создать ссылку может только администратор', url="") access_token = AuthService(self.session).create_deal_guest_token(request.deal_id) url = f"deals/{request.deal_id}?accessToken={access_token}" return DealCreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url) async def _create_deal_services_for_prefilling(self, old_deal: models.Deal, new_deal: models.Deal): for service in old_deal.services: deal_service = models.secondary.DealService( service_id=service.service_id, deal_id=new_deal.id, quantity=service.quantity, price=service.price, ) self.session.add(deal_service) async def _create_deal_products_for_prefilling(self, old_deal: models.Deal, new_deal: models.Deal): for old_deal_product in old_deal.products: deal_product = models.secondary.DealProduct( deal_id=new_deal.id, product_id=old_deal_product.product.id, quantity=old_deal_product.quantity, comment=old_deal_product.comment, ) self.session.add(deal_product) await self.session.flush() for old_service in old_deal_product.services: deal_product_service = models.secondary.DealProductService( deal_id=new_deal.id, product_id=old_deal_product.product.id, service_id=old_service.service.id, price=old_service.price ) self.session.add(deal_product_service) async def prefill_deal(self, user, request: DealPrefillRequest) -> DealPrefillResponse: old_deal: models.Deal = await self.get_by_id(user, request.old_deal_id, return_raw=True) new_deal: models.Deal = await self.get_by_id(user, request.new_deal_id, return_raw=True) await self._create_deal_services_for_prefilling(old_deal, new_deal) await self._create_deal_products_for_prefilling(old_deal, new_deal) await self.session.commit() return DealPrefillResponse(ok=True, message="Сделка успешно предзаполнена") # endregion # region Deal services async def add_services(self, request: DealAddServicesRequest): 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_id=service.id, deal_id=deal.id, quantity=quantity ) ) await self.session.commit() return DealAddServicesResponse(ok=True, message='Услуги успешно добавлены') async def update_service_quantity( self, user: Union[models.User, dict], request: DealUpdateServiceQuantityRequest ) -> DealUpdateServiceQuantityResponse: try: self.grant_access(user, request.deal_id) 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_id) ) if not deal_service: raise HTTPException(status_code=404, detail="Сделка не найдена") deal_service.quantity = request.quantity await self.session.commit() return DealUpdateServiceQuantityResponse(ok=True, message='Количество успешно обновлено') except Exception as e: await self.session.rollback() return DealUpdateServiceQuantityResponse(ok=False, message=str(e)) async def add_service(self, user: Union[models.User, dict], request: DealAddServiceRequest ) -> DealAddServiceResponse: try: self.grant_access(user, request.deal_id) deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id)) if not deal: raise HTTPException(status_code=404, detail="Сделка не найдена") 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 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_id) ) if deal_service: raise HTTPException(status_code=400, detail="Услуга уже добавлена") deal_service = models.secondary.DealService( deal_id=request.deal_id, service_id=request.service_id, quantity=request.quantity, price=request.price ) self.session.add(deal_service) await self.session.commit() return DealAddServiceResponse(ok=True, message='Услуга успешно добавлена') except Exception as e: await self.session.rollback() return DealAddServiceResponse(ok=False, message=str(e)) async def delete_service( self, user: Union[models.User, dict], request: DealDeleteServiceRequest ) -> DealDeleteServiceResponse: try: self.grant_access(user, request.deal_id) 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_id) ) if not deal_service: raise HTTPException(status_code=404, detail="Сделка не найдена") await self.session.delete(deal_service) await self.session.commit() return DealDeleteServiceResponse(ok=True, message='Услуга успешно удалена') except Exception as e: await self.session.rollback() return DealDeleteServiceResponse(ok=False, message=str(e)) async def delete_services( self, user: Union[models.User, dict], request: DealDeleteServicesRequest ) -> DealDeleteServicesResponse: try: self.grant_access(user, request) deal_services = await self.session.scalars( select(models.secondary.DealService) .where(models.secondary.DealService.deal_id == request.deal_id, models.secondary.DealService.service_id.in_(request.service_ids)) ) for deal_service in deal_services: await self.session.delete(deal_service) await self.session.commit() return DealDeleteServicesResponse(ok=True, message='Услуги успешно удалены') except Exception as e: await self.session.rollback() return DealDeleteServicesResponse(ok=False, message=str(e)) async def update_service( self, user: Union[models.User, dict], request: DealUpdateServiceRequest ) -> DealUpdateServiceResponse: try: self.grant_access(user, request.deal_id) 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'] del service_dict['employees'] 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) ) # Updating deleting previous employees delete_stmt = ( delete( models.deal_service_employees ) .where( models.deal_service_employees.c.deal_id == request.deal_id, models.deal_service_employees.c.service_id == request.service.service.id, ) ) await self.session.execute(delete_stmt) await self.session.flush() insert_data = [] for employee in request.service.employees: insert_data.append({ 'deal_id': request.deal_id, 'service_id': request.service.service.id, 'user_id': employee.id }) if insert_data: await self.session.execute( insert(models.deal_service_employees), insert_data ) await self.session.flush() await self.session.commit() return DealUpdateServiceQuantityResponse(ok=True, message='Услуга успешно обновлена') except Exception as e: await self.session.rollback() return DealUpdateServiceQuantityResponse(ok=False, message=str(e)) async def copy_services( self, user: Union[models.User, dict], request: DealServicesCopyRequest ) -> DealServicesCopyResponse: try: self.grant_access(user, request.deal_id) source_services_stmt = ( select( models.DealProductService ) .where( models.DealProductService.product_id == request.source_product_id, models.DealProductService.deal_id == request.deal_id, ) ) deal_product_services = ( ( await self.session.scalars( source_services_stmt ) ) .all() ) destination_deal_products_stmt = ( select( models.DealProduct ) .where( models.DealProduct.product_id.in_(request.destination_product_ids), models.DealProduct.deal_id == request.deal_id ) ) destination_deal_products = (await self.session.scalars(destination_deal_products_stmt)).all() insert_data = [] for deal_product in destination_deal_products: for service in deal_product_services: insert_data.append({ 'deal_id': request.deal_id, 'product_id': deal_product.product_id, 'service_id': service.service.id, 'price': service.price, 'is_fixed_price': service.is_fixed_price }) if not insert_data: return DealServicesCopyResponse(ok=True, message='Услуги успешно перенесены') # Deleting previous DealProductService-s delete_stmt = ( delete( models.DealProductService ) .where( models.DealProductService.product_id.in_(request.destination_product_ids), models.DealProductService.deal_id == request.deal_id ) ) await self.session.execute(delete_stmt) await self.session.flush() await self.session.execute( insert(models.DealProductService), insert_data ) await self.session.flush() await self.session.commit() return DealServicesCopyResponse(ok=True, message='Услуги успешно перенесены') except Exception as e: return DealServicesCopyResponse(ok=False, message=str(e)) @staticmethod def get_service_price(service: models.Service, quantity: int): price = 0 price_ranges: list[models.ServicePriceRange] = service.price_ranges for price_range in price_ranges: if price_range.from_quantity <= quantity <= price_range.to_quantity: price = price_range.price break if not price and len(price_ranges) > 0: price = price_ranges[0].price if not price: price = service.price return price # endregion # region Deal products async def update_product_quantity( self, user: Union[models.User, dict], request: DealUpdateProductQuantityRequest ) -> DealUpdateProductQuantityResponse: try: self.grant_access(user, request.deal_id) # check if there is no deal or no product with different exceptions 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) ) if not deal_product: raise HTTPException(status_code=404, detail="Сделка или товар не найдена") deal_product.quantity = request.quantity await self.session.commit() return DealUpdateProductQuantityResponse(ok=True, message='Количество успешно обновлено') except Exception as e: await self.session.rollback() return DealUpdateProductQuantityResponse(ok=False, message=str(e)) async def add_product( self, user: Union[models.User, dict], request: DealAddProductRequest ) -> DealAddProductResponse: try: self.grant_access(user, request.deal_id) 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.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.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.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: await self.session.rollback() return DealAddProductResponse(ok=False, message=str(e)) async def delete_product( self, user: Union[models.User, dict], request: DealDeleteProductRequest ) -> DealDeleteProductResponse: try: self.grant_access(user, request.deal_id) 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) ) if not deal_product: raise HTTPException(status_code=404, detail="Сделка не найдена") await self.session.delete(deal_product) await self.session.commit() return DealDeleteProductResponse(ok=True, message='Товар успешно удален') except Exception as e: await self.session.rollback() return DealDeleteProductResponse(ok=False, message=str(e)) async def delete_products( self, user: Union[models.User, dict], request: DealDeleteProductsRequest ) -> DealDeleteProductsResponse: try: self.grant_access(user, request.deal_id) deal_products = await self.session.scalars( select(models.secondary.DealProduct) .where(models.secondary.DealProduct.deal_id == request.deal_id, models.secondary.DealProduct.product_id.in_(request.product_ids)) ) for deal_product in deal_products: await self.session.delete(deal_product) await self.session.commit() return DealDeleteProductsResponse(ok=True, message='Товары успешно удалены') except Exception as e: await self.session.rollback() return DealDeleteProductsResponse(ok=False, message=str(e)) async def update_product( self, user: Union[models.User, dict], request: DealUpdateProductRequest ): try: self.grant_access(user, request.deal_id) 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 service.is_fixed_price = request_service.is_fixed_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 deal_product.comment = request.product.comment # Updating deleting old employees delete_stmt = ( delete( models.deal_product_service_employees ) .where( models.deal_product_service_employees.c.deal_id == request.deal_id, models.deal_product_service_employees.c.service_id.in_(request_services.union(database_services)), models.deal_product_service_employees.c.product_id == request.product.product.id ) ) await self.session.execute(delete_stmt) await self.session.flush() insert_data = [] for service in request.product.services: service: DealProductServiceSchema for employee in service.employees: insert_data.append({ 'deal_id': request.deal_id, 'service_id': service.service.id, 'product_id': request.product.product.id, 'user_id': employee.id }) if insert_data: await self.session.execute(insert(models.deal_product_service_employees), insert_data) await self.session.flush() await self.session.commit() return DealUpdateProductResponse(ok=True, message='Товар успешно обновлен') except Exception as e: await self.session.rollback() return DealUpdateProductResponse(ok=False, message=str(e)) async def add_kit_to_deal_product( self, user: Union[models.User, dict], request: DealProductAddKitRequest ) -> DealProductAddKitResponse: try: self.grant_access(user, request.deal_id) service_service = ServiceService(self.session) kit = await service_service.get_kit_by_id(request.kit_id) if not kit: return DealProductAddKitResponse(ok=False, message='Указанный набор услуг не найден') services: list[models.Service] = kit.services deal_product_stmt = ( select( models.DealProduct ) .where( models.DealProduct.deal_id == request.deal_id, models.DealProduct.product_id == request.product_id ) ) deal_product: Optional[models.DealProduct] = await self.session.scalar(deal_product_stmt) if not deal_product: return DealProductAddKitResponse(ok=False, message='Указанный товар не найден') insert_data = [] for service in services: service_price = self.get_service_price(service, deal_product.quantity) insert_data.append({ 'deal_id': request.deal_id, 'product_id': deal_product.product_id, 'service_id': service.id, 'price': service_price, }) if not insert_data: return DealProductAddKitResponse(ok=True, message='Набор услуг успешно добавлен к товару') # Deleting previous DealProductService-s delete_stmt = ( delete( models.DealProductService ) .where( models.DealProductService.product_id == deal_product.product_id, models.DealProductService.deal_id == request.deal_id ) ) await self.session.execute(delete_stmt) await self.session.flush() await self.session.execute( insert(models.DealProductService), insert_data ) await self.session.flush() await self.session.commit() return DealProductAddKitResponse(ok=True, message='Набор услуг успешно добавлен к товару') except Exception as e: return DealProductAddKitResponse(ok=False, message=str(e)) # endregion async def complete(self, user: User, request: DealCompleteRequest) -> DealCompleteResponse: try: # check for admin if not user.is_admin: return DealCompleteResponse(ok=False, message='Завершить сделку может только администратор') deal = await self._get_deal_by_id(request.deal_id) if not deal: return DealCompleteResponse(ok=False, message="Сделка не найдена") if deal.group: deals = await DealGroupService(self.session).complete_group(deal.group.id) for completed_deal in deals: await self.change_status(completed_deal, DealStatus.COMPLETED, user) else: deal.is_completed = True await self.change_status(deal, DealStatus.COMPLETED, user) await self.session.commit() return DealCompleteResponse(ok=True, message="Сделка успешно завершена") except Exception as e: await self.session.rollback() return DealCompleteResponse(ok=False, message=str(e)) async def get_quantity_dict(self, deals: List[models.Deal]): services_quantity = defaultdict(lambda: 0) for deal in deals: for product in deal.products: product: DealProduct for service in product.services: service: DealProductService services_quantity[service.service_id] += product.quantity for service in deal.services: service: models.DealService services_quantity[service.service_id] += service.quantity return services_quantity async def _recalculate_price_single(self, deal: models.Deal, quantity_dict: dict): services_quantity = quantity_dict services_prices = {} for product in deal.products: for service in product.services: if service.is_fixed_price: continue quantity = services_quantity[service.service_id] if service.service_id in services_prices: service.price = services_prices[service.service_id] continue price = self.get_service_price( service=service.service, quantity=quantity ) service.price = price services_prices[service.service_id] = price for service in deal.services: service: models.DealService if service.is_fixed_price: continue quantity = services_quantity[service.service_id] price = self.get_service_price( service=service.service, quantity=quantity ) print(service.service_id, price) service.price = price async def _recalculate_price_group(self, group: models.DealGroup): deals = await self.session.scalars( select(Deal) .options( selectinload(Deal.services) .joinedload(models.DealService.service), selectinload(Deal.products) .selectinload(DealProduct.services) .joinedload(DealProductService.service), ) .where(Deal.group == group) ) deals = list(deals.all()) services_quantity = await self.get_quantity_dict(deals) for deal in deals: await self._recalculate_price_single(deal, services_quantity) async def recalculate_price(self, request: DealRecalculatePriceRequest) -> DealRecalculatePriceResponse: try: deal_stmt = ( select( Deal ) .options( selectinload(Deal.services) .joinedload(models.DealService.service), selectinload(Deal.products) .selectinload(DealProduct.services) .joinedload(DealProductService.service), joinedload(Deal.group) ) .where(Deal.id == request.deal_id) ) deal: Deal = await self.session.scalar(deal_stmt) if not deal.group: quantity_dict = await self.get_quantity_dict([deal]) await self._recalculate_price_single(deal, quantity_dict) else: await self._recalculate_price_group(deal.group) await self.session.commit() return DealRecalculatePriceResponse(ok=True, message="Цены успешно пересчитаны") except Exception as e: return DealRecalculatePriceResponse(ok=False, message=str(e)) async def _assign_employee(self, deal: Deal, user: User) -> tuple[bool, str]: assigned_employee_ids = [assignment.user_id for assignment in deal.employees] if user.id in assigned_employee_ids: return False, "Работник уже назначен" assignment = DealEmployees(user_id=user.id, deal_id=deal.id, created_at=datetime.datetime.now()) self.session.add(assignment) await self.session.commit() return True, "Работник успешно назначен" async def _unassign_employee(self, deal: Deal, user: User) -> tuple[bool, str]: assigned_employee_ids = [assignment.user_id for assignment in deal.employees] if user.id not in assigned_employee_ids: return False, "Работник еще не назначен" stmt = delete(DealEmployees).where(and_(DealEmployees.user_id == user.id, DealEmployees.deal_id == deal.id)) await self.session.execute(stmt) await self.session.commit() return True, "Работник успешно удален" async def manage_employee(self, request: ManageEmployeeRequest) -> ManageEmployeeResponse: deal: Optional[Deal] = await self._get_deal_by_id(request.deal_id) if not deal: return ManageEmployeeResponse(ok=False, message=f"Сделка с ID {request.deal_id} не найдена") user: Optional[User] = await self.session.get(User, request.user_id) if not user: return ManageEmployeeResponse(ok=False, message=f"Пользователь с ID {request.user_id} не найден") if request.is_assign: ok, message = await self._assign_employee(deal, user) else: ok, message = await self._unassign_employee(deal, user) return ManageEmployeeResponse(ok=ok, message=message) async def get_available_employees_to_assign(self, deal_id: int) -> GetAvailableEmployeesToAssignResponse: assigned_users = select(DealEmployees.user_id).where(DealEmployees.deal_id == deal_id) stmt_free_employees = ( select(User) .where(and_( User.is_deleted == False, User.role_key == "employee", User.id.not_in(assigned_users), )) ) free_employees = (await self.session.execute(stmt_free_employees)).scalars().all() return GetAvailableEmployeesToAssignResponse(employees=free_employees) async def add_to_group(self, user: User, request: DealAddToGroupRequest) -> DealAddToGroupResponse: try: group_bill_request = await self.session.get(GroupBillRequest, request.group_id) if group_bill_request: raise Exception("Нельзя добавить сделку, так как на группу выставлен счёт.") # changing status if needed deal_id = await self.session.scalar( select(deal_relations.c.deal_id) .where(deal_relations.c.group_id == request.group_id) ) group_deal_status = await self.session.scalar( select(Deal.current_status) .where(Deal.id == deal_id) ) request_deal = await self.session.scalar( select(Deal).where(Deal.id == request.deal_id) ) if group_deal_status != request_deal.current_status: await self.change_status(request_deal, group_deal_status, user) insert_stmt = insert(deal_relations).values({ 'deal_id': request.deal_id, 'group_id': request.group_id }) await self.session.execute(insert_stmt) await self.session.commit() return DealAddToGroupResponse(ok=True, message="Сделка успешно добавлена в группу") except Exception as e: await self.session.rollback() return DealAddToGroupResponse(ok=False, message=str(e)) async def create_group(self, user: User, request: DealCreateGroupRequest) -> DealCreateGroupResponse: try: # getting lexorank for grop group = models.DealGroup( name='', lexorank=lexorank.middle(lexorank.Bucket.BUCEKT_0).__str__(), ) self.session.add(group) await self.session.flush() for deal_id in [request.dragging_deal_id, request.hovered_deal_id]: insert_stmt = insert(deal_relations).values({ 'deal_id': deal_id, 'group_id': group.id }) await self.session.execute(insert_stmt) # changing status if needed on draggable deal dragging_deal = await self.session.scalar( select(Deal).where(Deal.id == request.dragging_deal_id) ) dropped_deal = await self.session.scalar( select(Deal).where(Deal.id == request.hovered_deal_id) ) if dragging_deal.current_status != dropped_deal.current_status: await self.change_status(dragging_deal, DealStatus(dropped_deal.current_status), user) await self.session.commit() return DealCreateGroupResponse(ok=True, message="Группа успешно создана") except Exception as e: return DealCreateGroupResponse(ok=False, message=str(e)) async def remove_from_group(self, request: DealRemoveFromGroupRequest) -> DealRemoveFromGroupResponse: try: delete_stmt = ( delete(deal_relations) .where( deal_relations.c.deal_id == request.deal_id, ) ) await self.session.execute(delete_stmt) await self.session.commit() return DealRemoveFromGroupResponse(ok=True, message="Сделка успешно удалена из группы") except Exception as e: await self.session.rollback() return DealRemoveFromGroupResponse(ok=False, message=str(e)) async def update_group(self, request: DealGroupUpdateRequest) -> DealGroupUpdateResponse: try: group = await self.session.scalar( select(models.DealGroup).where(models.DealGroup.id == request.data.id) ) if not group: return DealGroupUpdateResponse(ok=False, message="Группа не найдена") # update by dictionary request_dict = request.data.dict() update_stmt = ( update( models.DealGroup ) .where(models.DealGroup.id == request.data.id) .values(**request_dict) ) await self.session.execute(update_stmt) await self.session.commit() return DealGroupUpdateResponse(ok=True, message="Группа успешно обновлена") except Exception as e: await self.session.rollback() return DealGroupUpdateResponse(ok=False, message=str(e)) async def change_group_status(self, user: User, request: DealGroupChangeStatusRequest) -> DealGroupChangeStatusResponse: try: # getting all deals in group deals = await self.session.scalars( select(deal_relations.c.deal_id) .where(deal_relations.c.group_id == request.group_id) ) for deal_id in deals: deal = await self.session.scalar( select(Deal).where(Deal.id == deal_id) ) await self.change_status(deal, DealStatus(request.new_status), user) await self.session.commit() return DealGroupChangeStatusResponse(ok=True, message="Статус группы успешно изменен") except Exception as e: await self.session.rollback() return DealGroupChangeStatusResponse(ok=False, message=str(e)) async def get_deals_grouped(self, deal:models.Deal)->List[models.Deal]: if not deal.group: return [deal] deals = await self.session.scalars( select(Deal) .options( selectinload(Deal.services) .joinedload(models.DealService.service), selectinload(Deal.products) .selectinload(DealProduct.services) .joinedload(DealProductService.service), ) .where(Deal.group == deal.group) ) deals = list(deals.all()) return deals