change price type int -> decimal field

This commit is contained in:
behruz-dev
2025-11-07 12:17:57 +05:00
parent c113f003df
commit 8dca5505d8
35 changed files with 584 additions and 195 deletions

View File

@@ -1,15 +1,26 @@
from django.contrib import admin
from core.apps.counterparty.models import Counterparty, CounterpartyFolder
from core.apps.counterparty.models import Counterparty, CounterpartyFolder, CounterpartyBalance
class CounterpartyBalanceInline(admin.StackedInline):
model = CounterpartyBalance
can_delete = False
verbose_name_plural = 'Balance'
fk_name = 'counterparty'
extra = 0
@admin.register(Counterparty)
class CounterpartyAdmin(admin.ModelAdmin):
list_display = ['id', 'name', 'phone', 'type', 'inn']
inlines = [CounterpartyBalanceInline]
def get_queryset(self, request):
return super().get_queryset(request).select_related('balance')
@admin.register(CounterpartyFolder)
class CounterpartyFolderAdmin(admin.ModelAdmin):
list_display = ['id', 'name']
list_filter = ['name']
list_filter = ['name']

View File

@@ -0,0 +1,71 @@
# Generated by Django 5.2.4 on 2025-11-07 11:09
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('counterparty', '0007_alter_counterparty_balance'),
]
operations = [
migrations.RemoveField(
model_name='counterparty',
name='balance',
),
migrations.RemoveField(
model_name='counterparty',
name='balance_currency',
),
migrations.RemoveField(
model_name='counterparty',
name='balance_date',
),
migrations.RemoveField(
model_name='counterparty',
name='debit_usd',
),
migrations.RemoveField(
model_name='counterparty',
name='debit_uzs',
),
migrations.RemoveField(
model_name='counterparty',
name='kredit_usd',
),
migrations.RemoveField(
model_name='counterparty',
name='kredit_uzs',
),
migrations.RemoveField(
model_name='counterparty',
name='total_debit',
),
migrations.RemoveField(
model_name='counterparty',
name='total_kredit',
),
migrations.CreateModel(
name='CounterpartyBalance',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('balance_uzs', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)),
('balance_usd', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)),
('balance_date', models.DateField(blank=True, null=True)),
('kredit_usd', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)),
('kredit_uzs', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)),
('debit_usd', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)),
('debit_uzs', models.DecimalField(decimal_places=2, default=0.0, max_digits=15)),
('counterparty', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='balance', to='counterparty.counterparty')),
],
options={
'verbose_name': 'Kontragent Balansi',
'verbose_name_plural': 'Kontragent Balanslari',
},
),
]

View File

