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):
BY_DATES = 0
BY_CLIENTS = 1
BY_STATUSES = 2
BY_WAREHOUSES = 3
BY_MARKETPLACES = 4
BY_MANAGERS = 5
BY_PROJECTS = 2
BY_BOARDS = 3
BY_STATUSES = 4
BY_WAREHOUSES = 5
BY_MARKETPLACES = 6
BY_MANAGERS = 7

View File

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

View File

@@ -1,6 +1,9 @@
from sqlalchemy.orm import configure_mappers
from .auth import *
from .project import *
from .board import *
from .status import *
from .deal import *
from .client import *
from .service import *
@@ -15,5 +18,6 @@ from .marketplace_products import *
from .deal_group import *
from .transaction import *
from .residues import *
from .shipping import *
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 .marketplace import BaseMarketplace
from .board import Board
from .status import DealStatus
from .shipping import Pallet, Box
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
class DealStatus(IntEnum):
class DealStatusEnum(IntEnum):
CREATED = 0
AWAITING_ACCEPTANCE = 1
READY_FOR_WORK = 2
@@ -53,7 +45,6 @@ class Deal(BaseModel):
id = Column(Integer, autoincrement=True, primary_key=True, index=True)
name = Column(String, 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 = 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_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: Mapped["ShippingWarehouse"] = relationship()
@@ -88,6 +87,12 @@ class Deal(BaseModel):
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
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')
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):
__tablename__ = 'deal_employees'
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 .deal import deal_router
from .group import deal_group_router
from .client import client_router
from .service import service_router
from .product import product_router
@@ -20,3 +21,6 @@ from .transaction import transaction_router
from .shipping import shipping_router
from .department import department_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
@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(
'/delete',
@@ -151,9 +140,10 @@ async def get_deal_by_id(
)
async def update_general_info(
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(
@@ -494,71 +484,3 @@ async def get_deal_products_barcodes_pdf(
# 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
from collections import defaultdict
from typing import List, Optional, Union, Dict, Tuple, TypedDict
from typing import List, Optional, Union
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.billing import DealBillRequestSchema,GroupBillRequestSchema
from schemas.billing import DealBillRequestSchema
from schemas.board import BoardSchema
from schemas.client import ClientSchema
from schemas.group import DealGroupSchema
from schemas.marketplace import BaseMarketplaceSchema
from schemas.product import ProductSchema
from schemas.service import ServiceSchema, ServicePriceCategorySchema
from schemas.shipping import PalletSchema, BoxSchema
from schemas.shipping_warehouse import ShippingWarehouseSchema, BaseShippingWarehouseSchema
from schemas.status import StatusSchema, DealStatusHistorySchema
from schemas.user import UserSchema
@@ -24,21 +25,13 @@ class FastDeal(BaseSchema):
acceptance_date: datetime.datetime
class DealGroupSchema(BaseSchema):
id: int
name: Optional[str] = None
lexorank: str
bill_request: Optional[GroupBillRequestSchema] = None
class DealSummary(BaseSchema):
id: int
name: str
client_name: str
changed_at: datetime.datetime
created_at: datetime.datetime
deadline: Optional[datetime.datetime] = None
status: int
status: StatusSchema
board: BoardSchema
total_price: int
rank: int
base_marketplace: Optional[BaseMarketplaceSchema] = None
@@ -75,15 +68,6 @@ class DealProductSchema(BaseSchema):
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):
user: UserSchema
created_at: datetime.datetime
@@ -94,7 +78,8 @@ class DealSchema(BaseSchema):
name: str
client_id: int
created_at: datetime.datetime
current_status: int
status: StatusSchema
board: BoardSchema
services: List[DealServiceSchema]
products: List[DealProductSchema]
status_history: List[DealStatusHistorySchema]
@@ -127,6 +112,8 @@ class DealGeneralInfoSchema(BaseSchema):
delivery_date: Optional[datetime.datetime] = None
receiving_slot_date: Optional[datetime.datetime] = None
manager: Optional[UserSchema] = None
board: BoardSchema
status: StatusSchema
class OptionalShippingWarehouseSchema(BaseShippingWarehouseSchema):
@@ -166,6 +153,7 @@ class DealChangeStatusRequest(BaseSchema):
class DealCreateRequest(BaseSchema):
name: str
status_id: int
class DealQuickCreateRequest(BaseSchema):
@@ -176,6 +164,7 @@ class DealQuickCreateRequest(BaseSchema):
shipping_warehouse: constr(strip_whitespace=True)
base_marketplace: BaseMarketplaceSchema
category: Optional[ServicePriceCategorySchema] = None
status_id: int
class DealSummaryRequest(BaseSchema):
@@ -243,7 +232,7 @@ class DealUpdateGeneralInfoRequest(BaseSchema):
class DealSummaryReorderRequest(BaseSchema):
deal_id: int
status: int
status_id: int
index: int
deadline: datetime.datetime | None = None
comment: str | None = None
@@ -300,32 +289,10 @@ class ManageEmployeeRequest(BaseSchema):
class CreateDealsFromExcelRequest(BaseSchema):
client_id: int
status_id: int
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
# region Responses
@@ -452,23 +419,4 @@ class CreateDealsFromExcelResponse(OkMessageSchema):
pass
class DealAddToGroupResponse(OkMessageSchema):
pass
class DealCreateGroupResponse(OkMessageSchema):
pass
class DealRemoveFromGroupResponse(OkMessageSchema):
pass
class DealGroupUpdateResponse(OkMessageSchema):
pass
class DealGroupChangeStatusResponse(OkMessageSchema):
pass
# 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]
client_id: int
base_marketplace_key: str
project_id: int
board_id: int
deal_status_id: int
manager_id: int
expense_tag_id: int
income_tag_id: int
is_completed_only: bool
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
from attr import dataclass
from fastapi import HTTPException
from sqlalchemy import select, func, update, delete, insert, and_
from sqlalchemy.orm import joinedload, selectinload
from starlette import status
from models import User, Service, Client, DealProductService, deal_relations, DealStatusHistory, Product, DealProduct
import models.deal
import models.secondary
from models import User, Service, Client, DealProductService, deal_relations, GroupBillRequest
from models.deal import *
from models.deal_group import DealGroup
from models.shipping import ShippingProduct
@@ -16,9 +17,9 @@ from schemas.deal import *
from services.auth import AuthService
from services.base import BaseService
from services.client import ClientService
from services.deal_group import DealGroupService
from services.service import ServiceService
from services.shipping_warehouse import ShippingWarehouseService
from services import deal_group
class DealService(BaseService):
@@ -36,9 +37,13 @@ class DealService(BaseService):
async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]:
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(
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()
if not deal:
prev = lexorank.middle(lexorank.Bucket.BUCEKT_0)
@@ -46,59 +51,46 @@ class DealService(BaseService):
return str(lexorank.parse(deal.lexorank).next())
async def change_status(self, deal: Deal,
status: DealStatus,
user: User,
status_id: int,
user: models.User,
deadline: datetime.datetime = None,
rank=None,
comment: str = ''):
if not deal.current_status == status:
if not deal.current_status_id == status_id:
deadline = deadline
status_change = DealStatusHistory(
deal_id=deal.id,
user_id=user.id,
changed_at=datetime.datetime.now(),
from_status=deal.current_status,
to_status=status,
from_status_id=deal.current_status_id,
to_status_id=status_id,
next_status_deadline=deadline,
comment=comment
)
self.session.add(status_change)
deal.current_status = status
deal.current_status_id = status_id
if not rank:
rank = await self._get_rank_for_deal(status)
rank = await self._get_rank_for_deal(status_id)
if rank:
deal.lexorank = rank
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:
deal = await self._get_deal_by_id(request.deal_id)
if not deal:
return DealDeleteResponse(ok=False, message="Сделка не найдена")
if deal.group:
await DealGroupService(self.session).delete_group(deal.group.id)
await deal_group.DealGroupService(self.session).delete_group(deal.group.id)
else:
deal.is_deleted = True
await self.session.commit()
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 = await client_service.get_by_name(request.client_name)
@@ -108,28 +100,25 @@ class DealService(BaseService):
request.client_name,
ClientDetailsSchema()
)
shipping_warehouse_service = ShippingWarehouseService(self.session)
shipping_warehouse = await shipping_warehouse_service.get_by_name(name=request.shipping_warehouse)
if not 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(
name=request.name,
created_at=datetime.datetime.now(),
client_id=client.id,
current_status=DealStatus.CREATED,
current_status_id=request.status_id,
board_id=deal_status.board_id,
lexorank=rank,
shipping_warehouse_id=shipping_warehouse.id,
base_marketplace_key=request.base_marketplace.key
)
self.session.add(deal)
await self.session.flush()
await self.change_status(deal,
DealStatus.AWAITING_ACCEPTANCE,
user,
deadline=request.acceptance_date,
comment=request.comment)
# add category if specified
if request.category:
deal_category = DealPriceCategory(
@@ -141,12 +130,12 @@ class DealService(BaseService):
await self.session.commit()
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
deal = await self._get_deal_by_id(request.deal_id)
if not deal:
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()
return DealChangeStatusResponse(ok=True)
@@ -199,7 +188,7 @@ class DealService(BaseService):
Deal,
func.coalesce(price_subquery.c.total_price, 0),
func.row_number().over(
partition_by=Deal.current_status,
partition_by=Deal.current_status_id,
order_by=Deal.lexorank
).label('rank'),
func.coalesce(products_quantity_subquery.c.total_quantity, 0)
@@ -208,7 +197,9 @@ class DealService(BaseService):
selectinload(Deal.status_history),
joinedload(Deal.client),
joinedload(Deal.shipping_warehouse),
joinedload(Deal.bill_request)
joinedload(Deal.bill_request),
joinedload(Deal.status),
joinedload(Deal.board),
)
.outerjoin(
price_subquery, Deal.id == price_subquery.c.deal_id,
@@ -231,8 +222,6 @@ class DealService(BaseService):
summaries = []
for deal, total_price, rank, products_count in deals_query.all():
deal: Deal
last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at)
deadline = last_status.next_status_deadline
base_marketplace = None
if deal.base_marketplace:
base_marketplace = BaseMarketplaceSchema.model_validate(deal.base_marketplace)
@@ -242,9 +231,8 @@ class DealService(BaseService):
id=deal.id,
client_name=deal.client.name,
name=deal.name,
changed_at=last_status.changed_at,
deadline=deadline,
status=last_status.to_status,
status=deal.status,
board=deal.board,
total_price=total_price,
rank=rank,
base_marketplace=base_marketplace,
@@ -308,6 +296,9 @@ class DealService(BaseService):
deal = await self.session.scalar(
select(Deal)
.options(
joinedload(Deal.status),
joinedload(Deal.board)
.joinedload(Board.project),
joinedload(Deal.shipping_warehouse),
joinedload(Deal.client)
.joinedload(Client.details),
@@ -350,7 +341,7 @@ class DealService(BaseService):
raise HTTPException(status_code=404, detail="Сделка не найдена")
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:
deal: Deal = await self.session.scalar(
select(Deal)
@@ -369,6 +360,15 @@ class DealService(BaseService):
deal.delivery_date = request.data.delivery_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:
for deal in deal.group.deals:
deal.is_accounted = request.data.is_accounted
@@ -402,7 +402,7 @@ class DealService(BaseService):
stmt = (
select(Deal)
.where(
Deal.current_status == request.status,
Deal.current_status_id == request.status_id,
Deal.id != request.deal_id,
Deal.is_deleted == False,
Deal.is_completed == False
@@ -433,7 +433,7 @@ class DealService(BaseService):
else:
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,
comment=request.comment,
rank=str(new_rank))
@@ -1079,12 +1079,9 @@ class DealService(BaseService):
return DealCompleteResponse(ok=False, message="Сделка не найдена")
if deal.group:
deals = await DealGroupService(self.session).complete_group(deal.group.id)
for completed_deal in deals:
await self.change_status(completed_deal, DealStatus.COMPLETED, user)
await deal_group.DealGroupService(self.session).complete_group(deal.group.id)
else:
deal.is_completed = True
await self.change_status(deal, DealStatus.COMPLETED, user)
await self.session.commit()
return DealCompleteResponse(ok=True, message="Сделка успешно завершена")
@@ -1234,14 +1231,15 @@ class DealService(BaseService):
async def _create_deal_from_excel(
self,
client: Client,
deal_status: DealStatus,
breakdown: CityBreakdownFromExcelSchema,
user: User,
) -> Deal:
rank = await self._get_rank_for_deal(DealStatus.CREATED)
rank = await self._get_rank_for_deal(deal_status.id)
deal = Deal(
name=f"{client.name} - {breakdown.base_marketplace.key.upper()} - {breakdown.shipping_warehouse.name}",
created_at=datetime.datetime.now(),
current_status=DealStatus.CREATED,
current_status_id=deal_status.id,
lexorank=rank,
client_id=client.id,
base_marketplace_key=breakdown.base_marketplace.key,
@@ -1250,19 +1248,8 @@ class DealService(BaseService):
self.session.add(deal)
await self.session.flush()
await self.change_status(deal, DealStatus.AWAITING_ACCEPTANCE, user)
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(
self,
shipping_warehouse: OptionalShippingWarehouseSchema,
@@ -1288,8 +1275,12 @@ class DealService(BaseService):
if not client:
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] = {}
group = await self._create_group()
group = await deal_group.DealGroupService(self.session).create_group_model()
for product_data in request.products:
for breakdown in product_data.cities_breakdown:
@@ -1298,7 +1289,7 @@ class DealService(BaseService):
key = f"{breakdown.shipping_warehouse.id} - {breakdown.base_marketplace.key}"
deal = deals_dict.get(key)
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
insert_stmt = insert(deal_relations).values({
@@ -1317,121 +1308,6 @@ class DealService(BaseService):
await self.session.commit()
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]:
if not deal.group:
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 models import DealService as DealServiceModel
from models.deal_group import DealGroup
from schemas.deal import *
from models import DealService as DealServiceModel, User, Deal, DealProduct, Product, GroupBillRequest
from models.deal_group import DealGroup, deal_relations
from schemas.group import *
from services.base import BaseService
from services.deal import DealService
class DealGroupService(BaseService):
@@ -25,6 +27,63 @@ class DealGroupService(BaseService):
)
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]:
deals = await self.get_deals_by_group_id(group_id)
for deal in deals:
@@ -36,3 +95,74 @@ class DealGroupService(BaseService):
for deal in deals:
deal.is_deleted = True
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 models import DealService, Deal, DealStatusHistory, DealProductService, DealProduct, Service, Client, \
ShippingWarehouse, BaseMarketplace, User
ShippingWarehouse, BaseMarketplace, User, Project, Board
from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataRequest, ProfitChartDataItem, \
GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters
from services.base import BaseService
@@ -24,7 +24,7 @@ class ProfitStatisticsService(BaseService):
'day',
Deal.created_at,
).label('date'),
Deal.current_status,
Deal.current_status_id,
)
.subquery()
)
@@ -51,7 +51,7 @@ class ProfitStatisticsService(BaseService):
'day',
last_statuses.c.changed_at,
).label('date'),
Deal.current_status,
Deal.current_status_id,
)
.join(last_statuses, last_statuses.c.deal_id == Deal.id)
.subquery()
@@ -97,8 +97,7 @@ class ProfitStatisticsService(BaseService):
return data
@staticmethod
def _get_stmt_deal_services(sub_filtered_status_history: Subquery):
def _get_stmt_deal_services(self, sub_filtered_status_history: Subquery):
return (
select(
Deal.id.label("deal_id"),
@@ -112,10 +111,24 @@ class ProfitStatisticsService(BaseService):
.join(DealService, Deal.id == DealService.deal_id)
.join(Service, DealService.service_id == Service.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")
)
@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
def _apply_filters(request: CommonProfitFilters, stmt_deal_services, stmt_deal_product_services):
if request.client_id != -1:
@@ -127,9 +140,21 @@ class ProfitStatisticsService(BaseService):
stmt_deal_product_services = stmt_deal_product_services.where(
Deal.base_marketplace_key == request.base_marketplace_key)
if request.deal_status_id != -1:
stmt_deal_services = stmt_deal_services.where(Deal.current_status == request.deal_status_id)
stmt_deal_product_services = stmt_deal_product_services.where(Deal.current_status == request.deal_status_id)
if request.project_id != -1:
stmt_deal_services = ProfitStatisticsService._board_ids_for_project(request.project_id, stmt_deal_services)
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:
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
@staticmethod
def _get_stmt_product_services():
def _get_stmt_product_services(self):
return (
select(
Deal.id.label("deal_id"),
@@ -154,7 +178,13 @@ class ProfitStatisticsService(BaseService):
)
)
.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)
)
@@ -226,16 +256,47 @@ class ProfitStatisticsService(BaseService):
)
@staticmethod
def _join_and_group_by_statuses(stmt):
def _join_and_group_by_projects(stmt):
return (
select(
Deal.current_status.label("grouped_value"),
Project.id,
Project.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)
.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
@@ -320,6 +381,7 @@ class ProfitStatisticsService(BaseService):
async def _get_data_grouped_by_date(self, request: CommonProfitFilters, is_chart: bool = True):
self.date_from, self.date_to = request.date_range
self.is_completed_only = request.is_completed_only
self.filters = request
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)
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:
date_from, date_to = request.date_range
@@ -449,6 +523,10 @@ class ProfitStatisticsService(BaseService):
return await self._get_table_grouped_by_dates(request)
case ProfitTableGroupBy.BY_CLIENTS:
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:
return await self._get_table_grouped_by_statuses(request)
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="Статус успешно удален")