feat: added tags for cards, aligned status headers

This commit is contained in:
2025-03-09 19:30:52 +04:00
parent 487174c4ff
commit 6030591e3c
17 changed files with 375 additions and 23 deletions

View File

@@ -10,3 +10,4 @@ class ProfitTableGroupBy(IntEnum):
BY_WAREHOUSES = 5 BY_WAREHOUSES = 5
BY_MARKETPLACES = 6 BY_MARKETPLACES = 6
BY_MANAGERS = 7 BY_MANAGERS = 7
BY_TAGS = 8

View File

@@ -57,6 +57,7 @@ routers_list = [
routers.project_router, routers.project_router,
routers.board_router, routers.board_router,
routers.status_router, routers.status_router,
routers.card_tag_router,
] ]
for router in routers_list: for router in routers_list:
app.include_router(router) app.include_router(router)

View File

@@ -6,6 +6,7 @@ from .board import *
from .status import * from .status import *
from .attribute import * from .attribute import *
from .card import * from .card import *
from .card_tag import *
from .auth import * from .auth import *
from .card import * from .card import *
from .client import * from .client import *

View File

@@ -10,8 +10,10 @@ from .shipping import Pallet, Box
from .shipping_warehouse import ShippingWarehouse from .shipping_warehouse import ShippingWarehouse
if TYPE_CHECKING: if TYPE_CHECKING:
from . import CardBillRequest, User, BaseModel, Board, CardStatus, CardGroup, CardAttribute, CardService as CardServiceModel, \ from . import (
CardProduct, Client CardBillRequest, User, BaseModel, Board, CardStatus, CardGroup, CardAttribute, Client, CardTag,
CardService as CardServiceModel, CardProduct,
)
class Card(BaseModel): class Card(BaseModel):
@@ -56,6 +58,13 @@ class Card(BaseModel):
lazy='joined', lazy='joined',
back_populates='cards' back_populates='cards'
) )
tags: Mapped[list['CardTag']] = relationship(
'CardTag',
secondary='cards_card_tags',
back_populates='cards',
lazy='selectin',
)
# endregion # endregion
# region Attributes handled by modules # region Attributes handled by modules
@@ -107,6 +116,7 @@ class Card(BaseModel):
# endregion # endregion
class CardEmployees(BaseModel): class CardEmployees(BaseModel):
__tablename__ = 'card_employees' __tablename__ = 'card_employees'
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), primary_key=True)
@@ -116,4 +126,3 @@ class CardEmployees(BaseModel):
card: Mapped[Card] = relationship('Card', back_populates='employees', lazy='selectin') card: Mapped[Card] = relationship('Card', back_populates='employees', lazy='selectin')
created_at: Mapped[datetime] = mapped_column() created_at: Mapped[datetime] = mapped_column()

45
models/card_tag.py Normal file
View File

@@ -0,0 +1,45 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, Column, Table, UniqueConstraint, Index
from sqlalchemy.orm import mapped_column, Mapped, relationship
from models import BaseModel
if TYPE_CHECKING:
from models import Project, Card
cards_card_tags = Table(
'cards_card_tags',
BaseModel.metadata,
Column('card_id', ForeignKey('cards.id'), primary_key=True),
Column('card_tag_id', ForeignKey('card_tags.id'), primary_key=True),
)
class CardTag(BaseModel):
__tablename__ = 'card_tags'
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(nullable=False)
is_deleted: Mapped[bool] = mapped_column(default=False, server_default='0')
project_id: Mapped[int] = mapped_column(
ForeignKey('projects.id'),
nullable=False,
)
project: Mapped['Project'] = relationship(
'Project',
back_populates='tags',
lazy='noload',
)
cards: Mapped[list['Card']] = relationship(
secondary='cards_card_tags',
lazy='noload',
back_populates='tags',
)
__table_args__ = (
Index('idx_card_name_project_id', 'name', 'project_id', 'is_deleted'),
)

View File

