feat: projects and boards

This commit is contained in:
2025-02-07 20:08:14 +04:00
parent 2aa84837e4
commit 9ee3f87de9
25 changed files with 1312 additions and 387 deletions

View File

@@ -4,7 +4,9 @@ from enum import IntEnum
class ProfitTableGroupBy(IntEnum): class ProfitTableGroupBy(IntEnum):
BY_DATES = 0 BY_DATES = 0
BY_CLIENTS = 1 BY_CLIENTS = 1
BY_STATUSES = 2 BY_PROJECTS = 2
BY_WAREHOUSES = 3 BY_BOARDS = 3
BY_MARKETPLACES = 4 BY_STATUSES = 4
BY_MANAGERS = 5 BY_WAREHOUSES = 5
BY_MARKETPLACES = 6
BY_MANAGERS = 7

View File

@@ -32,6 +32,7 @@ app.add_middleware(
routers_list = [ routers_list = [
routers.auth_router, routers.auth_router,
routers.deal_router, routers.deal_router,
routers.deal_group_router,
routers.client_router, routers.client_router,
routers.service_router, routers.service_router,
routers.product_router, routers.product_router,
@@ -52,6 +53,9 @@ routers_list = [
routers.shipping_router, routers.shipping_router,
routers.department_router, routers.department_router,
routers.residues_router, routers.residues_router,
routers.project_router,
routers.board_router,
routers.status_router,
] ]
for router in routers_list: for router in routers_list:
app.include_router(router) app.include_router(router)

View File

@@ -1,6 +1,9 @@
from sqlalchemy.orm import configure_mappers from sqlalchemy.orm import configure_mappers
from .auth import * from .auth import *
from .project import *
from .board import *
from .status import *
from .deal import * from .deal import *
from .client import * from .client import *
from .service import * from .service import *
@@ -15,5 +18,6 @@ from .marketplace_products import *
from .deal_group import * from .deal_group import *
from .transaction import * from .transaction import *
from .residues import * from .residues import *
from .shipping import *
configure_mappers() configure_mappers()

31
models/board.py Normal file
View File

@@ -0,0 +1,31 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models import BaseModel
if TYPE_CHECKING:
from models import Project, DealStatus, Deal
class Board(BaseModel):
__tablename__ = "boards"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
created_at: Mapped[datetime] = mapped_column(nullable=False)
is_deleted: Mapped[bool] = mapped_column(default=False)
ordinal_number: Mapped[int] = mapped_column(nullable=False)
project_id: Mapped[int] = mapped_column(ForeignKey('projects.id'), nullable=False)
project: Mapped["Project"] = relationship(
"Project",
back_populates="boards",
lazy="selectin",
)
deal_statuses: Mapped[list["DealStatus"]] = relationship("DealStatus", back_populates="board", lazy="selectin", cascade="all,delete")
deals: Mapped[list["Deal"]] = relationship("Deal", uselist=True, back_populates="board", lazy="selectin")

View File

@@ -7,6 +7,8 @@ from sqlalchemy.orm import relationship, backref, Mapped, mapped_column
from models.base import BaseModel from models.base import BaseModel
from .marketplace import BaseMarketplace from .marketplace import BaseMarketplace
from .board import Board
from .status import DealStatus
from .shipping import Pallet, Box from .shipping import Pallet, Box
from .shipping_warehouse import ShippingWarehouse from .shipping_warehouse import ShippingWarehouse
@@ -19,18 +21,8 @@ if TYPE_CHECKING:
) )
# @unique
# class DealStatus(IntEnum):
# CREATED = 0
# AWAITING_ACCEPTANCE = 1
# PACKAGING = 2
# AWAITING_SHIPMENT = 3
# AWAITING_PAYMENT = 4
# COMPLETED = 5
# CANCELLED = 6
@unique @unique
class DealStatus(IntEnum): class DealStatusEnum(IntEnum):
CREATED = 0 CREATED = 0
AWAITING_ACCEPTANCE = 1 AWAITING_ACCEPTANCE = 1
READY_FOR_WORK = 2 READY_FOR_WORK = 2
@@ -53,7 +45,6 @@ class Deal(BaseModel):
id = Column(Integer, autoincrement=True, primary_key=True, index=True) id = Column(Integer, autoincrement=True, primary_key=True, index=True)
name = Column(String, nullable=False, comment='Название сделки') name = Column(String, nullable=False, comment='Название сделки')
created_at = Column(DateTime, nullable=False, comment='Дата создания') created_at = Column(DateTime, nullable=False, comment='Дата создания')
current_status = Column(Integer, nullable=False, comment='Текущий статус')
client_id = Column(Integer, ForeignKey('clients.id', ondelete='CASCADE'), nullable=False, comment='ID клиента') client_id = Column(Integer, ForeignKey('clients.id', ondelete='CASCADE'), nullable=False, comment='ID клиента')
client = relationship('Client', backref=backref('deals', cascade="all, delete-orphan")) client = relationship('Client', backref=backref('deals', cascade="all, delete-orphan"))
@@ -65,6 +56,14 @@ class Deal(BaseModel):
is_locked: Mapped[bool] = mapped_column(default=False, server_default='0') is_locked: Mapped[bool] = mapped_column(default=False, server_default='0')
is_accounted: Mapped[bool] = mapped_column(default=True, server_default='1') is_accounted: Mapped[bool] = mapped_column(default=True, server_default='1')
current_status_id: Mapped[int] = mapped_column(
ForeignKey("deal_statuses.id"),
nullable=False,
comment='Текущий статус',
)
status: Mapped["DealStatus"] = relationship(lazy="selectin")
shipping_warehouse_id: Mapped[int] = mapped_column(ForeignKey('shipping_warehouses.id'), nullable=True) shipping_warehouse_id: Mapped[int] = mapped_column(ForeignKey('shipping_warehouses.id'), nullable=True)
shipping_warehouse: Mapped["ShippingWarehouse"] = relationship() shipping_warehouse: Mapped["ShippingWarehouse"] = relationship()
@@ -88,6 +87,12 @@ class Deal(BaseModel):
order_by="desc(DealProduct.product_id)" order_by="desc(DealProduct.product_id)"
) )
board_id: Mapped[int] = mapped_column(ForeignKey('boards.id'), nullable=True, server_default='1')
board: Mapped[Board] = relationship(
"Board",
back_populates="deals",
)
# TODO remake with sequence # TODO remake with sequence
lexorank = Column(String, nullable=False, comment='Lexorank', index=True) lexorank = Column(String, nullable=False, comment='Lexorank', index=True)
@@ -112,25 +117,6 @@ class Deal(BaseModel):
employees: Mapped[list['DealEmployees']] = relationship(back_populates='deal', lazy='selectin') employees: Mapped[list['DealEmployees']] = relationship(back_populates='deal', lazy='selectin')
class DealStatusHistory(BaseModel):
__tablename__ = 'deals_status_history'
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
deal_id = Column(Integer, ForeignKey('deals.id'), nullable=False, comment='ID сделки')
deal = relationship('Deal', back_populates='status_history')
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
user = relationship('User')
changed_at = Column(DateTime, nullable=False, comment='Дата и время когда произошла смена статуса')
from_status = Column(Integer, nullable=False, comment='Предыдущий статус')
to_status = Column(Integer, nullable=False, comment='Новый статус')
next_status_deadline = Column(DateTime,
comment='Дедлайн до которого сделку нужно перевести на следующий этап')
comment = Column(String, nullable=False, comment='Коментарий', server_default='')
class DealEmployees(BaseModel): class DealEmployees(BaseModel):
__tablename__ = 'deal_employees' __tablename__ = 'deal_employees'
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), primary_key=True)

24
models/project.py Normal file
View File

@@ -0,0 +1,24 @@
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy.orm import mapped_column, Mapped, relationship
from models import BaseModel
if TYPE_CHECKING:
from board import Board
class Project(BaseModel):
__tablename__ = "projects"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
created_at: Mapped[datetime] = mapped_column(nullable=False)
is_deleted: Mapped[bool] = mapped_column(default=False)
boards: Mapped[list["Board"]] = relationship(
"Board",
back_populates="project",
lazy="noload",
)

61
models/status.py Normal file
View File

@@ -0,0 +1,61 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, Column, Integer, DateTime, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models import BaseModel
if TYPE_CHECKING:
from models import Board
class DealStatus(BaseModel):
__tablename__ = "deal_statuses"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
ordinal_number: Mapped[int] = mapped_column(nullable=False)
is_finishing: Mapped[bool] = mapped_column(default=False, nullable=False)
is_deleted: Mapped[bool] = mapped_column(default=False, nullable=False)
board_id: Mapped[int] = mapped_column(ForeignKey('boards.id'), nullable=False)
board: Mapped["Board"] = relationship("Board", back_populates="deal_statuses")
class DealStatusHistory(BaseModel):
__tablename__ = 'deals_status_history'
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
deal_id = Column(Integer, ForeignKey('deals.id'), nullable=False, comment='ID сделки')
deal = relationship('Deal', back_populates='status_history')
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
user = relationship('User')
changed_at = Column(DateTime, nullable=False, comment='Дата и время когда произошла смена статуса')
from_status_id: Mapped[int] = mapped_column(
ForeignKey('deal_statuses.id'),
nullable=False,
comment='Предыдущий статус',
)
from_status: Mapped[DealStatus] = relationship(
'DealStatus',
foreign_keys=[from_status_id],
lazy='joined',
)
to_status_id: Mapped[int] = mapped_column(
ForeignKey('deal_statuses.id'),
nullable=False,
comment='Новый статус',
)
to_status: Mapped[DealStatus] = relationship(
'DealStatus',
foreign_keys=[to_status_id],
lazy='joined',
)
next_status_deadline = Column(DateTime,
comment='Дедлайн до которого сделку нужно перевести на следующий этап')
comment = Column(String, nullable=False, comment='Коментарий', server_default='')