@@ -1,3 +1,5 @@
from decimal import Decimal
from django.db import models
from core.apps.shared.models import BaseModel, Region, District
@@ -41,24 +43,71 @@ class Counterparty(BaseModel):
district = models.ForeignKey(
District, on_delete=models.SET_NULL, null=True, blank=True, related_name='counterparties'
)
balance = models.BigIntegerField(null=True, blank=True)
balance_currency = models.CharField(
max_length=3, choices=[('usd', 'usd'), ('uzs', 'uzs')], null=True, blank=True
)
balance_date = models.DateField(null=True, blank=True)
comment = models.TextField(null=True, blank=True)
is_archived = models.BooleanField(default=False)
debit_usd = models.BigIntegerField(default=0, null=True, blank=True)
debit_uzs = models.BigIntegerField(default=0, null=True, blank=True)
total_debit = models.BigIntegerField(default=0, null=True, blank=True)
kredit_usd = models.BigIntegerField(default=0, null=True, blank=True)
kredit_uzs = models.BigIntegerField(default=0, null=True, blank=True)
total_kredit = models.BigIntegerField(default=0, null=True, blank=True)
def __str__(self):
return self.name
class Meta:
verbose_name = 'Kontragent'
verbose_name_plural = 'Kontragentlar'
class CounterpartyBalance(BaseModel):
counterparty = models.OneToOneField(Counterparty, on_delete=models.CASCADE, related_name='balance')
# balance
balance_uzs = models.DecimalField(max_digits=15, decimal_places=2, default=0.00)
balance_usd = models.DecimalField(max_digits=15, decimal_places=2, default=0.00)
# balance date
balance_date = models.DateField(null=True, blank=True)
# kreditor -> qazrdorlik
kredit_usd = models.DecimalField(max_digits=15, decimal_places=2, default=0.00)
kredit_uzs = models.DecimalField(max_digits=15, decimal_places=2, default=0.00)
# debitor -> xaqdorlik
debit_usd = models.DecimalField(max_digits=15, decimal_places=2, default=0.00)
debit_uzs = models.DecimalField(max_digits=15, decimal_places=2, default=0.00)
def save(self, *args, **kwargs):
if self.balance_usd > 0:
self.debit_usd = self.balance_usd
self.kredit_usd = Decimal('0.00')
elif self.balance_usd < 0:
self.kredit_usd = abs(self.balance_usd)
self.debit_usd = Decimal('0.00')
if self.balance_uzs > 0:
self.debit_uzs = self.balance_uzs
self.kredit_uzs = Decimal('0.00')
elif self.balance_uzs < 0:
self.kredit_uzs = abs(self.balance_uzs)
self.debit_uzs = Decimal('0.00')
super().save(*args, **kwargs)
total_usd = self.total_balance_usd
total_uzs = self.total_balance_uzs
if total_usd < 0 or total_uzs < 0:
self.counterparty.status = 'CREDITOR'
elif total_usd > 0 or total_uzs > 0:
self.counterparty.status = 'DEBITOR'
else:
self.counterparty.status = 'CREDITOR'
self.counterparty.save(update_fields=['status'])
@property
def total_balance_usd(self):
return (self.debit_usd or Decimal(0)) - (self.kredit_usd or Decimal(0))
@property
def total_balance_uzs(self):
return (self.debit_uzs or Decimal(0)) - (self.kredit_uzs or Decimal(0))
def __str__(self):
return f"{self.counterparty.name} | USD: {self.total_balance_usd} | UZS: {self.total_balance_uzs}"
class Meta:
verbose_name = "Kontragent Balansi"
verbose_name_plural = "Kontragent Balanslari"

View File

@@ -2,19 +2,25 @@ from django.db import transaction
from rest_framework import serializers
from core.apps.counterparty.models import Counterparty, CounterpartyFolder
from core.apps.counterparty.models import Counterparty, CounterpartyFolder, CounterpartyBalance
from core.apps.shared.models import Region, District
from core.apps.counterparty.serializers.counterparty_balance import (
CounterpartyBalanceSerializer,
CounterpartyBalanceCreateSerializer,
CounterpartyBalanceUpdateSerializer
)
class CounterpartyListSerializer(serializers.ModelSerializer):
balance = CounterpartyBalanceSerializer()
class Meta:
model = Counterparty
fields = [
'id', 'inn', 'name', 'phone', 'type', 'folder', 'type', 'region', 'district',
'balance', 'balance_currency', 'balance_date', 'comment', 'is_archived',
'kredit_usd', 'kredit_uzs', 'total_kredit', 'debit_usd', 'debit_uzs', 'total_debit',
'comment', 'is_archived', 'balance',
]
class CounterpartySerializer(serializers.ModelSerializer):
class Meta:
@@ -32,9 +38,8 @@ class CounterpartyCreateSerializer(serializers.Serializer):
folder_id = serializers.UUIDField(required=False)
region_id = serializers.UUIDField(required=False)
district_id = serializers.UUIDField(required=False)
balance = serializers.IntegerField(required=False)
balance_date = serializers.DateField(required=False)
comment = serializers.CharField(required=False)
balance = CounterpartyBalanceCreateSerializer(required=False)
def validate(self, data):
if data.get('folder_id'):
@@ -55,7 +60,7 @@ class CounterpartyCreateSerializer(serializers.Serializer):
def create(self, validated_data):
with transaction.atomic():
return Counterparty.objects.create(
counterparty = Counterparty.objects.create(
inn=validated_data.get('inn'),
name=validated_data.get('name'),
phone=validated_data.get('phone'),
@@ -63,18 +68,25 @@ class CounterpartyCreateSerializer(serializers.Serializer):
folder=validated_data.get('folder'),
region=validated_data.get('region'),
district=validated_data.get('district'),
balance=validated_data.get('balance'),
balance_date=validated_data.get('balance_date'),
comment=validated_data.get('comment'),
)
CounterpartyBalance.objects.create(
counterparty=counterparty,
balance_uzs=validated_data.get('balance').get('balance_uzs'),
balance_usd=validated_data.get('balance').get('balance_usd'),
balance_date=validated_data.get('balance').get('balance_date'),
)
return counterparty
class CounterpartyUpdateSerializer(serializers.ModelSerializer):
balance = CounterpartyBalanceUpdateSerializer(required=False)
class Meta:
model = Counterparty
fields = [
'inn', 'name', 'phone', 'type', 'folder', 'region', 'district', 'balance',
'balance_currency', 'balance_date', 'comment'
'inn', 'name', 'phone', 'type', 'folder', 'region', 'district',
'balance', 'comment'
]
extra_kwargs = {
'name': {'required': False},
@@ -82,8 +94,24 @@ class CounterpartyUpdateSerializer(serializers.ModelSerializer):
'folder': {'required': False},
'region': {'required': False},
'district': {'required': False},
'balance': {'required': False},
'balance_currency': {'required': False},
'balance_date': {'required': False},
'comment': {'required': False}
}
}
def update(self, instance, validated_data):
instance.inn = validated_data.get('inn', instance.inn)
instance.name = validated_data.get('name', instance.name)
instance.phone = validated_data.get('phone', instance.phone)
instance.type = validated_data.get('type', instance.type)
instance.folder = validated_data.get('folder', instance.folder)
instance.region = validated_data.get('region', instance.region)
instance.district = validated_data.get('district', instance.district)
instance.comment = validated_data.get('district', instance.comment)
# balance
balance_data = validated_data.get('balance')
instance.balance.balance_uzs = balance_data.get('balance_uzs', instance.balance.balance_uzs)
instance.balance.balance_usd = balance_data.get('balance_usd', instance.balance.balance_usd)
instance.balance.balance_date = balance_data.get('balance_date', instance.balance.balance_date)
instance.balance.save()
instance.save()
return instance