@@ -9,6 +9,7 @@ if TYPE_CHECKING:
from board import Board from board import Board
from attribute import Attribute from attribute import Attribute
from module import Module from module import Module
from card_tag import CardTag
class Project(BaseModel): class Project(BaseModel):
@@ -27,7 +28,6 @@ class Project(BaseModel):
attributes: Mapped[list['Attribute']] = relationship( attributes: Mapped[list['Attribute']] = relationship(
'Attribute', 'Attribute',
uselist=True,
secondary='project_attribute', secondary='project_attribute',
back_populates='projects', back_populates='projects',
lazy='selectin', lazy='selectin',
@@ -35,10 +35,16 @@ class Project(BaseModel):
modules: Mapped[list['Module']] = relationship( modules: Mapped[list['Module']] = relationship(
'Module', 'Module',
uselist=True,
secondary='project_module', secondary='project_module',
back_populates='projects', back_populates='projects',
lazy='selectin', lazy='selectin',
order_by='asc(Module.id)', order_by='asc(Module.id)',
) )
tags: Mapped[list['CardTag']] = relationship(
'CardTag',
back_populates='project',
primaryjoin="and_(Project.id == CardTag.project_id, CardTag.is_deleted == False)",
order_by='asc(CardTag.id)',
lazy='selectin',
)

View File

@@ -25,3 +25,4 @@ from .residues import residues_router
from .project import project_router from .project import project_router
from .board import board_router from .board import board_router
from .status import status_router from .status import status_router
from .card_tag import card_tag_router

63
routers/card_tag.py Normal file
View File

@@ -0,0 +1,63 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session
from schemas.card_tag import *
from services.auth import authorized_user
from services.card_tag import CardTagService
card_tag_router = APIRouter(
prefix='/card-tag',
tags=['card-tag'],
dependencies=[Depends(authorized_user)]
)
@card_tag_router.post(
'/',
operation_id='create_tag',
response_model=CreateTagResponse
)
async def create_tag(
request: CreateTagRequest,
session: Annotated[AsyncSession, Depends(get_session)]
):
return await CardTagService(session).create(request)
@card_tag_router.patch(
'/',
operation_id='update_tag',
response_model=UpdateTagResponse
)
async def update_tag(
request: UpdateTagRequest,
session: Annotated[AsyncSession, Depends(get_session)]
):
return await CardTagService(session).update(request)
@card_tag_router.delete(
'/{card_tag_id}',
response_model=DeleteTagResponse,
operation_id="delete_tag",
)
async def delete_tag(
session: Annotated[AsyncSession, Depends(get_session)],
card_tag_id: int,
):
return await CardTagService(session).delete(card_tag_id)
@card_tag_router.post(
'/switch',
response_model=SwitchTagResponse,
operation_id='switch_tag',
)
async def switch_tag(
session: Annotated[AsyncSession, Depends(get_session)],
request: SwitchTagRequest,
):
return await CardTagService(session).switch_tag(request)

View File

@@ -7,6 +7,7 @@ from schemas.attribute import CardAttributeSchema
from schemas.base import BaseSchema, OkMessageSchema from schemas.base import BaseSchema, OkMessageSchema
from schemas.billing import CardBillRequestSchema from schemas.billing import CardBillRequestSchema
from schemas.board import BoardSchema from schemas.board import BoardSchema
from schemas.card_tag import CardTagSchema
from schemas.client import ClientSchema from schemas.client import ClientSchema
from schemas.group import CardGroupSchema from schemas.group import CardGroupSchema
from schemas.marketplace import BaseMarketplaceSchema from schemas.marketplace import BaseMarketplaceSchema
@@ -41,6 +42,7 @@ class CardSummary(BaseSchema):
rank: int rank: int
base_marketplace: Optional[BaseMarketplaceSchema] = None base_marketplace: Optional[BaseMarketplaceSchema] = None
total_products: int total_products: int
tags: list[CardTagSchema]
shipment_warehouse_id: Optional[int] shipment_warehouse_id: Optional[int]
shipment_warehouse_name: Optional[str] shipment_warehouse_name: Optional[str]
@@ -100,6 +102,7 @@ class BaseCardSchema(BaseSchema):
pallets: List[PalletSchema] = [] pallets: List[PalletSchema] = []
boxes: List[BoxSchema] = [] boxes: List[BoxSchema] = []
employees: List[CardEmployeesSchema] = [] employees: List[CardEmployeesSchema] = []
tags: List[CardTagSchema] = []
class CardSchema(BaseCardSchema): class CardSchema(BaseCardSchema):
@@ -115,6 +118,7 @@ class CardGeneralInfoSchema(BaseSchemaWithAttributes):
board_id: int board_id: int
status_id: int status_id: int
client_id: Optional[int] client_id: Optional[int]
tags: List[str]
class ProductsAndServicesGeneralInfoSchema(BaseSchema): class ProductsAndServicesGeneralInfoSchema(BaseSchema):

53
schemas/card_tag.py Normal file
View File

@@ -0,0 +1,53 @@
from typing import Optional
from schemas.base import BaseSchema, OkMessageSchema
# region Entities
class BaseCardTagSchema(BaseSchema):
name: str
project_id: int
class CardTagSchema(BaseCardTagSchema):
id: int
# endregion
# region Requests
class CreateTagRequest(BaseSchema):
tag: BaseCardTagSchema
class UpdateTagRequest(BaseSchema):
tag: CardTagSchema
class SwitchTagRequest(BaseSchema):
tag_id: int
card_id: Optional[int] = None
group_id: Optional[int] = None
# endregion
# region Responses
class CreateTagResponse(OkMessageSchema):
pass
class UpdateTagResponse(OkMessageSchema):
pass
class DeleteTagResponse(OkMessageSchema):
pass
class SwitchTagResponse(OkMessageSchema):
pass
# endregion

View File

@@ -2,6 +2,7 @@ from typing import Optional
from schemas.attribute import AttributeSchema from schemas.attribute import AttributeSchema
from schemas.base import BaseSchema, OkMessageSchema from schemas.base import BaseSchema, OkMessageSchema
from schemas.card_tag import CardTagSchema
# region Entities # region Entities
@@ -26,6 +27,7 @@ class ModuleSchema(BaseSchema):
class ProjectSchema(ProjectGeneralInfoSchema): class ProjectSchema(ProjectGeneralInfoSchema):
attributes: list[AttributeSchema] attributes: list[AttributeSchema]
modules: list[ModuleSchema] modules: list[ModuleSchema]
tags: list[CardTagSchema]
class FullProjectSchema(ProjectSchema): class FullProjectSchema(ProjectSchema):

View File

@@ -32,6 +32,7 @@ class CommonProfitFilters(BaseSchema):
project_id: int project_id: int
board_id: int board_id: int
card_status_id: int card_status_id: int
card_tag_id: int
manager_id: int manager_id: int
expense_tag_id: int expense_tag_id: int
income_tag_id: int income_tag_id: int

View File

@@ -148,8 +148,6 @@ class BarcodeService(BaseService):
"template": barcode_template, "template": barcode_template,
"num_duplicates": card_product.quantity "num_duplicates": card_product.quantity
}) })
else:
print("jaja")
default_generator = DefaultBarcodeGenerator() default_generator = DefaultBarcodeGenerator()
filename = f'{card.id}_deal_barcodes.pdf' filename = f'{card.id}_deal_barcodes.pdf'