View File

@@ -1,5 +1,6 @@
from .auth import auth_router from .auth import auth_router
from .deal import deal_router from .deal import deal_router
from .group import deal_group_router
from .client import client_router from .client import client_router
from .service import service_router from .service import service_router
from .product import product_router from .product import product_router
@@ -20,3 +21,6 @@ from .transaction import transaction_router
from .shipping import shipping_router from .shipping import shipping_router
from .department import department_router from .department import department_router
from .residues import residues_router from .residues import residues_router
from .project import project_router
from .board import board_router
from .status import status_router

79
routers/board.py Normal file
View File

@@ -0,0 +1,79 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from schemas.board import *
from services.auth import guest_user, authorized_user
from services.board import BoardService
board_router = APIRouter(
prefix="/board",
tags=["board"],
dependencies=[Depends(guest_user)]
)
@board_router.get(
"/{project_id}",
response_model=GetBoardsResponse,
operation_id="get_boards",
)
async def get_boards(
session: Annotated[AsyncSession, Depends(get_session)],
project_id: int,
):
return await BoardService(session).get_boards(project_id)
@board_router.post(
'/',
response_model=CreateBoardResponse,
operation_id="create_board",
dependencies=[Depends(authorized_user)]
)
async def create_board(
session: Annotated[AsyncSession, Depends(get_session)],
request: CreateBoardRequest,
):
return await BoardService(session).create_board(request)
@board_router.patch(
'/',
response_model=UpdateBoardResponse,
operation_id="update_board",
dependencies=[Depends(authorized_user)]
)
async def update_board(
session: Annotated[AsyncSession, Depends(get_session)],
request: UpdateBoardRequest,
):
return await BoardService(session).update_board(request)
@board_router.patch(
'/order',
response_model=UpdateBoardOrderResponse,
operation_id="update_board_order",
dependencies=[Depends(authorized_user)]
)
async def update_board_order(
session: Annotated[AsyncSession, Depends(get_session)],
request: UpdateBoardOrderRequest,
):
return await BoardService(session).update_board_order(request)
@board_router.delete(
'/{board_id}',
response_model=DeleteBoardResponse,
operation_id="delete_board",
dependencies=[Depends(authorized_user)]
)
async def delete_board(
session: Annotated[AsyncSession, Depends(get_session)],
board_id: int,
):
return await BoardService(session).delete_board(board_id)

View File