View File

@@ -0,0 +1,28 @@
from rest_framework import serializers
from core.apps.counterparty.models import CounterpartyBalance
class CounterpartyBalanceSerializer(serializers.ModelSerializer):
class Meta:
model = CounterpartyBalance
fields = [
'id', 'balance_uzs', 'balance_usd', 'balance_date', 'kredit_usd', 'kredit_uzs',
'debit_usd', 'debit_uzs', 'total_balance_usd', 'total_balance_uzs',
]
class CounterpartyBalanceCreateSerializer(serializers.ModelSerializer):
class Meta:
model = CounterpartyBalance
fields = [
'balance_uzs', 'balance_usd', 'balance_date'
]
class CounterpartyBalanceUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = CounterpartyBalance
fields = [
'balance_uzs', 'balance_usd', 'balance_date'
]

View File

@@ -17,7 +17,6 @@ urlpatterns = [
path('all/', cp_views.CounterpartiesApiView.as_view()),
path("<uuid:id>/", cp_views.CounterpartyDetailApiView.as_view()),
path('<uuid:id>/un_archive/', cp_views.UnArchiveCounterpartyApiView.as_view()),
path('all/', cp_views.AllCounterpartyListApiView.as_view()),
path("<uuid:id>/statistics/", cp_views.CounterPartyIncomeExpenceStatisticsApiView.as_view()),
]
)),

View File

