From ffe9215892abca70102f606463ecc6126736b2a3 Mon Sep 17 00:00:00 2001 From: behruz-dev Date: Mon, 8 Sep 2025 17:32:39 +0500 Subject: [PATCH] add: add income model and income create and list apig --- core/apps/finance/admin/__init__.py | 3 +- core/apps/finance/admin/income.py | 9 ++ core/apps/finance/migrations/0007_income.py | 42 ++++++++ ...udit_alter_income_counterparty_and_more.py | 36 +++++++ core/apps/finance/models/income.py | 8 +- .../finance/serializers/cash_transaction.py | 14 ++- core/apps/finance/serializers/income.py | 97 +++++++++++++++++++ core/apps/finance/urls.py | 7 ++ core/apps/finance/views/income.py | 49 ++++++++++ 9 files changed, 256 insertions(+), 9 deletions(-) create mode 100644 core/apps/finance/admin/income.py create mode 100644 core/apps/finance/migrations/0007_income.py create mode 100644 core/apps/finance/migrations/0008_alter_income_audit_alter_income_counterparty_and_more.py create mode 100644 core/apps/finance/serializers/income.py create mode 100644 core/apps/finance/views/income.py diff --git a/core/apps/finance/admin/__init__.py b/core/apps/finance/admin/__init__.py index 2feea35..ecdca82 100644 --- a/core/apps/finance/admin/__init__.py +++ b/core/apps/finance/admin/__init__.py @@ -1,3 +1,4 @@ from .cash_transaction import * from .payment_type import * -from .type_income import * \ No newline at end of file +from .type_income import * +from .income import * \ No newline at end of file diff --git a/core/apps/finance/admin/income.py b/core/apps/finance/admin/income.py new file mode 100644 index 0000000..b392c2c --- /dev/null +++ b/core/apps/finance/admin/income.py @@ -0,0 +1,9 @@ +from django.contrib import admin + +from core.apps.finance.models import Income + + +@admin.register(Income) +class IncomeAdmin(admin.ModelAdmin): + list_display = ['id', 'price', 'cash_transaction'] + list_filter = ['cash_transaction', 'payment_type'] diff --git a/core/apps/finance/migrations/0007_income.py b/core/apps/finance/migrations/0007_income.py new file mode 100644 index 0000000..ccbe6b4 --- /dev/null +++ b/core/apps/finance/migrations/0007_income.py @@ -0,0 +1,42 @@ +# Generated by Django 5.2.4 on 2025-09-08 16:18 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('counterparty', '0004_counterparty_status'), + ('finance', '0006_typeincome'), + ('projects', '0016_estimatework_employee_estimatework_end_date_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Income', + 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)), + ('currency', models.CharField(choices=[('uzs', 'uzs'), ('usd', 'usd')], max_length=3)), + ('price', models.PositiveBigIntegerField()), + ('exchange_rate', models.PositiveBigIntegerField(default=0)), + ('date', models.DateField()), + ('comment', models.TextField(blank=True, null=True)), + ('file', models.FileField(blank=True, null=True, upload_to='finance/income/file/')), + ('audit', models.CharField(max_length=200)), + ('cash_transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incomes', to='finance.cashtransaction')), + ('counterparty', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incomes', to='counterparty.counterparty')), + ('payment_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incomes', to='finance.paymenttype')), + ('project', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incomes', to='projects.project')), + ('project_folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incomes', to='projects.projectfolder')), + ('type_income', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incomes', to='finance.typeincome')), + ], + options={ + 'verbose_name': 'kirim', + 'verbose_name_plural': 'kirimlar', + }, + ), + ] diff --git a/core/apps/finance/migrations/0008_alter_income_audit_alter_income_counterparty_and_more.py b/core/apps/finance/migrations/0008_alter_income_audit_alter_income_counterparty_and_more.py new file mode 100644 index 0000000..ebed460 --- /dev/null +++ b/core/apps/finance/migrations/0008_alter_income_audit_alter_income_counterparty_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 5.2.4 on 2025-09-08 16:19 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('counterparty', '0004_counterparty_status'), + ('finance', '0007_income'), + ('projects', '0016_estimatework_employee_estimatework_end_date_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='income', + name='audit', + field=models.CharField(blank=True, max_length=200, null=True), + ), + migrations.AlterField( + model_name='income', + name='counterparty', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incomes', to='counterparty.counterparty'), + ), + migrations.AlterField( + model_name='income', + name='project', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incomes', to='projects.project'), + ), + migrations.AlterField( + model_name='income', + name='type_income', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incomes', to='finance.typeincome'), + ), + ] diff --git a/core/apps/finance/models/income.py b/core/apps/finance/models/income.py index ee4f6f0..3445041 100644 --- a/core/apps/finance/models/income.py +++ b/core/apps/finance/models/income.py @@ -14,13 +14,13 @@ class Income(BaseModel): 'projects.ProjectFolder', on_delete=models.CASCADE, related_name='incomes' ) project = models.ForeignKey( - 'projects.Project', on_delete=models.SET_NULL, related_name='incomes', null=True + 'projects.Project', on_delete=models.SET_NULL, related_name='incomes', null=True, blank=True ) counterparty = models.ForeignKey( - Counterparty, on_delete=models.SET_NULL, related_name='incomes', null=True + Counterparty, on_delete=models.SET_NULL, related_name='incomes', null=True, blank=True ) type_income = models.ForeignKey( - TypeIncome, on_delete=models.SET_NULL, related_name='incomes', null=True + TypeIncome, on_delete=models.SET_NULL, related_name='incomes', null=True, blank=True ) currency = models.CharField(choices=[('uzs', 'uzs'),('usd', 'usd')], max_length=3) @@ -29,7 +29,7 @@ class Income(BaseModel): date = models.DateField() comment = models.TextField(null=True, blank=True) file = models.FileField(upload_to='finance/income/file/', null=True, blank=True) - audit = models.CharField(max_length=200) + audit = models.CharField(max_length=200, null=True, blank=True) def __str__(self): return f'{self.cash_transaction} kassa uchun kirim {self.price}' diff --git a/core/apps/finance/serializers/cash_transaction.py b/core/apps/finance/serializers/cash_transaction.py index 5f8667d..1070e77 100644 --- a/core/apps/finance/serializers/cash_transaction.py +++ b/core/apps/finance/serializers/cash_transaction.py @@ -39,16 +39,22 @@ class CashTransactionUpdateSerializer(serializers.ModelSerializer): 'name', 'payment_type', 'employees', 'status', 'folder', ] + class CashTransactionCreateSerializer(serializers.Serializer): payment_type_id = serializers.UUIDField() - employee_ids = serializers.ListSerializer(child=serializers.UUIDField()) + employee_ids = serializers.ListSerializer(child=serializers.UUIDField(), write_only=True) name = serializers.CharField() status = serializers.BooleanField() folder_id = serializers.UUIDField(required=False) + def validate_name(self, value): + if CashTransaction.objects.filter(name=value).exists(): + raise serializers.ValidationError('cash transaction with this name already exists') + return value + def validate(self, data): - payment_type = PaymentType.objects.filter(id=data['id']).first() - if payment_type: + payment_type = PaymentType.objects.filter(id=data['payment_type_id']).first() + if not payment_type: raise serializers.ValidationError("Payment Type not found") if data.get('folder_id'): folder = CashTransactionFolder.objects.filter(id=data.get('folder_id')).first() @@ -60,7 +66,7 @@ class CashTransactionCreateSerializer(serializers.Serializer): def create(self, validated_data): with transaction.atomic(): - employee_ids = validated_data.pop('employee_ids') + employee_ids = validated_data.pop('employee_ids', []) cash_transaction = CashTransaction.objects.create( name=validated_data.get('name'), payment_type=validated_data.get('payment_type'), diff --git a/core/apps/finance/serializers/income.py b/core/apps/finance/serializers/income.py new file mode 100644 index 0000000..b86413e --- /dev/null +++ b/core/apps/finance/serializers/income.py @@ -0,0 +1,97 @@ +from django.db import transaction + +from rest_framework import serializers + +from core.apps.finance.models import Income + + +class IncomeListSerializer(serializers.ModelSerializer): + cash_transaction = serializers.SerializerMethodField(method_name='get_cash_transaction') + payment_type = serializers.SerializerMethodField(method_name='get_payment_type') + project_folder = serializers.SerializerMethodField(method_name='get_project_folder') + project = serializers.SerializerMethodField(method_name='get_project') + counterparty = serializers.SerializerMethodField(method_name='get_counterparty') + type_income = serializers.SerializerMethodField(method_name='get_type_income') + + class Meta: + model = Income + fields = [ + 'id', 'cash_transaction', 'payment_type', 'project_folder', 'project', + 'counterparty', 'type_income', 'currency', 'price', 'exchange_rate', 'date', + 'comment', 'file', 'audit' + ] + + def get_cash_transaction(self, obj): + return { + 'id': obj.cash_transaction.id, + 'name': obj.cash_transaction.name + } + + def get_payment_type(self, obj): + return { + 'id': obj.payment_type.id, + 'name': obj.payment_type.name + } + + def get_project_folder(self, obj): + return { + 'id': obj.project_folder.id, + 'name': obj.project_folder.name + } + + def get_project(self, obj): + return { + 'id': obj.project.id, + 'name': obj.project.name + } if obj.project else None + + def get_counterparty(self, obj): + return { + 'id': obj.counterparty.id, + 'name': obj.counterparty.name + } if obj.counterparty else None + + def get_type_income(self, obj): + return { + 'id': obj.type_income.id, + 'name': obj.type_income.name + } if obj.type_income else None + + +class IncomeCreateSerializer(serializers.ModelSerializer): + class Meta: + model = Income + fields = [ + 'cash_transaction', 'payment_type', 'project_folder', 'project', + 'counterparty', 'type_income', 'currency', 'price', 'exchange_rate', 'date', + 'comment', 'file', 'audit' + ] + + def create(self, validated_data): + with transaction.atomic(): + income = Income.objects.create( + cash_transaction=validated_data['cash_transaction'], + payment_type=validated_data['payment_type'], + project_folder=validated_data['project_folder'], + project=validated_data.get('project'), + counterparty=validated_data.get('counterparty'), + type_income=validated_data.get('type_income'), + currency=validated_data.get('currency'), + price=validated_data.get('price') * validated_data.get('exchange_rate') if validated_data.get('exchange_rate') else validated_data.get('price'), + exchange_rate=validated_data.get('exchange_rate'), + date=validated_data.get('date'), + comment=validated_data.get('comment'), + file=validated_data.get('file'), + audit=validated_data.get('audit') + ) + cash_transaction = income.cash_transaction + + if validated_data.get('currency') == 'uzs': + cash_transaction.income_balance_uzs += income.price + cash_transaction.total_balance_uzs = cash_transaction.income_balance_uzs - cash_transaction.expence_balance_uzs + elif validated_data.get('currency') == 'usd': + cash_transaction.income_balance_usd += income.price + cash_transaction.total_balance_usd = cash_transaction.income_balance_usd - cash_transaction.expence_balance_usd + + cash_transaction.save() + return income \ No newline at end of file diff --git a/core/apps/finance/urls.py b/core/apps/finance/urls.py index d00c14a..2341885 100644 --- a/core/apps/finance/urls.py +++ b/core/apps/finance/urls.py @@ -4,6 +4,7 @@ from core.apps.finance.views import cash_transaction as cash_views from core.apps.finance.views import cash_transaction_folder as folder_views from core.apps.finance.views import payment_type as pt_views from core.apps.finance.views import type_income as ti_views +from core.apps.finance.views import income as income_views urlpatterns = [ @@ -38,5 +39,11 @@ urlpatterns = [ path('/update/', ti_views.TypeIncomeUpdateApiView.as_view()), path('/delete/', ti_views.TypeIncomeDeleteApiView.as_view()), ] + )), + path('income/', include( + [ + path('list/', income_views.IncomeListApiView.as_view()), + path('create/', income_views.IncomeCreateApiView.as_view()), + ] )) ] \ No newline at end of file diff --git a/core/apps/finance/views/income.py b/core/apps/finance/views/income.py new file mode 100644 index 0000000..4b3fdc0 --- /dev/null +++ b/core/apps/finance/views/income.py @@ -0,0 +1,49 @@ +from django.shortcuts import get_object_or_404 + +from rest_framework import generics, views, parsers +from rest_framework.response import Response + +from core.apps.finance.models import Income +from core.apps.finance.serializers import income as serializers +from core.apps.accounts.permissions.permissions import HasRolePermission + + +class IncomeListApiView(generics.GenericAPIView): + serializer_class = serializers.IncomeListSerializer + queryset = Income.objects.select_related( + 'cash_transaction', 'payment_type', 'project_folder', 'project', 'counterparty', 'type_income' + ) + permission_classes = [HasRolePermission] + + 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 IncomeCreateApiView(generics.GenericAPIView): + serializer_class = serializers.IncomeCreateSerializer + queryset = Income.objects.all() + permission_classes = [HasRolePermission] + parser_classes = [parsers.FormParser, parsers.MultiPartParser] + + def post(self, request): + ser = self.serializer_class(data=request.data) + if ser.is_valid(raise_exception=True): + ser.save() + return Response( + { + 'success': True, + 'message': 'income created' + }, + status=201 + ) + return Response( + { + 'success': False, + 'message': 'income create failed', + 'error': ser.errors, + }, + status=400 + ) \ No newline at end of file