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

119
services/board.py Normal file
View File

@@ -0,0 +1,119 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import select, and_, func
from models import Board, Deal
from schemas.board import *
from services.base import BaseService
class BoardService(BaseService):
async def _get_boards_for_project(self, project_id: int) -> list[Board]:
stmt = (
select(Board)
.where(
and_(
Board.is_deleted == False,
Board.project_id == project_id,
)
)
.order_by(Board.ordinal_number)
)
boards = (await self.session.scalars(stmt)).all()
return list(boards)
async def get_boards(self, project_id: int) -> GetBoardsResponse:
boards = await self._get_boards_for_project(project_id)
return GetBoardsResponse(boards=boards)
async def create_board(self, request: CreateBoardRequest) -> CreateBoardResponse:
boards = await self._get_boards_for_project(request.board.project_id)
if len(boards) == 0:
ordinal_number = 1
else:
ordinal_number = boards[-1].ordinal_number + 1
board = Board(
**request.board.model_dump(),
created_at=datetime.now(),
ordinal_number=ordinal_number,
)
self.session.add(board)
await self.session.commit()
return CreateBoardResponse(ok=True, message="Доска успешно создана")
async def _get_board_by_id(self, board_id: int) -> Optional[Board]:
return await self.session.get(Board, board_id)
async def update_board(self, request: UpdateBoardRequest) -> UpdateBoardResponse:
board = await self._get_board_by_id(request.board.id)
if not board:
return UpdateBoardResponse(ok=False, message=f"Доска с ID {request.board.id} не найдена")
board.name = request.board.name
await self.session.commit()
return UpdateBoardResponse(ok=True, message="Доска успешно обновлена")
async def update_board_order(self, request: UpdateBoardOrderRequest) -> UpdateBoardOrderResponse:
boards = await self._get_boards_for_project(request.project_id)
board_idx = 0
while board_idx < len(boards) and boards[board_idx].id != request.board_id:
board_idx += 1
if board_idx == len(boards):
return UpdateBoardOrderResponse(ok=False, message=f"Доска с ID {request.board_id} не найдена в проекте")
board = boards.pop(board_idx)
boards.insert(request.new_ordinal_number - 1, board)
new_ordinal_number = 1
for board in boards:
board.ordinal_number = new_ordinal_number
new_ordinal_number += 1
await self.session.commit()
return UpdateBoardOrderResponse(ok=True, message="Порядок досок изменен")
async def _count_deals_in_progress(self, board_id: int) -> int:
stmt = (
select(func.count(Deal.id))
.where(
and_(
Deal.board_id == board_id,
Deal.is_deleted == False,
Deal.is_completed == False,
)
)
)
return (await self.session.scalars(stmt)).first()
async def _count_deals(self, board_id: int) -> int:
stmt = (
select(func.count(Deal.id))
.where(Deal.board_id == board_id)
)
return (await self.session.scalars(stmt)).first()
async def delete_board(self, board_id: int) -> DeleteBoardResponse:
board = await self._get_board_by_id(board_id)
if not board:
return DeleteBoardResponse(ok=False, message=f"Доска с ID {board_id} не найдена")
count_deals_in_progress = await self._count_deals_in_progress(board_id)
if count_deals_in_progress != 0:
return DeleteBoardResponse(
ok=False,
message=f"Нельзя удалить доску с активными сделками",
)
count_deals = await self._count_deals(board_id)
if count_deals == 0:
await self.session.delete(board)
else:
board.is_deleted = True
for status in board.deal_statuses:
status.is_deleted = True
await self.session.commit()
return DeleteBoardResponse(ok=True, message="Доска успешно удалена")

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]

View File

