feat: projects and boards

This commit is contained in:
2025-02-07 20:08:14 +04:00
parent 2aa84837e4
commit 9ee3f87de9
25 changed files with 1312 additions and 387 deletions

View File

@@ -1,13 +1,14 @@
from collections import defaultdict
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
from models import User, Service, Client, DealProductService, deal_relations, DealStatusHistory, Product, DealProduct
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
@@ -16,9 +17,9 @@ 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
from services import deal_group
class DealService(BaseService):
@@ -36,9 +37,13 @@ class DealService(BaseService):
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:
async def _get_rank_for_deal(self, status_id: int) -> str:
deal_query = await self.session.execute(
select(Deal).where(Deal.current_status == deal_status).order_by(Deal.lexorank.desc()).limit(1))
select(Deal)
.where(Deal.current_status_id == status_id)
.order_by(Deal.lexorank.desc())
.limit(1)
)
deal = deal_query.scalar_one_or_none()
if not deal:
prev = lexorank.middle(lexorank.Bucket.BUCEKT_0)
@@ -46,59 +51,46 @@ class DealService(BaseService):
return str(lexorank.parse(deal.lexorank).next())
async def change_status(self, deal: Deal,
status: DealStatus,
user: User,
status_id: int,
user: models.User,
deadline: datetime.datetime = None,
rank=None,
comment: str = ''):
if not deal.current_status == status:
if not deal.current_status_id == status_id:
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,
from_status_id=deal.current_status_id,
to_status_id=status_id,
next_status_deadline=deadline,
comment=comment
)
self.session.add(status_change)
deal.current_status = status
deal.current_status_id = status_id
if not rank:
rank = await self._get_rank_for_deal(status)
rank = await self._get_rank_for_deal(status_id)
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)
await deal_group.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:
async def quick_create(self, request: DealQuickCreateRequest, user: models.User) -> DealQuickCreateResponse:
deal_status = await self.session.get(DealStatus, request.status_id)
if not deal_status:
raise HTTPException(status_code=400, detail="Указан некорректный статус")
client_service = ClientService(self.session)
client = await client_service.get_by_name(request.client_name)
@@ -108,28 +100,25 @@ class DealService(BaseService):
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)
rank = await self._get_rank_for_deal(request.status_id)
deal = Deal(
name=request.name,
created_at=datetime.datetime.now(),
client_id=client.id,
current_status=DealStatus.CREATED,
current_status_id=request.status_id,
board_id=deal_status.board_id,
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(
@@ -141,12 +130,12 @@ class DealService(BaseService):
await self.session.commit()
return DealQuickCreateResponse(deal_id=deal.id)
async def change_status_manual(self, request: DealChangeStatusRequest, user: User) -> DealChangeStatusResponse:
async def change_status_manual(self, request: DealChangeStatusRequest, user: models.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.change_status(deal, request.new_status, user)
await self.session.commit()
return DealChangeStatusResponse(ok=True)
@@ -199,7 +188,7 @@ class DealService(BaseService):
Deal,
func.coalesce(price_subquery.c.total_price, 0),
func.row_number().over(
partition_by=Deal.current_status,
partition_by=Deal.current_status_id,
order_by=Deal.lexorank
).label('rank'),
func.coalesce(products_quantity_subquery.c.total_quantity, 0)
@@ -208,7 +197,9 @@ class DealService(BaseService):
selectinload(Deal.status_history),
joinedload(Deal.client),
joinedload(Deal.shipping_warehouse),
joinedload(Deal.bill_request)
joinedload(Deal.bill_request),
joinedload(Deal.status),
joinedload(Deal.board),
)
.outerjoin(
price_subquery, Deal.id == price_subquery.c.deal_id,
@@ -231,8 +222,6 @@ class DealService(BaseService):
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)
@@ -242,9 +231,8 @@ class DealService(BaseService):
id=deal.id,
client_name=deal.client.name,
name=deal.name,
changed_at=last_status.changed_at,
deadline=deadline,
status=last_status.to_status,
status=deal.status,
board=deal.board,
total_price=total_price,
rank=rank,
base_marketplace=base_marketplace,
@@ -308,6 +296,9 @@ class DealService(BaseService):
deal = await self.session.scalar(
select(Deal)
.options(
joinedload(Deal.status),
joinedload(Deal.board)
.joinedload(Board.project),
joinedload(Deal.shipping_warehouse),
joinedload(Deal.client)
.joinedload(Client.details),
@@ -350,7 +341,7 @@ class DealService(BaseService):
raise HTTPException(status_code=404, detail="Сделка не найдена")
return DealSchema.model_validate(deal)
async def update_general_info(self, request: DealUpdateGeneralInfoRequest) -> DealUpdateGeneralInfoResponse:
async def update_general_info(self, request: DealUpdateGeneralInfoRequest, user: User) -> DealUpdateGeneralInfoResponse:
try:
deal: Deal = await self.session.scalar(
select(Deal)
@@ -369,6 +360,15 @@ class DealService(BaseService):
deal.delivery_date = request.data.delivery_date
deal.receiving_slot_date = request.data.receiving_slot_date
if deal.board_id != request.data.board.id or deal.current_status_id != request.data.status.id:
if deal.group:
for deal in deal.group.deals:
deal.board_id = request.data.board.id
await self.change_status(deal, request.data.status.id, user)
else:
deal.board_id = request.data.board.id
await self.change_status(deal, request.data.status.id, user)
if deal.group:
for deal in deal.group.deals:
deal.is_accounted = request.data.is_accounted
@@ -402,7 +402,7 @@ class DealService(BaseService):
stmt = (
select(Deal)
.where(
Deal.current_status == request.status,
Deal.current_status_id == request.status_id,
Deal.id != request.deal_id,
Deal.is_deleted == False,
Deal.is_completed == False
@@ -433,7 +433,7 @@ class DealService(BaseService):
else:
new_rank = lexorank.middle(lexorank.Bucket.BUCEKT_0)
await self.change_status(deal, request.status, user,
await self.change_status(deal, request.status_id, user,
deadline=request.deadline,
comment=request.comment,
rank=str(new_rank))
@@ -1079,12 +1079,9 @@ class DealService(BaseService):
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)
await deal_group.DealGroupService(self.session).complete_group(deal.group.id)
else:
deal.is_completed = True
await self.change_status(deal, DealStatus.COMPLETED, user)
await self.session.commit()
return DealCompleteResponse(ok=True, message="Сделка успешно завершена")
@@ -1234,14 +1231,15 @@ class DealService(BaseService):
async def _create_deal_from_excel(
self,
client: Client,
deal_status: DealStatus,
breakdown: CityBreakdownFromExcelSchema,
user: User,
) -> Deal:
rank = await self._get_rank_for_deal(DealStatus.CREATED)
rank = await self._get_rank_for_deal(deal_status.id)
deal = Deal(
name=f"{client.name} - {breakdown.base_marketplace.key.upper()} - {breakdown.shipping_warehouse.name}",
created_at=datetime.datetime.now(),
current_status=DealStatus.CREATED,
current_status_id=deal_status.id,
lexorank=rank,
client_id=client.id,
base_marketplace_key=breakdown.base_marketplace.key,
@@ -1250,19 +1248,8 @@ class DealService(BaseService):
self.session.add(deal)
await self.session.flush()
await self.change_status(deal, DealStatus.AWAITING_ACCEPTANCE, user)
return deal
async def _create_group(self) -> DealGroup:
group = models.DealGroup(
name='',
lexorank=lexorank.middle(lexorank.Bucket.BUCEKT_0).__str__(),
)
self.session.add(group)
await self.session.flush()
return group
async def _get_or_create_warehouse(
self,
shipping_warehouse: OptionalShippingWarehouseSchema,
@@ -1288,8 +1275,12 @@ class DealService(BaseService):
if not client:
return CreateDealsFromExcelResponse(ok=False, message=f"Клиент с ID {request.client_id} не найден")
deal_status: Optional[DealStatus] = await self.session.get(DealStatus, request.status_id)
if not deal_status:
return CreateDealsFromExcelResponse(ok=False, message=f"Статус с ID {request.status_id} не найден")
deals_dict: dict[str, Deal] = {}
group = await self._create_group()
group = await deal_group.DealGroupService(self.session).create_group_model()
for product_data in request.products:
for breakdown in product_data.cities_breakdown:
@@ -1298,7 +1289,7 @@ class DealService(BaseService):
key = f"{breakdown.shipping_warehouse.id} - {breakdown.base_marketplace.key}"
deal = deals_dict.get(key)
if not deal:
deal = await self._create_deal_from_excel(client, breakdown, user)
deal = await self._create_deal_from_excel(client, deal_status, breakdown, user)
deals_dict[key] = deal
insert_stmt = insert(deal_relations).values({
@@ -1317,121 +1308,6 @@ class DealService(BaseService):
await self.session.commit()
return CreateDealsFromExcelResponse(ok=True, message="Сделки успешно созданы")
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 = await self._create_group()
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]