@@ -24,17 +24,6 @@ deal_router = APIRouter(
# region Deal # region Deal
@deal_router.post(
'/create',
dependencies=[Depends(authorized_user)]
)
async def create(
request: DealCreateRequest,
session: Annotated[AsyncSession, Depends(get_session)],
user: Annotated[User, Depends(get_current_user)]
):
return await DealService(session).create(request, user)
@deal_router.post( @deal_router.post(
'/delete', '/delete',
@@ -151,9 +140,10 @@ async def get_deal_by_id(
) )
async def update_general_info( async def update_general_info(
request: DealUpdateGeneralInfoRequest, request: DealUpdateGeneralInfoRequest,
session: Annotated[AsyncSession, Depends(get_session)] session: Annotated[AsyncSession, Depends(get_session)],
user: CurrentUserDependency,
): ):
return await DealService(session).update_general_info(request) return await DealService(session).update_general_info(request, user)
@deal_router.post( @deal_router.post(
@@ -494,71 +484,3 @@ async def get_deal_products_barcodes_pdf(
# endregion # endregion
# region Deal groups
@deal_router.post(
'/add-to-group',
response_model=DealAddToGroupResponse,
operation_id='add_deal_to_group',
dependencies=[Depends(authorized_user)]
)
async def add_to_group(
request: DealAddToGroupRequest,
session: SessionDependency,
user: CurrentUserDependency
):
return await DealService(session).add_to_group(user, request)
@deal_router.post(
'/create-group',
response_model=DealCreateGroupResponse,
operation_id='create_deal_group',
dependencies=[Depends(authorized_user)]
)
async def create_group(
request: DealCreateGroupRequest,
session: SessionDependency,
user: CurrentUserDependency
):
return await DealService(session).create_group(user, request)
@deal_router.post(
'/remove-from-group',
response_model=DealRemoveFromGroupResponse,
operation_id='remove_deal_from_group',
dependencies=[Depends(authorized_user)]
)
async def remove_from_group(
request: DealRemoveFromGroupRequest,
session: SessionDependency,
):
return await DealService(session).remove_from_group( request)
# route to update group name
@deal_router.post(
'/group/update',
response_model=DealGroupUpdateResponse,
operation_id='update_deal_group',
dependencies=[Depends(authorized_user)]
)
async def update_group(
request: DealGroupUpdateRequest,
session: SessionDependency,
):
return await DealService(session).update_group(request)
# route to change group status
@deal_router.post(
'/group/change-status',
response_model=DealGroupChangeStatusResponse,
operation_id='change_deal_group_status',
dependencies=[Depends(authorized_user)]
)
async def change_group_status(
request: DealGroupChangeStatusRequest,
session: SessionDependency,
user: CurrentUserDependency
):
return await DealService(session).change_group_status(user,request)
# endregion

79
routers/group.py Normal file
View File

@@ -0,0 +1,79 @@
from fastapi import APIRouter, Depends
from backend.dependecies import SessionDependency, CurrentUserDependency
from schemas.group import *
from services.auth import authorized_user
from services.deal_group import DealGroupService
deal_group_router = APIRouter(
prefix='/deal-group',
tags=['deal-group'],
)
@deal_group_router.patch(
'/',
response_model=DealGroupUpdateResponse,
operation_id='update_deal_group',
dependencies=[Depends(authorized_user)]
)
async def update_group(
request: DealGroupUpdateRequest,
session: SessionDependency,
):
return await DealGroupService(session).update_group(request)
@deal_group_router.post(
'/',
response_model=DealCreateGroupResponse,
operation_id='create_deal_group',
dependencies=[Depends(authorized_user)]
)
async def create_group(
request: DealCreateGroupRequest,
session: SessionDependency,
user: CurrentUserDependency
):
return await DealGroupService(session).create_group(user, request)
@deal_group_router.patch(
'/change-status',
response_model=DealGroupChangeStatusResponse,
operation_id='change_status',
dependencies=[Depends(authorized_user)]
)
async def change_status(
request: DealGroupChangeStatusRequest,
session: SessionDependency,
user: CurrentUserDependency
):
return await DealGroupService(session).change_group_status(user,request)
@deal_group_router.post(
'/deal',
response_model=DealAddToGroupResponse,
operation_id='add_deal',
dependencies=[Depends(authorized_user)]
)
async def add_deal(
request: DealAddToGroupRequest,
session: SessionDependency,
user: CurrentUserDependency
):
return await DealGroupService(session).add_deal(user, request)
@deal_group_router.delete(
'/deal',
response_model=DealRemoveFromGroupResponse,
operation_id='remove_deal',
dependencies=[Depends(authorized_user)]
)
async def remove_deal(
request: DealRemoveFromGroupRequest,
session: SessionDependency,
):
return await DealGroupService(session).remove_deal(request)

65
routers/project.py Normal file
View File

@@ -0,0 +1,65 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from schemas.project import *
from services.auth import guest_user, authorized_user
from services.project import ProjectService
project_router = APIRouter(
prefix="/project",
tags=["project"],
dependencies=[Depends(guest_user)]
)
@project_router.get(
"/",
response_model=GetProjectsResponse,
operation_id="get_projects",
)
async def get_projects(
session: Annotated[AsyncSession, Depends(get_session)],
):
return await ProjectService(session).get_projects()
@project_router.post(
'/',
response_model=CreateProjectResponse,
operation_id="create_project",
dependencies=[Depends(authorized_user)]
)
async def create_project(
session: Annotated[AsyncSession, Depends(get_session)],
request: CreateProjectRequest,
):
return await ProjectService(session).create_project(request)
@project_router.patch(
'/',
response_model=UpdateProjectResponse,
operation_id="update_project",
dependencies=[Depends(authorized_user)]
)
async def update_project(
session: Annotated[AsyncSession, Depends(get_session)],
request: UpdateProjectRequest,
):
return await ProjectService(session).update_project(request)
@project_router.delete(
'/{project_id}',
response_model=DeleteProjectResponse,
operation_id="delete_project",
dependencies=[Depends(authorized_user)]
)
async def delete_project(
session: Annotated[AsyncSession, Depends(get_session)],
project_id: int,
):
return await ProjectService(session).delete_project(project_id)

67
routers/status.py Normal file
View File

@@ -0,0 +1,67 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from schemas.status import *
from services.auth import guest_user, authorized_user
from services.status import StatusService
status_router = APIRouter(
prefix="/status",
tags=["status"],
dependencies=[Depends(guest_user)]
)
@status_router.post(
'/',
response_model=CreateStatusResponse,
operation_id="create_status",
dependencies=[Depends(authorized_user)]
)
async def create_status(
session: Annotated[AsyncSession, Depends(get_session)],
request: CreateStatusRequest,
):
return await StatusService(session).create_status(request)
@status_router.patch(
'/',
response_model=UpdateStatusResponse,
operation_id="update_status",
dependencies=[Depends(authorized_user)]
)
async def update_status(
session: Annotated[AsyncSession, Depends(get_session)],
request: UpdateStatusRequest,
):
return await StatusService(session).update_status(request)
@status_router.patch(
'/order',
response_model=UpdateStatusOrderResponse,
operation_id="update_status_order",
dependencies=[Depends(authorized_user)]
)
async def update_status_order(
session: Annotated[AsyncSession, Depends(get_session)],
request: UpdateStatusOrderRequest,
):
return await StatusService(session).update_status_order(request)
@status_router.delete(
'/{status_id}',
response_model=DeleteStatusResponse,
operation_id="delete_status",
dependencies=[Depends(authorized_user)]
)
async def delete_status(
session: Annotated[AsyncSession, Depends(get_session)],
status_id: int,
):
return await StatusService(session).delete_status(status_id)

61
schemas/board.py Normal file
View File

@@ -0,0 +1,61 @@
from schemas.base import BaseSchema, OkMessageSchema
from schemas.project import ProjectSchema
from schemas.status import StatusSchema
# region Entities
class BaseBoardSchema(BaseSchema):
name: str
project_id: int
class BoardSchema(BaseBoardSchema):
id: int
ordinal_number: int
deal_statuses: list[StatusSchema]
project: ProjectSchema
# endregion
# region Requests
class CreateBoardRequest(BaseSchema):
board: BaseBoardSchema
class UpdateBoardRequest(BaseSchema):
board: BoardSchema
class UpdateBoardOrderRequest(BaseSchema):
project_id: int
board_id: int
new_ordinal_number: int
# endregion
# region Responses
class GetBoardsResponse(BaseSchema):
boards: list[BoardSchema]
class CreateBoardResponse(OkMessageSchema):
pass
class UpdateBoardResponse(OkMessageSchema):
pass
class UpdateBoardOrderResponse(OkMessageSchema):
pass
class DeleteBoardResponse(OkMessageSchema):
pass
# endregion

View File

@@ -1,18 +1,19 @@
import datetime import datetime
from collections import defaultdict from typing import List, Optional, Union
from typing import List, Optional, Union, Dict, Tuple, TypedDict
from pydantic import constr, field_validator from pydantic import constr
from models import ServiceCategoryPrice, ServicePriceCategory, Deal, Product, DealProduct, DealStatusHistory
from schemas.base import BaseSchema, OkMessageSchema from schemas.base import BaseSchema, OkMessageSchema
from schemas.billing import DealBillRequestSchema,GroupBillRequestSchema from schemas.billing import DealBillRequestSchema
from schemas.board import BoardSchema
from schemas.client import ClientSchema from schemas.client import ClientSchema
from schemas.group import DealGroupSchema
from schemas.marketplace import BaseMarketplaceSchema from schemas.marketplace import BaseMarketplaceSchema
from schemas.product import ProductSchema from schemas.product import ProductSchema
from schemas.service import ServiceSchema, ServicePriceCategorySchema from schemas.service import ServiceSchema, ServicePriceCategorySchema
from schemas.shipping import PalletSchema, BoxSchema from schemas.shipping import PalletSchema, BoxSchema
from schemas.shipping_warehouse import ShippingWarehouseSchema, BaseShippingWarehouseSchema from schemas.shipping_warehouse import ShippingWarehouseSchema, BaseShippingWarehouseSchema
from schemas.status import StatusSchema, DealStatusHistorySchema
from schemas.user import UserSchema from schemas.user import UserSchema
@@ -24,21 +25,13 @@ class FastDeal(BaseSchema):
acceptance_date: datetime.datetime acceptance_date: datetime.datetime
class DealGroupSchema(BaseSchema):
id: int
name: Optional[str] = None
lexorank: str
bill_request: Optional[GroupBillRequestSchema] = None
class DealSummary(BaseSchema): class DealSummary(BaseSchema):
id: int id: int
name: str name: str
client_name: str client_name: str
changed_at: datetime.datetime
created_at: datetime.datetime created_at: datetime.datetime
deadline: Optional[datetime.datetime] = None status: StatusSchema
status: int board: BoardSchema
total_price: int total_price: int
rank: int rank: int
base_marketplace: Optional[BaseMarketplaceSchema] = None base_marketplace: Optional[BaseMarketplaceSchema] = None
@@ -75,15 +68,6 @@ class DealProductSchema(BaseSchema):
comment: str = "" comment: str = ""
class DealStatusHistorySchema(BaseSchema):
user: UserSchema
changed_at: datetime.datetime
from_status: int
to_status: int
next_status_deadline: datetime.datetime | None
comment: str | None = None
class DealEmployeesSchema(BaseSchema): class DealEmployeesSchema(BaseSchema):
user: UserSchema user: UserSchema
created_at: datetime.datetime created_at: datetime.datetime
@@ -94,7 +78,8 @@ class DealSchema(BaseSchema):
name: str name: str
client_id: int client_id: int
created_at: datetime.datetime created_at: datetime.datetime
current_status: int status: StatusSchema
board: BoardSchema
services: List[DealServiceSchema] services: List[DealServiceSchema]
products: List[DealProductSchema] products: List[DealProductSchema]
status_history: List[DealStatusHistorySchema] status_history: List[DealStatusHistorySchema]
@@ -127,6 +112,8 @@ class DealGeneralInfoSchema(BaseSchema):
delivery_date: Optional[datetime.datetime] = None delivery_date: Optional[datetime.datetime] = None
receiving_slot_date: Optional[datetime.datetime] = None receiving_slot_date: Optional[datetime.datetime] = None
manager: Optional[UserSchema] = None manager: Optional[UserSchema] = None
board: BoardSchema
status: StatusSchema
class OptionalShippingWarehouseSchema(BaseShippingWarehouseSchema): class OptionalShippingWarehouseSchema(BaseShippingWarehouseSchema):
@@ -166,6 +153,7 @@ class DealChangeStatusRequest(BaseSchema):
class DealCreateRequest(BaseSchema): class DealCreateRequest(BaseSchema):
name: str name: str
status_id: int
class DealQuickCreateRequest(BaseSchema): class DealQuickCreateRequest(BaseSchema):
@@ -176,6 +164,7 @@ class DealQuickCreateRequest(BaseSchema):
shipping_warehouse: constr(strip_whitespace=True) shipping_warehouse: constr(strip_whitespace=True)
base_marketplace: BaseMarketplaceSchema base_marketplace: BaseMarketplaceSchema
category: Optional[ServicePriceCategorySchema] = None category: Optional[ServicePriceCategorySchema] = None
status_id: int
class DealSummaryRequest(BaseSchema): class DealSummaryRequest(BaseSchema):
@@ -243,7 +232,7 @@ class DealUpdateGeneralInfoRequest(BaseSchema):
class DealSummaryReorderRequest(BaseSchema): class DealSummaryReorderRequest(BaseSchema):
deal_id: int deal_id: int
status: int status_id: int
index: int index: int
deadline: datetime.datetime | None = None deadline: datetime.datetime | None = None
comment: str | None = None comment: str | None = None
@@ -300,32 +289,10 @@ class ManageEmployeeRequest(BaseSchema):
class CreateDealsFromExcelRequest(BaseSchema): class CreateDealsFromExcelRequest(BaseSchema):
client_id: int client_id: int
status_id: int
products: list[ProductFromExcelSchema] products: list[ProductFromExcelSchema]
class DealAddToGroupRequest(BaseSchema):
deal_id: int
group_id: int
class DealCreateGroupRequest(BaseSchema):
dragging_deal_id: int
hovered_deal_id: int
class DealRemoveFromGroupRequest(BaseSchema):
deal_id: int
class DealGroupUpdateRequest(BaseSchema):
data: DealGroupSchema
class DealGroupChangeStatusRequest(BaseSchema):
group_id: int
new_status: int
# endregion Requests # endregion Requests
# region Responses # region Responses
@@ -452,23 +419,4 @@ class CreateDealsFromExcelResponse(OkMessageSchema):
pass pass
class DealAddToGroupResponse(OkMessageSchema):
pass
class DealCreateGroupResponse(OkMessageSchema):
pass
class DealRemoveFromGroupResponse(OkMessageSchema):
pass
class DealGroupUpdateResponse(OkMessageSchema):
pass
class DealGroupChangeStatusResponse(OkMessageSchema):
pass
# endregion Responses # endregion Responses

66
schemas/group.py Normal file
View File

@@ -0,0 +1,66 @@
from typing import Optional
from schemas.base import BaseSchema, OkMessageSchema
from schemas.billing import GroupBillRequestSchema
# region Entities
class DealGroupSchema(BaseSchema):
id: int
name: Optional[str] = None
lexorank: str
bill_request: Optional[GroupBillRequestSchema] = None
# endregion
# region Requests
class DealGroupUpdateRequest(BaseSchema):
data: DealGroupSchema
class DealCreateGroupRequest(BaseSchema):
dragging_deal_id: int
hovered_deal_id: int
class DealGroupChangeStatusRequest(BaseSchema):
group_id: int
new_status: int
class DealAddToGroupRequest(BaseSchema):
deal_id: int
group_id: int
class DealRemoveFromGroupRequest(BaseSchema):
deal_id: int
# endregion
# region Responses
class DealCreateGroupResponse(OkMessageSchema):
pass
class DealGroupUpdateResponse(OkMessageSchema):
pass
class DealGroupChangeStatusResponse(OkMessageSchema):
pass
class DealAddToGroupResponse(OkMessageSchema):
pass
class DealRemoveFromGroupResponse(OkMessageSchema):
pass
# endregion

50
schemas/project.py Normal file
View File

@@ -0,0 +1,50 @@
from schemas.base import BaseSchema, OkMessageSchema
# region Entities
class BaseProjectSchema(BaseSchema):
name: str
class ProjectSchema(BaseProjectSchema):
id: int
class ProjectSchemaWithCount(ProjectSchema):
boards_count: int
# endregion
# region Requests
class CreateProjectRequest(BaseSchema):
project: BaseProjectSchema
class UpdateProjectRequest(BaseSchema):
project: ProjectSchema
# endregion
# region Responses
class GetProjectsResponse(BaseSchema):
projects: list[ProjectSchemaWithCount]
class CreateProjectResponse(OkMessageSchema):
pass
class UpdateProjectResponse(OkMessageSchema):
pass
class DeleteProjectResponse(OkMessageSchema):
pass
# endregion

View File

@@ -29,10 +29,13 @@ class CommonProfitFilters(BaseSchema):
date_range: Tuple[datetime.date, datetime.date] date_range: Tuple[datetime.date, datetime.date]
client_id: int client_id: int
base_marketplace_key: str base_marketplace_key: str
project_id: int
board_id: int
deal_status_id: int deal_status_id: int
manager_id: int manager_id: int
expense_tag_id: int expense_tag_id: int
income_tag_id: int income_tag_id: int
is_completed_only: bool
class GetProfitChartDataRequest(CommonProfitFilters): class GetProfitChartDataRequest(CommonProfitFilters):

65
schemas/status.py Normal file
View File

@@ -0,0 +1,65 @@
from datetime import datetime
from schemas.base import BaseSchema, OkMessageSchema
from schemas.user import UserSchema
# region Entities
class BaseStatusSchema(BaseSchema):
name: str
board_id: int
class StatusSchema(BaseStatusSchema):
id: int
ordinal_number: int
is_deleted: bool = False
class DealStatusHistorySchema(BaseSchema):
user: UserSchema
changed_at: datetime
from_status: StatusSchema
to_status: StatusSchema
next_status_deadline: datetime | None
comment: str | None = None
# endregion
# region Requests
class CreateStatusRequest(BaseSchema):
status: BaseStatusSchema
class UpdateStatusRequest(BaseSchema):
status: StatusSchema
class UpdateStatusOrderRequest(BaseSchema):
board_id: int
status_id: int
new_ordinal_number: int
# endregion
# region Responses
class CreateStatusResponse(OkMessageSchema):
pass
class UpdateStatusResponse(OkMessageSchema):
pass
class UpdateStatusOrderResponse(OkMessageSchema):
pass
class DeleteStatusResponse(OkMessageSchema):
pass
# endregion

119
services/board.py Normal file
View File

@@ -0,0 +1,119 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import select, and_, func
from models import Board, Deal
from schemas.board import *
from services.base import BaseService
class BoardService(BaseService):
async def _get_boards_for_project(self, project_id: int) -> list[Board]:
stmt = (
select(Board)
.where(
and_(
Board.is_deleted == False,
Board.project_id == project_id,
)
)
.order_by(Board.ordinal_number)
)
boards = (await self.session.scalars(stmt)).all()
return list(boards)
async def get_boards(self, project_id: int) -> GetBoardsResponse:
boards = await self._get_boards_for_project(project_id)
return GetBoardsResponse(boards=boards)
async def create_board(self, request: CreateBoardRequest) -> CreateBoardResponse:
boards = await self._get_boards_for_project(request.board.project_id)
if len(boards) == 0:
ordinal_number = 1
else:
ordinal_number = boards[-1].ordinal_number + 1
board = Board(
**request.board.model_dump(),
created_at=datetime.now(),
ordinal_number=ordinal_number,
)
self.session.add(board)
await self.session.commit()
return CreateBoardResponse(ok=True, message="Доска успешно создана")
async def _get_board_by_id(self, board_id: int) -> Optional[Board]:
return await self.session.get(Board, board_id)
async def update_board(self, request: UpdateBoardRequest) -> UpdateBoardResponse:
board = await self._get_board_by_id(request.board.id)
if not board:
return UpdateBoardResponse(ok=False, message=f"Доска с ID {request.board.id} не найдена")
board.name = request.board.name
await self.session.commit()
return UpdateBoardResponse(ok=True, message="Доска успешно обновлена")
async def update_board_order(self, request: UpdateBoardOrderRequest) -> UpdateBoardOrderResponse:
boards = await self._get_boards_for_project(request.project_id)
board_idx = 0
while board_idx < len(boards) and boards[board_idx].id != request.board_id:
board_idx += 1
if board_idx == len(boards):
return UpdateBoardOrderResponse(ok=False, message=f"Доска с ID {request.board_id} не найдена в проекте")
board = boards.pop(board_idx)
boards.insert(request.new_ordinal_number - 1, board)
new_ordinal_number = 1
for board in boards:
board.ordinal_number = new_ordinal_number
new_ordinal_number += 1
await self.session.commit()
return UpdateBoardOrderResponse(ok=True, message="Порядок досок изменен")
async def _count_deals_in_progress(self, board_id: int) -> int:
stmt = (
select(func.count(Deal.id))
.where(
and_(
Deal.board_id == board_id,
Deal.is_deleted == False,
Deal.is_completed == False,
)
)
)
return (await self.session.scalars(stmt)).first()
async def _count_deals(self, board_id: int) -> int:
stmt = (
select(func.count(Deal.id))
.where(Deal.board_id == board_id)
)
return (await self.session.scalars(stmt)).first()
async def delete_board(self, board_id: int) -> DeleteBoardResponse:
board = await self._get_board_by_id(board_id)
if not board:
return DeleteBoardResponse(ok=False, message=f"Доска с ID {board_id} не найдена")
count_deals_in_progress = await self._count_deals_in_progress(board_id)
if count_deals_in_progress != 0:
return DeleteBoardResponse(
ok=False,
message=f"Нельзя удалить доску с активными сделками",
)
count_deals = await self._count_deals(board_id)
if count_deals == 0:
await self.session.delete(board)
else:
board.is_deleted = True
for status in board.deal_statuses:
status.is_deleted = True
await self.session.commit()
return DeleteBoardResponse(ok=True, message="Доска успешно удалена")

View File

@@ -1,13 +1,14 @@
from collections import defaultdict
import lexorank import lexorank
from attr import dataclass
from fastapi import HTTPException from fastapi import HTTPException
from sqlalchemy import select, func, update, delete, insert, and_ from sqlalchemy import select, func, update, delete, insert, and_
from sqlalchemy.orm import joinedload, selectinload from sqlalchemy.orm import joinedload, selectinload
from starlette import status from starlette import status
from models import User, Service, Client, DealProductService, deal_relations, DealStatusHistory, Product, DealProduct
import models.deal import models.deal
import models.secondary import models.secondary
from models import User, Service, Client, DealProductService, deal_relations, GroupBillRequest
from models.deal import * from models.deal import *
from models.deal_group import DealGroup from models.deal_group import DealGroup
from models.shipping import ShippingProduct from models.shipping import ShippingProduct
@@ -16,9 +17,9 @@ from schemas.deal import *
from services.auth import AuthService from services.auth import AuthService
from services.base import BaseService from services.base import BaseService
from services.client import ClientService from services.client import ClientService
from services.deal_group import DealGroupService
from services.service import ServiceService from services.service import ServiceService
from services.shipping_warehouse import ShippingWarehouseService from services.shipping_warehouse import ShippingWarehouseService
from services import deal_group
class DealService(BaseService): class DealService(BaseService):
@@ -36,9 +37,13 @@ class DealService(BaseService):
async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]: async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]:
return await self.session.get(Deal, deal_id) return await self.session.get(Deal, deal_id)
async def _get_rank_for_deal(self, deal_status: DealStatus) -> str: async def _get_rank_for_deal(self, status_id: int) -> str:
deal_query = await self.session.execute( deal_query = await self.session.execute(
select(Deal).where(Deal.current_status == deal_status).order_by(Deal.lexorank.desc()).limit(1)) select(Deal)
.where(Deal.current_status_id == status_id)
.order_by(Deal.lexorank.desc())
.limit(1)
)
deal = deal_query.scalar_one_or_none() deal = deal_query.scalar_one_or_none()
if not deal: if not deal:
prev = lexorank.middle(lexorank.Bucket.BUCEKT_0) prev = lexorank.middle(lexorank.Bucket.BUCEKT_0)
@@ -46,59 +51,46 @@ class DealService(BaseService):
return str(lexorank.parse(deal.lexorank).next()) return str(lexorank.parse(deal.lexorank).next())
async def change_status(self, deal: Deal, async def change_status(self, deal: Deal,
status: DealStatus, status_id: int,
user: User, user: models.User,
deadline: datetime.datetime = None, deadline: datetime.datetime = None,
rank=None, rank=None,
comment: str = ''): comment: str = ''):
if not deal.current_status == status: if not deal.current_status_id == status_id:
deadline = deadline deadline = deadline
status_change = DealStatusHistory( status_change = DealStatusHistory(
deal_id=deal.id, deal_id=deal.id,
user_id=user.id, user_id=user.id,
changed_at=datetime.datetime.now(), changed_at=datetime.datetime.now(),
from_status=deal.current_status, from_status_id=deal.current_status_id,
to_status=status, to_status_id=status_id,
next_status_deadline=deadline, next_status_deadline=deadline,
comment=comment comment=comment
) )
self.session.add(status_change) self.session.add(status_change)
deal.current_status = status deal.current_status_id = status_id
if not rank: if not rank:
rank = await self._get_rank_for_deal(status) rank = await self._get_rank_for_deal(status_id)
if rank: if rank:
deal.lexorank = rank deal.lexorank = rank
await self.session.flush() await self.session.flush()
async def create(self, request: DealCreateRequest, user: User) -> DealCreateResponse:
rank = self._get_rank_for_deal(DealStatus.CREATED)
deal = Deal(
name=request.name,
created_at=datetime.datetime.now(),
current_status=DealStatus.CREATED,
lexorank=rank
)
self.session.add(deal)
await self.session.flush()
# Append status history
await self.change_status(deal, DealStatus.AWAITING_ACCEPTANCE, user)
await self.session.commit()
return DealCreateResponse(ok=True)
async def delete(self, request: DealDeleteRequest) -> DealDeleteResponse: async def delete(self, request: DealDeleteRequest) -> DealDeleteResponse:
deal = await self._get_deal_by_id(request.deal_id) deal = await self._get_deal_by_id(request.deal_id)
if not deal: if not deal:
return DealDeleteResponse(ok=False, message="Сделка не найдена") return DealDeleteResponse(ok=False, message="Сделка не найдена")
if deal.group: if deal.group:
await DealGroupService(self.session).delete_group(deal.group.id) await deal_group.DealGroupService(self.session).delete_group(deal.group.id)
else: else:
deal.is_deleted = True deal.is_deleted = True
await self.session.commit() await self.session.commit()
return DealDeleteResponse(ok=True, message="Сделка успешно удалена") return DealDeleteResponse(ok=True, message="Сделка успешно удалена")
async def quick_create(self, request: DealQuickCreateRequest, user: User) -> DealQuickCreateResponse: async def quick_create(self, request: DealQuickCreateRequest, user: models.User) -> DealQuickCreateResponse:
deal_status = await self.session.get(DealStatus, request.status_id)
if not deal_status:
raise HTTPException(status_code=400, detail="Указан некорректный статус")
client_service = ClientService(self.session) client_service = ClientService(self.session)
client = await client_service.get_by_name(request.client_name) client = await client_service.get_by_name(request.client_name)
@@ -108,28 +100,25 @@ class DealService(BaseService):
request.client_name, request.client_name,
ClientDetailsSchema() ClientDetailsSchema()
) )
shipping_warehouse_service = ShippingWarehouseService(self.session) shipping_warehouse_service = ShippingWarehouseService(self.session)
shipping_warehouse = await shipping_warehouse_service.get_by_name(name=request.shipping_warehouse) shipping_warehouse = await shipping_warehouse_service.get_by_name(name=request.shipping_warehouse)
if not shipping_warehouse: if not shipping_warehouse:
shipping_warehouse = await shipping_warehouse_service.create_by_name(name=request.shipping_warehouse) shipping_warehouse = await shipping_warehouse_service.create_by_name(name=request.shipping_warehouse)
rank = await self._get_rank_for_deal(DealStatus.CREATED) rank = await self._get_rank_for_deal(request.status_id)
deal = Deal( deal = Deal(
name=request.name, name=request.name,
created_at=datetime.datetime.now(), created_at=datetime.datetime.now(),
client_id=client.id, client_id=client.id,
current_status=DealStatus.CREATED, current_status_id=request.status_id,
board_id=deal_status.board_id,
lexorank=rank, lexorank=rank,
shipping_warehouse_id=shipping_warehouse.id, shipping_warehouse_id=shipping_warehouse.id,
base_marketplace_key=request.base_marketplace.key base_marketplace_key=request.base_marketplace.key
) )
self.session.add(deal) self.session.add(deal)
await self.session.flush() await self.session.flush()
await self.change_status(deal,
DealStatus.AWAITING_ACCEPTANCE,
user,
deadline=request.acceptance_date,
comment=request.comment)
# add category if specified # add category if specified
if request.category: if request.category:
deal_category = DealPriceCategory( deal_category = DealPriceCategory(
@@ -141,12 +130,12 @@ class DealService(BaseService):
await self.session.commit() await self.session.commit()
return DealQuickCreateResponse(deal_id=deal.id) return DealQuickCreateResponse(deal_id=deal.id)
async def change_status_manual(self, request: DealChangeStatusRequest, user: User) -> DealChangeStatusResponse: async def change_status_manual(self, request: DealChangeStatusRequest, user: models.User) -> DealChangeStatusResponse:
# Changing current status # Changing current status
deal = await self._get_deal_by_id(request.deal_id) deal = await self._get_deal_by_id(request.deal_id)
if not deal: if not deal:
return DealChangeStatusResponse(ok=False) return DealChangeStatusResponse(ok=False)
await self.change_status(deal, DealStatus(request.new_status), user) await self.change_status(deal, request.new_status, user)
await self.session.commit() await self.session.commit()
return DealChangeStatusResponse(ok=True) return DealChangeStatusResponse(ok=True)
@@ -199,7 +188,7 @@ class DealService(BaseService):
Deal, Deal,
func.coalesce(price_subquery.c.total_price, 0), func.coalesce(price_subquery.c.total_price, 0),
func.row_number().over( func.row_number().over(
partition_by=Deal.current_status, partition_by=Deal.current_status_id,
order_by=Deal.lexorank order_by=Deal.lexorank
).label('rank'), ).label('rank'),
func.coalesce(products_quantity_subquery.c.total_quantity, 0) func.coalesce(products_quantity_subquery.c.total_quantity, 0)
@@ -208,7 +197,9 @@ class DealService(BaseService):
selectinload(Deal.status_history), selectinload(Deal.status_history),
joinedload(Deal.client), joinedload(Deal.client),
joinedload(Deal.shipping_warehouse), joinedload(Deal.shipping_warehouse),
joinedload(Deal.bill_request) joinedload(Deal.bill_request),
joinedload(Deal.status),
joinedload(Deal.board),
) )
.outerjoin( .outerjoin(
price_subquery, Deal.id == price_subquery.c.deal_id, price_subquery, Deal.id == price_subquery.c.deal_id,
@@ -231,8 +222,6 @@ class DealService(BaseService):
summaries = [] summaries = []
for deal, total_price, rank, products_count in deals_query.all(): for deal, total_price, rank, products_count in deals_query.all():
deal: Deal deal: Deal
last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at)
deadline = last_status.next_status_deadline
base_marketplace = None base_marketplace = None
if deal.base_marketplace: if deal.base_marketplace:
base_marketplace = BaseMarketplaceSchema.model_validate(deal.base_marketplace) base_marketplace = BaseMarketplaceSchema.model_validate(deal.base_marketplace)
@@ -242,9 +231,8 @@ class DealService(BaseService):
id=deal.id, id=deal.id,
client_name=deal.client.name, client_name=deal.client.name,
name=deal.name, name=deal.name,
changed_at=last_status.changed_at, status=deal.status,
deadline=deadline, board=deal.board,
status=last_status.to_status,
total_price=total_price, total_price=total_price,
rank=rank, rank=rank,
base_marketplace=base_marketplace, base_marketplace=base_marketplace,
@@ -308,6 +296,9 @@ class DealService(BaseService):
deal = await self.session.scalar( deal = await self.session.scalar(
select(Deal) select(Deal)
.options( .options(
joinedload(Deal.status),
joinedload(Deal.board)
.joinedload(Board.project),
joinedload(Deal.shipping_warehouse), joinedload(Deal.shipping_warehouse),
joinedload(Deal.client) joinedload(Deal.client)
.joinedload(Client.details), .joinedload(Client.details),
@@ -350,7 +341,7 @@ class DealService(BaseService):
raise HTTPException(status_code=404, detail="Сделка не найдена") raise HTTPException(status_code=404, detail="Сделка не найдена")
return DealSchema.model_validate(deal) return DealSchema.model_validate(deal)
async def update_general_info(self, request: DealUpdateGeneralInfoRequest) -> DealUpdateGeneralInfoResponse: async def update_general_info(self, request: DealUpdateGeneralInfoRequest, user: User) -> DealUpdateGeneralInfoResponse:
try: try:
deal: Deal = await self.session.scalar( deal: Deal = await self.session.scalar(
select(Deal) select(Deal)
@@ -369,6 +360,15 @@ class DealService(BaseService):
deal.delivery_date = request.data.delivery_date deal.delivery_date = request.data.delivery_date
deal.receiving_slot_date = request.data.receiving_slot_date deal.receiving_slot_date = request.data.receiving_slot_date
if deal.board_id != request.data.board.id or deal.current_status_id != request.data.status.id:
if deal.group:
for deal in deal.group.deals:
deal.board_id = request.data.board.id
await self.change_status(deal, request.data.status.id, user)
else:
deal.board_id = request.data.board.id
await self.change_status(deal, request.data.status.id, user)
if deal.group: if deal.group:
for deal in deal.group.deals: for deal in deal.group.deals:
deal.is_accounted = request.data.is_accounted deal.is_accounted = request.data.is_accounted
@@ -402,7 +402,7 @@ class DealService(BaseService):
stmt = ( stmt = (
select(Deal) select(Deal)
.where( .where(
Deal.current_status == request.status, Deal.current_status_id == request.status_id,
Deal.id != request.deal_id, Deal.id != request.deal_id,
Deal.is_deleted == False, Deal.is_deleted == False,
Deal.is_completed == False Deal.is_completed == False
@@ -433,7 +433,7 @@ class DealService(BaseService):
else: else:
new_rank = lexorank.middle(lexorank.Bucket.BUCEKT_0) new_rank = lexorank.middle(lexorank.Bucket.BUCEKT_0)
await self.change_status(deal, request.status, user, await self.change_status(deal, request.status_id, user,
deadline=request.deadline, deadline=request.deadline,
comment=request.comment, comment=request.comment,
rank=str(new_rank)) rank=str(new_rank))
@@ -1079,12 +1079,9 @@ class DealService(BaseService):
return DealCompleteResponse(ok=False, message="Сделка не найдена") return DealCompleteResponse(ok=False, message="Сделка не найдена")
if deal.group: if deal.group:
deals = await DealGroupService(self.session).complete_group(deal.group.id) await deal_group.DealGroupService(self.session).complete_group(deal.group.id)
for completed_deal in deals:
await self.change_status(completed_deal, DealStatus.COMPLETED, user)
else: else:
deal.is_completed = True deal.is_completed = True
await self.change_status(deal, DealStatus.COMPLETED, user)
await self.session.commit() await self.session.commit()
return DealCompleteResponse(ok=True, message="Сделка успешно завершена") return DealCompleteResponse(ok=True, message="Сделка успешно завершена")
@@ -1234,14 +1231,15 @@ class DealService(BaseService):
async def _create_deal_from_excel( async def _create_deal_from_excel(
self, self,
client: Client, client: Client,
deal_status: DealStatus,
breakdown: CityBreakdownFromExcelSchema, breakdown: CityBreakdownFromExcelSchema,
user: User, user: User,
) -> Deal: ) -> Deal:
rank = await self._get_rank_for_deal(DealStatus.CREATED) rank = await self._get_rank_for_deal(deal_status.id)
deal = Deal( deal = Deal(
name=f"{client.name} - {breakdown.base_marketplace.key.upper()} - {breakdown.shipping_warehouse.name}", name=f"{client.name} - {breakdown.base_marketplace.key.upper()} - {breakdown.shipping_warehouse.name}",
created_at=datetime.datetime.now(), created_at=datetime.datetime.now(),
current_status=DealStatus.CREATED, current_status_id=deal_status.id,
lexorank=rank, lexorank=rank,
client_id=client.id, client_id=client.id,
base_marketplace_key=breakdown.base_marketplace.key, base_marketplace_key=breakdown.base_marketplace.key,
@@ -1250,19 +1248,8 @@ class DealService(BaseService):
self.session.add(deal) self.session.add(deal)
await self.session.flush() await self.session.flush()
await self.change_status(deal, DealStatus.AWAITING_ACCEPTANCE, user)
return deal return deal
async def _create_group(self) -> DealGroup:
group = models.DealGroup(
name='',
lexorank=lexorank.middle(lexorank.Bucket.BUCEKT_0).__str__(),
)
self.session.add(group)
await self.session.flush()
return group
async def _get_or_create_warehouse( async def _get_or_create_warehouse(
self, self,
shipping_warehouse: OptionalShippingWarehouseSchema, shipping_warehouse: OptionalShippingWarehouseSchema,
@@ -1288,8 +1275,12 @@ class DealService(BaseService):
if not client: if not client:
return CreateDealsFromExcelResponse(ok=False, message=f"Клиент с ID {request.client_id} не найден") return CreateDealsFromExcelResponse(ok=False, message=f"Клиент с ID {request.client_id} не найден")
deal_status: Optional[DealStatus] = await self.session.get(DealStatus, request.status_id)
if not deal_status:
return CreateDealsFromExcelResponse(ok=False, message=f"Статус с ID {request.status_id} не найден")
deals_dict: dict[str, Deal] = {} deals_dict: dict[str, Deal] = {}
group = await self._create_group() group = await deal_group.DealGroupService(self.session).create_group_model()
for product_data in request.products: for product_data in request.products:
for breakdown in product_data.cities_breakdown: for breakdown in product_data.cities_breakdown:
@@ -1298,7 +1289,7 @@ class DealService(BaseService):
key = f"{breakdown.shipping_warehouse.id} - {breakdown.base_marketplace.key}" key = f"{breakdown.shipping_warehouse.id} - {breakdown.base_marketplace.key}"
deal = deals_dict.get(key) deal = deals_dict.get(key)
if not deal: if not deal:
deal = await self._create_deal_from_excel(client, breakdown, user) deal = await self._create_deal_from_excel(client, deal_status, breakdown, user)
deals_dict[key] = deal deals_dict[key] = deal
insert_stmt = insert(deal_relations).values({ insert_stmt = insert(deal_relations).values({
@@ -1317,121 +1308,6 @@ class DealService(BaseService):
await self.session.commit() await self.session.commit()
return CreateDealsFromExcelResponse(ok=True, message="Сделки успешно созданы") return CreateDealsFromExcelResponse(ok=True, message="Сделки успешно созданы")
async def add_to_group(self, user: User, request: DealAddToGroupRequest) -> DealAddToGroupResponse:
try:
group_bill_request = await self.session.get(GroupBillRequest, request.group_id)
if group_bill_request:
raise Exception("Нельзя добавить сделку, так как на группу выставлен счёт.")
# changing status if needed
deal_id = await self.session.scalar(
select(deal_relations.c.deal_id)
.where(deal_relations.c.group_id == request.group_id)
)
group_deal_status = await self.session.scalar(
select(Deal.current_status)
.where(Deal.id == deal_id)
)
request_deal = await self.session.scalar(
select(Deal).where(Deal.id == request.deal_id)
)
if group_deal_status != request_deal.current_status:
await self.change_status(request_deal, group_deal_status, user)
insert_stmt = insert(deal_relations).values({
'deal_id': request.deal_id,
'group_id': request.group_id
})
await self.session.execute(insert_stmt)
await self.session.commit()
return DealAddToGroupResponse(ok=True, message="Сделка успешно добавлена в группу")
except Exception as e:
await self.session.rollback()
return DealAddToGroupResponse(ok=False, message=str(e))
async def create_group(self, user: User, request: DealCreateGroupRequest) -> DealCreateGroupResponse:
try:
# getting lexorank for grop
group = await self._create_group()
for deal_id in [request.dragging_deal_id, request.hovered_deal_id]:
insert_stmt = insert(deal_relations).values({
'deal_id': deal_id,
'group_id': group.id
})
await self.session.execute(insert_stmt)
# changing status if needed on draggable deal
dragging_deal = await self.session.scalar(
select(Deal).where(Deal.id == request.dragging_deal_id)
)
dropped_deal = await self.session.scalar(
select(Deal).where(Deal.id == request.hovered_deal_id)
)
if dragging_deal.current_status != dropped_deal.current_status:
await self.change_status(dragging_deal, DealStatus(dropped_deal.current_status), user)
await self.session.commit()
return DealCreateGroupResponse(ok=True, message="Группа успешно создана")
except Exception as e:
return DealCreateGroupResponse(ok=False, message=str(e))
async def remove_from_group(self, request: DealRemoveFromGroupRequest) -> DealRemoveFromGroupResponse:
try:
delete_stmt = (
delete(deal_relations)
.where(
deal_relations.c.deal_id == request.deal_id,
)
)
await self.session.execute(delete_stmt)
await self.session.commit()
return DealRemoveFromGroupResponse(ok=True, message="Сделка успешно удалена из группы")
except Exception as e:
await self.session.rollback()
return DealRemoveFromGroupResponse(ok=False, message=str(e))
async def update_group(self, request: DealGroupUpdateRequest) -> DealGroupUpdateResponse:
try:
group = await self.session.scalar(
select(models.DealGroup).where(models.DealGroup.id == request.data.id)
)
if not group:
return DealGroupUpdateResponse(ok=False, message="Группа не найдена")
# update by dictionary
request_dict = request.data.dict()
update_stmt = (
update(
models.DealGroup
)
.where(models.DealGroup.id == request.data.id)
.values(**request_dict)
)
await self.session.execute(update_stmt)
await self.session.commit()
return DealGroupUpdateResponse(ok=True, message="Группа успешно обновлена")
except Exception as e:
await self.session.rollback()
return DealGroupUpdateResponse(ok=False, message=str(e))
async def change_group_status(self, user: User,
request: DealGroupChangeStatusRequest) -> DealGroupChangeStatusResponse:
try:
# getting all deals in group
deals = await self.session.scalars(
select(deal_relations.c.deal_id)
.where(deal_relations.c.group_id == request.group_id)
)
for deal_id in deals:
deal = await self.session.scalar(
select(Deal).where(Deal.id == deal_id)
)
await self.change_status(deal, DealStatus(request.new_status), user)
await self.session.commit()
return DealGroupChangeStatusResponse(ok=True, message="Статус группы успешно изменен")
except Exception as e:
await self.session.rollback()
return DealGroupChangeStatusResponse(ok=False, message=str(e))
async def get_deals_grouped(self, deal: models.Deal) -> List[models.Deal]: async def get_deals_grouped(self, deal: models.Deal) -> List[models.Deal]:
if not deal.group: if not deal.group:
return [deal] return [deal]

View File

@@ -1,10 +1,12 @@
from sqlalchemy import select from lexorank import lexorank
from sqlalchemy import select, insert, update, delete
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from models import DealService as DealServiceModel from models import DealService as DealServiceModel, User, Deal, DealProduct, Product, GroupBillRequest
from models.deal_group import DealGroup from models.deal_group import DealGroup, deal_relations
from schemas.deal import * from schemas.group import *
from services.base import BaseService from services.base import BaseService
from services.deal import DealService
class DealGroupService(BaseService): class DealGroupService(BaseService):
@@ -25,6 +27,63 @@ class DealGroupService(BaseService):
) )
return group.deals if group else [] return group.deals if group else []
async def create_group_model(self) -> DealGroup:
group = DealGroup(
name='',
lexorank=lexorank.middle(lexorank.Bucket.BUCEKT_0).__str__(),
)
self.session.add(group)
await self.session.flush()
return group
async def create_group(self, user: User, request: DealCreateGroupRequest) -> DealCreateGroupResponse:
try:
group = await self.create_group_model()
for deal_id in [request.dragging_deal_id, request.hovered_deal_id]:
insert_stmt = insert(deal_relations).values({
'deal_id': deal_id,
'group_id': group.id
})
await self.session.execute(insert_stmt)
# changing status if needed on draggable deal
dragging_deal = await self.session.scalar(
select(Deal).where(Deal.id == request.dragging_deal_id)
)
dropped_deal = await self.session.scalar(
select(Deal).where(Deal.id == request.hovered_deal_id)
)
if dragging_deal.current_status_id != dropped_deal.current_status_id:
deal_service = DealService(self.session)
await deal_service.change_status(dragging_deal, dropped_deal.current_status_id, user)
await self.session.commit()
return DealCreateGroupResponse(ok=True, message="Группа успешно создана")
except Exception as e:
return DealCreateGroupResponse(ok=False, message=str(e))
async def update_group(self, request: DealGroupUpdateRequest) -> DealGroupUpdateResponse:
try:
group = await self.session.scalar(
select(DealGroup).where(DealGroup.id == request.data.id)
)
if not group:
return DealGroupUpdateResponse(ok=False, message="Группа не найдена")
# update by dictionary
request_dict = request.data.model_dump()
request_dict.pop("bill_request", None)
update_stmt = (
update(DealGroup)
.where(DealGroup.id == request.data.id)
.values(**request_dict)
)
await self.session.execute(update_stmt)
await self.session.commit()
return DealGroupUpdateResponse(ok=True, message="Группа успешно обновлена")
except Exception as e:
await self.session.rollback()
return DealGroupUpdateResponse(ok=False, message=str(e))
async def complete_group(self, group_id: int) -> list[Deal]: async def complete_group(self, group_id: int) -> list[Deal]:
deals = await self.get_deals_by_group_id(group_id) deals = await self.get_deals_by_group_id(group_id)
for deal in deals: for deal in deals:
@@ -36,3 +95,74 @@ class DealGroupService(BaseService):
for deal in deals: for deal in deals:
deal.is_deleted = True deal.is_deleted = True
await self.session.commit() await self.session.commit()
async def change_group_status(
self,
user: User,
request: DealGroupChangeStatusRequest,
) -> DealGroupChangeStatusResponse:
try:
# getting all deals in group
deals = await self.session.scalars(
select(deal_relations.c.deal_id)
.where(deal_relations.c.group_id == request.group_id)
)
deal_service = DealService(self.session)
for deal_id in deals:
deal = await self.session.scalar(
select(Deal).where(Deal.id == deal_id)
)
await deal_service.change_status(deal, request.new_status, user)
await self.session.commit()
return DealGroupChangeStatusResponse(ok=True, message="Статус группы успешно изменен")
except Exception as e:
await self.session.rollback()
return DealGroupChangeStatusResponse(ok=False, message=str(e))
async def add_deal(self, user: User, request: DealAddToGroupRequest) -> DealAddToGroupResponse:
try:
group_bill_request = await self.session.get(GroupBillRequest, request.group_id)
if group_bill_request:
raise Exception("Нельзя добавить сделку, так как на группу выставлен счёт.")
# changing status if needed
deal_id = await self.session.scalar(
select(deal_relations.c.deal_id)
.where(deal_relations.c.group_id == request.group_id)
)
group_deal_status = await self.session.scalar(
select(Deal.current_status_id)
.where(Deal.id == deal_id)
)
request_deal = await self.session.scalar(
select(Deal).where(Deal.id == request.deal_id)
)
if group_deal_status != request_deal.current_status_id:
await DealService(self.session).change_status(request_deal, group_deal_status, user)
insert_stmt = insert(deal_relations).values({
'deal_id': request.deal_id,
'group_id': request.group_id
})
await self.session.execute(insert_stmt)
await self.session.commit()
return DealAddToGroupResponse(ok=True, message="Сделка успешно добавлена в группу")
except Exception as e:
await self.session.rollback()
return DealAddToGroupResponse(ok=False, message=str(e))
async def remove_deal(self, request: DealRemoveFromGroupRequest) -> DealRemoveFromGroupResponse:
try:
delete_stmt = (
delete(deal_relations)
.where(
deal_relations.c.deal_id == request.deal_id,
)
)
await self.session.execute(delete_stmt)
await self.session.commit()
return DealRemoveFromGroupResponse(ok=True, message="Сделка успешно удалена из группы")
except Exception as e:
await self.session.rollback()
return DealRemoveFromGroupResponse(ok=False, message=str(e))

73
services/project.py Normal file
View File

@@ -0,0 +1,73 @@
from datetime import datetime
from sqlalchemy import select, update, func, delete, and_
from models import Project, Board
from schemas.project import *
from services.base import BaseService
class ProjectService(BaseService):
async def get_projects(self) -> GetProjectsResponse:
boards_sub = (
select(Board)
.where(Board.is_deleted == False)
.subquery()
)
stmt = (
select(
Project.id,
Project.name,
func.count(boards_sub.c.id)
)
.join(boards_sub, Project.id == boards_sub.c.project_id, isouter=True)
.where(Project.is_deleted == False)
.group_by(Project.id, Project.name)
.order_by(Project.name)
)
project_data = (await self.session.execute(stmt)).all()
projects = []
for project_id, name, boards_count in project_data:
project = ProjectSchemaWithCount(id=project_id, name=name, boards_count=boards_count)
projects.append(project)
return GetProjectsResponse(projects=projects)
async def create_project(self, request: CreateProjectRequest) -> CreateProjectResponse:
project = Project(
name=request.project.name,
created_at=datetime.now(),
)
self.session.add(project)
await self.session.commit()
return UpdateProjectResponse(ok=True, message="Проект успешно создан")
async def update_project(self, request: UpdateProjectRequest) -> UpdateProjectResponse:
stmt = (
update(Project)
.where(Project.id == request.project.id)
.values(name=request.project.name)
)
await self.session.execute(stmt)
await self.session.commit()
return UpdateProjectResponse(ok=True, message="Проект успешно изменен")
async def delete_project(self, project_id: int) -> DeleteProjectResponse:
stmt_boards = select(Board).where(Board.project_id == project_id)
boards = (await self.session.scalars(stmt_boards)).all()
if len(boards) == 0:
stmt = (
delete(Project)
.where(Project.id == project_id)
)
else:
stmt = (
update(Project)
.where(Project.id == project_id)
.values(is_deleted=True)
)
await self.session.execute(stmt)
await self.session.commit()
return DeleteProjectResponse(ok=True, message="Проект успешно удален")

View File

@@ -5,7 +5,7 @@ from sqlalchemy import select, and_, union_all, func, Subquery, literal
from enums.profit_table_group_by import ProfitTableGroupBy from enums.profit_table_group_by import ProfitTableGroupBy
from models import DealService, Deal, DealStatusHistory, DealProductService, DealProduct, Service, Client, \ from models import DealService, Deal, DealStatusHistory, DealProductService, DealProduct, Service, Client, \
ShippingWarehouse, BaseMarketplace, User ShippingWarehouse, BaseMarketplace, User, Project, Board
from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataRequest, ProfitChartDataItem, \ from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataRequest, ProfitChartDataItem, \
GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters
from services.base import BaseService from services.base import BaseService
@@ -24,7 +24,7 @@ class ProfitStatisticsService(BaseService):
'day', 'day',
Deal.created_at, Deal.created_at,
).label('date'), ).label('date'),
Deal.current_status, Deal.current_status_id,
) )
.subquery() .subquery()
) )
@@ -51,7 +51,7 @@ class ProfitStatisticsService(BaseService):
'day', 'day',
last_statuses.c.changed_at, last_statuses.c.changed_at,
).label('date'), ).label('date'),
Deal.current_status, Deal.current_status_id,
) )
.join(last_statuses, last_statuses.c.deal_id == Deal.id) .join(last_statuses, last_statuses.c.deal_id == Deal.id)
.subquery() .subquery()
@@ -97,8 +97,7 @@ class ProfitStatisticsService(BaseService):
return data return data
@staticmethod def _get_stmt_deal_services(self, sub_filtered_status_history: Subquery):
def _get_stmt_deal_services(sub_filtered_status_history: Subquery):
return ( return (
select( select(
Deal.id.label("deal_id"), Deal.id.label("deal_id"),
@@ -112,10 +111,24 @@ class ProfitStatisticsService(BaseService):
.join(DealService, Deal.id == DealService.deal_id) .join(DealService, Deal.id == DealService.deal_id)
.join(Service, DealService.service_id == Service.id) .join(Service, DealService.service_id == Service.id)
.join(sub_filtered_status_history, Deal.id == sub_filtered_status_history.c.deal_id) .join(sub_filtered_status_history, Deal.id == sub_filtered_status_history.c.deal_id)
.where(and_(Deal.is_deleted == False, Deal.is_accounted == True)) .where(
and_(
Deal.is_deleted == False,
Deal.is_accounted == True,
Deal.is_completed == True if self.is_completed_only else True
)
)
.group_by(Deal.id, "date") .group_by(Deal.id, "date")
) )
@staticmethod
def _board_ids_for_project(project_id: int, stmt):
board_ids_stmt = (
select(Board.id)
.where(Board.project_id == project_id)
)
return stmt.where(Deal.board_id.in_(board_ids_stmt))
@staticmethod @staticmethod
def _apply_filters(request: CommonProfitFilters, stmt_deal_services, stmt_deal_product_services): def _apply_filters(request: CommonProfitFilters, stmt_deal_services, stmt_deal_product_services):
if request.client_id != -1: if request.client_id != -1:
@@ -127,9 +140,21 @@ class ProfitStatisticsService(BaseService):
stmt_deal_product_services = stmt_deal_product_services.where( stmt_deal_product_services = stmt_deal_product_services.where(
Deal.base_marketplace_key == request.base_marketplace_key) Deal.base_marketplace_key == request.base_marketplace_key)
if request.deal_status_id != -1: if request.project_id != -1:
stmt_deal_services = stmt_deal_services.where(Deal.current_status == request.deal_status_id) stmt_deal_services = ProfitStatisticsService._board_ids_for_project(request.project_id, stmt_deal_services)
stmt_deal_product_services = stmt_deal_product_services.where(Deal.current_status == request.deal_status_id) stmt_deal_product_services = ProfitStatisticsService._board_ids_for_project(
request.project_id,
stmt_deal_product_services,
)
if request.board_id != -1:
stmt_deal_services = stmt_deal_services.where(Deal.board_id == request.board_id)
stmt_deal_product_services = stmt_deal_product_services.where(Deal.board_id == request.board_id)
if request.deal_status_id != -1:
stmt_deal_services = stmt_deal_services.where(Deal.current_status_id == request.deal_status_id)
stmt_deal_product_services = stmt_deal_product_services.where(
Deal.current_status_id == request.deal_status_id)
if request.manager_id != -1: if request.manager_id != -1:
stmt_deal_services = stmt_deal_services.where(Deal.manager_id == request.manager_id) stmt_deal_services = stmt_deal_services.where(Deal.manager_id == request.manager_id)
@@ -137,8 +162,7 @@ class ProfitStatisticsService(BaseService):
return stmt_deal_services, stmt_deal_product_services return stmt_deal_services, stmt_deal_product_services
@staticmethod def _get_stmt_product_services(self):
def _get_stmt_product_services():
return ( return (
select( select(
Deal.id.label("deal_id"), Deal.id.label("deal_id"),
@@ -154,7 +178,13 @@ class ProfitStatisticsService(BaseService):
) )
) )
.join(Service, DealProductService.service_id == Service.id) .join(Service, DealProductService.service_id == Service.id)
.where(and_(Deal.is_deleted == False, Deal.is_accounted == True)) .where(
and_(
Deal.is_deleted == False,
Deal.is_accounted == True,
Deal.is_completed == True if self.is_completed_only else True,
)
)
.group_by(Deal.id) .group_by(Deal.id)
) )
@@ -226,16 +256,47 @@ class ProfitStatisticsService(BaseService):
) )
@staticmethod @staticmethod
def _join_and_group_by_statuses(stmt): def _join_and_group_by_projects(stmt):
return ( return (
select( select(
Deal.current_status.label("grouped_value"), Project.id,
Project.name.label("grouped_value"),
func.count(stmt.c.deal_id).label("deals_count"), func.count(stmt.c.deal_id).label("deals_count"),
func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.revenue).label("revenue"),
func.sum(stmt.c.profit).label("profit"), func.sum(stmt.c.profit).label("profit"),
) )
.join(Deal, Deal.id == stmt.c.deal_id) .join(Deal, Deal.id == stmt.c.deal_id)
.group_by(Deal.current_status) .join(Board, Board.id == Deal.board_id)
.join(Project, Project.id == Board.project_id)
.group_by(Project.id, Project.name)
)
@staticmethod
def _join_and_group_by_boards(stmt):
return (
select(
Board.id,
Board.name.label("grouped_value"),
func.count(stmt.c.deal_id).label("deals_count"),
func.sum(stmt.c.revenue).label("revenue"),
func.sum(stmt.c.profit).label("profit"),
)
.join(Deal, Deal.id == stmt.c.deal_id)
.join(Board, Board.id == Deal.board_id)
.group_by(Board.id, Board.name)
)
@staticmethod
def _join_and_group_by_statuses(stmt):
return (
select(
Deal.current_status_id.label("grouped_value"),
func.count(stmt.c.deal_id).label("deals_count"),
func.sum(stmt.c.revenue).label("revenue"),
func.sum(stmt.c.profit).label("profit"),
)
.join(Deal, Deal.id == stmt.c.deal_id)
.group_by(Deal.current_status_id)
) )
@staticmethod @staticmethod
@@ -320,6 +381,7 @@ class ProfitStatisticsService(BaseService):
async def _get_data_grouped_by_date(self, request: CommonProfitFilters, is_chart: bool = True): async def _get_data_grouped_by_date(self, request: CommonProfitFilters, is_chart: bool = True):
self.date_from, self.date_to = request.date_range self.date_from, self.date_to = request.date_range
self.is_completed_only = request.is_completed_only
self.filters = request self.filters = request
sub_deals_dates = self._get_deals_dates(request.deal_status_id) sub_deals_dates = self._get_deals_dates(request.deal_status_id)
@@ -399,6 +461,18 @@ class ProfitStatisticsService(BaseService):
return await self._table_data_from_stmt(stmt_grouped_by_clients) return await self._table_data_from_stmt(stmt_grouped_by_clients)
async def _get_table_grouped_by_projects(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse:
sub_grouped_by_deals = self._get_common_table_grouped(request)
stmt_grouped_by_projects = self._join_and_group_by_projects(sub_grouped_by_deals)
return await self._table_data_from_stmt(stmt_grouped_by_projects)
async def _get_table_grouped_by_boards(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse:
sub_grouped_by_deals = self._get_common_table_grouped(request)
stmt_grouped_by_boards = self._join_and_group_by_boards(sub_grouped_by_deals)
return await self._table_data_from_stmt(stmt_grouped_by_boards)
async def _get_table_grouped_by_statuses(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: async def _get_table_grouped_by_statuses(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse:
date_from, date_to = request.date_range date_from, date_to = request.date_range
@@ -449,6 +523,10 @@ class ProfitStatisticsService(BaseService):
return await self._get_table_grouped_by_dates(request) return await self._get_table_grouped_by_dates(request)
case ProfitTableGroupBy.BY_CLIENTS: case ProfitTableGroupBy.BY_CLIENTS:
return await self._get_table_grouped_by_clients(request) return await self._get_table_grouped_by_clients(request)
case ProfitTableGroupBy.BY_PROJECTS:
return await self._get_table_grouped_by_projects(request)
case ProfitTableGroupBy.BY_BOARDS:
return await self._get_table_grouped_by_boards(request)
case ProfitTableGroupBy.BY_STATUSES: case ProfitTableGroupBy.BY_STATUSES:
return await self._get_table_grouped_by_statuses(request) return await self._get_table_grouped_by_statuses(request)
case ProfitTableGroupBy.BY_WAREHOUSES: case ProfitTableGroupBy.BY_WAREHOUSES:

128
services/status.py Normal file
View File

@@ -0,0 +1,128 @@
from typing import Optional
from sqlalchemy import select, and_, func
from models import DealStatus, Deal
from schemas.status import *
from services.base import BaseService
class StatusService(BaseService):
async def _get_statuses_for_board(self, board_id: int) -> list[DealStatus]:
stmt = (
select(DealStatus)
.where(
and_(
DealStatus.board_id == board_id,
DealStatus.is_deleted == False,
)
)
.order_by(DealStatus.ordinal_number)
)
statuses = (await self.session.scalars(stmt)).all()
return list(statuses)
async def _get_status_by_id(self, status_id: int) -> Optional[DealStatus]:
stmt = (
select(DealStatus)
.where(DealStatus.id == status_id)
)
status = await self.session.scalar(stmt)
return status
async def create_status(self, request: CreateStatusRequest) -> CreateStatusResponse:
statuses = await self._get_statuses_for_board(request.status.board_id)
if len(statuses) == 0:
ordinal_number = 1
else:
statuses[-1].is_finishing = False
ordinal_number = statuses[-1].ordinal_number + 1
status = DealStatus(
**request.status.model_dump(),
ordinal_number=ordinal_number,
is_finishing=True,
)
self.session.add(status)
await self.session.commit()
return CreateStatusResponse(ok=True, message="Статус успешно создан")
async def update_status(self, request: UpdateStatusRequest) -> UpdateStatusResponse:
status = await self._get_status_by_id(request.status.id)
if not status:
return UpdateStatusResponse(ok=False, message=f"Статус с ID {request.status.id} не найден")
status.name = request.status.name
await self.session.commit()
return UpdateStatusResponse(ok=True, message="Статус успешно изменен")
async def update_status_order(self, request: UpdateStatusOrderRequest) -> UpdateStatusOrderResponse:
statuses = await self._get_statuses_for_board(request.board_id)
status_idx = 0
while status_idx < len(statuses) and statuses[status_idx].id != request.status_id:
status_idx += 1
if status_idx == len(statuses):
return UpdateStatusOrderResponse(ok=False, message=f"Статус с ID {request.status_id} не найден")
status = statuses.pop(status_idx)
statuses.insert(request.new_ordinal_number - 1, status)
new_ordinal_number = 1
for status in statuses:
status.ordinal_number = new_ordinal_number
status.is_finishing = False
new_ordinal_number += 1
statuses[-1].is_finishing = True
await self.session.commit()
return UpdateStatusOrderResponse(ok=True, message="Порядок статусов изменен")
async def _count_deals_in_progress(self, status_id: int) -> int:
stmt = (
select(func.count(Deal.id))
.where(
and_(
Deal.current_status_id == status_id,
Deal.is_deleted == False,
Deal.is_completed == False,
)
)
)
return (await self.session.scalars(stmt)).first()
async def _count_deals(self, status_id: int) -> int:
stmt = (
select(func.count(Deal.id))
.where(Deal.current_status_id == status_id)
)
return (await self.session.scalars(stmt)).first()
async def _set_finishing_flag_to_prev_status(self, status: DealStatus):
statuses = await self._get_statuses_for_board(status.board_id)
if len(statuses) < 2:
return
statuses[-2].is_finishing = True
statuses[-1].is_finishing = False
async def delete_status(self, status_id: int) -> DeleteStatusResponse:
status = await self._get_status_by_id(status_id)
if not status:
return DeleteStatusResponse(ok=False, message=f"Статус с ID {status_id} не найден")
count_deals_in_progress = await self._count_deals_in_progress(status_id)
if count_deals_in_progress != 0:
return DeleteStatusResponse(ok=False, message="Нельзя удалить статус с активными сделками")
if status.is_finishing:
await self._set_finishing_flag_to_prev_status(status)
count_deals = await self._count_deals(status_id)
if count_deals == 0:
await self.session.delete(status)
else:
status.is_deleted = True
await self.session.commit()
return DeleteStatusResponse(ok=True, message="Статус успешно удален")