feat: tags for expenses, filters by tags in statistics

This commit is contained in:
2024-11-27 15:49:33 +04:00
parent 48c88cb93a
commit 88ce256850
7 changed files with 271 additions and 36 deletions

View File

@@ -1,15 +1,14 @@
from datetime import datetime
from typing import Optional
import math
from fastapi import HTTPException
from sqlalchemy import delete, select, func
from fastapi import status
from sqlalchemy import delete, select, func, update, insert
from models import User
from models.expense import Expense
from schemas.base import PaginationSchema, PaginationInfoSchema
from schemas.expense import UpdateExpenseResponse, UpdateExpenseRequest, DeleteExpenseResponse, GetAllExpensesResponse
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
@@ -47,23 +46,37 @@ class ExpensesService(BaseService):
)
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)
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)
expense = await self.get_by_id(request.expense.id or -1)
if not expense:
expense = Expense(
created_at=datetime.now(),
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(expense)
self.session.add(new_expense)
await self.add_tags(new_expense, request.expense.tags)
await self.session.commit()
return UpdateExpenseResponse(ok=True, message='Запись о расходах успешно создана')
@@ -72,6 +85,7 @@ class ExpensesService(BaseService):
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='Запись о расходах успешно изменена')
@@ -83,3 +97,71 @@ class ExpensesService(BaseService):
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 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='Тег удален')

View File

@@ -1,7 +1,10 @@
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 models import PaymentRecord, Expense
from models import PaymentRecord, Expense, expenses_expense_tags
from schemas.statistics import CommonProfitFilters
from services.base import BaseService
from services.statistics.common import generate_date_range
@@ -10,19 +13,9 @@ class ExpensesStatisticsService(BaseService):
date_from: date
date_to: date
def _get_expenses_sub(self, model, date_column, amount_column) -> Subquery:
all_dates = generate_date_range(self.date_from, self.date_to, ["expenses"])
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 = (
@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"),
@@ -31,9 +24,53 @@ class ExpensesStatisticsService(BaseService):
.order_by(all_dates.c.date)
.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 (
select(
deals_by_dates.c.date,
@@ -46,15 +83,15 @@ class ExpensesStatisticsService(BaseService):
.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):
self.date_from, self.date_to = date_from, date_to
def apply_expenses(self, filters: CommonProfitFilters, deals_by_dates: Subquery):
self.date_from, self.date_to = filters.date_range
# 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)
# 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)
return deals_by_dates

View File

@@ -294,8 +294,7 @@ class ProfitStatisticsService(BaseService):
expenses_statistics_service = ExpensesStatisticsService(self.session)
stmt_deals_applied_expenses = expenses_statistics_service.apply_expenses(
self.date_from,
self.date_to,
self.filters,
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):
self.date_from, self.date_to = request.date_range
self.filters = request
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):
self.date_from, self.date_to = request.date_range
self.filters = request
sub_deals_dates = self._get_deals_dates(request.deal_status_id)