View File

@@ -14,6 +14,7 @@ from schemas.client import ClientDetailsSchema
from services import card_group from services import card_group
from services.auth import AuthService from services.auth import AuthService
from services.base import BaseService from services.base import BaseService
from services.card_tag import CardTagService
from services.client import ClientService from services.client import ClientService
from services.service import ServiceService from services.service import ServiceService
from services.shipping_warehouse import ShippingWarehouseService from services.shipping_warehouse import ShippingWarehouseService
@@ -240,6 +241,7 @@ class CardsService(BaseService):
shipment_warehouse_name=shipment_warehouse_name, shipment_warehouse_name=shipment_warehouse_name,
total_products=products_count, total_products=products_count,
bill_request=card.bill_request, bill_request=card.bill_request,
tags=card.tags,
) )
) )
return CardSummaryResponse(summaries=summaries) return CardSummaryResponse(summaries=summaries)
@@ -347,7 +349,8 @@ class CardsService(BaseService):
select(Card) select(Card)
.options( .options(
selectinload(Card.group) selectinload(Card.group)
.selectinload(CardGroup.cards) .selectinload(CardGroup.cards),
joinedload(Card.board),
) )
.where(Card.id == request.card_id) .where(Card.id == request.card_id)
) )
@@ -374,6 +377,9 @@ class CardsService(BaseService):
else: else:
card.manager = None card.manager = None
tag_service = CardTagService(self.session)
await tag_service.apply_tags(card, request.data.tags)
card_attrs_handler = CardAttributesCommandHandler(self.session) card_attrs_handler = CardAttributesCommandHandler(self.session)
await card_attrs_handler.set_attributes(card, request.data.attributes) await card_attrs_handler.set_attributes(card, request.data.attributes)
@@ -388,7 +394,7 @@ class CardsService(BaseService):
): ):
card: Optional[Card] = await self.session.get(Card, request.card_id) card: Optional[Card] = await self.session.get(Card, request.card_id)
if not card: if not card:
raise HTTPException(status_code=404, detail='Карточка не найдена') return ProductsAndServicesGeneralInfoRequest(ok=False, message='Карточка не найдена')
# Updating shipping warehouse # Updating shipping warehouse
shipping_warehouse_service = ShippingWarehouseService(self.session) shipping_warehouse_service = ShippingWarehouseService(self.session)
@@ -409,7 +415,7 @@ class CardsService(BaseService):
async def update_card_manager(self, request: UpdateCardManagerRequest) -> UpdateCardManagerResponse: async def update_card_manager(self, request: UpdateCardManagerRequest) -> UpdateCardManagerResponse:
card: Optional[Card] = await self.session.get(Card, request.card_id) card: Optional[Card] = await self.session.get(Card, request.card_id)
if not card: if not card:
raise HTTPException(status_code=404, detail='Карточка не найдена') return UpdateCardManagerResponse(ok=False, message='Карточка не найдена')
card.manager_id = request.manager_id card.manager_id = request.manager_id
await self.session.commit() await self.session.commit()
@@ -418,7 +424,7 @@ class CardsService(BaseService):
async def update_card_client(self, request: UpdateCardClientRequest) -> UpdateCardClientResponse: async def update_card_client(self, request: UpdateCardClientRequest) -> UpdateCardClientResponse:
card: Optional[Card] = await self.session.get(Card, request.card_id) card: Optional[Card] = await self.session.get(Card, request.card_id)
if not card: if not card:
raise HTTPException(status_code=404, detail='Карточка не найдена') return UpdateCardClientResponse(ok=False, message='Карточка не найдена')
card.client_id = request.client_id card.client_id = request.client_id
await self.session.commit() await self.session.commit()
@@ -513,8 +519,6 @@ class CardsService(BaseService):
return CardAddKitResponse(ok=False, message=str(e)) return CardAddKitResponse(ok=False, message=str(e))
def create_guest_url(self, user: User, request: CardCreateGuestUrlRequest) -> CardCreateGuestUrlResponse: def create_guest_url(self, user: User, request: CardCreateGuestUrlRequest) -> CardCreateGuestUrlResponse:
# if not user.is_admin:
# return CardCreateGuestUrlResponse(ok=False, message='Создать ссылку может только администратор', url="")
access_token = AuthService(self.session).create_deal_guest_token(request.card_id) access_token = AuthService(self.session).create_deal_guest_token(request.card_id)
url = f"deals/{request.card_id}?accessToken={access_token}" url = f"deals/{request.card_id}?accessToken={access_token}"
return CardCreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url) return CardCreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url)

