feat: deal group

This commit is contained in:
2024-11-08 15:58:41 +03:00
parent fced9b8101
commit 25e6cf0e7e
8 changed files with 400 additions and 57 deletions

View File

@@ -12,6 +12,6 @@ from .marketplace import *
from .payroll import * from .payroll import *
from .billing import * from .billing import *
from .marketplace_products import * from .marketplace_products import *
# from .deal_group import * from .deal_group import *
configure_mappers() configure_mappers()

View File

@@ -11,7 +11,7 @@ from .shipping_warehouse import ShippingWarehouse
if TYPE_CHECKING: if TYPE_CHECKING:
from . import (DealBillRequest, ServicePriceCategory, from . import (DealBillRequest, ServicePriceCategory,
# DealGroup DealGroup
) )
@@ -91,13 +91,12 @@ class Deal(BaseModel):
category: Mapped[Optional["ServicePriceCategory"]] = relationship('ServicePriceCategory', category: Mapped[Optional["ServicePriceCategory"]] = relationship('ServicePriceCategory',
secondary=DealPriceCategory.__table__, secondary=DealPriceCategory.__table__,
lazy='joined') lazy='joined')
# group: Mapped[Optional["DealGroup"]] = relationship( group: Mapped[Optional["DealGroup"]] = relationship(
# 'DealGroup', 'DealGroup',
# secondary='deal_relations', secondary='deal_relations',
# lazy='joined', lazy='joined',
# uselist=False, back_populates='deals'
# back_populates='deals' )
# )
class DealStatusHistory(BaseModel): class DealStatusHistory(BaseModel):

34
models/deal_group.py Normal file
View File

@@ -0,0 +1,34 @@
from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, Table, Column
from sqlalchemy.orm import mapped_column, Mapped, relationship
from models import BaseModel
if TYPE_CHECKING:
from models import Deal
class DealGroup(BaseModel):
__tablename__ = 'deal_groups'
id: Mapped[int] = mapped_column(
primary_key=True
)
name: Mapped[Optional[str]] = mapped_column(
nullable=True
)
lexorank: Mapped[str] = mapped_column(
nullable=False
)
deals: Mapped["Deal"] = relationship(
back_populates='group',
secondary='deal_relations'
)
deal_relations = Table(
'deal_relations',
BaseModel.metadata,
Column('deal_id', ForeignKey('deals.id'), primary_key=True, unique=True),
Column('group_id', ForeignKey('deal_groups.id'), primary_key=True)
)

View File

