feat: groups
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
@ -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"),
|
||||
|
||||
17
models/deal_group.py
Normal file
17
models/deal_group.py
Normal file
@ -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")
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
52
repositories/deal_group.py
Normal file
52
repositories/deal_group.py
Normal file
@ -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()
|
||||
59
routers/deal_group.py
Normal file
59
routers/deal_group.py
Normal file
@ -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)
|
||||
@ -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
|
||||
|
||||
60
schemas/deal_group.py
Normal file
60
schemas/deal_group.py
Normal file
@ -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
|
||||
@ -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
|
||||
|
||||
48
services/deal_group.py
Normal file
48
services/deal_group.py
Normal file
@ -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="Группа сделок удалена")
|
||||
Reference in New Issue
Block a user