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

View File

@ -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
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 .status import StatusRepository as StatusRepository
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.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()

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 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
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 .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
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="Группа сделок удалена")