@@ -1,10 +1,12 @@
from sqlalchemy import select
from lexorank import lexorank
from sqlalchemy import select, insert, update, delete
from sqlalchemy.orm import selectinload
from models import DealService as DealServiceModel
from models.deal_group import DealGroup
from schemas.deal import *
from models import DealService as DealServiceModel, User, Deal, DealProduct, Product, GroupBillRequest
from models.deal_group import DealGroup, deal_relations
from schemas.group import *
from services.base import BaseService
from services.deal import DealService
class DealGroupService(BaseService):
@@ -25,6 +27,63 @@ class DealGroupService(BaseService):
)
return group.deals if group else []
async def create_group_model(self) -> DealGroup:
group = DealGroup(
name='',
lexorank=lexorank.middle(lexorank.Bucket.BUCEKT_0).__str__(),
)
self.session.add(group)
await self.session.flush()
return group
async def create_group(self, user: User, request: DealCreateGroupRequest) -> DealCreateGroupResponse:
try:
group = await self.create_group_model()
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_id != dropped_deal.current_status_id:
deal_service = DealService(self.session)
await deal_service.change_status(dragging_deal, dropped_deal.current_status_id, user)
await self.session.commit()
return DealCreateGroupResponse(ok=True, message="Группа успешно создана")
except Exception as e:
return DealCreateGroupResponse(ok=False, message=str(e))
async def update_group(self, request: DealGroupUpdateRequest) -> DealGroupUpdateResponse:
try:
group = await self.session.scalar(
select(DealGroup).where(DealGroup.id == request.data.id)
)
if not group:
return DealGroupUpdateResponse(ok=False, message="Группа не найдена")
# update by dictionary
request_dict = request.data.model_dump()
request_dict.pop("bill_request", None)
update_stmt = (
update(DealGroup)
.where(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 complete_group(self, group_id: int) -> list[Deal]:
deals = await self.get_deals_by_group_id(group_id)
for deal in deals:
@@ -36,3 +95,74 @@ class DealGroupService(BaseService):
for deal in deals:
deal.is_deleted = True
await self.session.commit()
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)
)
deal_service = DealService(self.session)
for deal_id in deals:
deal = await self.session.scalar(
select(Deal).where(Deal.id == deal_id)
)
await deal_service.change_status(deal, 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 add_deal(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_id)
.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_id:
await DealService(self.session).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 remove_deal(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))

73
services/project.py Normal file
View File

@@ -0,0 +1,73 @@
from datetime import datetime
from sqlalchemy import select, update, func, delete, and_
from models import Project, Board
from schemas.project import *
from services.base import BaseService
class ProjectService(BaseService):
async def get_projects(self) -> GetProjectsResponse:
boards_sub = (
select(Board)
.where(Board.is_deleted == False)
.subquery()
)
stmt = (
select(
Project.id,
Project.name,
func.count(boards_sub.c.id)
)
.join(boards_sub, Project.id == boards_sub.c.project_id, isouter=True)
.where(Project.is_deleted == False)
.group_by(Project.id, Project.name)
.order_by(Project.name)
)
project_data = (await self.session.execute(stmt)).all()
projects = []
for project_id, name, boards_count in project_data:
project = ProjectSchemaWithCount(id=project_id, name=name, boards_count=boards_count)
projects.append(project)
return GetProjectsResponse(projects=projects)
async def create_project(self, request: CreateProjectRequest) -> CreateProjectResponse:
project = Project(
name=request.project.name,
created_at=datetime.now(),
)
self.session.add(project)
await self.session.commit()
return UpdateProjectResponse(ok=True, message="Проект успешно создан")
async def update_project(self, request: UpdateProjectRequest) -> UpdateProjectResponse:
stmt = (
update(Project)
.where(Project.id == request.project.id)
.values(name=request.project.name)
)
await self.session.execute(stmt)
await self.session.commit()
return UpdateProjectResponse(ok=True, message="Проект успешно изменен")
async def delete_project(self, project_id: int) -> DeleteProjectResponse:
stmt_boards = select(Board).where(Board.project_id == project_id)
boards = (await self.session.scalars(stmt_boards)).all()
if len(boards) == 0:
stmt = (
delete(Project)
.where(Project.id == project_id)
)
else:
stmt = (
update(Project)
.where(Project.id == project_id)
.values(is_deleted=True)
)
await self.session.execute(stmt)
await self.session.commit()
return DeleteProjectResponse(ok=True, message="Проект успешно удален")