122
services/card_tag.py Normal file
View File

@@ -0,0 +1,122 @@
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from models import CardTag, Card, CardGroup
from schemas.card_tag import *
from services.base import BaseService
class CardTagService(BaseService):
async def _get_by_name_and_project_id(self, name: str, project_id: int) -> Optional[CardTag]:
stmt = (
select(CardTag)
.where(
CardTag.name == name,
CardTag.project_id == project_id,
CardTag.is_deleted == False,
)
)
card_tag = await self.session.scalars(stmt)
return card_tag.first()
async def create(self, request: CreateTagRequest) -> CreateTagResponse:
existing_tag = await self._get_by_name_and_project_id(request.tag.name, request.tag.project_id)
if existing_tag:
return UpdateTagResponse(ok=False, message='Тег с таким названием уже существует')
tag = CardTag(name=request.tag.name, project_id=request.tag.project_id)
self.session.add(tag)
await self.session.commit()
return CreateTagResponse(ok=True, message='Тег успешно создан')
async def update(self, request: UpdateTagRequest) -> UpdateTagResponse:
card_tag = await self.session.get(CardTag, request.tag.id)
if not card_tag:
return UpdateTagResponse(ok=False, message=f'Тег с ID {request.tag.id} не найден')
if card_tag.name != request.tag.name:
existing_tag = await self._get_by_name_and_project_id(request.tag.name, request.tag.project_id)
if existing_tag:
return UpdateTagResponse(ok=False, message='Тег с таким названием уже существует')
card_tag.name = request.tag.name
await self.session.commit()
return UpdateTagResponse(ok=True, message='Тег успешно обновлен')
async def delete(self, card_tag_id: int) -> DeleteTagResponse:
card_tag = await self.session.get(CardTag, card_tag_id)
if not card_tag:
return DeleteTagResponse(ok=False, message=f'Тег с ID {card_tag_id} не найден')
card_tag.is_deleted = True
await self.session.commit()
return DeleteTagResponse(ok=True, message='Тег успешно удален')
async def _switch_tag_in_card(self, card_tag: CardTag, card_id: int) -> tuple[bool, str]:
stmt = (
select(Card)
.options(selectinload(Card.tags))
.where(Card.id == card_id)
)
card: Optional[Card] = (await self.session.scalars(stmt)).first()
if not card:
return False, f'Карточка с ID {card_id} не найдена'
if card_tag in card.tags:
card.tags.remove(card_tag)
return True, 'Тег откреплен от карточки'
card.tags.append(card_tag)
return True, 'Тег прикреплен к карточке'
async def _get_cards_by_group_id(self, group_id) -> list[Card]:
group: CardGroup | None = await self.session.scalar(
select(CardGroup)
.where(CardGroup.id == group_id)
)
return group.cards if group else []
async def _switch_tag_in_group(self, card_tag: CardTag, group_id: int):
cards = await self._get_cards_by_group_id(group_id)
for card in cards:
if card_tag in card.tags:
card.tags.remove(card_tag)
else:
card.tags.append(card_tag)
return True, 'Теги группы изменены'
async def switch_tag(self, request: SwitchTagRequest) -> SwitchTagResponse:
card_tag: Optional[CardTag] = await self.session.get(CardTag, request.tag_id)
if not card_tag:
return SwitchTagResponse(ok=False, message=f'Тег с ID {request.tag_id} не найден')
if request.card_id:
ok, message = await self._switch_tag_in_card(card_tag, request.card_id)
else:
ok, message = await self._switch_tag_in_group(card_tag, request.group_id)
await self.session.commit()
return SwitchTagResponse(ok=True, message=message)
async def apply_tags(self, card: Card, tag_names: list[str]):
stmt = (
select(CardTag)
.where(
CardTag.name.in_(tag_names),
CardTag.is_deleted == False,
CardTag.project_id == card.board.project_id
)
)
tags = (await self.session.scalars(stmt)).all()
cards = [card]
if card.group:
cards = await self._get_cards_by_group_id(card.group.id)
for card in cards:
card.tags = tags
await self.session.flush()

