diff --git a/models/__init__.py b/models/__init__.py index 01a265a..29e8be3 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -9,6 +9,7 @@ from .built_in_module import ( # noqa: F401 built_in_module_dependencies as built_in_module_dependencies, ) from .deal import Deal as Deal +from .deal_group import DealGroup as DealGroup from .project import Project as Project from .status import Status as Status, DealStatusHistory as DealStatusHistory diff --git a/models/deal.py b/models/deal.py index 6e3258c..a766e06 100644 --- a/models/deal.py +++ b/models/deal.py @@ -7,7 +7,7 @@ from models.base import BaseModel from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin if TYPE_CHECKING: - from models import Status, Board, DealStatusHistory + from models import Status, Board, DealStatusHistory, DealGroup from modules.clients.models import Client @@ -34,6 +34,13 @@ class Deal(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin): lazy="noload", ) + group_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("deal_groups.id"), default=None, server_default=None + ) + group: Mapped[Optional["DealGroup"]] = relationship( + lazy="noload", back_populates="deals" + ) + # module client client_id: Mapped[Optional[int]] = mapped_column( ForeignKey("clients.id", ondelete="CASCADE"), diff --git a/models/deal_group.py b/models/deal_group.py new file mode 100644 index 0000000..c45f368 --- /dev/null +++ b/models/deal_group.py @@ -0,0 +1,17 @@ +from typing import TYPE_CHECKING, Optional + +from sqlalchemy.orm import mapped_column, Mapped, relationship + +from models.base import BaseModel +from models.mixins import IdMixin + +if TYPE_CHECKING: + from models import Deal + + +class DealGroup(BaseModel, IdMixin): + __tablename__ = "deal_groups" + + name: Mapped[Optional[str]] = mapped_column() + lexorank: Mapped[str] = mapped_column() + deals: Mapped[list["Deal"]] = relationship(back_populates="group", lazy="noload") diff --git a/repositories/__init__.py b/repositories/__init__.py index 148fd12..a1ac5ac 100644 --- a/repositories/__init__.py +++ b/repositories/__init__.py @@ -3,3 +3,4 @@ from .deal import DealRepository as DealRepository from .project import ProjectRepository as ProjectRepository from .status import StatusRepository as StatusRepository from .built_in_module import BuiltInModuleRepository as BuiltInModuleRepository +from .deal_group import DealGroupRepository as DealGroupRepository diff --git a/repositories/deal.py b/repositories/deal.py index 7994d3e..14ac510 100644 --- a/repositories/deal.py +++ b/repositories/deal.py @@ -1,5 +1,5 @@ from sqlalchemy import func -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import joinedload, selectinload from models import Deal, Board, DealStatusHistory from modules.fulfillment_base.models import ( @@ -10,7 +10,7 @@ from modules.fulfillment_base.models import ( ) from repositories.mixins import * from schemas.base import SortDir -from schemas.deal import UpdateDealSchema, CreateDealSchema, DealSchema +from schemas.deal import UpdateDealSchema, CreateDealSchema from utils.sorting import apply_sorting @@ -95,7 +95,11 @@ class DealRepository( products_quantity_subquery, Deal.id == products_quantity_subquery.c.deal_id, ) - .options(joinedload(Deal.status), joinedload(Deal.board)) + .options( + joinedload(Deal.status), + joinedload(Deal.board), + selectinload(Deal.group), + ) .where(Deal.is_deleted.is_(False)) ) @@ -123,21 +127,45 @@ class DealRepository( rows: list[tuple[Deal, int, int]] = (await self.session.execute(stmt)).all() return rows, total_items + async def get_by_group_id(self, group_id: int) -> list[Deal]: + stmt = ( + select(Deal) + .where(Deal.group_id == group_id, Deal.is_deleted.is_(False)) + .options(joinedload(Deal.status), joinedload(Deal.board)) + ) + result = await self.session.execute(stmt) + return list(result.scalars().all()) + + async def get_by_ids(self, deal_ids: list[int]) -> list[Deal]: + stmt = ( + select(Deal) + .where(Deal.id.in_(deal_ids), Deal.is_deleted.is_(False)) + .options(joinedload(Deal.status), joinedload(Deal.board)) + ) + result = await self.session.execute(stmt) + return list(result.scalars().all()) + def _process_get_by_id_stmt(self, stmt: Select) -> Select: return stmt.options(joinedload(Deal.status), joinedload(Deal.board)) + async def update_status(self, deal: Deal, status_id: int): + if deal.status_id == status_id: + return + + deal.status_history.append( + DealStatusHistory( + from_status_id=deal.status_id, + to_status_id=status_id, + ) + ) + deal.status_id = status_id + async def update(self, deal: Deal, data: UpdateDealSchema) -> Deal: fields = ["lexorank", "name", "board_id"] deal = await self._apply_update_data_to_model(deal, data, False, fields) - if data.status_id and deal.status_id != data.status_id: - deal.status_history.append( - DealStatusHistory( - from_status_id=deal.status_id, - to_status_id=data.status_id, - ) - ) - deal.status_id = data.status_id + if data.status_id: + await self.update_status(deal, data.status_id) self.session.add(deal) await self.session.commit() diff --git a/repositories/deal_group.py b/repositories/deal_group.py new file mode 100644 index 0000000..a57e17c --- /dev/null +++ b/repositories/deal_group.py @@ -0,0 +1,52 @@ +from models import DealGroup, Deal +from repositories import DealRepository +from repositories.mixins import * +from schemas.deal_group import UpdateDealGroupSchema + + +class DealGroupRepository( + BaseRepository, + RepGetByIdMixin[DealGroup], + RepUpdateMixin[DealGroup, UpdateDealGroupSchema], +): + entity_class = DealGroup + + async def create(self, deals: list[Deal], lexorank: str) -> DealGroup: + group = DealGroup(deals=deals, lexorank=lexorank) + self.session.add(group) + await self.session.commit() + await self.session.refresh(group) + return group + + async def update(self, entity: DealGroup, data: UpdateDealGroupSchema) -> DealGroup: + if data.status_id: + deal_repo = DealRepository(self.session) + deals = await deal_repo.get_by_group_id(entity.id) + for deal in deals: + await deal_repo.update_status(deal, data.status_id) + del data.status_id + return await self._apply_update_data_to_model(entity, data, True) + + async def update_group_deals( + self, group_id: int, old_deals: list[Deal], new_deals: list[Deal] + ): + old_set = set(old_deals) + new_set = set(new_deals) + deals_to_remove = old_set - new_set + deals_to_add = new_set - old_set + + for deal in deals_to_remove: + deal.group_id = None + for deal in deals_to_add: + deal.group_id = group_id + + self.session.add_all([*deals_to_remove, *deals_to_add]) + await self.session.commit() + + async def delete(self, group_id: int) -> None: + deal_repo = DealRepository(self.session) + deals = await deal_repo.get_by_group_id(group_id) + for deal in deals: + deal.is_deleted = True + self.session.add(deal) + await self.session.commit() diff --git a/routers/deal_group.py b/routers/deal_group.py new file mode 100644 index 0000000..738bbb6 --- /dev/null +++ b/routers/deal_group.py @@ -0,0 +1,59 @@ +from pathlib import Path + +from fastapi import APIRouter + +from backend.dependecies import SessionDependency +from schemas.deal_group import * +from services import DealGroupService + +router = APIRouter(tags=["deal-group"]) + + +@router.patch( + "/{pk}", + response_model=UpdateDealGroupResponse, + operation_id="update_deal_group", +) +async def update_group( + request: UpdateDealGroupRequest, + session: SessionDependency, + pk: int = Path(), +): + return await DealGroupService(session).update(pk, request) + + +@router.post( + "/", + response_model=CreateDealGroupResponse, + operation_id="create_deal_group", +) +async def create_group( + request: CreateDealGroupRequest, + session: SessionDependency, +): + return await DealGroupService(session).create(request) + + +@router.delete( + "/{pk}", + response_model=DeleteDealGroupResponse, + operation_id="delete_deal_group", +) +async def delete_group( + session: SessionDependency, + pk: int = Path(), +): + return await DealGroupService(session).delete(pk) + + +@router.post( + "/{pk}/deals", + response_model=UpdateDealsInGroupResponse, + operation_id="update_deals_in_group", +) +async def update_deals_in_group( + request: UpdateDealsInGroupRequest, + session: SessionDependency, + pk: int = Path(), +): + return await DealGroupService(session).update_deals_in_group(pk, request) diff --git a/schemas/deal.py b/schemas/deal.py index c3ed83b..f564999 100644 --- a/schemas/deal.py +++ b/schemas/deal.py @@ -4,6 +4,7 @@ from typing import Optional from modules.clients.schemas.client import ClientSchema from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema from schemas.board import BoardSchema +from schemas.deal_group import DealGroupSchema from schemas.status import StatusSchema @@ -17,6 +18,7 @@ class DealSchema(BaseSchema): status: StatusSchema board: BoardSchema created_at: datetime + group: Optional[DealGroupSchema] # FF module products_quantity: int = 0 total_price: float = 0 diff --git a/schemas/deal_group.py b/schemas/deal_group.py new file mode 100644 index 0000000..1344819 --- /dev/null +++ b/schemas/deal_group.py @@ -0,0 +1,60 @@ +from typing import Optional + +from schemas.base import BaseSchema, BaseResponse + + +# region Entities + + +class DealGroupSchema(BaseSchema): + id: int + name: Optional[str] = None + lexorank: str + + +class UpdateDealGroupSchema(BaseSchema): + name: Optional[str] = None + lexorank: Optional[str] = None + status_id: Optional[int] = None + + +# endregion + +# region Requests + + +class CreateDealGroupRequest(BaseSchema): + main_deal_id: int + other_deal_ids: list[int] + + +class UpdateDealGroupRequest(BaseSchema): + entity: UpdateDealGroupSchema + + +class UpdateDealsInGroupRequest(BaseSchema): + deal_ids: list[int] + + +# endregion + +# region Responses + + +class CreateDealGroupResponse(BaseSchema): + entity: DealGroupSchema + + +class UpdateDealGroupResponse(BaseResponse): + pass + + +class UpdateDealsInGroupResponse(BaseResponse): + pass + + +class DeleteDealGroupResponse(BaseResponse): + pass + + +# endregion diff --git a/services/__init__.py b/services/__init__.py index 19d4325..1eb8b8e 100644 --- a/services/__init__.py +++ b/services/__init__.py @@ -2,3 +2,4 @@ from .board import BoardService as BoardService from .deal import DealService as DealService from .project import ProjectService as ProjectService from .status import StatusService as StatusService +from .deal_group import DealGroupService as DealGroupService diff --git a/services/deal_group.py b/services/deal_group.py new file mode 100644 index 0000000..8d8d176 --- /dev/null +++ b/services/deal_group.py @@ -0,0 +1,48 @@ +from lexorank import lexorank + +from models import DealGroup, Deal +from repositories import DealGroupRepository, DealRepository +from schemas.deal_group import * +from services.mixins import * + + +class DealGroupService( + ServiceUpdateMixin[DealGroup, UpdateDealGroupRequest], + ServiceDeleteMixin[DealGroup], +): + entity_updated_msg = "Группа сделок обновлена" + + def __init__(self, session: AsyncSession): + self.repository = DealGroupRepository(session) + + async def sync_deals_with_main_deal(self, main_deal: Deal, other_deals: list[Deal]): + deal_repo = DealRepository(self.repository.session) + for deal in other_deals: + if deal.status_id != main_deal.status_id: + await deal_repo.update_status(deal, main_deal.status_id) + + async def create(self, request: CreateDealGroupRequest) -> CreateDealGroupResponse: + deal_repo = DealRepository(self.repository.session) + main_deal: Deal = await deal_repo.get_by_id(request.main_deal_id) + other_deals: list[Deal] = await deal_repo.get_by_ids(request.other_deal_ids) + await self.sync_deals_with_main_deal(main_deal, other_deals) + group_lexorank = main_deal.lexorank + main_deal.lexorank = lexorank.middle(lexorank.Bucket.BUCEKT_0).__str__() + group = await self.repository.create([main_deal, *other_deals], group_lexorank) + return CreateDealGroupResponse(entity=DealGroupSchema.model_validate(group)) + + async def update_deals_in_group( + self, group_id: int, request: UpdateDealsInGroupRequest + ) -> UpdateDealsInGroupResponse: + deal_repo = DealRepository(self.repository.session) + old_deals = await deal_repo.get_by_group_id(group_id) + new_deals = await deal_repo.get_by_ids(request.deal_ids) + + await self.sync_deals_with_main_deal(old_deals[0], new_deals) + await self.repository.update_group_deals(group_id, old_deals, new_deals) + + return UpdateDealsInGroupResponse(ok=True, message=self.entity_updated_msg) + + async def delete(self, group_id: int) -> DeleteDealGroupResponse: + await self.repository.delete(group_id) + return DeleteDealGroupResponse(ok=True, message="Группа сделок удалена")