feat: added tags for cards, aligned status headers
This commit is contained in:
@@ -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
|
||||||
|
|||||||
1
main.py
1
main.py
@@ -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)
|
||||||
|
|||||||
@@ -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 *
|
||||||
|
|||||||
@@ -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
45
models/card_tag.py
Normal 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'),
|
||||||
|
)
|
||||||
@@ -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',
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
63
routers/card_tag.py
Normal 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)
|
||||||
@@ -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
53
schemas/card_tag.py
Normal 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
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
122
services/card_tag.py
Normal 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()
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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, sub_filtered_status_history.c.date)
|
||||||
.group_by(Card.id, "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='Указана некорректная группировка')
|
||||||
|
|||||||
Reference in New Issue
Block a user