feat: income
This commit is contained in:
2
main.py
2
main.py
@@ -47,7 +47,7 @@ routers_list = [
|
||||
routers.task_router,
|
||||
routers.statistics_router,
|
||||
routers.work_shifts_router,
|
||||
routers.expense_router,
|
||||
routers.transaction_router,
|
||||
routers.shipping_router,
|
||||
]
|
||||
for router in routers_list:
|
||||
|
||||
@@ -13,6 +13,6 @@ from .payroll import *
|
||||
from .billing import *
|
||||
from .marketplace_products import *
|
||||
from .deal_group import *
|
||||
from .expense import *
|
||||
from .transaction import *
|
||||
|
||||
configure_mappers()
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
from datetime import datetime, date
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Table, Column
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from models import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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):
|
||||
__tablename__ = 'expenses'
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
created_at: Mapped[datetime] = mapped_column(nullable=False)
|
||||
spent_date: Mapped[date] = mapped_column(nullable=False)
|
||||
name: Mapped[str] = mapped_column()
|
||||
comment: Mapped[str] = mapped_column()
|
||||
amount: Mapped[float] = mapped_column()
|
||||
|
||||
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], lazy="selectin")
|
||||
|
||||
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',
|
||||
)
|
||||
54
models/transaction.py
Normal file
54
models/transaction.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from datetime import datetime, date
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Table, Column, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from models import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models import User
|
||||
|
||||
|
||||
transactions_transaction_tags = Table(
|
||||
'transactions_transaction_tags',
|
||||
BaseModel.metadata,
|
||||
Column('transaction_id', ForeignKey('transactions.id', ondelete='CASCADE'), primary_key=True),
|
||||
Column('transaction_tag_id', ForeignKey('transaction_tags.id'), primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
class Transaction(BaseModel):
|
||||
__tablename__ = 'transactions'
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
created_at: Mapped[datetime] = mapped_column(nullable=False)
|
||||
spent_date: Mapped[date] = mapped_column(nullable=False)
|
||||
name: Mapped[str] = mapped_column()
|
||||
comment: Mapped[str] = mapped_column()
|
||||
amount: Mapped[float] = mapped_column()
|
||||
is_income: Mapped[bool] = mapped_column(server_default=text("false"), 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], lazy="selectin")
|
||||
|
||||
tags: Mapped[list["TransactionTag"]] = relationship(
|
||||
secondary=transactions_transaction_tags,
|
||||
lazy='selectin',
|
||||
back_populates='transactions',
|
||||
cascade='all, delete',
|
||||
)
|
||||
|
||||
|
||||
class TransactionTag(BaseModel):
|
||||
__tablename__ = 'transaction_tags'
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(index=True)
|
||||
is_income: Mapped[bool] = mapped_column(server_default=text("false"), nullable=False)
|
||||
|
||||
transactions: Mapped[list["Transaction"]] = relationship(
|
||||
secondary=transactions_transaction_tags,
|
||||
lazy='selectin',
|
||||
back_populates='tags',
|
||||
)
|
||||
@@ -15,5 +15,5 @@ from .billing import billing_router
|
||||
from .task import task_router
|
||||
from .work_shifts import work_shifts_router
|
||||
from .statistics import statistics_router
|
||||
from .expense import expense_router
|
||||
from .transaction import transaction_router
|
||||
from .shipping import shipping_router
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from backend.dependecies import SessionDependency, CurrentUserDependency, PaginationDependency
|
||||
from schemas.expense import *
|
||||
from services.auth import authorized_user
|
||||
from services.expenses import ExpensesService
|
||||
|
||||
expense_router = APIRouter(
|
||||
prefix="/expense",
|
||||
tags=["expense"],
|
||||
dependencies=[Depends(authorized_user)]
|
||||
)
|
||||
|
||||
|
||||
# region Expenses
|
||||
|
||||
@expense_router.get(
|
||||
'/get-all',
|
||||
operation_id='get_all_expenses',
|
||||
response_model=GetAllExpensesResponse,
|
||||
)
|
||||
async def get_all(
|
||||
session: SessionDependency,
|
||||
pagination: PaginationDependency,
|
||||
):
|
||||
return await ExpensesService(session).get_all(pagination)
|
||||
|
||||
|
||||
@expense_router.post(
|
||||
'/update',
|
||||
operation_id='update_expense',
|
||||
response_model=UpdateExpenseResponse,
|
||||
)
|
||||
async def update_expense(
|
||||
session: SessionDependency,
|
||||
request: UpdateExpenseRequest,
|
||||
user: CurrentUserDependency,
|
||||
):
|
||||
return await ExpensesService(session).update_expense(user, request)
|
||||
|
||||
|
||||
@expense_router.delete(
|
||||
'/delete/{expense_id}',
|
||||
operation_id='delete_expense',
|
||||
response_model=DeleteExpenseResponse,
|
||||
)
|
||||
async def delete_expense(
|
||||
session: SessionDependency,
|
||||
expense_id: int,
|
||||
):
|
||||
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
|
||||
118
routers/transaction.py
Normal file
118
routers/transaction.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from backend.dependecies import SessionDependency, CurrentUserDependency, PaginationDependency
|
||||
from schemas.transaction import *
|
||||
from services.auth import authorized_user
|
||||
from services.transaction import TransactionsService
|
||||
|
||||
transaction_router = APIRouter(
|
||||
prefix="/transaction",
|
||||
tags=["transaction"],
|
||||
dependencies=[Depends(authorized_user)]
|
||||
)
|
||||
|
||||
|
||||
# region Expenses
|
||||
|
||||
@transaction_router.post(
|
||||
'/get-all',
|
||||
operation_id='get_all_transactions',
|
||||
response_model=GetAllTransactionsResponse,
|
||||
)
|
||||
async def get_all(
|
||||
session: SessionDependency,
|
||||
pagination: PaginationDependency,
|
||||
request: GetAllTransactionsRequest,
|
||||
):
|
||||
return await TransactionsService(session).get_all(pagination, request)
|
||||
|
||||
|
||||
@transaction_router.post(
|
||||
'/update',
|
||||
operation_id='update_transaction',
|
||||
response_model=UpdateTransactionResponse,
|
||||
)
|
||||
async def update_transaction(
|
||||
session: SessionDependency,
|
||||
request: UpdateTransactionRequest,
|
||||
user: CurrentUserDependency,
|
||||
):
|
||||
return await TransactionsService(session).update_transaction(user, request)
|
||||
|
||||
|
||||
@transaction_router.delete(
|
||||
'/delete/{transaction_id}',
|
||||
operation_id='delete_transaction',
|
||||
response_model=DeleteTransactionResponse,
|
||||
)
|
||||
async def delete_transaction(
|
||||
session: SessionDependency,
|
||||
transaction_id: int,
|
||||
):
|
||||
return await TransactionsService(session).delete_transaction(transaction_id)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Expense tags
|
||||
|
||||
|
||||
@transaction_router.get(
|
||||
'/get-all-tags',
|
||||
operation_id='get_all_transaction_tags',
|
||||
response_model=GetAllTransactionTagsResponse,
|
||||
)
|
||||
async def get_all_transaction_tags(
|
||||
session: SessionDependency,
|
||||
):
|
||||
return await TransactionsService(session).get_all_tags()
|
||||
|
||||
|
||||
@transaction_router.get(
|
||||
'/get-tags/{is_income}',
|
||||
operation_id='get_transaction_tags',
|
||||
response_model=GetTransactionTagsResponse,
|
||||
)
|
||||
async def get_transaction_tags(
|
||||
session: SessionDependency,
|
||||
is_income: bool,
|
||||
):
|
||||
return await TransactionsService(session).get_tags(is_income)
|
||||
|
||||
|
||||
@transaction_router.post(
|
||||
'/create-tag',
|
||||
operation_id='create_transaction_tag',
|
||||
response_model=UpdateTransactionTagResponse,
|
||||
)
|
||||
async def create_transaction_tag(
|
||||
session: SessionDependency,
|
||||
request: CreateTransactionTagRequest,
|
||||
):
|
||||
return await TransactionsService(session).create_tag(request)
|
||||
|
||||
|
||||
@transaction_router.post(
|
||||
'/update-tag',
|
||||
operation_id='update_transaction_tag',
|
||||
response_model=UpdateTransactionTagResponse,
|
||||
)
|
||||
async def update_transaction_tag(
|
||||
session: SessionDependency,
|
||||
request: UpdateTransactionTagRequest,
|
||||
):
|
||||
return await TransactionsService(session).update_tag(request)
|
||||
|
||||
|
||||
@transaction_router.delete(
|
||||
'/delete-tag/{tag_id}',
|
||||
operation_id='delete_transaction_tag',
|
||||
response_model=DeleteTransactionTagResponse,
|
||||
)
|
||||
async def update_transaction_tag(
|
||||
session: SessionDependency,
|
||||
tag_id: int,
|
||||
):
|
||||
return await TransactionsService(session).delete_tag(tag_id)
|
||||
|
||||
# endregion
|
||||
@@ -1,86 +0,0 @@
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from schemas.base import OkMessageSchema, BaseSchema, PaginationInfoSchema
|
||||
from schemas.user import UserSchema
|
||||
|
||||
|
||||
# region Entities
|
||||
|
||||
class BaseExpenseTagSchema(BaseSchema):
|
||||
name: str
|
||||
|
||||
|
||||
class ExpenseTagSchema(BaseExpenseTagSchema):
|
||||
id: int
|
||||
|
||||
|
||||
class ExpenseSchemaBase(BaseSchema):
|
||||
id: int
|
||||
name: str
|
||||
comment: str
|
||||
amount: float
|
||||
created_by_user: UserSchema
|
||||
spent_date: datetime.date
|
||||
tags: list[ExpenseTagSchema]
|
||||
|
||||
|
||||
class UpdateExpenseSchema(BaseSchema):
|
||||
id: Optional[int] = None
|
||||
name: str
|
||||
comment: Optional[str] = ""
|
||||
amount: float
|
||||
spent_date: datetime.date
|
||||
tags: list[str] = []
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Requests
|
||||
|
||||
|
||||
class UpdateExpenseRequest(BaseSchema):
|
||||
expense: UpdateExpenseSchema
|
||||
|
||||
|
||||
class CreateExpenseTagRequest(BaseSchema):
|
||||
tag: BaseExpenseTagSchema
|
||||
|
||||
|
||||
class UpdateExpenseTagRequest(BaseSchema):
|
||||
tag: ExpenseTagSchema
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Responses
|
||||
|
||||
class GetAllExpensesResponse(BaseSchema):
|
||||
expenses: list[ExpenseSchemaBase]
|
||||
pagination_info: PaginationInfoSchema
|
||||
|
||||
|
||||
class UpdateExpenseResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteExpenseResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
|
||||
class GetAllExpenseTagsResponse(BaseSchema):
|
||||
tags: list[ExpenseTagSchema]
|
||||
|
||||
|
||||
class CreateExpenseTagResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateExpenseTagResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteExpenseTagResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
# endregion
|
||||
@@ -31,7 +31,8 @@ class CommonProfitFilters(BaseSchema):
|
||||
base_marketplace_key: str
|
||||
deal_status_id: int
|
||||
manager_id: int
|
||||
tag_id: int
|
||||
expense_tag_id: int
|
||||
income_tag_id: int
|
||||
|
||||
|
||||
class GetProfitChartDataRequest(CommonProfitFilters):
|
||||
|
||||
97
schemas/transaction.py
Normal file
97
schemas/transaction.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from schemas.base import OkMessageSchema, BaseSchema, PaginationInfoSchema
|
||||
from schemas.user import UserSchema
|
||||
|
||||
|
||||
# region Entities
|
||||
|
||||
class BaseTransactionTagSchema(BaseSchema):
|
||||
name: str
|
||||
is_income: bool
|
||||
|
||||
|
||||
class TransactionTagSchema(BaseTransactionTagSchema):
|
||||
id: int
|
||||
|
||||
|
||||
class TransactionSchemaBase(BaseSchema):
|
||||
id: int
|
||||
name: str
|
||||
comment: str
|
||||
amount: float
|
||||
created_by_user: UserSchema
|
||||
spent_date: datetime.date
|
||||
is_income: bool
|
||||
tags: list[TransactionTagSchema]
|
||||
|
||||
|
||||
class UpdateTransactionSchema(BaseSchema):
|
||||
id: Optional[int] = None
|
||||
name: str
|
||||
comment: Optional[str] = ""
|
||||
amount: float
|
||||
spent_date: datetime.date
|
||||
is_income: bool
|
||||
tags: list[str] = []
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Requests
|
||||
|
||||
|
||||
class GetAllTransactionsRequest(BaseSchema):
|
||||
is_income: bool
|
||||
|
||||
|
||||
class UpdateTransactionRequest(BaseSchema):
|
||||
transaction: UpdateTransactionSchema
|
||||
|
||||
|
||||
class CreateTransactionTagRequest(BaseSchema):
|
||||
tag: BaseTransactionTagSchema
|
||||
|
||||
|
||||
class UpdateTransactionTagRequest(BaseSchema):
|
||||
tag: TransactionTagSchema
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Responses
|
||||
|
||||
class GetAllTransactionsResponse(BaseSchema):
|
||||
transactions: list[TransactionSchemaBase]
|
||||
pagination_info: PaginationInfoSchema
|
||||
|
||||
|
||||
class UpdateTransactionResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteTransactionResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
|
||||
class GetTransactionTagsResponse(BaseSchema):
|
||||
tags: list[TransactionTagSchema]
|
||||
|
||||
|
||||
class GetAllTransactionTagsResponse(BaseSchema):
|
||||
tags: list[TransactionTagSchema]
|
||||
|
||||
|
||||
class CreateTransactionTagResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateTransactionTagResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteTransactionTagResponse(OkMessageSchema):
|
||||
pass
|
||||
|
||||
# endregion
|
||||
@@ -1,170 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
import math
|
||||
from fastapi import HTTPException
|
||||
from fastapi import status
|
||||
from sqlalchemy import delete, select, func, update, insert
|
||||
|
||||
from models import User
|
||||
from models.expense import Expense, ExpenseTag
|
||||
from schemas.base import PaginationSchema
|
||||
from schemas.expense import *
|
||||
from services.base import BaseService
|
||||
from utils.dependecies import is_valid_pagination
|
||||
|
||||
|
||||
class ExpensesService(BaseService):
|
||||
async def get_all(self, pagination: PaginationSchema) -> GetAllExpensesResponse:
|
||||
if not is_valid_pagination(pagination):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid pagination')
|
||||
page = max(0, pagination.page - 1)
|
||||
|
||||
stmt = (
|
||||
select(Expense)
|
||||
.order_by(Expense.spent_date.desc())
|
||||
.offset(page * pagination.items_per_page)
|
||||
.limit(pagination.items_per_page)
|
||||
)
|
||||
|
||||
total_records = await self.session.scalar(select(func.count()).select_from(Expense))
|
||||
if not total_records:
|
||||
return GetAllExpensesResponse(
|
||||
expenses=[],
|
||||
pagination_info=PaginationInfoSchema(
|
||||
total_pages=0,
|
||||
total_items=0
|
||||
)
|
||||
)
|
||||
total_items = total_records
|
||||
total_pages = math.ceil(total_records / pagination.items_per_page)
|
||||
|
||||
expenses = await self.session.execute(stmt)
|
||||
expenses = expenses.scalars().all()
|
||||
response = GetAllExpensesResponse(
|
||||
expenses=expenses,
|
||||
pagination_info=PaginationInfoSchema(
|
||||
total_items=total_items,
|
||||
total_pages=total_pages
|
||||
)
|
||||
)
|
||||
return response
|
||||
|
||||
async def get_by_id(self, expense_id: int) -> Optional[Expense]:
|
||||
expense = await self.session.get(Expense, expense_id)
|
||||
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:
|
||||
expense = await self.get_by_id(request.expense.id or -1)
|
||||
|
||||
if not expense:
|
||||
new_expense = Expense(
|
||||
created_at=datetime.datetime.now(),
|
||||
name=request.expense.name,
|
||||
comment=request.expense.comment,
|
||||
amount=request.expense.amount,
|
||||
spent_date=request.expense.spent_date,
|
||||
created_by_user_id=user.id,
|
||||
)
|
||||
self.session.add(new_expense)
|
||||
await self.add_tags(new_expense, request.expense.tags)
|
||||
await self.session.commit()
|
||||
return UpdateExpenseResponse(ok=True, message='Запись о расходах успешно создана')
|
||||
|
||||
expense.name = request.expense.name
|
||||
expense.amount = request.expense.amount
|
||||
expense.comment = request.expense.comment
|
||||
expense.spent_date = request.expense.spent_date
|
||||
self.session.add(expense)
|
||||
await self.add_tags(expense, request.expense.tags)
|
||||
await self.session.commit()
|
||||
return UpdateExpenseResponse(ok=True, message='Запись о расходах успешно изменена')
|
||||
|
||||
async def delete_expense(self, expense_id) -> DeleteExpenseResponse:
|
||||
stmt = (
|
||||
delete(Expense)
|
||||
.where(Expense.id == expense_id)
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
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 CreateExpenseTagResponse(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 CreateExpenseTagResponse(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 UpdateExpenseTagResponse(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='Тег удален')
|
||||
@@ -3,24 +3,24 @@ from datetime import date
|
||||
from sqlalchemy import select, func, Subquery, cast, CTE
|
||||
from sqlalchemy.dialects.postgresql import TIMESTAMP
|
||||
|
||||
from models import PaymentRecord, Expense, expenses_expense_tags
|
||||
from models import PaymentRecord
|
||||
from schemas.statistics import CommonProfitFilters
|
||||
from services.base import BaseService
|
||||
from services.statistics.common import generate_date_range
|
||||
|
||||
|
||||
class ExpensesStatisticsService(BaseService):
|
||||
class PaymentStatisticsService(BaseService):
|
||||
date_from: date
|
||||
date_to: date
|
||||
|
||||
@staticmethod
|
||||
def _fill_date_gaps(expenses: Subquery, all_dates: CTE) -> Subquery:
|
||||
def _fill_date_gaps(payments: Subquery, all_dates: CTE) -> Subquery:
|
||||
return (
|
||||
select(
|
||||
all_dates.c.date,
|
||||
(all_dates.c.expenses + func.coalesce(expenses.c.expenses, 0)).label("expenses"),
|
||||
(all_dates.c.expenses + func.coalesce(payments.c.expenses, 0)).label("expenses"),
|
||||
)
|
||||
.join(expenses, all_dates.c.date == expenses.c.date, isouter=True)
|
||||
.join(payments, all_dates.c.date == payments.c.date, isouter=True)
|
||||
.order_by(all_dates.c.date)
|
||||
.subquery()
|
||||
)
|
||||
@@ -40,58 +40,24 @@ class ExpensesStatisticsService(BaseService):
|
||||
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):
|
||||
def _apply_payments(deals_by_dates: Subquery, expenses_subquery: Subquery):
|
||||
return (
|
||||
select(
|
||||
deals_by_dates.c.date,
|
||||
deals_by_dates.c.deals_count,
|
||||
deals_by_dates.c.revenue,
|
||||
(func.coalesce(deals_by_dates.c.profit, 0) - func.coalesce(expenses_subquery.c.expenses, 0)).label(
|
||||
"profit"),
|
||||
(func.coalesce(deals_by_dates.c.profit, 0) - func.coalesce(expenses_subquery.c.expenses, 0))
|
||||
.label("profit"),
|
||||
(deals_by_dates.c.expenses + expenses_subquery.c.expenses).label("expenses"),
|
||||
)
|
||||
.join(expenses_subquery, expenses_subquery.c.date == deals_by_dates.c.date)
|
||||
)
|
||||
|
||||
def apply_expenses(self, filters: CommonProfitFilters, deals_by_dates: Subquery):
|
||||
def apply_payments(self, filters: CommonProfitFilters, deals_by_dates: Subquery):
|
||||
self.date_from, self.date_to = filters.date_range
|
||||
|
||||
# Apply salary expenses
|
||||
salary_expenses = self._get_payment_records_sub()
|
||||
deals_by_dates = self._apply_expenses(deals_by_dates, salary_expenses)
|
||||
|
||||
# Apply additional expenses
|
||||
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_payments(deals_by_dates, salary_expenses)
|
||||
|
||||
return deals_by_dates
|
||||
|
||||
@@ -10,7 +10,8 @@ from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataReq
|
||||
GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters
|
||||
from services.base import BaseService
|
||||
from services.statistics.common import generate_date_range
|
||||
from services.statistics.expenses_statistics import ExpensesStatisticsService
|
||||
from services.statistics.expenses_statistics import PaymentStatisticsService
|
||||
from services.statistics.transactions_statistics import TransactionsStatisticsService
|
||||
|
||||
|
||||
class ProfitStatisticsService(BaseService):
|
||||
@@ -302,13 +303,18 @@ class ProfitStatisticsService(BaseService):
|
||||
sub_grouped_by_deals = self._group_by_deals(sub_union)
|
||||
sub_deals_grouped_by_date = self._group_by_date(sub_grouped_by_deals).subquery()
|
||||
|
||||
expenses_statistics_service = ExpensesStatisticsService(self.session)
|
||||
stmt_deals_applied_expenses = expenses_statistics_service.apply_expenses(
|
||||
expenses_statistics_service = PaymentStatisticsService(self.session)
|
||||
stmt_deals_applied_expenses = expenses_statistics_service.apply_payments(
|
||||
self.filters,
|
||||
sub_deals_grouped_by_date
|
||||
)
|
||||
transactions_statistics_service = TransactionsStatisticsService(self.session)
|
||||
stmt_deals_applied_transactions = transactions_statistics_service.apply_transactions(
|
||||
self.filters,
|
||||
stmt_deals_applied_expenses.subquery()
|
||||
)
|
||||
|
||||
result = await self.session.execute(stmt_deals_applied_expenses)
|
||||
result = await self.session.execute(stmt_deals_applied_transactions)
|
||||
rows = result.all()
|
||||
return rows
|
||||
|
||||
|
||||
93
services/statistics/transactions_statistics.py
Normal file
93
services/statistics/transactions_statistics.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy import select, func, Subquery, cast, CTE, case, or_, and_
|
||||
from sqlalchemy.dialects.postgresql import TIMESTAMP
|
||||
|
||||
from models import Transaction, transactions_transaction_tags, TransactionTag
|
||||
from schemas.statistics import CommonProfitFilters
|
||||
from services.base import BaseService
|
||||
from services.statistics.common import generate_date_range
|
||||
|
||||
|
||||
class TransactionsStatisticsService(BaseService):
|
||||
date_from: date
|
||||
date_to: date
|
||||
|
||||
@staticmethod
|
||||
def _fill_date_gaps(expenses: Subquery, all_dates: CTE) -> Subquery:
|
||||
return (
|
||||
select(
|
||||
all_dates.c.date,
|
||||
(all_dates.c.expenses + func.coalesce(expenses.c.expenses, 0)).label("expenses"),
|
||||
(all_dates.c.revenue + func.coalesce(expenses.c.revenue, 0)).label("revenue"),
|
||||
)
|
||||
.join(expenses, all_dates.c.date == expenses.c.date, isouter=True)
|
||||
.order_by(all_dates.c.date)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
def _get_additional_transactions_sub(self, income_tag_id: int, expense_tag_id: int) -> Subquery:
|
||||
all_dates = generate_date_range(self.date_from, self.date_to, ["expenses", "revenue"])
|
||||
|
||||
filtered_tags_sub = (
|
||||
select(TransactionTag)
|
||||
.where(
|
||||
or_(
|
||||
and_(TransactionTag.is_income == True,
|
||||
or_(income_tag_id == -1, TransactionTag.id == income_tag_id)),
|
||||
and_(TransactionTag.is_income == False,
|
||||
or_(expense_tag_id == -1, TransactionTag.id == expense_tag_id)),
|
||||
)
|
||||
)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
transaction_ids = (
|
||||
select(transactions_transaction_tags.c.transaction_id)
|
||||
.join(filtered_tags_sub, filtered_tags_sub.c.id == transactions_transaction_tags.c.transaction_tag_id)
|
||||
.group_by(transactions_transaction_tags.c.transaction_id)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
transactions = (
|
||||
select(Transaction)
|
||||
.join(transaction_ids, transaction_ids.c.transaction_id == Transaction.id)
|
||||
.subquery()
|
||||
)
|
||||
|
||||
transactions = (
|
||||
select(
|
||||
func.sum(case((transactions.c.is_income, transactions.c.amount), else_=0)).label("revenue"),
|
||||
func.sum(case((~transactions.c.is_income, transactions.c.amount), else_=0)).label("expenses"),
|
||||
cast(transactions.c.spent_date, TIMESTAMP(timezone=False)).label("date"),
|
||||
)
|
||||
.where(transactions.c.spent_date.between(self.date_from, self.date_to))
|
||||
.group_by("date")
|
||||
.subquery()
|
||||
)
|
||||
|
||||
expenses_with_filled_gaps = self._fill_date_gaps(transactions, all_dates)
|
||||
return expenses_with_filled_gaps
|
||||
|
||||
@staticmethod
|
||||
def _apply_transactions(deals_by_dates: Subquery, transactions: Subquery):
|
||||
return (
|
||||
select(
|
||||
deals_by_dates.c.date,
|
||||
deals_by_dates.c.deals_count,
|
||||
(deals_by_dates.c.revenue + transactions.c.revenue).label("revenue"),
|
||||
(func.coalesce(deals_by_dates.c.profit, 0) - func.coalesce(transactions.c.expenses, 0) + func.coalesce(
|
||||
transactions.c.revenue, 0))
|
||||
.label("profit"),
|
||||
(deals_by_dates.c.expenses + transactions.c.expenses).label("expenses"),
|
||||
)
|
||||
.join(transactions, transactions.c.date == deals_by_dates.c.date)
|
||||
)
|
||||
|
||||
def apply_transactions(self, filters: CommonProfitFilters, deals_by_dates: Subquery):
|
||||
self.date_from, self.date_to = filters.date_range
|
||||
|
||||
additional_expenses = self._get_additional_transactions_sub(filters.income_tag_id, filters.expense_tag_id)
|
||||
deals_by_dates = self._apply_transactions(deals_by_dates, additional_expenses)
|
||||
|
||||
return deals_by_dates
|
||||
186
services/transaction.py
Normal file
186
services/transaction.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from datetime import datetime
|
||||
|
||||
import math
|
||||
from fastapi import HTTPException
|
||||
from fastapi import status
|
||||
from sqlalchemy import delete, select, func, update, insert, and_
|
||||
|
||||
from models import User
|
||||
from models.transaction import Transaction, TransactionTag
|
||||
from schemas.base import PaginationSchema
|
||||
from schemas.transaction import *
|
||||
from services.base import BaseService
|
||||
from utils.dependecies import is_valid_pagination
|
||||
|
||||
|
||||
class TransactionsService(BaseService):
|
||||
async def get_all(self, pagination: PaginationSchema, request: GetAllTransactionsRequest) -> GetAllTransactionsResponse:
|
||||
if not is_valid_pagination(pagination):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid pagination')
|
||||
page = max(0, pagination.page - 1)
|
||||
is_income = request.is_income
|
||||
|
||||
stmt = (
|
||||
select(Transaction)
|
||||
.where(Transaction.is_income == is_income)
|
||||
.order_by(Transaction.spent_date.desc())
|
||||
.offset(page * pagination.items_per_page)
|
||||
.limit(pagination.items_per_page)
|
||||
)
|
||||
|
||||
total_records = await self.session.scalar(
|
||||
select(func.count())
|
||||
.select_from(Transaction)
|
||||
.where(Transaction.is_income == is_income)
|
||||
)
|
||||
if not total_records:
|
||||
return GetAllTransactionsResponse(
|
||||
transactions=[],
|
||||
pagination_info=PaginationInfoSchema(
|
||||
total_pages=0,
|
||||
total_items=0
|
||||
)
|
||||
)
|
||||
total_items = total_records
|
||||
total_pages = math.ceil(total_records / pagination.items_per_page)
|
||||
|
||||
transactions = await self.session.execute(stmt)
|
||||
transactions = transactions.scalars().all()
|
||||
response = GetAllTransactionsResponse(
|
||||
transactions=transactions,
|
||||
pagination_info=PaginationInfoSchema(
|
||||
total_items=total_items,
|
||||
total_pages=total_pages
|
||||
)
|
||||
)
|
||||
return response
|
||||
|
||||
async def get_by_id(self, transaction_id: int) -> Optional[Transaction]:
|
||||
transaction = await self.session.get(Transaction, transaction_id)
|
||||
return transaction
|
||||
|
||||
async def add_tags(self, transaction: Transaction, tag_names: list[str]):
|
||||
tags = []
|
||||
for tag_name in tag_names:
|
||||
existing_tag = await self.get_tag_by_name(tag_name, transaction.is_income)
|
||||
if existing_tag:
|
||||
tags.append(existing_tag)
|
||||
else:
|
||||
tag = TransactionTag(name=tag_name, is_income=transaction.is_income)
|
||||
self.session.add(tag)
|
||||
tags.append(tag)
|
||||
|
||||
transaction.tags = tags
|
||||
|
||||
async def update_transaction(self, user: User, request: UpdateTransactionRequest) -> UpdateTransactionResponse:
|
||||
transaction = await self.get_by_id(request.transaction.id or -1)
|
||||
|
||||
if not transaction:
|
||||
new_transaction = Transaction(
|
||||
created_at=datetime.datetime.now(),
|
||||
name=request.transaction.name,
|
||||
comment=request.transaction.comment,
|
||||
amount=request.transaction.amount,
|
||||
spent_date=request.transaction.spent_date,
|
||||
created_by_user_id=user.id,
|
||||
is_income=request.transaction.is_income,
|
||||
)
|
||||
self.session.add(new_transaction)
|
||||
await self.add_tags(new_transaction, request.transaction.tags)
|
||||
await self.session.commit()
|
||||
return UpdateTransactionResponse(ok=True, message='Запись успешно создана')
|
||||
|
||||
transaction.name = request.transaction.name
|
||||
transaction.amount = request.transaction.amount
|
||||
transaction.comment = request.transaction.comment
|
||||
transaction.spent_date = request.transaction.spent_date
|
||||
self.session.add(transaction)
|
||||
await self.add_tags(transaction, request.transaction.tags)
|
||||
await self.session.commit()
|
||||
return UpdateTransactionResponse(ok=True, message='Запись успешно изменена')
|
||||
|
||||
async def delete_transaction(self, expense_id) -> DeleteTransactionResponse:
|
||||
stmt = (
|
||||
delete(Transaction)
|
||||
.where(Transaction.id == expense_id)
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
return DeleteTransactionResponse(ok=True, message='Запись успешно удалена')
|
||||
|
||||
async def get_all_tags(self) -> GetAllTransactionTagsResponse:
|
||||
stmt = (
|
||||
select(TransactionTag)
|
||||
.order_by(TransactionTag.id)
|
||||
)
|
||||
tags = await self.session.execute(stmt)
|
||||
return GetAllTransactionTagsResponse(tags=tags.scalars().all())
|
||||
|
||||
async def get_tags(self, is_income: bool) -> GetTransactionTagsResponse:
|
||||
stmt = (
|
||||
select(TransactionTag)
|
||||
.where(TransactionTag.is_income == is_income)
|
||||
.order_by(TransactionTag.id)
|
||||
)
|
||||
tags = await self.session.execute(stmt)
|
||||
return GetTransactionTagsResponse(tags=tags.scalars().all())
|
||||
|
||||
async def get_tag_by_id(self, tag_id: int) -> Optional[TransactionTag]:
|
||||
return await self.session.get(TransactionTag, tag_id)
|
||||
|
||||
async def get_tag_by_name(self, tag_name: str, is_income: bool) -> Optional[TransactionTag]:
|
||||
stmt = (
|
||||
select(TransactionTag)
|
||||
.where(and_(TransactionTag.name == tag_name, TransactionTag.is_income == is_income))
|
||||
)
|
||||
tag = await self.session.scalar(stmt)
|
||||
return tag
|
||||
|
||||
async def create_tag(self, request: CreateTransactionTagRequest) -> CreateTransactionTagResponse:
|
||||
tag = await self.get_tag_by_name(request.tag.name, request.tag.is_income)
|
||||
if tag:
|
||||
return CreateTransactionTagResponse(ok=False, message='Такой тег уже есть.')
|
||||
|
||||
tag_dict = request.tag.model_dump()
|
||||
stmt = (
|
||||
insert(TransactionTag)
|
||||
.values(**tag_dict)
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
return CreateTransactionTagResponse(ok=True, message='Тег успешно создан.')
|
||||
|
||||
async def update_tag(self, request: UpdateTransactionTagRequest) -> UpdateTransactionTagResponse:
|
||||
tag = await self.get_tag_by_name(request.tag.name, request.tag.is_income)
|
||||
if tag:
|
||||
return UpdateTransactionTagResponse(ok=False, message='Тег с таким названием уже есть.')
|
||||
|
||||
tag = await self.get_tag_by_id(request.tag.id)
|
||||
if not tag:
|
||||
return UpdateTransactionTagResponse(ok=False, message='Тег не найден.')
|
||||
|
||||
tag_dict = request.tag.model_dump()
|
||||
del tag_dict['id']
|
||||
stmt = (
|
||||
update(TransactionTag)
|
||||
.where(TransactionTag.id == request.tag.id)
|
||||
.values(**tag_dict)
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
return UpdateTransactionTagResponse(ok=True, message='Тег успешно изменен.')
|
||||
|
||||
async def delete_tag(self, tag_id: int) -> DeleteTransactionTagResponse:
|
||||
tag = await self.get_tag_by_id(tag_id)
|
||||
if not tag:
|
||||
return DeleteTransactionTagResponse(ok=False, message='Тег не найден.')
|
||||
if len(tag.transactions) > 0:
|
||||
return DeleteTransactionTagResponse(ok=False, message='Тег прикреплен к записи о расходах/доходах.')
|
||||
|
||||
stmt = (
|
||||
delete(TransactionTag)
|
||||
.where(TransactionTag.id == tag_id)
|
||||
)
|
||||
await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
return DeleteTransactionTagResponse(ok=True, message='Тег удален')
|
||||
Reference in New Issue
Block a user