feat: tags for expenses, filters by tags in statistics
This commit is contained in:
		@@ -1,7 +1,7 @@
 | 
				
			|||||||
from datetime import datetime, date
 | 
					from datetime import datetime, date
 | 
				
			||||||
from typing import TYPE_CHECKING
 | 
					from typing import TYPE_CHECKING
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from sqlalchemy import ForeignKey
 | 
					from sqlalchemy import ForeignKey, Table, Column
 | 
				
			||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
 | 
					from sqlalchemy.orm import Mapped, mapped_column, relationship
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from models import BaseModel
 | 
					from models import BaseModel
 | 
				
			||||||
@@ -10,6 +10,14 @@ if TYPE_CHECKING:
 | 
				
			|||||||
    from models import User
 | 
					    from models import User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					expenses_expense_tags = Table(
 | 
				
			||||||
 | 
					    'expenses_expense_tags',
 | 
				
			||||||
 | 
					    BaseModel.metadata,
 | 
				
			||||||
 | 
					    Column('expense_id', ForeignKey('expenses.id', ondelete='CASCADE'), primary_key=True),
 | 
				
			||||||
 | 
					    Column('expense_tag_id', ForeignKey('expense_tags.id'), primary_key=True),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Expense(BaseModel):
 | 
					class Expense(BaseModel):
 | 
				
			||||||
    __tablename__ = 'expenses'
 | 
					    __tablename__ = 'expenses'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,3 +30,22 @@ class Expense(BaseModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    created_by_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
 | 
					    created_by_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
 | 
				
			||||||
    created_by_user: Mapped["User"] = relationship(foreign_keys=[created_by_user_id])
 | 
					    created_by_user: Mapped["User"] = relationship(foreign_keys=[created_by_user_id])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tags: Mapped[list["ExpenseTag"]] = relationship(
 | 
				
			||||||
 | 
					        secondary=expenses_expense_tags,
 | 
				
			||||||
 | 
					        lazy='selectin',
 | 
				
			||||||
 | 
					        back_populates='expenses',
 | 
				
			||||||
 | 
					        cascade='all, delete',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ExpenseTag(BaseModel):
 | 
				
			||||||
 | 
					    __tablename__ = 'expense_tags'
 | 
				
			||||||
 | 
					    id: Mapped[int] = mapped_column(primary_key=True)
 | 
				
			||||||
 | 
					    name: Mapped[str] = mapped_column(unique=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expenses: Mapped[list["Expense"]] = relationship(
 | 
				
			||||||
 | 
					        secondary=expenses_expense_tags,
 | 
				
			||||||
 | 
					        lazy='selectin',
 | 
				
			||||||
 | 
					        back_populates='tags',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
from fastapi import APIRouter, Depends
 | 
					from fastapi import APIRouter, Depends
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from backend.dependecies import SessionDependency, CurrentUserDependency, PaginationDependency
 | 
					from backend.dependecies import SessionDependency, CurrentUserDependency, PaginationDependency
 | 
				
			||||||
from schemas.expense import GetAllExpensesResponse, UpdateExpenseResponse, UpdateExpenseRequest, DeleteExpenseResponse
 | 
					from schemas.expense import *
 | 
				
			||||||
from services.auth import authorized_user
 | 
					from services.auth import authorized_user
 | 
				
			||||||
from services.expenses import ExpensesService
 | 
					from services.expenses import ExpensesService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,6 +12,8 @@ expense_router = APIRouter(
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# region Expenses
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@expense_router.get(
 | 
					@expense_router.get(
 | 
				
			||||||
    '/get-all',
 | 
					    '/get-all',
 | 
				
			||||||
    operation_id='get_all_expenses',
 | 
					    operation_id='get_all_expenses',
 | 
				
			||||||
@@ -47,3 +49,55 @@ async def delete_expense(
 | 
				
			|||||||
        expense_id: int,
 | 
					        expense_id: int,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    return await ExpensesService(session).delete_expense(expense_id)
 | 
					    return await ExpensesService(session).delete_expense(expense_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# region Expense tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@expense_router.get(
 | 
				
			||||||
 | 
					    '/get-all-tags',
 | 
				
			||||||
 | 
					    operation_id='get_all_expense_tags',
 | 
				
			||||||
 | 
					    response_model=GetAllExpenseTagsResponse,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def get_all(
 | 
				
			||||||
 | 
					        session: SessionDependency,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    return await ExpensesService(session).get_all_tags()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@expense_router.post(
 | 
				
			||||||
 | 
					    '/create-tag',
 | 
				
			||||||
 | 
					    operation_id='create_expense_tag',
 | 
				
			||||||
 | 
					    response_model=UpdateExpenseTagResponse,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def update_expense(
 | 
				
			||||||
 | 
					        session: SessionDependency,
 | 
				
			||||||
 | 
					        request: CreateExpenseTagRequest,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    return await ExpensesService(session).create_tag(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@expense_router.post(
 | 
				
			||||||
 | 
					    '/update-tag',
 | 
				
			||||||
 | 
					    operation_id='update_expense_tag',
 | 
				
			||||||
 | 
					    response_model=UpdateExpenseTagResponse,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def update_expense(
 | 
				
			||||||
 | 
					        session: SessionDependency,
 | 
				
			||||||
 | 
					        request: UpdateExpenseTagRequest,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    return await ExpensesService(session).update_tag(request)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@expense_router.delete(
 | 
				
			||||||
 | 
					    '/delete-tag/{tag_id}',
 | 
				
			||||||
 | 
					    operation_id='delete_expense_tag',
 | 
				
			||||||
 | 
					    response_model=DeleteExpenseTagResponse,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					async def update_expense(
 | 
				
			||||||
 | 
					        session: SessionDependency,
 | 
				
			||||||
 | 
					        tag_id: int,
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    return await ExpensesService(session).delete_tag(tag_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# endregion
 | 
				
			||||||
@@ -7,6 +7,14 @@ from schemas.user import UserSchema
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# region Entities
 | 
					# region Entities
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class BaseExpenseTagSchema(BaseSchema):
 | 
				
			||||||
 | 
					    name: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ExpenseTagSchema(BaseExpenseTagSchema):
 | 
				
			||||||
 | 
					    id: int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ExpenseSchemaBase(BaseSchema):
 | 
					class ExpenseSchemaBase(BaseSchema):
 | 
				
			||||||
    id: int
 | 
					    id: int
 | 
				
			||||||
    name: str
 | 
					    name: str
 | 
				
			||||||
@@ -14,6 +22,7 @@ class ExpenseSchemaBase(BaseSchema):
 | 
				
			|||||||
    amount: float
 | 
					    amount: float
 | 
				
			||||||
    created_by_user: UserSchema
 | 
					    created_by_user: UserSchema
 | 
				
			||||||
    spent_date: datetime.date
 | 
					    spent_date: datetime.date
 | 
				
			||||||
 | 
					    tags: list[ExpenseTagSchema]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UpdateExpenseSchema(BaseSchema):
 | 
					class UpdateExpenseSchema(BaseSchema):
 | 
				
			||||||
@@ -22,6 +31,7 @@ class UpdateExpenseSchema(BaseSchema):
 | 
				
			|||||||
    comment: Optional[str] = ""
 | 
					    comment: Optional[str] = ""
 | 
				
			||||||
    amount: float
 | 
					    amount: float
 | 
				
			||||||
    spent_date: datetime.date
 | 
					    spent_date: datetime.date
 | 
				
			||||||
 | 
					    tags: list[str] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# endregion
 | 
					# endregion
 | 
				
			||||||
@@ -33,6 +43,14 @@ class UpdateExpenseRequest(BaseSchema):
 | 
				
			|||||||
    expense: UpdateExpenseSchema
 | 
					    expense: UpdateExpenseSchema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateExpenseTagRequest(BaseSchema):
 | 
				
			||||||
 | 
					    tag: BaseExpenseTagSchema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdateExpenseTagRequest(BaseSchema):
 | 
				
			||||||
 | 
					    tag: ExpenseTagSchema
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# endregion
 | 
					# endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# region Responses
 | 
					# region Responses
 | 
				
			||||||
@@ -49,4 +67,20 @@ class UpdateExpenseResponse(OkMessageSchema):
 | 
				
			|||||||
class DeleteExpenseResponse(OkMessageSchema):
 | 
					class DeleteExpenseResponse(OkMessageSchema):
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class GetAllExpenseTagsResponse(BaseSchema):
 | 
				
			||||||
 | 
					    tags: list[ExpenseTagSchema]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateExpenseTagResponse(OkMessageSchema):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UpdateExpenseTagResponse(OkMessageSchema):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DeleteExpenseTagResponse(OkMessageSchema):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# endregion
 | 
					# endregion
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
from optparse import Option
 | 
					 | 
				
			||||||
from typing import List, Tuple, Optional
 | 
					from typing import List, Tuple, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from enums.profit_table_group_by import ProfitTableGroupBy
 | 
					from enums.profit_table_group_by import ProfitTableGroupBy
 | 
				
			||||||
@@ -32,6 +31,7 @@ class CommonProfitFilters(BaseSchema):
 | 
				
			|||||||
    base_marketplace_key: str
 | 
					    base_marketplace_key: str
 | 
				
			||||||
    deal_status_id: int
 | 
					    deal_status_id: int
 | 
				
			||||||
    manager_id: int
 | 
					    manager_id: int
 | 
				
			||||||
 | 
					    tag_id: int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GetProfitChartDataRequest(CommonProfitFilters):
 | 
					class GetProfitChartDataRequest(CommonProfitFilters):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,14 @@
 | 
				
			|||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
from typing import Optional
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import math
 | 
					import math
 | 
				
			||||||
from fastapi import HTTPException
 | 
					from fastapi import HTTPException
 | 
				
			||||||
from sqlalchemy import delete, select, func
 | 
					 | 
				
			||||||
from fastapi import status
 | 
					from fastapi import status
 | 
				
			||||||
 | 
					from sqlalchemy import delete, select, func, update, insert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from models import User
 | 
					from models import User
 | 
				
			||||||
from models.expense import Expense
 | 
					from models.expense import Expense, ExpenseTag
 | 
				
			||||||
from schemas.base import PaginationSchema, PaginationInfoSchema
 | 
					from schemas.base import PaginationSchema
 | 
				
			||||||
from schemas.expense import UpdateExpenseResponse, UpdateExpenseRequest, DeleteExpenseResponse, GetAllExpensesResponse
 | 
					from schemas.expense import *
 | 
				
			||||||
from services.base import BaseService
 | 
					from services.base import BaseService
 | 
				
			||||||
from utils.dependecies import is_valid_pagination
 | 
					from utils.dependecies import is_valid_pagination
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -47,23 +46,37 @@ class ExpensesService(BaseService):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        return response
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def get_by_id(self, expense_id) -> Optional[Expense]:
 | 
					    async def get_by_id(self, expense_id: int) -> Optional[Expense]:
 | 
				
			||||||
        expense = await self.session.get(Expense, expense_id)
 | 
					        expense = await self.session.get(Expense, expense_id)
 | 
				
			||||||
        return expense
 | 
					        return expense
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def add_tags(self, expense: Expense, tag_names: list[str]):
 | 
				
			||||||
 | 
					        tags = []
 | 
				
			||||||
 | 
					        for tag_name in tag_names:
 | 
				
			||||||
 | 
					            existing_tag = await self.get_tag_by_name(tag_name)
 | 
				
			||||||
 | 
					            if existing_tag:
 | 
				
			||||||
 | 
					                tags.append(existing_tag)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                tag = ExpenseTag(name=tag_name)
 | 
				
			||||||
 | 
					                self.session.add(tag)
 | 
				
			||||||
 | 
					                tags.append(tag)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expense.tags = tags
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def update_expense(self, user: User, request: UpdateExpenseRequest) -> UpdateExpenseResponse:
 | 
					    async def update_expense(self, user: User, request: UpdateExpenseRequest) -> UpdateExpenseResponse:
 | 
				
			||||||
        expense = await self.get_by_id(request.expense.id)
 | 
					        expense = await self.get_by_id(request.expense.id or -1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not expense:
 | 
					        if not expense:
 | 
				
			||||||
            expense = Expense(
 | 
					            new_expense = Expense(
 | 
				
			||||||
                created_at=datetime.now(),
 | 
					                created_at=datetime.datetime.now(),
 | 
				
			||||||
                name=request.expense.name,
 | 
					                name=request.expense.name,
 | 
				
			||||||
                comment=request.expense.comment,
 | 
					                comment=request.expense.comment,
 | 
				
			||||||
                amount=request.expense.amount,
 | 
					                amount=request.expense.amount,
 | 
				
			||||||
                spent_date=request.expense.spent_date,
 | 
					                spent_date=request.expense.spent_date,
 | 
				
			||||||
                created_by_user_id=user.id,
 | 
					                created_by_user_id=user.id,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            self.session.add(expense)
 | 
					            self.session.add(new_expense)
 | 
				
			||||||
 | 
					            await self.add_tags(new_expense, request.expense.tags)
 | 
				
			||||||
            await self.session.commit()
 | 
					            await self.session.commit()
 | 
				
			||||||
            return UpdateExpenseResponse(ok=True, message='Запись о расходах успешно создана')
 | 
					            return UpdateExpenseResponse(ok=True, message='Запись о расходах успешно создана')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -72,6 +85,7 @@ class ExpensesService(BaseService):
 | 
				
			|||||||
        expense.comment = request.expense.comment
 | 
					        expense.comment = request.expense.comment
 | 
				
			||||||
        expense.spent_date = request.expense.spent_date
 | 
					        expense.spent_date = request.expense.spent_date
 | 
				
			||||||
        self.session.add(expense)
 | 
					        self.session.add(expense)
 | 
				
			||||||
 | 
					        await self.add_tags(expense, request.expense.tags)
 | 
				
			||||||
        await self.session.commit()
 | 
					        await self.session.commit()
 | 
				
			||||||
        return UpdateExpenseResponse(ok=True, message='Запись о расходах успешно изменена')
 | 
					        return UpdateExpenseResponse(ok=True, message='Запись о расходах успешно изменена')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -83,3 +97,71 @@ class ExpensesService(BaseService):
 | 
				
			|||||||
        await self.session.execute(stmt)
 | 
					        await self.session.execute(stmt)
 | 
				
			||||||
        await self.session.commit()
 | 
					        await self.session.commit()
 | 
				
			||||||
        return DeleteExpenseResponse(ok=True, message='Запись о расходах успешно удалена')
 | 
					        return DeleteExpenseResponse(ok=True, message='Запись о расходах успешно удалена')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def get_all_tags(self) -> GetAllExpenseTagsResponse:
 | 
				
			||||||
 | 
					        stmt = (
 | 
				
			||||||
 | 
					            select(ExpenseTag)
 | 
				
			||||||
 | 
					            .order_by(ExpenseTag.id)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        tags = await self.session.execute(stmt)
 | 
				
			||||||
 | 
					        return GetAllExpenseTagsResponse(tags=tags.scalars().all())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def get_tag_by_id(self, expense_tag_id: int) -> Optional[ExpenseTag]:
 | 
				
			||||||
 | 
					        return await self.session.get(ExpenseTag, expense_tag_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def get_tag_by_name(self, expense_tag_name: str) -> Optional[ExpenseTag]:
 | 
				
			||||||
 | 
					        stmt = (
 | 
				
			||||||
 | 
					            select(ExpenseTag)
 | 
				
			||||||
 | 
					            .where(ExpenseTag.name == expense_tag_name)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        tag = await self.session.scalar(stmt)
 | 
				
			||||||
 | 
					        return tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def create_tag(self, request: CreateExpenseTagRequest) -> CreateExpenseTagResponse:
 | 
				
			||||||
 | 
					        tag = await self.get_tag_by_name(request.tag.name)
 | 
				
			||||||
 | 
					        if tag:
 | 
				
			||||||
 | 
					            return UpdateExpenseResponse(ok=False, message='Ошибка. Такой тег уже есть.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tag_dict = request.tag.model_dump()
 | 
				
			||||||
 | 
					        stmt = (
 | 
				
			||||||
 | 
					            insert(ExpenseTag)
 | 
				
			||||||
 | 
					            .values(**tag_dict)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        await self.session.execute(stmt)
 | 
				
			||||||
 | 
					        await self.session.commit()
 | 
				
			||||||
 | 
					        return UpdateExpenseResponse(ok=True, message='Тег успешно создан.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def update_tag(self, request: UpdateExpenseTagRequest) -> UpdateExpenseTagResponse:
 | 
				
			||||||
 | 
					        tag = await self.get_tag_by_name(request.tag.name)
 | 
				
			||||||
 | 
					        if tag:
 | 
				
			||||||
 | 
					            return UpdateExpenseTagResponse(ok=False, message='Ошибка. Тег с таким названием уже есть.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tag = await self.get_tag_by_id(request.tag.id)
 | 
				
			||||||
 | 
					        if not tag:
 | 
				
			||||||
 | 
					            return UpdateExpenseTagResponse(ok=False, message='Ошибка. Тег не найден.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tag_dict = request.tag.model_dump()
 | 
				
			||||||
 | 
					        del tag_dict['id']
 | 
				
			||||||
 | 
					        stmt = (
 | 
				
			||||||
 | 
					            update(ExpenseTag)
 | 
				
			||||||
 | 
					            .where(ExpenseTag.id == request.tag.id)
 | 
				
			||||||
 | 
					            .values(**tag_dict)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        await self.session.execute(stmt)
 | 
				
			||||||
 | 
					        await self.session.commit()
 | 
				
			||||||
 | 
					        return UpdateExpenseResponse(ok=True, message='Тег успешно изменен.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def delete_tag(self, tag_id: int) -> DeleteExpenseTagResponse:
 | 
				
			||||||
 | 
					        tag = await self.get_tag_by_id(tag_id)
 | 
				
			||||||
 | 
					        if not tag:
 | 
				
			||||||
 | 
					            return DeleteExpenseTagResponse(ok=False, message='Ошибка. Тег не найден.')
 | 
				
			||||||
 | 
					        if len(tag.expenses) > 0:
 | 
				
			||||||
 | 
					            return DeleteExpenseTagResponse(ok=False, message='Ошибка. Тег прикреплен к записи о расходах.')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stmt = (
 | 
				
			||||||
 | 
					            delete(ExpenseTag)
 | 
				
			||||||
 | 
					            .where(ExpenseTag.id == tag_id)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        await self.session.execute(stmt)
 | 
				
			||||||
 | 
					        await self.session.commit()
 | 
				
			||||||
 | 
					        return DeleteExpenseTagResponse(ok=True, message='Тег удален')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,10 @@
 | 
				
			|||||||
from datetime import date
 | 
					from datetime import date
 | 
				
			||||||
from sqlalchemy import select, func, Subquery, cast
 | 
					
 | 
				
			||||||
 | 
					from sqlalchemy import select, func, Subquery, cast, CTE
 | 
				
			||||||
from sqlalchemy.dialects.postgresql import TIMESTAMP
 | 
					from sqlalchemy.dialects.postgresql import TIMESTAMP
 | 
				
			||||||
from models import PaymentRecord, Expense
 | 
					
 | 
				
			||||||
 | 
					from models import PaymentRecord, Expense, expenses_expense_tags
 | 
				
			||||||
 | 
					from schemas.statistics import CommonProfitFilters
 | 
				
			||||||
from services.base import BaseService
 | 
					from services.base import BaseService
 | 
				
			||||||
from services.statistics.common import generate_date_range
 | 
					from services.statistics.common import generate_date_range
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,19 +13,9 @@ class ExpensesStatisticsService(BaseService):
 | 
				
			|||||||
    date_from: date
 | 
					    date_from: date
 | 
				
			||||||
    date_to: date
 | 
					    date_to: date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _get_expenses_sub(self, model, date_column, amount_column) -> Subquery:
 | 
					    @staticmethod
 | 
				
			||||||
        all_dates = generate_date_range(self.date_from, self.date_to, ["expenses"])
 | 
					    def _fill_date_gaps(expenses: Subquery, all_dates: CTE) -> Subquery:
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
        expenses = (
 | 
					 | 
				
			||||||
            select(
 | 
					 | 
				
			||||||
                func.sum(getattr(model, amount_column)).label("expenses"),
 | 
					 | 
				
			||||||
                cast(getattr(model, date_column), TIMESTAMP(timezone=False)).label("date"),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .group_by("date")
 | 
					 | 
				
			||||||
            .subquery()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        expenses_with_gaps_filled = (
 | 
					 | 
				
			||||||
            select(
 | 
					            select(
 | 
				
			||||||
                all_dates.c.date,
 | 
					                all_dates.c.date,
 | 
				
			||||||
                (all_dates.c.expenses + func.coalesce(expenses.c.expenses, 0)).label("expenses"),
 | 
					                (all_dates.c.expenses + func.coalesce(expenses.c.expenses, 0)).label("expenses"),
 | 
				
			||||||
@@ -31,9 +24,53 @@ class ExpensesStatisticsService(BaseService):
 | 
				
			|||||||
            .order_by(all_dates.c.date)
 | 
					            .order_by(all_dates.c.date)
 | 
				
			||||||
            .subquery()
 | 
					            .subquery()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return expenses_with_gaps_filled
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _apply_expenses(self, deals_by_dates: Subquery, expenses_subquery: Subquery):
 | 
					    def _get_payment_records_sub(self) -> Subquery:
 | 
				
			||||||
 | 
					        all_dates = generate_date_range(self.date_from, self.date_to, ["expenses"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expenses = (
 | 
				
			||||||
 | 
					            select(
 | 
				
			||||||
 | 
					                func.sum(PaymentRecord.amount).label("expenses"),
 | 
				
			||||||
 | 
					                cast(PaymentRecord.start_date, TIMESTAMP(timezone=False)).label("date"),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .group_by("date")
 | 
				
			||||||
 | 
					            .subquery()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expenses_with_filled_gaps = self._fill_date_gaps(expenses, all_dates)
 | 
				
			||||||
 | 
					        return expenses_with_filled_gaps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_additional_expenses_sub(self, tag_id: int) -> Subquery:
 | 
				
			||||||
 | 
					        all_dates = generate_date_range(self.date_from, self.date_to, ["expenses"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expenses = (
 | 
				
			||||||
 | 
					            select(Expense)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if tag_id != -1:
 | 
				
			||||||
 | 
					            expenses = (
 | 
				
			||||||
 | 
					                expenses
 | 
				
			||||||
 | 
					                .join(expenses_expense_tags)
 | 
				
			||||||
 | 
					                .where(expenses_expense_tags.c.expense_tag_id == tag_id)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expenses = expenses.subquery()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expenses = (
 | 
				
			||||||
 | 
					            select(
 | 
				
			||||||
 | 
					                func.sum(expenses.c.amount).label("expenses"),
 | 
				
			||||||
 | 
					                cast(expenses.c.spent_date, TIMESTAMP(timezone=False)).label("date"),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .where(expenses.c.spent_date.between(self.date_from, self.date_to))
 | 
				
			||||||
 | 
					            .group_by("date")
 | 
				
			||||||
 | 
					            .subquery()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expenses_with_filled_gaps = self._fill_date_gaps(expenses, all_dates)
 | 
				
			||||||
 | 
					        return expenses_with_filled_gaps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def _apply_expenses(deals_by_dates: Subquery, expenses_subquery: Subquery):
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            select(
 | 
					            select(
 | 
				
			||||||
                deals_by_dates.c.date,
 | 
					                deals_by_dates.c.date,
 | 
				
			||||||
@@ -46,15 +83,15 @@ class ExpensesStatisticsService(BaseService):
 | 
				
			|||||||
            .join(expenses_subquery, expenses_subquery.c.date == deals_by_dates.c.date)
 | 
					            .join(expenses_subquery, expenses_subquery.c.date == deals_by_dates.c.date)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def apply_expenses(self, date_from: date, date_to: date, deals_by_dates: Subquery):
 | 
					    def apply_expenses(self, filters: CommonProfitFilters, deals_by_dates: Subquery):
 | 
				
			||||||
        self.date_from, self.date_to = date_from, date_to
 | 
					        self.date_from, self.date_to = filters.date_range
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Apply salary expenses
 | 
					        # Apply salary expenses
 | 
				
			||||||
        salary_expenses = self._get_expenses_sub(PaymentRecord, "start_date", "amount")
 | 
					        salary_expenses = self._get_payment_records_sub()
 | 
				
			||||||
        deals_by_dates = self._apply_expenses(deals_by_dates, salary_expenses)
 | 
					        deals_by_dates = self._apply_expenses(deals_by_dates, salary_expenses)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Apply additional expenses
 | 
					        # Apply additional expenses
 | 
				
			||||||
        additional_expenses = self._get_expenses_sub(Expense, "spent_date", "amount")
 | 
					        additional_expenses = self._get_additional_expenses_sub(filters.tag_id)
 | 
				
			||||||
        deals_by_dates = self._apply_expenses(deals_by_dates, additional_expenses)
 | 
					        deals_by_dates = self._apply_expenses(deals_by_dates, additional_expenses)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return deals_by_dates
 | 
					        return deals_by_dates
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -294,8 +294,7 @@ class ProfitStatisticsService(BaseService):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        expenses_statistics_service = ExpensesStatisticsService(self.session)
 | 
					        expenses_statistics_service = ExpensesStatisticsService(self.session)
 | 
				
			||||||
        stmt_deals_applied_expenses = expenses_statistics_service.apply_expenses(
 | 
					        stmt_deals_applied_expenses = expenses_statistics_service.apply_expenses(
 | 
				
			||||||
            self.date_from,
 | 
					            self.filters,
 | 
				
			||||||
            self.date_to,
 | 
					 | 
				
			||||||
            sub_deals_grouped_by_date
 | 
					            sub_deals_grouped_by_date
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -305,6 +304,7 @@ class ProfitStatisticsService(BaseService):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    async def _get_data_grouped_by_date(self, request: CommonProfitFilters, is_chart: bool = True):
 | 
					    async def _get_data_grouped_by_date(self, request: CommonProfitFilters, is_chart: bool = True):
 | 
				
			||||||
        self.date_from, self.date_to = request.date_range
 | 
					        self.date_from, self.date_to = request.date_range
 | 
				
			||||||
 | 
					        self.filters = request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sub_deals_dates = self._get_deals_dates(request.deal_status_id)
 | 
					        sub_deals_dates = self._get_deals_dates(request.deal_status_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -345,6 +345,7 @@ class ProfitStatisticsService(BaseService):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def _get_common_table_grouped(self, request: GetProfitTableDataRequest):
 | 
					    def _get_common_table_grouped(self, request: GetProfitTableDataRequest):
 | 
				
			||||||
        self.date_from, self.date_to = request.date_range
 | 
					        self.date_from, self.date_to = request.date_range
 | 
				
			||||||
 | 
					        self.filters = request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sub_deals_dates = self._get_deals_dates(request.deal_status_id)
 | 
					        sub_deals_dates = self._get_deals_dates(request.deal_status_id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user