feat: groups

This commit is contained in:
2025-10-17 19:48:51 +04:00
parent 35869e2ea5
commit 6b1b4109c6
11 changed files with 288 additions and 12 deletions

View File

@ -9,6 +9,7 @@ from .built_in_module import ( # noqa: F401
built_in_module_dependencies as built_in_module_dependencies, built_in_module_dependencies as built_in_module_dependencies,
) )
from .deal import Deal as Deal from .deal import Deal as Deal
from .deal_group import DealGroup as DealGroup
from .project import Project as Project from .project import Project as Project
from .status import Status as Status, DealStatusHistory as DealStatusHistory from .status import Status as Status, DealStatusHistory as DealStatusHistory

View File

@ -7,7 +7,7 @@ from models.base import BaseModel
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from models import Status, Board, DealStatusHistory from models import Status, Board, DealStatusHistory, DealGroup
from modules.clients.models import Client from modules.clients.models import Client
@ -34,6 +34,13 @@ class Deal(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
lazy="noload", 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 # module client
client_id: Mapped[Optional[int]] = mapped_column( client_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("clients.id", ondelete="CASCADE"), ForeignKey("clients.id", ondelete="CASCADE"),

17
models/deal_group.py Normal file
View 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")

View File

@ -3,3 +3,4 @@ from .deal import DealRepository as DealRepository
from .project import ProjectRepository as ProjectRepository from .project import ProjectRepository as ProjectRepository
from .status import StatusRepository as StatusRepository from .status import StatusRepository as StatusRepository
from .built_in_module import BuiltInModuleRepository as BuiltInModuleRepository from .built_in_module import BuiltInModuleRepository as BuiltInModuleRepository
from .deal_group import DealGroupRepository as DealGroupRepository

View File

@ -1,5 +1,5 @@
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload, selectinload
from models import Deal, Board, DealStatusHistory from models import Deal, Board, DealStatusHistory
from modules.fulfillment_base.models import ( from modules.fulfillment_base.models import (
@ -10,7 +10,7 @@ from modules.fulfillment_base.models import (
) )
from repositories.mixins import * from repositories.mixins import *
from schemas.base import SortDir from schemas.base import SortDir
from schemas.deal import UpdateDealSchema, CreateDealSchema, DealSchema from schemas.deal import UpdateDealSchema, CreateDealSchema
from utils.sorting import apply_sorting from utils.sorting import apply_sorting
@ -95,7 +95,11 @@ class DealRepository(
products_quantity_subquery, products_quantity_subquery,
Deal.id == products_quantity_subquery.c.deal_id, 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)) .where(Deal.is_deleted.is_(False))
) )
@ -123,21 +127,45 @@ class DealRepository(
rows: list[tuple[Deal, int, int]] = (await self.session.execute(stmt)).all() rows: list[tuple[Deal, int, int]] = (await self.session.execute(stmt)).all()
return rows, total_items 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: def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(joinedload(Deal.status), joinedload(Deal.board)) 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: async def update(self, deal: Deal, data: UpdateDealSchema) -> Deal:
fields = ["lexorank", "name", "board_id"] fields = ["lexorank", "name", "board_id"]
deal = await self._apply_update_data_to_model(deal, data, False, fields) deal = await self._apply_update_data_to_model(deal, data, False, fields)
if data.status_id and deal.status_id != data.status_id: if data.status_id:
deal.status_history.append( await self.update_status(deal, data.status_id)
DealStatusHistory(
from_status_id=deal.status_id,
to_status_id=data.status_id,
)
)
deal.status_id = data.status_id
self.session.add(deal) self.session.add(deal)
await self.session.commit() await self.session.commit()

View 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
View 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)

View File

@ -4,6 +4,7 @@ from typing import Optional
from modules.clients.schemas.client import ClientSchema from modules.clients.schemas.client import ClientSchema
from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema
from schemas.board import BoardSchema from schemas.board import BoardSchema
from schemas.deal_group import DealGroupSchema
from schemas.status import StatusSchema from schemas.status import StatusSchema
@ -17,6 +18,7 @@ class DealSchema(BaseSchema):
status: StatusSchema status: StatusSchema
board: BoardSchema board: BoardSchema
created_at: datetime created_at: datetime
group: Optional[DealGroupSchema]
# FF module # FF module
products_quantity: int = 0 products_quantity: int = 0
total_price: float = 0 total_price: float = 0

60
schemas/deal_group.py Normal file
View 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

View File

@ -2,3 +2,4 @@ from .board import BoardService as BoardService
from .deal import DealService as DealService from .deal import DealService as DealService
from .project import ProjectService as ProjectService from .project import ProjectService as ProjectService
from .status import StatusService as StatusService from .status import StatusService as StatusService
from .deal_group import DealGroupService as DealGroupService

48
services/deal_group.py Normal file
View 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="Группа сделок удалена")