@@ -42,7 +42,7 @@ async def create_deal_bill(
operation_id='cancel_deal_bill', operation_id='cancel_deal_bill',
response_model=CancelDealBillResponse response_model=CancelDealBillResponse
) )
async def create_deal_bill( async def cancel_deal_billing(
session: SessionDependency, session: SessionDependency,
request: CancelDealBillRequest, request: CancelDealBillRequest,
user: CurrentUserDependency user: CurrentUserDependency

View File

@@ -442,4 +442,73 @@ async def get_deal_products_barcodes_pdf(
mime_type='application/pdf' mime_type='application/pdf'
) )
# endregion
# region Deal groups
@deal_router.post(
'/add-to-group',
response_model=DealAddToGroupResponse,
operation_id='add_deal_to_group',
dependencies=[Depends(authorized_user)]
)
async def add_to_group(
request: DealAddToGroupRequest,
session: SessionDependency,
user: CurrentUserDependency
):
return await DealService(session).add_to_group(user, request)
@deal_router.post(
'/create-group',
response_model=DealCreateGroupResponse,
operation_id='create_deal_group',
dependencies=[Depends(authorized_user)]
)
async def create_group(
request: DealCreateGroupRequest,
session: SessionDependency,
user: CurrentUserDependency
):
return await DealService(session).create_group(user, request)
@deal_router.post(
'/remove-from-group',
response_model=DealRemoveFromGroupResponse,
operation_id='remove_deal_from_group',
dependencies=[Depends(authorized_user)]
)
async def remove_from_group(
request: DealRemoveFromGroupRequest,
session: SessionDependency,
):
return await DealService(session).remove_from_group( request)
# route to update group name
@deal_router.post(
'/group/update',
response_model=DealGroupUpdateResponse,
operation_id='update_deal_group',
dependencies=[Depends(authorized_user)]
)
async def update_group(
request: DealGroupUpdateRequest,
session: SessionDependency,
):
return await DealService(session).update_group(request)
# route to change group status
@deal_router.post(
'/group/change-status',
response_model=DealGroupChangeStatusResponse,
operation_id='change_deal_group_status',
dependencies=[Depends(authorized_user)]
)
async def change_group_status(
request: DealGroupChangeStatusRequest,
session: SessionDependency,
user: CurrentUserDependency
):
return await DealService(session).change_group_status(user,request)
# endregion # endregion

View File

@@ -48,7 +48,7 @@ class DealSummary(BaseSchema):
delivery_date: Optional[datetime.datetime] = None delivery_date: Optional[datetime.datetime] = None
receiving_slot_date: Optional[datetime.datetime] = None receiving_slot_date: Optional[datetime.datetime] = None
bill_request: Optional[DealBillRequestSchema] = None bill_request: Optional[DealBillRequestSchema] = None
# group: Optional[DealGroupSchema] = None group: Optional[DealGroupSchema] = None
class DealServiceSchema(BaseSchema): class DealServiceSchema(BaseSchema):
@@ -249,6 +249,29 @@ class DealRecalculatePriceRequest(BaseSchema):
deal_id: int deal_id: int
class DealAddToGroupRequest(BaseSchema):
deal_id: int
group_id: int
class DealCreateGroupRequest(BaseSchema):
dragging_deal_id: int
hovered_deal_id: int
class DealRemoveFromGroupRequest(BaseSchema):
deal_id: int
class DealGroupUpdateRequest(BaseSchema):
data: DealGroupSchema
class DealGroupChangeStatusRequest(BaseSchema):
group_id: int
new_status: int
# endregion Requests # endregion Requests
# region Responses # region Responses
@@ -356,4 +379,25 @@ class DealPrefillResponse(OkMessageSchema):
class DealRecalculatePriceResponse(OkMessageSchema): class DealRecalculatePriceResponse(OkMessageSchema):
pass pass
class DealAddToGroupResponse(OkMessageSchema):
pass
class DealCreateGroupResponse(OkMessageSchema):
pass
class DealRemoveFromGroupResponse(OkMessageSchema):
pass
class DealGroupUpdateResponse(OkMessageSchema):
pass
class DealGroupChangeStatusResponse(OkMessageSchema):
pass
# endregion Responses # endregion Responses

View File

@@ -1,3 +1,4 @@
from collections import defaultdict
from io import BytesIO from io import BytesIO
from typing import List from typing import List
@@ -10,11 +11,12 @@ from weasyprint import HTML, CSS
import backend.config import backend.config
import constants import constants
import models
from constants import MONTHS, ENV from constants import MONTHS, ENV
from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \ from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \
BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, DeleteBillRequestSchema, \ BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, DeleteBillRequestSchema, \
ProductBillingDocumentPdf, ServiceBillingDocumentPdf ProductBillingDocumentPdf, ServiceBillingDocumentPdf
from models import DealBillRequest, Deal, DealProduct, DealService as DealServiceModel from models import DealBillRequest, Deal, DealProduct, DealService as DealServiceModel, DealProductService
from schemas.billing import * from schemas.billing import *
from services.base import BaseService from services.base import BaseService
from services.deal import DealService from services.deal import DealService
@@ -73,25 +75,54 @@ class BillingService(BaseService):
deal_service = DealService(self.session) deal_service = DealService(self.session)
billing_client = BillingClient(backend.config.BILLING_API_KEY) billing_client = BillingClient(backend.config.BILLING_API_KEY)
deal: Deal = await deal_service.get_by_id(user, request.deal_id) basic_deal: Deal = await deal_service.get_by_id(user, request.deal_id, return_raw=True)
deals = await deal_service.get_deals_grouped(basic_deal)
billing_request_values: List[CreateBillingRequestValue] = [] billing_request_values: List[CreateBillingRequestValue] = []
for product in deal.products:
for service in product.services: psq = defaultdict(lambda: defaultdict(lambda: 0))
sq = defaultdict(lambda: 0)
services_dict = {}
products_dict = {}
for deal in deals:
for product in deal.products:
product: DealProduct
for service in product.services:
service: DealProductService
psq[product.product_id][service.service_id] += product.quantity
products_dict[product.product_id] = product.product
services_dict[service.service_id] = service.service
for deal in deals:
for service in deal.services:
service: models.DealService
services_dict[service.service_id] = service.service
sq[service.service_id] += service.quantity
for product_id, services_ids in psq.items():
product: models.Product = products_dict[product_id]
for service_id in services_ids:
service = services_dict[service_id]
service: models.Service
quantity = psq[product_id][service_id]
billing_request_values.append( billing_request_values.append(
CreateBillingRequestValue( CreateBillingRequestValue(
name=f'[{product.product.name}] - {service.service.name}', name=f'[{product.name}] - {service.name}',
price=service.price, price=service.price,
amount=product.quantity amount=quantity
) )
) )
for service in deal.services: for service_id, quantity in sq.items():
service: models.Service = services_dict[service_id]
billing_request_values.append( billing_request_values.append(
CreateBillingRequestValue( CreateBillingRequestValue(
name=f'{service.service.name}', name=f'{service.name}',
price=service.price, price=service.price,
amount=service.quantity amount=quantity
) )
) )
deal = basic_deal
create_bill_request = CreateBillRequestSchema( create_bill_request = CreateBillRequestSchema(
listener_transaction_id=deal.id, listener_transaction_id=deal.id,
payer_name=deal.client.name, payer_name=deal.client.name,

View File

@@ -1,4 +1,5 @@
import lexorank import lexorank
from attr import dataclass
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import select, func, update, delete, insert from sqlalchemy import select, func, update, delete, insert
from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm import joinedload, selectinload
@@ -6,7 +7,7 @@ from starlette import status
import models.deal import models.deal
import models.secondary import models.secondary
from models import User, Service, Client, DealProductService from models import User, Service, Client, DealProductService, deal_relations
from models.deal import * from models.deal import *
from schemas.client import ClientDetailsSchema from schemas.client import ClientDetailsSchema
from schemas.deal import * from schemas.deal import *
@@ -248,7 +249,7 @@ class DealService(BaseService):
delivery_date=deal.delivery_date, delivery_date=deal.delivery_date,
receiving_slot_date=deal.receiving_slot_date, receiving_slot_date=deal.receiving_slot_date,
bill_request=deal.bill_request, bill_request=deal.bill_request,
# group=deal.group group=deal.group
) )
) )
return DealSummaryResponse(summaries=summaries) return DealSummaryResponse(summaries=summaries)
@@ -1048,6 +1049,66 @@ class DealService(BaseService):
await self.session.rollback() await self.session.rollback()
return DealCompleteResponse(ok=False, message=str(e)) 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: async def recalculate_price(self, request: DealRecalculatePriceRequest) -> DealRecalculatePriceResponse:
try: try:
deal_stmt = ( deal_stmt = (
@@ -1060,46 +1121,151 @@ class DealService(BaseService):
selectinload(Deal.products) selectinload(Deal.products)
.selectinload(DealProduct.services) .selectinload(DealProduct.services)
.joinedload(DealProductService.service), .joinedload(DealProductService.service),
joinedload(Deal.group)
) )
.where(Deal.id == request.deal_id) .where(Deal.id == request.deal_id)
) )
deal: Deal = await self.session.scalar(deal_stmt) deal: Deal = await self.session.scalar(deal_stmt)
services_quantity = defaultdict(lambda: 0) if not deal.group:
for product in deal.products: quantity_dict = await self.get_quantity_dict([deal])
product: DealProduct await self._recalculate_price_single(deal, quantity_dict)
for service in product.services: else:
service: DealProductService await self._recalculate_price_group(deal.group)
if service.is_fixed_price:
continue
services_quantity[service.service_id] += product.quantity
services_prices = {}
for product in deal.products:
for service in product.services:
if service.is_fixed_price:
continue
quantity = services_quantity[service.service_id]
print(service.service_id, quantity)
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
)
print(service.service_id, price)
service.price = price
services_prices[service.service_id] = price
for service in deal.services:
service: models.DealService
if service.is_fixed_price:
continue
price = self.get_service_price(
service=service.service,
quantity=service.quantity
)
service.price = price
await self.session.commit() await self.session.commit()
return DealRecalculatePriceResponse(ok=True, message="Цены успешно пересчитаны") return DealRecalculatePriceResponse(ok=True, message="Цены успешно пересчитаны")
except Exception as e: except Exception as e:
return DealRecalculatePriceResponse(ok=False, message=str(e)) return DealRecalculatePriceResponse(ok=False, message=str(e))
async def add_to_group(self, user: User, request: DealAddToGroupRequest) -> DealAddToGroupResponse:
try:
# 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