View File

@@ -5,7 +5,7 @@ from sqlalchemy import select, and_, union_all, func, Subquery, literal
from enums.profit_table_group_by import ProfitTableGroupBy
from models import DealService, Deal, DealStatusHistory, DealProductService, DealProduct, Service, Client, \
ShippingWarehouse, BaseMarketplace, User
ShippingWarehouse, BaseMarketplace, User, Project, Board
from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataRequest, ProfitChartDataItem, \
GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters
from services.base import BaseService
@@ -24,7 +24,7 @@ class ProfitStatisticsService(BaseService):
'day',
Deal.created_at,
).label('date'),
Deal.current_status,
Deal.current_status_id,
)
.subquery()
)
@@ -51,7 +51,7 @@ class ProfitStatisticsService(BaseService):
'day',
last_statuses.c.changed_at,
).label('date'),
Deal.current_status,
Deal.current_status_id,
)
.join(last_statuses, last_statuses.c.deal_id == Deal.id)
.subquery()
@@ -97,8 +97,7 @@ class ProfitStatisticsService(BaseService):
return data
@staticmethod
def _get_stmt_deal_services(sub_filtered_status_history: Subquery):
def _get_stmt_deal_services(self, sub_filtered_status_history: Subquery):
return (
select(
Deal.id.label("deal_id"),
@@ -112,10 +111,24 @@ class ProfitStatisticsService(BaseService):
.join(DealService, Deal.id == DealService.deal_id)
.join(Service, DealService.service_id == Service.id)
.join(sub_filtered_status_history, Deal.id == sub_filtered_status_history.c.deal_id)
.where(and_(Deal.is_deleted == False, Deal.is_accounted == True))
.where(
and_(
Deal.is_deleted == False,
Deal.is_accounted == True,
Deal.is_completed == True if self.is_completed_only else True
)
)
.group_by(Deal.id, "date")
)
@staticmethod
def _board_ids_for_project(project_id: int, stmt):
board_ids_stmt = (
select(Board.id)
.where(Board.project_id == project_id)
)
return stmt.where(Deal.board_id.in_(board_ids_stmt))
@staticmethod
def _apply_filters(request: CommonProfitFilters, stmt_deal_services, stmt_deal_product_services):
if request.client_id != -1:
@@ -127,9 +140,21 @@ class ProfitStatisticsService(BaseService):
stmt_deal_product_services = stmt_deal_product_services.where(
Deal.base_marketplace_key == request.base_marketplace_key)
if request.deal_status_id != -1:
stmt_deal_services = stmt_deal_services.where(Deal.current_status == request.deal_status_id)
stmt_deal_product_services = stmt_deal_product_services.where(Deal.current_status == request.deal_status_id)
if request.project_id != -1:
stmt_deal_services = ProfitStatisticsService._board_ids_for_project(request.project_id, stmt_deal_services)
stmt_deal_product_services = ProfitStatisticsService._board_ids_for_project(
request.project_id,
stmt_deal_product_services,
)
if request.board_id != -1:
stmt_deal_services = stmt_deal_services.where(Deal.board_id == request.board_id)
stmt_deal_product_services = stmt_deal_product_services.where(Deal.board_id == request.board_id)
if request.deal_status_id != -1:
stmt_deal_services = stmt_deal_services.where(Deal.current_status_id == request.deal_status_id)
stmt_deal_product_services = stmt_deal_product_services.where(
Deal.current_status_id == request.deal_status_id)
if request.manager_id != -1:
stmt_deal_services = stmt_deal_services.where(Deal.manager_id == request.manager_id)
@@ -137,8 +162,7 @@ class ProfitStatisticsService(BaseService):
return stmt_deal_services, stmt_deal_product_services
@staticmethod
def _get_stmt_product_services():
def _get_stmt_product_services(self):
return (
select(
Deal.id.label("deal_id"),
@@ -154,7 +178,13 @@ class ProfitStatisticsService(BaseService):
)
)
.join(Service, DealProductService.service_id == Service.id)
.where(and_(Deal.is_deleted == False, Deal.is_accounted == True))
.where(
and_(
Deal.is_deleted == False,
Deal.is_accounted == True,
Deal.is_completed == True if self.is_completed_only else True,
)
)
.group_by(Deal.id)
)
@@ -226,16 +256,47 @@ class ProfitStatisticsService(BaseService):
)
@staticmethod
def _join_and_group_by_statuses(stmt):
def _join_and_group_by_projects(stmt):
return (
select(
Deal.current_status.label("grouped_value"),
Project.id,
Project.name.label("grouped_value"),
func.count(stmt.c.deal_id).label("deals_count"),
func.sum(stmt.c.revenue).label("revenue"),
func.sum(stmt.c.profit).label("profit"),
)
.join(Deal, Deal.id == stmt.c.deal_id)
.group_by(Deal.current_status)
.join(Board, Board.id == Deal.board_id)
.join(Project, Project.id == Board.project_id)
.group_by(Project.id, Project.name)
)
@staticmethod
def _join_and_group_by_boards(stmt):
return (
select(
Board.id,
Board.name.label("grouped_value"),
func.count(stmt.c.deal_id).label("deals_count"),
func.sum(stmt.c.revenue).label("revenue"),
func.sum(stmt.c.profit).label("profit"),
)
.join(Deal, Deal.id == stmt.c.deal_id)
.join(Board, Board.id == Deal.board_id)
.group_by(Board.id, Board.name)
)
@staticmethod
def _join_and_group_by_statuses(stmt):
return (
select(
Deal.current_status_id.label("grouped_value"),
func.count(stmt.c.deal_id).label("deals_count"),
func.sum(stmt.c.revenue).label("revenue"),
func.sum(stmt.c.profit).label("profit"),
)
.join(Deal, Deal.id == stmt.c.deal_id)
.group_by(Deal.current_status_id)
)
@staticmethod
@@ -320,6 +381,7 @@ class ProfitStatisticsService(BaseService):
async def _get_data_grouped_by_date(self, request: CommonProfitFilters, is_chart: bool = True):
self.date_from, self.date_to = request.date_range
self.is_completed_only = request.is_completed_only
self.filters = request
sub_deals_dates = self._get_deals_dates(request.deal_status_id)
@@ -399,6 +461,18 @@ class ProfitStatisticsService(BaseService):
return await self._table_data_from_stmt(stmt_grouped_by_clients)
async def _get_table_grouped_by_projects(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse:
sub_grouped_by_deals = self._get_common_table_grouped(request)
stmt_grouped_by_projects = self._join_and_group_by_projects(sub_grouped_by_deals)
return await self._table_data_from_stmt(stmt_grouped_by_projects)
async def _get_table_grouped_by_boards(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse:
sub_grouped_by_deals = self._get_common_table_grouped(request)
stmt_grouped_by_boards = self._join_and_group_by_boards(sub_grouped_by_deals)
return await self._table_data_from_stmt(stmt_grouped_by_boards)
async def _get_table_grouped_by_statuses(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse:
date_from, date_to = request.date_range
@@ -449,6 +523,10 @@ class ProfitStatisticsService(BaseService):
return await self._get_table_grouped_by_dates(request)
case ProfitTableGroupBy.BY_CLIENTS:
return await self._get_table_grouped_by_clients(request)
case ProfitTableGroupBy.BY_PROJECTS:
return await self._get_table_grouped_by_projects(request)
case ProfitTableGroupBy.BY_BOARDS:
return await self._get_table_grouped_by_boards(request)
case ProfitTableGroupBy.BY_STATUSES:
return await self._get_table_grouped_by_statuses(request)
case ProfitTableGroupBy.BY_WAREHOUSES:

128
services/status.py Normal file
View File

@@ -0,0 +1,128 @@
from typing import Optional
from sqlalchemy import select, and_, func
from models import DealStatus, Deal
from schemas.status import *
from services.base import BaseService
class StatusService(BaseService):
async def _get_statuses_for_board(self, board_id: int) -> list[DealStatus]:
stmt = (
select(DealStatus)
.where(
and_(
DealStatus.board_id == board_id,
DealStatus.is_deleted == False,
)
)
.order_by(DealStatus.ordinal_number)
)
statuses = (await self.session.scalars(stmt)).all()
return list(statuses)
async def _get_status_by_id(self, status_id: int) -> Optional[DealStatus]:
stmt = (
select(DealStatus)
.where(DealStatus.id == status_id)
)
status = await self.session.scalar(stmt)
return status
async def create_status(self, request: CreateStatusRequest) -> CreateStatusResponse:
statuses = await self._get_statuses_for_board(request.status.board_id)
if len(statuses) == 0:
ordinal_number = 1
else:
statuses[-1].is_finishing = False
ordinal_number = statuses[-1].ordinal_number + 1
status = DealStatus(
**request.status.model_dump(),
ordinal_number=ordinal_number,
is_finishing=True,
)
self.session.add(status)
await self.session.commit()
return CreateStatusResponse(ok=True, message="Статус успешно создан")
async def update_status(self, request: UpdateStatusRequest) -> UpdateStatusResponse:
status = await self._get_status_by_id(request.status.id)
if not status:
return UpdateStatusResponse(ok=False, message=f"Статус с ID {request.status.id} не найден")
status.name = request.status.name
await self.session.commit()
return UpdateStatusResponse(ok=True, message="Статус успешно изменен")
async def update_status_order(self, request: UpdateStatusOrderRequest) -> UpdateStatusOrderResponse:
statuses = await self._get_statuses_for_board(request.board_id)
status_idx = 0
while status_idx < len(statuses) and statuses[status_idx].id != request.status_id:
status_idx += 1
if status_idx == len(statuses):
return UpdateStatusOrderResponse(ok=False, message=f"Статус с ID {request.status_id} не найден")
status = statuses.pop(status_idx)
statuses.insert(request.new_ordinal_number - 1, status)
new_ordinal_number = 1
for status in statuses:
status.ordinal_number = new_ordinal_number
status.is_finishing = False
new_ordinal_number += 1
statuses[-1].is_finishing = True
await self.session.commit()
return UpdateStatusOrderResponse(ok=True, message="Порядок статусов изменен")
async def _count_deals_in_progress(self, status_id: int) -> int:
stmt = (
select(func.count(Deal.id))
.where(
and_(
Deal.current_status_id == status_id,
Deal.is_deleted == False,
Deal.is_completed == False,
)
)
)
return (await self.session.scalars(stmt)).first()
async def _count_deals(self, status_id: int) -> int:
stmt = (
select(func.count(Deal.id))
.where(Deal.current_status_id == status_id)
)
return (await self.session.scalars(stmt)).first()
async def _set_finishing_flag_to_prev_status(self, status: DealStatus):
statuses = await self._get_statuses_for_board(status.board_id)
if len(statuses) < 2:
return
statuses[-2].is_finishing = True
statuses[-1].is_finishing = False
async def delete_status(self, status_id: int) -> DeleteStatusResponse:
status = await self._get_status_by_id(status_id)
if not status:
return DeleteStatusResponse(ok=False, message=f"Статус с ID {status_id} не найден")
count_deals_in_progress = await self._count_deals_in_progress(status_id)
if count_deals_in_progress != 0:
return DeleteStatusResponse(ok=False, message="Нельзя удалить статус с активными сделками")
if status.is_finishing:
await self._set_finishing_flag_to_prev_status(status)
count_deals = await self._count_deals(status_id)
if count_deals == 0:
await self.session.delete(status)
else:
status.is_deleted = True
await self.session.commit()
return DeleteStatusResponse(ok=True, message="Статус успешно удален")