Files
Fulfillment-Backend/services/deal.py

1287 lines
56 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import lexorank
from attr import dataclass
from fastapi import HTTPException
from sqlalchemy import select, func, update, delete, insert
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 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(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),
)
.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).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
# 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
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:
await DealGroupService(self.session).complete_group(deal.group.id)
else:
deal.is_completed = True
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 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