from datetime import datetime from fastapi import HTTPException from sqlalchemy import select, and_, union_all, func, Subquery, literal from enums.profit_table_group_by import ProfitTableGroupBy from models import DealService, Deal, DealStatusHistory, DealProductService, DealProduct, Service, Client, \ ShippingWarehouse, BaseMarketplace, User from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataRequest, ProfitChartDataItem, \ GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters from services.base import BaseService from services.statistics.common import generate_date_range from services.statistics.expenses_statistics import PaymentStatisticsService from services.statistics.transactions_statistics import TransactionsStatisticsService class ProfitStatisticsService(BaseService): @staticmethod def _get_sub_deals_created_at(date_from: datetime.date, date_to: datetime.date): deals_created_at = ( select( Deal.id.label('deal_id'), func.date_trunc( 'day', Deal.created_at, ).label('date'), Deal.current_status, ) .subquery() ) return ( select(deals_created_at) .where(deals_created_at.c.date.between(date_from, date_to)) .subquery() ) @staticmethod def _get_sub_status_history(): last_statuses = ( select( DealStatusHistory.deal_id, func.max(DealStatusHistory.changed_at).label('changed_at') ) .group_by(DealStatusHistory.deal_id) .subquery() ) return ( select( Deal.id.label('deal_id'), func.date_trunc( 'day', last_statuses.c.changed_at, ).label('date'), Deal.current_status, ) .join(last_statuses, last_statuses.c.deal_id == Deal.id) .subquery() ) @staticmethod def _get_filtered_sub_status_history(date_from: datetime.date, date_to: datetime.date): sub_status_history = ProfitStatisticsService._get_sub_status_history() return ( select(sub_status_history) .where(sub_status_history.c.date.between(date_from, date_to)) .subquery() ) def _get_deals_dates(self, deal_status_id: int): if deal_status_id == -1: return ProfitStatisticsService._get_sub_deals_created_at(self.date_from, self.date_to) return ProfitStatisticsService._get_filtered_sub_status_history(self.date_from, self.date_to) @staticmethod def _to_schema(rows, is_chart: bool = True) -> ( list[ProfitChartDataItem] | list[ProfitTableDataItem] ): data = [] for row in rows: if is_chart: data_item = ProfitChartDataItem( date=row.date.date(), deals_count=row.deals_count, profit=row.profit, revenue=row.revenue, expenses=row.expenses, ) else: data_item = ProfitTableDataItem( grouped_value=row.date.date(), deals_count=row.deals_count, profit=row.profit, revenue=row.revenue, expenses=row.expenses, ) data.append(data_item) return data @staticmethod def _get_stmt_deal_services(sub_filtered_status_history: Subquery): return ( select( Deal.id.label("deal_id"), func.date_trunc( "day", sub_filtered_status_history.c.date, ).label("date"), func.sum(DealService.price * DealService.quantity).label("revenue"), func.sum(DealService.price * DealService.quantity).label("profit"), ) .join(DealService, Deal.id == DealService.deal_id) .join(Service, DealService.service_id == Service.id) .join(sub_filtered_status_history, Deal.id == sub_filtered_status_history.c.deal_id) .where(Deal.is_deleted == False) .group_by(Deal.id, "date") ) @staticmethod def _apply_filters(request: CommonProfitFilters, stmt_deal_services, stmt_deal_product_services): if request.client_id != -1: stmt_deal_services = stmt_deal_services.where(Deal.client_id == request.client_id) stmt_deal_product_services = stmt_deal_product_services.where(Deal.client_id == request.client_id) if request.base_marketplace_key != "all": stmt_deal_services = stmt_deal_services.where(Deal.base_marketplace_key == request.base_marketplace_key) stmt_deal_product_services = stmt_deal_product_services.where( Deal.base_marketplace_key == request.base_marketplace_key) if request.deal_status_id != -1: stmt_deal_services = stmt_deal_services.where(Deal.current_status == request.deal_status_id) stmt_deal_product_services = stmt_deal_product_services.where(Deal.current_status == request.deal_status_id) if request.manager_id != -1: stmt_deal_services = stmt_deal_services.where(Deal.manager_id == request.manager_id) stmt_deal_product_services = stmt_deal_product_services.where(Deal.manager_id == request.manager_id) return stmt_deal_services, stmt_deal_product_services @staticmethod def _get_stmt_product_services(): return ( select( Deal.id.label("deal_id"), func.sum(DealProductService.price * DealProduct.quantity).label("revenue"), func.sum(DealProductService.price * DealProduct.quantity).label("profit"), ) .join(DealProduct, Deal.id == DealProduct.deal_id) .join( DealProductService, and_( DealProductService.deal_id == Deal.id, DealProductService.product_id == DealProduct.product_id, ) ) .join(Service, DealProductService.service_id == Service.id) .where(Deal.is_deleted == False) .group_by(Deal.id) ) @staticmethod def _get_joined_deals_and_statuses(sub_deal_product_services: Subquery, sub_deals_dates: Subquery): return ( select( sub_deal_product_services.c.deal_id, func.date_trunc( "day", sub_deals_dates.c.date ).label("date"), sub_deal_product_services.c.revenue.label("revenue"), sub_deal_product_services.c.profit.label("profit"), ) .join(sub_deals_dates, sub_deal_product_services.c.deal_id == sub_deals_dates.c.deal_id) ) def _group_by_date(self, stmt): all_dates = generate_date_range(self.date_from, self.date_to, ["deals_count", "revenue", "profit", "expenses"]) deals = ( select( stmt.c.date, func.count(stmt.c.deal_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) .group_by(stmt.c.date) .subquery() ) deals_with_filled_gaps = ( select( all_dates.c.date, (all_dates.c.deals_count + func.coalesce(deals.c.deals_count, 0)).label("deals_count"), (all_dates.c.revenue + func.coalesce(deals.c.revenue, 0)).label("revenue"), (all_dates.c.profit + func.coalesce(deals.c.profit, 0)).label("profit"), literal(0).label("expenses"), ) .join(deals, all_dates.c.date == deals.c.date, isouter=True) .order_by(all_dates.c.date.asc()) ) return deals_with_filled_gaps @staticmethod def _group_by_deals(stmt_union: Subquery): return ( select( stmt_union.c.deal_id, stmt_union.c.date, func.sum(stmt_union.c.profit).label("profit"), func.sum(stmt_union.c.revenue).label("revenue"), ) .group_by(stmt_union.c.deal_id, stmt_union.c.date) ) @staticmethod def _join_and_group_by_clients(stmt): return ( select( Client.id, Client.name.label("grouped_value"), func.count(stmt.c.deal_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) .join(Deal, Deal.id == stmt.c.deal_id) .join(Client, Client.id == Deal.client_id) .group_by(Client.id, Client.name) ) @staticmethod def _join_and_group_by_statuses(stmt): return ( select( Deal.current_status.label("grouped_value"), func.count(stmt.c.deal_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) .join(Deal, Deal.id == stmt.c.deal_id) .group_by(Deal.current_status) ) @staticmethod def _join_and_group_by_warehouses(stmt): return ( select( ShippingWarehouse.id, ShippingWarehouse.name.label("grouped_value"), ShippingWarehouse.is_deleted, func.count(stmt.c.deal_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) .join(Deal, Deal.id == stmt.c.deal_id) .join(ShippingWarehouse, Deal.shipping_warehouse_id == ShippingWarehouse.id) .where(ShippingWarehouse.is_deleted == False) .group_by(ShippingWarehouse.is_deleted, ShippingWarehouse.id, ShippingWarehouse.name) ) @staticmethod def _join_and_group_by_marketplaces(stmt): return ( select( BaseMarketplace.name.label("grouped_value"), func.count(stmt.c.deal_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) .join(Deal, Deal.id == stmt.c.deal_id) .join(BaseMarketplace, Deal.base_marketplace_key == BaseMarketplace.key) .group_by(BaseMarketplace.name) ) @staticmethod def _join_and_group_by_managers(stmt): managers = ( select(User) .where(User.role_key == "employee") .subquery() ) return ( select( managers.c.id, (managers.c.first_name + " " + managers.c.second_name).label("grouped_value"), func.count(stmt.c.deal_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) .join(Deal, Deal.id == stmt.c.deal_id) .join(managers, managers.c.id == Deal.manager_id) .group_by(managers.c.id, "grouped_value") ) async def _get_data_rows_grouped_by_date( self, stmt_deal_services, stmt_deal_product_services, sub_deals_dates: Subquery, ): sub_deal_product_services = stmt_deal_product_services.subquery() stmt_join_deals_statuses = self._get_joined_deals_and_statuses(sub_deal_product_services, sub_deals_dates) sub_union = union_all(stmt_deal_services, stmt_join_deals_statuses).subquery() 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 = 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_transactions) rows = result.all() return rows 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) stmt_deal_services = self._get_stmt_deal_services(sub_deals_dates) stmt_deal_product_services = self._get_stmt_product_services() stmt_deal_services, stmt_deal_product_services = self._apply_filters( request, stmt_deal_services, stmt_deal_product_services ) rows = await self._get_data_rows_grouped_by_date( stmt_deal_services, stmt_deal_product_services, sub_deals_dates ) return self._to_schema(rows, is_chart) async def get_profit_chart_data(self, request: GetProfitChartDataRequest) -> GetProfitChartDataResponse: data = await self._get_data_grouped_by_date(request) return GetProfitChartDataResponse(data=data) async def _get_table_grouped_by_dates(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: data = await self._get_data_grouped_by_date(request, False) return GetProfitTableDataResponse(data=data) async def _table_data_from_stmt(self, stmt) -> GetProfitTableDataResponse: result = await self.session.execute(stmt) rows = result.all() data = [] for row in rows: data.append(ProfitTableDataItem( grouped_value=row.grouped_value, revenue=row.revenue, profit=row.profit, deals_count=row.deals_count, )) return GetProfitTableDataResponse(data=data) 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) stmt_deal_services = self._get_stmt_deal_services(sub_deals_dates) stmt_deal_product_services = self._get_stmt_product_services() stmt_deal_product_services = ( select( stmt_deal_product_services.c.deal_id, func.date_trunc( "day", Deal.created_at ).label("date"), stmt_deal_product_services.c.revenue.label("revenue"), stmt_deal_product_services.c.profit.label("profit"), ) .join(Deal, Deal.id == stmt_deal_product_services.c.deal_id) ) stmt_deal_services, stmt_deal_product_services = self._apply_filters( request, stmt_deal_services, stmt_deal_product_services ) sub_union = union_all(stmt_deal_services, stmt_deal_product_services).subquery() sub_grouped_by_deals = self._group_by_deals(sub_union) return sub_grouped_by_deals async def _get_table_grouped_by_clients(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: sub_grouped_by_deals = self._get_common_table_grouped(request) stmt_grouped_by_clients = self._join_and_group_by_clients(sub_grouped_by_deals) return await self._table_data_from_stmt(stmt_grouped_by_clients) async def _get_table_grouped_by_statuses(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: date_from, date_to = request.date_range sub_deals_dates = self._get_filtered_sub_status_history(date_from, date_to) stmt_deal_services = self._get_stmt_deal_services(sub_deals_dates) stmt_deal_product_services = self._get_stmt_product_services() stmt_deal_services, stmt_deal_product_services = self._apply_filters( request, stmt_deal_services, stmt_deal_product_services ) stmt_join_deals_statuses = self._get_joined_deals_and_statuses( stmt_deal_product_services.subquery(), sub_deals_dates ) sub_union = union_all(stmt_deal_services, stmt_join_deals_statuses).subquery() sub_grouped_by_deals = self._group_by_deals(sub_union) stmt_grouped_by_date = self._join_and_group_by_statuses(sub_grouped_by_deals) return await self._table_data_from_stmt(stmt_grouped_by_date) async def _get_table_grouped_by_warehouses(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: sub_grouped_by_deals = self._get_common_table_grouped(request) stmt_grouped_by_warehouses = self._join_and_group_by_warehouses(sub_grouped_by_deals) return await self._table_data_from_stmt(stmt_grouped_by_warehouses) async def _get_table_grouped_by_marketplace(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: sub_grouped_by_deals = self._get_common_table_grouped(request) stmt_grouped_by_marketplaces = self._join_and_group_by_marketplaces(sub_grouped_by_deals) return await self._table_data_from_stmt(stmt_grouped_by_marketplaces) async def _get_table_grouped_by_managers(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: sub_grouped_by_deals = self._get_common_table_grouped(request) stmt_grouped_by_managers = self._join_and_group_by_managers(sub_grouped_by_deals) return await self._table_data_from_stmt(stmt_grouped_by_managers) async def get_profit_table_data(self, request: GetProfitTableDataRequest) -> GetProfitTableDataResponse: match request.group_table_by: case ProfitTableGroupBy.BY_DATES: return await self._get_table_grouped_by_dates(request) case ProfitTableGroupBy.BY_CLIENTS: return await self._get_table_grouped_by_clients(request) case ProfitTableGroupBy.BY_STATUSES: return await self._get_table_grouped_by_statuses(request) case ProfitTableGroupBy.BY_WAREHOUSES: return await self._get_table_grouped_by_warehouses(request) case ProfitTableGroupBy.BY_MARKETPLACES: return await self._get_table_grouped_by_marketplace(request) case ProfitTableGroupBy.BY_MANAGERS: return await self._get_table_grouped_by_managers(request) raise HTTPException(status_code=400, detail='Указана некорректная группировка')