feat: projects and boards
This commit is contained in:
@@ -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
|
||||
|
||||
4
main.py
4
main.py
@@ -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)
|
||||
|
||||
@@ -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
31
models/board.py
Normal 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")
|
||||
@@ -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
24
models/project.py
Normal 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
61
models/status.py
Normal 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='')
|
||||
@@ -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
79
routers/board.py
Normal 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)
|
||||
@@ -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
79
routers/group.py
Normal 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
65
routers/project.py
Normal 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
67
routers/status.py
Normal 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
61
schemas/board.py
Normal 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
|
||||
@@ -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
66
schemas/group.py
Normal 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
50
schemas/project.py
Normal 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
|
||||
@@ -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
65
schemas/status.py
Normal 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
119
services/board.py
Normal 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="Доска успешно удалена")
|
||||
244
services/deal.py
244
services/deal.py
@@ -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]
|
||||
|
||||
@@ -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
73
services/project.py
Normal 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="Проект успешно удален")
|
||||
@@ -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
128
services/status.py
Normal 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="Статус успешно удален")
|
||||
Reference in New Issue
Block a user