View File

@@ -1,11 +1,10 @@
from datetime import datetime from datetime import datetime
from typing import Optional
from sqlalchemy import select, update, func, delete from sqlalchemy import select, update, func, delete
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from card_attributes import CardAttributesCommandHandler from card_attributes import CardAttributesCommandHandler
from models import Project, Board, Module, Attribute from models import Project, Board, Module
from schemas.project import * from schemas.project import *
from services.base import BaseService from services.base import BaseService
@@ -30,6 +29,7 @@ class ProjectService(BaseService):
selectinload(Project.attributes), selectinload(Project.attributes),
selectinload(Project.modules), selectinload(Project.modules),
) )
.where(Project.is_deleted == False)
.order_by(Project.id) .order_by(Project.id)
) )
project_data = (await self.session.execute(stmt)).all() project_data = (await self.session.execute(stmt)).all()
@@ -42,6 +42,7 @@ class ProjectService(BaseService):
boards_count=boards_count, boards_count=boards_count,
attributes=project.attributes, attributes=project.attributes,
modules=project.modules, modules=project.modules,
tags=project.tags,
) )
projects.append(project_schema) projects.append(project_schema)

View File

@@ -5,7 +5,7 @@ from sqlalchemy import select, and_, union_all, func, Subquery, literal
from enums.profit_table_group_by import ProfitTableGroupBy from enums.profit_table_group_by import ProfitTableGroupBy
from models import CardService, Card, CardStatusHistory, CardProductService, CardProduct, Service, Client, \ from models import CardService, Card, CardStatusHistory, CardProductService, CardProduct, Service, Client, \
ShippingWarehouse, BaseMarketplace, User, Project, Board ShippingWarehouse, BaseMarketplace, User, Project, Board, CardTag, cards_card_tags, user_position
from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataRequest, ProfitChartDataItem, \ from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataRequest, ProfitChartDataItem, \
GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters
from services.base import BaseService from services.base import BaseService
@@ -112,13 +112,11 @@ class ProfitStatisticsService(BaseService):
.join(Service, CardService.service_id == Service.id) .join(Service, CardService.service_id == Service.id)
.join(sub_filtered_status_history, Card.id == sub_filtered_status_history.c.card_id) .join(sub_filtered_status_history, Card.id == sub_filtered_status_history.c.card_id)
.where( .where(
and_( Card.is_deleted == False,
Card.is_deleted == False, Card.is_services_profit_accounted == True,
Card.is_services_profit_accounted == True, Card.is_completed == True if self.is_completed_only else True,
Card.is_completed == True if self.is_completed_only else True
)
) )
.group_by(Card.id, "date") .group_by(Card.id, sub_filtered_status_history.c.date)
) )
@staticmethod @staticmethod
@@ -129,6 +127,14 @@ class ProfitStatisticsService(BaseService):
) )
return stmt.where(Card.board_id.in_(board_ids_stmt)) return stmt.where(Card.board_id.in_(board_ids_stmt))
@staticmethod
def _apply_tag_filter(tag_id: int, stmt):
sub_card_ids = (
select(cards_card_tags.c.card_id)
.where(cards_card_tags.c.card_tag_id == tag_id)
)
return stmt.where(Card.id.in_(sub_card_ids))
@staticmethod @staticmethod
def _apply_filters(request: CommonProfitFilters, stmt_card_services, stmt_card_product_services): def _apply_filters(request: CommonProfitFilters, stmt_card_services, stmt_card_product_services):
if request.client_id != -1: if request.client_id != -1:
@@ -156,6 +162,13 @@ class ProfitStatisticsService(BaseService):
stmt_card_product_services = stmt_card_product_services.where( stmt_card_product_services = stmt_card_product_services.where(
Card.current_status_id == request.card_status_id) Card.current_status_id == request.card_status_id)
if request.card_tag_id != -1:
stmt_card_services = ProfitStatisticsService._apply_tag_filter(request.card_tag_id, stmt_card_services)
stmt_card_product_services = ProfitStatisticsService._apply_tag_filter(
request.card_tag_id,
stmt_card_product_services
)
if request.manager_id != -1: if request.manager_id != -1:
stmt_card_services = stmt_card_services.where(Card.manager_id == request.manager_id) stmt_card_services = stmt_card_services.where(Card.manager_id == request.manager_id)
stmt_card_product_services = stmt_card_product_services.where(Card.manager_id == request.manager_id) stmt_card_product_services = stmt_card_product_services.where(Card.manager_id == request.manager_id)
@@ -334,7 +347,8 @@ class ProfitStatisticsService(BaseService):
def _join_and_group_by_managers(stmt): def _join_and_group_by_managers(stmt):
managers = ( managers = (
select(User) select(User)
.where(User.role_key == "employee") .join(user_position)
.where(and_(User.is_deleted == False, user_position.c.position_key == "sales_manager"))
.subquery() .subquery()
) )
return ( return (
@@ -350,6 +364,23 @@ class ProfitStatisticsService(BaseService):
.group_by(managers.c.id, "grouped_value") .group_by(managers.c.id, "grouped_value")
) )
@staticmethod
def _join_and_group_by_tags(stmt):
return (
select(
CardTag.id,
CardTag.name.label("grouped_value"),
CardTag.is_deleted,
func.count(stmt.c.card_id).label("cards_count"),
func.sum(stmt.c.revenue).label("revenue"),
func.sum(stmt.c.profit).label("profit"),
)
.join(cards_card_tags, cards_card_tags.c.card_id == stmt.c.card_id)
.join(CardTag, cards_card_tags.c.card_tag_id == CardTag.id)
.where(CardTag.is_deleted == False)
.group_by(CardTag.is_deleted, CardTag.id, CardTag.name)
)
async def _get_data_rows_grouped_by_date( async def _get_data_rows_grouped_by_date(
self, self,
stmt_card_services, stmt_card_services,
@@ -476,6 +507,7 @@ class ProfitStatisticsService(BaseService):
async def _get_table_grouped_by_statuses(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: async def _get_table_grouped_by_statuses(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse:
date_from, date_to = request.date_range date_from, date_to = request.date_range
self.is_completed_only = request.is_completed_only
sub_cards_dates = self._get_filtered_sub_status_history(date_from, date_to) sub_cards_dates = self._get_filtered_sub_status_history(date_from, date_to)
@@ -518,6 +550,12 @@ class ProfitStatisticsService(BaseService):
return await self._table_data_from_stmt(stmt_grouped_by_managers) return await self._table_data_from_stmt(stmt_grouped_by_managers)
async def _get_table_grouped_by_tags(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse:
sub_grouped_by_cards = self._get_common_table_grouped(request)
stmt_grouped_by_tags = self._join_and_group_by_tags(sub_grouped_by_cards)
return await self._table_data_from_stmt(stmt_grouped_by_tags)
async def get_profit_table_data(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: async def get_profit_table_data(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse:
match request.group_table_by: match request.group_table_by:
case ProfitTableGroupBy.BY_DATES: case ProfitTableGroupBy.BY_DATES:
@@ -536,5 +574,7 @@ class ProfitStatisticsService(BaseService):
return await self._get_table_grouped_by_marketplace(request) return await self._get_table_grouped_by_marketplace(request)
case ProfitTableGroupBy.BY_MANAGERS: case ProfitTableGroupBy.BY_MANAGERS:
return await self._get_table_grouped_by_managers(request) return await self._get_table_grouped_by_managers(request)
case ProfitTableGroupBy.BY_TAGS:
return await self._get_table_grouped_by_tags(request)
raise HTTPException(status_code=400, detail='Указана некорректная группировка') raise HTTPException(status_code=400, detail='Указана некорректная группировка')