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,
|
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
|
||||||
|
|
||||||
|
|||||||
@ -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
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 .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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
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 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
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 .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
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