399 lines
15 KiB
Python
399 lines
15 KiB
Python
from decimal import Decimal
|
||
from collections import defaultdict
|
||
|
||
from django.db.models import Sum, Count, Q
|
||
from django.shortcuts import get_object_or_404
|
||
|
||
from rest_framework import generics, views, filters
|
||
from rest_framework.response import Response
|
||
|
||
from django_filters.rest_framework.backends import DjangoFilterBackend
|
||
|
||
# accounts
|
||
from core.apps.accounts.permissions.permissions import HasRolePermission
|
||
# shared
|
||
from core.apps.shared.paginations.custom import CustomPageNumberPagination
|
||
# counterparty
|
||
from core.apps.counterparty.models import Counterparty, CounterpartyFolder, CounterpartyBalance
|
||
from core.apps.counterparty.serializers import counterparty as serializers
|
||
from core.apps.counterparty.filters.counterparty import CounterpartyFilter
|
||
# finance
|
||
from core.apps.finance.models import Expence, Income
|
||
from core.apps.finance.serializers.income import IncomeListSerializer
|
||
from core.apps.finance.serializers.expence import ExpenceListSerializer
|
||
# orders
|
||
from core.apps.orders.models import Party
|
||
from core.apps.orders.serializers.party import PartyAKTSerializer
|
||
|
||
|
||
class CounterpartyListApiView(generics.ListAPIView):
|
||
serializer_class = serializers.CounterpartyListSerializer
|
||
queryset = Counterparty.objects\
|
||
.select_related('balance')\
|
||
.exclude(is_archived=True)\
|
||
.exclude(folder__isnull=False)\
|
||
.order_by('-created_at')
|
||
pagination_class = [HasRolePermission]
|
||
pagination_class = CustomPageNumberPagination
|
||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||
filterset_class = CounterpartyFilter
|
||
search_fields = [
|
||
'name', 'inn'
|
||
]
|
||
|
||
|
||
class CounterpartyCreateApiView(generics.GenericAPIView):
|
||
serializer_class = serializers.CounterpartyCreateSerializer
|
||
queryset = Counterparty.objects.all()
|
||
permission_classes = [HasRolePermission]
|
||
|
||
def post(self, request):
|
||
serializer = self.serializer_class(data=request.data)
|
||
if serializer.is_valid(raise_exception=True):
|
||
data = serializer.save()
|
||
return Response(
|
||
{
|
||
'success': True,
|
||
'message': 'Conterparty Created',
|
||
'data': serializers.CounterpartyListSerializer(data).data
|
||
},
|
||
status=201
|
||
)
|
||
return Response(
|
||
{'success': False, 'message': serializer.errors},
|
||
status=400
|
||
)
|
||
|
||
|
||
class ArchiveCounterpartyApiView(views.APIView):
|
||
permission_classes = [HasRolePermission]
|
||
|
||
def get(self, request, id):
|
||
counterparty = get_object_or_404(Counterparty, id=id)
|
||
counterparty.is_archived = True
|
||
counterparty.save()
|
||
return Response(
|
||
{'success': True, 'message': 'counterparty archived'},
|
||
status=200
|
||
)
|
||
|
||
|
||
class ArchivedCounterpartyListApiView(generics.ListAPIView):
|
||
serializer_class = serializers.CounterpartyListSerializer
|
||
queryset = Counterparty.objects.exclude(is_archived=False).select_related('balance').order_by('-created_at')
|
||
pagination_class = [HasRolePermission]
|
||
pagination_class = CustomPageNumberPagination
|
||
|
||
|
||
class CounterpartyDeleteApiView(views.APIView):
|
||
permission_classes = [HasRolePermission]
|
||
|
||
def delete(self, request, id):
|
||
counterparty = get_object_or_404(Counterparty, id=id)
|
||
counterparty.delete()
|
||
return Response(
|
||
{'success': True, 'message': 'counterparty deleted'},
|
||
status=204
|
||
)
|
||
|
||
|
||
class CounterpartyUpdateApiView(generics.UpdateAPIView):
|
||
permission_classes = [HasRolePermission]
|
||
lookup_field = 'id'
|
||
serializer_class = serializers.CounterpartyUpdateSerializer
|
||
queryset = Counterparty.objects.all()
|
||
|
||
|
||
class FolderCounterpartyListApiView(generics.GenericAPIView):
|
||
serializer_class = serializers.CounterpartyListSerializer
|
||
queryset = Counterparty.objects.exclude(is_archived=True).select_related('balance').order_by('-created_at')
|
||
permission_classes = [HasRolePermission]
|
||
filter_backends = [filters.SearchFilter]
|
||
search_fields = [
|
||
'name', 'inn'
|
||
]
|
||
|
||
def get(self, reuqest, folder_id):
|
||
folder = get_object_or_404(CounterpartyFolder, id=folder_id)
|
||
queryset = self.queryset.filter(folder=folder).exclude(folder__isnull=True)
|
||
page = self.paginate_queryset(self.filter_queryset(queryset))
|
||
if page is not None:
|
||
serializer = self.serializer_class(page, many=True)
|
||
return self.get_paginated_response(serializer.data)
|
||
|
||
|
||
class CounterpartyStatisticsApiView(views.APIView):
|
||
permission_classes = [HasRolePermission]
|
||
|
||
def get(self, request):
|
||
counterparty_ids = request.query_params.getlist('counterparty')
|
||
|
||
if counterparty_ids:
|
||
queryset = Counterparty.objects.filter(id__in=counterparty_ids)
|
||
else:
|
||
queryset = Counterparty.objects.all()
|
||
|
||
balance_qs = CounterpartyBalance.objects.filter(counterparty__in=queryset)
|
||
|
||
stats = balance_qs.aggregate(
|
||
total_balance_uzs=Sum('balance_uzs'),
|
||
total_balance_usd=Sum('balance_usd'),
|
||
total_debit_uzs=Sum('debit_uzs'),
|
||
total_kredit_uzs=Sum('kredit_uzs'),
|
||
total_debit_usd=Sum('debit_usd'),
|
||
total_kredit_usd=Sum('kredit_usd'),
|
||
)
|
||
|
||
counterparty_stats = queryset.aggregate(
|
||
total_counterparties=Count('id'),
|
||
total_creditors=Count('id', filter=Q(status='CREDITOR')),
|
||
total_debtors=Count('id', filter=Q(status='DEBITOR')),
|
||
)
|
||
|
||
result = {
|
||
**counterparty_stats,
|
||
**stats,
|
||
}
|
||
|
||
for key, value in result.items():
|
||
if value is None:
|
||
result[key] = 0
|
||
|
||
result['total_debit_uzs'] = abs(Decimal(result['total_debit_uzs']))
|
||
result['total_debit_usd'] = abs(Decimal(result['total_debit_usd']))
|
||
|
||
return Response(result)
|
||
|
||
|
||
class CounterpartiesApiView(generics.GenericAPIView):
|
||
serializer_class = serializers.CounterpartyListSerializer
|
||
queryset = Counterparty.objects.order_by('-created_at').select_related('balance')
|
||
permission_classes = [HasRolePermission]
|
||
filter_backends = [filters.SearchFilter]
|
||
search_fields = [
|
||
'name', 'inn'
|
||
]
|
||
|
||
def get(self, request):
|
||
page = self.paginate_queryset(self.filter_queryset(self.queryset))
|
||
if page is not None:
|
||
ser = self.serializer_class(page, many=True)
|
||
return self.get_paginated_response(ser.data)
|
||
|
||
|
||
class CounterpartyDetailApiView(views.APIView):
|
||
permission_classes = [HasRolePermission]
|
||
|
||
def get(self, request, id):
|
||
obj = get_object_or_404(Counterparty, id=id)
|
||
serializer = serializers.CounterpartyListSerializer(obj)
|
||
return Response(serializer.data, status=200)
|
||
|
||
|
||
class UnArchiveCounterpartyApiView(views.APIView):
|
||
permission_classes = [HasRolePermission]
|
||
|
||
def get(self, request, id):
|
||
obj = get_object_or_404(Counterparty, id=id)
|
||
obj.is_archived = False
|
||
obj.save()
|
||
return Response(
|
||
{'success': True, 'message': 'Conterparty unarchived'}, status=200
|
||
)
|
||
|
||
|
||
class CounterPartyIncomeExpenceStatisticsApiView(views.APIView):
|
||
permission_classes = [HasRolePermission]
|
||
|
||
def get(self, request, id):
|
||
counterparty = get_object_or_404(Counterparty, id=id)
|
||
|
||
incomes = Income.objects.filter(counterparty=counterparty, is_deleted=False)
|
||
expences = Expence.objects.filter(counterparty=counterparty, is_deleted=False)
|
||
|
||
income_by_currency = {'uzs': {'total': Decimal(0), 'count': 0, 'amount_uzs': Decimal(0)},
|
||
'usd': {'total': Decimal(0), 'count': 0, 'amount_uzs': Decimal(0)}}
|
||
for income in incomes:
|
||
currency = income.currency
|
||
amount = Decimal(income.price or 0)
|
||
rate = Decimal(income.exchange_rate or 1)
|
||
income_by_currency[currency]['total'] += amount
|
||
income_by_currency[currency]['count'] += 1
|
||
income_by_currency[currency]['amount_uzs'] += amount * rate if currency == 'usd' else amount
|
||
|
||
expence_by_currency = {'uzs': {'total': Decimal(0), 'count': 0, 'amount_uzs': Decimal(0)},
|
||
'usd': {'total': Decimal(0), 'count': 0, 'amount_uzs': Decimal(0)}}
|
||
for expence in expences:
|
||
currency = expence.currency
|
||
amount = Decimal(expence.price or 0)
|
||
rate = Decimal(expence.exchange_rate or 1)
|
||
expence_by_currency[currency]['total'] += amount
|
||
expence_by_currency[currency]['count'] += 1
|
||
expence_by_currency[currency]['amount_uzs'] += amount * rate if currency == 'usd' else amount
|
||
|
||
total_income_uzs = sum(v['amount_uzs'] for v in income_by_currency.values())
|
||
total_expence_uzs = sum(v['amount_uzs'] for v in expence_by_currency.values())
|
||
total_income_usd = income_by_currency['usd']['total']
|
||
total_expence_usd = expence_by_currency['usd']['total']
|
||
|
||
balance_uzs = counterparty.balance.total_balance_uzs
|
||
balance_usd = counterparty.balance.total_balance_usd
|
||
|
||
if balance_uzs > 0:
|
||
status = 'positive'
|
||
elif balance_uzs < 0:
|
||
status = 'negative'
|
||
else:
|
||
status = 'zero'
|
||
|
||
data = {
|
||
'counterparty': {
|
||
'id': counterparty.id,
|
||
'name': counterparty.name
|
||
},
|
||
'income': {
|
||
'by_currency': income_by_currency,
|
||
'total_uzs': total_income_uzs,
|
||
'total_usd': total_income_usd,
|
||
'total_count': sum(v['count'] for v in income_by_currency.values())
|
||
},
|
||
'expence': {
|
||
'by_currency': expence_by_currency,
|
||
'total_uzs': total_expence_uzs,
|
||
'total_usd': total_expence_usd,
|
||
'total_count': sum(v['count'] for v in expence_by_currency.values())
|
||
},
|
||
'balance': {
|
||
'uzs': balance_uzs,
|
||
'usd': balance_usd,
|
||
'status': status,
|
||
'total_balance': counterparty.balance.total_balance
|
||
}
|
||
}
|
||
|
||
return Response(data, status=200)
|
||
|
||
|
||
|
||
class CounterpartyAKTApiView(views.APIView):
|
||
permission_classes = [HasRolePermission]
|
||
|
||
def get(self, request, id):
|
||
# TODO: filterlar
|
||
date = request.query_params.get('date', None)
|
||
end_date = request.query_params.get('end_date', None)
|
||
project_folder = request.query_params.getlist('folder', None)
|
||
project = request.query_params.getlist('project', None)
|
||
currency = request.query_params.get('currency', 'uzs')
|
||
|
||
counterparty = get_object_or_404(Counterparty, id=id)
|
||
parties = Party.objects.prefetch_related('orders', 'orders__product').filter(
|
||
orders__counterparty=counterparty, is_deleted=False, process=100, payment_percentage=100
|
||
).distinct().order_by('-created_at')
|
||
expences = Expence.objects.select_related().filter(counterparty=counterparty, is_deleted=False).distinct().order_by('-created_at')
|
||
incomes = Income.objects.filter(counterparty=counterparty, is_deleted=False).distinct().order_by('-created_at')
|
||
|
||
# TODO: date va end date boyicha filter
|
||
if date:
|
||
parties = parties.filter(close_date__gte=date)
|
||
expences = expences.filter(created_at__gte=date)
|
||
incomes = incomes.filter(created_at__gte=date)
|
||
if end_date:
|
||
parties = parties.filter(close_date__lte=end_date)
|
||
expences = expences.filter(created_at__lte=end_date)
|
||
incomes = incomes.filter(created_at__lte=end_date)
|
||
|
||
# TODO: project folder va project boyicha filter
|
||
if project_folder:
|
||
parties = parties.filter(orders__project_folder=project_folder).distinct()
|
||
expences = expences.filter(project_folder=project_folder)
|
||
incomes = incomes.filter(project_folder=project_folder)
|
||
if project:
|
||
parties = parties.filter(orders__project=project).distinct()
|
||
expences = expences.filter(project=project)
|
||
incomes = incomes.filter(project=project)
|
||
|
||
# TODO: currency boyicha filter
|
||
if currency:
|
||
parties = parties.filter(currency=currency)
|
||
expences = expences.filter(currency=currency)
|
||
incomes = incomes.filter(currency=currency)
|
||
|
||
# TODO: date boyicha guruhlash
|
||
daily_breakdown = self._group_by_date(parties, incomes, expences)
|
||
|
||
# TODO: total kreditni hisoblash kerak: Sum(party total_price) + Sum(income total_price)
|
||
parties_total_price = parties.aggregate(total_price=Sum('party_amount__total_price'))['total_price'] or 0
|
||
income_total_price = incomes.aggregate(total_price=Sum('price'))['total_price'] or 0
|
||
total_kredit = Decimal(parties_total_price) + Decimal(income_total_price)
|
||
|
||
# TODO: total debitni hisoblash kerak: Sum(expence total_price)
|
||
expence_total_balance = expences.aggregate(total_price=Sum('price'))['total_price'] or 0
|
||
total_debit = expence_total_balance
|
||
|
||
# TODO: final balanceni hisoblash kerak: total_kredit - total_debit = final balance => negative or positive
|
||
final_balance = total_kredit - total_debit
|
||
|
||
# TODO: final balance typeni topish kerak -> debit or kredit: if negative == debit, positive == kredit
|
||
type = 'debit' if final_balance < 0 else 'kredit'
|
||
|
||
response = {
|
||
"daily_breakdown": daily_breakdown,
|
||
"total_kredit": str(total_kredit),
|
||
"total_debit": str(total_debit),
|
||
"final_balance": {
|
||
"balance": str(final_balance) if not str(final_balance).startswith('-') else str(final_balance).replace('-', ''),
|
||
"type": type,
|
||
}
|
||
}
|
||
|
||
return Response(response, status=200)
|
||
|
||
def _group_by_date(self, parties, incomes, expences):
|
||
daily_data = defaultdict(list)
|
||
|
||
# PARTIYALAR (kirim)
|
||
for party in parties:
|
||
date_key = party.closed_date.strftime('%Y-%m-%d')
|
||
|
||
daily_data[date_key].append({
|
||
"type": "kirim",
|
||
"price": str(getattr(party.party_amount, 'calculated_amount', 0) or 0),
|
||
"partiya": PartyAKTSerializer(party).data,
|
||
"kirim": None,
|
||
"chiqim": None
|
||
})
|
||
|
||
# INCOMES (kirim)
|
||
for income in incomes:
|
||
date_key = income.created_at.strftime('%Y-%m-%d')
|
||
|
||
daily_data[date_key].append({
|
||
"type": "kirim",
|
||
"price": str(income.price or 0),
|
||
"partiya": None, # ❗ KIRIM ichida PARTIYA BO‘LMAYDI
|
||
"kirim": IncomeListSerializer(income).data,
|
||
"chiqim": None
|
||
})
|
||
|
||
# EXPENCES (chiqim)
|
||
for expence in expences:
|
||
date_key = expence.created_at.strftime('%Y-%m-%d')
|
||
|
||
daily_data[date_key].append({
|
||
"type": "chiqim",
|
||
"price": str(expence.price or 0),
|
||
"partiya": None,
|
||
"kirim": None,
|
||
"chiqim": ExpenceListSerializer(expence).data
|
||
})
|
||
|
||
# Yakuniy format
|
||
result = []
|
||
for date_key in sorted(daily_data.keys(), reverse=True):
|
||
result.append({
|
||
"date": date_key,
|
||
"items": daily_data[date_key]
|
||
})
|
||
|
||
return result |