@@ -1,21 +1,32 @@
from django.db.models import Sum
from decimal import Decimal
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
from core.apps.counterparty.models import Counterparty, CounterpartyFolder
# 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
class CounterpartyListApiView(generics.ListAPIView):
serializer_class = serializers.CounterpartyListSerializer
queryset = Counterparty.objects.exclude(is_archived=True).exclude(folder__isnull=False)
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]
@@ -63,7 +74,7 @@ class ArchiveCounterpartyApiView(views.APIView):
class ArchivedCounterpartyListApiView(generics.ListAPIView):
serializer_class = serializers.CounterpartyListSerializer
queryset = Counterparty.objects.exclude(is_archived=False)
queryset = Counterparty.objects.exclude(is_archived=False).select_related('balance').order_by('-created_at')
pagination_class = [HasRolePermission]
pagination_class = CustomPageNumberPagination
@@ -89,7 +100,7 @@ class CounterpartyUpdateApiView(generics.UpdateAPIView):
class FolderCounterpartyListApiView(generics.GenericAPIView):
serializer_class = serializers.CounterpartyListSerializer
queryset = Counterparty.objects.exclude(is_archived=True)
queryset = Counterparty.objects.exclude(is_archived=True).select_related('balance').order_by('-created_at')
permission_classes = [HasRolePermission]
filter_backends = [filters.SearchFilter]
search_fields = [
@@ -110,25 +121,44 @@ class CounterpartyStatisticsApiView(views.APIView):
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()
res = queryset.aggregate(
kredit_usd=Sum('kredit_usd'),
kredit_uzs=Sum('kredit_uzs'),
total_kredit=Sum('total_kredit'),
debit_usd=Sum('debit_usd'),
debit_uzs=Sum('debit_uzs'),
total_debut=Sum('total_debit'),
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'),
)
return Response(res)
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
return Response(result)
class CounterpartiesApiView(generics.GenericAPIView):
serializer_class = serializers.CounterpartyListSerializer
queryset = Counterparty.objects.all()
queryset = Counterparty.objects.order_by('-created_at').select_related('balance')
permission_classes = [HasRolePermission]
filter_backends = [filters.SearchFilter]
search_fields = [
@@ -163,101 +193,78 @@ class UnArchiveCounterpartyApiView(views.APIView):
)
class AllCounterpartyListApiView(generics.GenericAPIView):
serializer_class = serializers.CounterpartyListSerializer
queryset = Counterparty.objects.all()
permission_classes = [HasRolePermission]
filter_backends = [filters.SearchFilter]
search_fields = [
'name'
]
def get(self, request):
page = self.paginate_queryset(self.queryset)
if page is not None:
serializer = self.serializer_class(page, many=True)
return self.get_paginated_response(serializer.data)
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': 0, 'count': 0, 'amount_uzs': 0},
'usd': {'total': 0, 'count': 0, 'amount_uzs': 0}
}
# Income va Expence querysetlari
incomes = Income.objects.filter(counterparty=counterparty, is_deleted=False)
expences = Expence.objects.filter(counterparty=counterparty, is_deleted=False)
# Balanslar valyutalar bo'yicha
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 = income.price
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
if currency == 'usd':
income_by_currency[currency]['amount_uzs'] += amount * income.exchange_rate
else:
income_by_currency[currency]['amount_uzs'] += amount
expence_by_currency = {
'uzs': {'total': 0, 'count': 0, 'amount_uzs': 0},
'usd': {'total': 0, 'count': 0, 'amount_uzs': 0}
}
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 = expence.price
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
if currency == 'usd':
expence_by_currency[currency]['amount_uzs'] += amount * expence.exchange_rate
else:
expence_by_currency[currency]['amount_uzs'] += amount
total_income_uzs = sum(data['amount_uzs'] for data in income_by_currency.values())
total_expence_uzs = sum(data['amount_uzs'] for data in expence_by_currency.values())
balance_uzs = total_income_uzs - total_expence_uzs
total_income_usd = income_by_currency.get('usd', {}).get('total', 0)
total_expence_usd = expence_by_currency.get('usd', {}).get('total', 0)
balance_usd = total_income_usd - total_expence_usd
expence_by_currency[currency]['amount_uzs'] += amount * rate if currency == 'usd' else amount
# Income/Expence summalari
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']
# Kontragent balansi
balance_obj, _ = CounterpartyBalance.objects.get_or_create(counterparty=counterparty)
balance_uzs = balance_obj.total_balance_uzs + (total_income_uzs - total_expence_uzs)
balance_usd = balance_obj.total_balance_usd + (total_income_usd - total_expence_usd)
# Status aniqlash
if balance_uzs > 0:
status = 'positive'
elif balance_uzs < 0:
status = 'negative'
else:
status = 'zero'
data = {
'counterparty': {
'id': counterparty.id,
'name': counterparty.name,
'name': counterparty.name
},
'income': {
'by_currency': income_by_currency,
'total_uzs': total_income_uzs,
'total_usd': total_income_usd,
'total_count': sum(data['count'] for data in income_by_currency.values())
'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(data['count'] for data in expence_by_currency.values())
'total_count': sum(v['count'] for v in expence_by_currency.values())
},
'balance': {
'uzs': balance_uzs,
'usd': balance_usd,
'status': 'positive' if balance_uzs > 0 else 'negative' if balance_uzs < 0 else 'zero'
'status': status
}
}
return Response(data, status=200)