diff --git a/core/apps/wherehouse/admin/stock_movement.py b/core/apps/wherehouse/admin/stock_movement.py index 9107eed..a66093b 100644 --- a/core/apps/wherehouse/admin/stock_movement.py +++ b/core/apps/wherehouse/admin/stock_movement.py @@ -1,8 +1,22 @@ from django.contrib import admin -from core.apps.wherehouse.models.stock_movemend import StockMovemend +from core.apps.wherehouse.models.stock_movemend import StockMovemend, StockMovmendProduct + + +class StockMovemendProductInline(admin.TabularInline): + model = StockMovmendProduct + extra = 0 + + def has_add_permission(self, request, obj): + return False @admin.register(StockMovemend) class StockMovemendAdmin(admin.ModelAdmin): - list_display = ['id','wherehouse_to', 'wherehouse_from', 'product', 'quantity', 'movemend_type'] \ No newline at end of file + list_display = ['id', 'wherehouse_to', 'wherehouse_from', 'movemend_type'] + inlines = [StockMovemendProductInline] + + +@admin.register(StockMovmendProduct) +class StockMovemendProductAdmin(admin.ModelAdmin): + list_display = ['id', 'inventory', 'quantity'] \ No newline at end of file diff --git a/core/apps/wherehouse/apps.py b/core/apps/wherehouse/apps.py index 8fad9aa..9f70cf8 100644 --- a/core/apps/wherehouse/apps.py +++ b/core/apps/wherehouse/apps.py @@ -6,4 +6,6 @@ class WherehouseConfig(AppConfig): name = 'core.apps.wherehouse' def ready(self): - from . import admin \ No newline at end of file + from . import admin + from . import signals + \ No newline at end of file diff --git a/core/apps/wherehouse/migrations/0010_remove_stockmovemend_product_and_more.py b/core/apps/wherehouse/migrations/0010_remove_stockmovemend_product_and_more.py new file mode 100644 index 0000000..33f35ef --- /dev/null +++ b/core/apps/wherehouse/migrations/0010_remove_stockmovemend_product_and_more.py @@ -0,0 +1,72 @@ +# Generated by Django 5.2.4 on 2025-08-28 10:41 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('products', '0006_alter_product_type'), + ('wherehouse', '0009_alter_invalidproduct_comment'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='stockmovemend', + name='product', + ), + migrations.RemoveField( + model_name='stockmovemend', + name='quantity', + ), + migrations.AddField( + model_name='stockmovemend', + name='comment', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='stockmovemend', + name='date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='stockmovemend', + name='file', + field=models.FileField(blank=True, null=True, upload_to=''), + ), + migrations.AddField( + model_name='stockmovemend', + name='number', + field=models.IntegerField(default=1), + ), + migrations.AddField( + model_name='stockmovemend', + name='recipient', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stock_movmends', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='stockmovemend', + name='movemend_type', + field=models.CharField(choices=[('EXPECTED', 'kutilmoqda'), ('ACCEPTED', 'qabul qilingan'), ('CANCELLED', 'bekor qilingan')], default='EXPECTED', max_length=20), + ), + migrations.CreateModel( + name='StockMovmendProduct', + 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)), + ('quantity', models.PositiveIntegerField()), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movmend_products', to='products.product')), + ('stock_movemend', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movmend_products', to='wherehouse.stockmovemend')), + ('unity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='products.unity')), + ], + options={ + 'verbose_name': "Ko'chirilgan mahsulot", + 'verbose_name_plural': "Ko'chirilgan mahsulotlar", + }, + ), + ] diff --git a/core/apps/wherehouse/migrations/0011_remove_stockmovmendproduct_product_and_more.py b/core/apps/wherehouse/migrations/0011_remove_stockmovmendproduct_product_and_more.py new file mode 100644 index 0000000..23fd278 --- /dev/null +++ b/core/apps/wherehouse/migrations/0011_remove_stockmovmendproduct_product_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.4 on 2025-08-28 11:15 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0016_estimatework_employee_estimatework_end_date_and_more'), + ('wherehouse', '0010_remove_stockmovemend_product_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='stockmovmendproduct', + name='product', + ), + migrations.RemoveField( + model_name='stockmovmendproduct', + name='unity', + ), + migrations.AddField( + model_name='stockmovemend', + name='project', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_movmends', to='projects.project'), + ), + migrations.AddField( + model_name='stockmovemend', + name='project_folder', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stock_movmends', to='projects.projectfolder'), + ), + migrations.AddField( + model_name='stockmovmendproduct', + name='inventory', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='movmend_products', to='wherehouse.inventory'), + ), + ] diff --git a/core/apps/wherehouse/migrations/0012_alter_stockmovemend_recipient.py b/core/apps/wherehouse/migrations/0012_alter_stockmovemend_recipient.py new file mode 100644 index 0000000..5b8d41d --- /dev/null +++ b/core/apps/wherehouse/migrations/0012_alter_stockmovemend_recipient.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.4 on 2025-08-28 11:38 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wherehouse', '0011_remove_stockmovmendproduct_product_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='stockmovemend', + name='recipient', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stock_movmends', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/core/apps/wherehouse/models/stock_movemend.py b/core/apps/wherehouse/models/stock_movemend.py index c43acd3..c7197c7 100644 --- a/core/apps/wherehouse/models/stock_movemend.py +++ b/core/apps/wherehouse/models/stock_movemend.py @@ -2,32 +2,63 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from core.apps.shared.models import BaseModel -from core.apps.wherehouse.models.wherehouse import WhereHouse -from core.apps.products.models.product import Product +# wherehouse +from core.apps.wherehouse.models import WhereHouse, Inventory +# accounts +from core.apps.accounts.models import User +# projects +from core.apps.projects.models import Project, ProjectFolder class StockMovemend(BaseModel): TYPE = ( - ('IN', 'in'), - ('OUT', 'out'), - ('TRANSFER', 'transfer'), + ('EXPECTED', 'kutilmoqda'), + ('ACCEPTED', 'qabul qilingan'), + ('CANCELLED', 'bekor qilingan'), ) + number = models.IntegerField(default=1) wherehouse_to = models.ForeignKey( WhereHouse, on_delete=models.CASCADE, related_name='stocks_to' ) wherehouse_from = models.ForeignKey( WhereHouse, on_delete=models.CASCADE, related_name='stocks_from' ) - product = models.ForeignKey( - Product, on_delete=models.CASCADE, related_name='stocks' + recipient = models.ForeignKey( + User, on_delete=models.SET_NULL, related_name='stock_movmends', null=True, blank=True ) - quantity = models.PositiveIntegerField(default=0) - movemend_type = models.CharField(max_length=20, choices=TYPE) + project_folder = models.ForeignKey( + ProjectFolder, on_delete=models.CASCADE, related_name='stock_movmends', null=True + ) + project = models.ForeignKey( + Project, on_delete=models.SET_NULL, related_name='stock_movmends', null=True + ) + movemend_type = models.CharField(max_length=20, choices=TYPE, default='EXPECTED') + date = models.DateField(null=True, blank=True) + comment = models.TextField(null=True, blank=True) + file = models.FileField(null=True, blank=True) def __str__(self): - return f'{self.product} send from {self.wherehouse_from} to {self.wherehouse_to}' + return f'{self.wherehouse_from} to {self.wherehouse_to}' class Meta: verbose_name = _('Mahsulotlarni kochirish') verbose_name_plural = _('Mahsulotlarni kochirish') + + +class StockMovmendProduct(BaseModel): + inventory = models.ForeignKey( + Inventory, on_delete=models.CASCADE, related_name='movmend_products', + null=True + ) + quantity = models.PositiveIntegerField() + stock_movemend = models.ForeignKey( + StockMovemend, on_delete=models.CASCADE, related_name='movmend_products' + ) + + def __str__(self): + return str(self.inventory) + + class Meta: + verbose_name = "Ko'chirilgan mahsulot" + verbose_name_plural = "Ko'chirilgan mahsulotlar" diff --git a/core/apps/wherehouse/serializers/stock_movmend.py b/core/apps/wherehouse/serializers/stock_movmend.py new file mode 100644 index 0000000..075bdae --- /dev/null +++ b/core/apps/wherehouse/serializers/stock_movmend.py @@ -0,0 +1,142 @@ +from django.db import transaction + +from rest_framework import serializers + +from core.apps.wherehouse.models import StockMovemend, StockMovmendProduct, Inventory, WhereHouse +from core.apps.products.models import Unity, Product +from core.apps.projects.models import ProjectFolder, Project + + +class StockMovmendProductSerializer(serializers.Serializer): + inventory_id = serializers.UUIDField() + quantity = serializers.IntegerField() + + def validate(self, data): + inventory = Inventory.objects.filter(id=data['inventory_id']).first() + if not inventory: + raise serializers.ValidationError("Inventory not found") + data['inventory'] = inventory + return data + + +class StockMovmendCreateSerializer(serializers.Serializer): + products = StockMovmendProductSerializer(many=True) + project_folder_id = serializers.UUIDField() + project_id = serializers.UUIDField(required=False) + wherehouse_to_id = serializers.UUIDField() + wherehouse_from_id = serializers.UUIDField() + date = serializers.DateField() + comment = serializers.CharField(required=False) + file = serializers.FileField(required=False) + + def validate(self, data): + project_folder = ProjectFolder.objects.filter(id=data['project_folder_id']).first() + if not project_folder: + raise serializers.ValidationError("Project Folder not found") + if data.get('project_id'): + project = Project.objects.filter(id=data['project_id']).first() + if not project: + raise serializers.ValidationError("Project not found") + data['project'] = project + wherehouse_to = WhereHouse.objects.filter(id=data['wherehouse_to_id']).first() + if not wherehouse_to: + raise serializers.ValidationError("WhereHouse to not found") + wherehouse_from = WhereHouse.objects.filter(id=data['wherehouse_from_id']).first() + if not wherehouse_from: + raise serializers.ValidationError("WhereHouse from not found") + data['project_folder'] = project_folder + data['wherehouse_to'] = wherehouse_to + data['wherehouse_from'] = wherehouse_from + return data + + def create(self, validated_data): + with transaction.atomic(): + products = validated_data.pop('products') + stock_movemend = StockMovemend.objects.create( + project_folder=validated_data.get('project_folder'), + project=validated_data.get('project'), + date=validated_data.get('date'), + comment=validated_data.get('comment'), + file=validated_data.get('file'), + wherehouse_to=validated_data.get('wherehouse_to'), + wherehouse_from=validated_data.get('wherehouse_from'), + ) + movmend_products = [] + for product in products: + movmend_products.append(StockMovmendProduct( + inventory=product.get('inventory'), + quantity=product.get('quantity'), + stock_movemend=stock_movemend, + )) + StockMovmendProduct.objects.bulk_create(movmend_products) + return stock_movemend + + +class StockMovemendProductListSerializer(serializers.ModelSerializer): + product = serializers.SerializerMethodField(method_name='get_product') + unity = serializers.SerializerMethodField(method_name='get_unity') + + class Meta: + model = StockMovmendProduct + fields = [ + 'id', 'product', 'unity', 'quantity' + ] + + def get_product(self, obj): + return { + 'id': obj.inventory.product.id, + 'name': obj.inventory.product.name, + 'type': obj.inventory.product.type, + } + + def get_unity(self, obj): + return { + 'id': obj.inventory.unity.id, + 'value': obj.inventory.unity.value, + } + + +class StockMovemendListSerializer(serializers.ModelSerializer): + movmend_products = StockMovemendProductListSerializer(many=True) + wherehouse_to = serializers.SerializerMethodField(method_name='get_wherehouse_to') + wherehouse_from = serializers.SerializerMethodField(method_name='get_wherehouse_from') + recipient = serializers.SerializerMethodField(method_name='get_recipient') + project_folder = serializers.SerializerMethodField(method_name='get_project_folder') + project = serializers.SerializerMethodField(method_name='get_project') + + class Meta: + model = StockMovemend + fields = [ + 'id', 'number', 'wherehouse_to', 'wherehouse_from', 'recipient', 'project_folder', + 'project', 'movemend_type', 'date', 'comment', 'file', 'movmend_products' + ] + + def get_wherehouse_to(self, obj): + return { + 'id': obj.wherehouse_to.id, + 'name': obj.wherehouse_to.name, + } + + def get_wherehouse_from(self, obj): + return { + 'id': obj.wherehouse_from.id, + 'name': obj.wherehouse_from.name, + } + + def get_recipient(self, obj): + return { + 'id': obj.recipient.id, + 'full_name': obj.recipient.full_name, + } if obj.recipient else None + + 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 \ No newline at end of file diff --git a/core/apps/wherehouse/signals/__init__.py b/core/apps/wherehouse/signals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/wherehouse/signals/stock_movemend.py b/core/apps/wherehouse/signals/stock_movemend.py new file mode 100644 index 0000000..bcce1c6 --- /dev/null +++ b/core/apps/wherehouse/signals/stock_movemend.py @@ -0,0 +1,11 @@ +from django.dispatch import receiver +from django.db.models.signals import post_save + +from core.apps.wherehouse.models import StockMovemend + +@receiver(post_save, sender=StockMovemend) +def set_stock_movemend_number(sender, instance, created, **kwargs): + if created: + last_party = StockMovemend.objects.order_by('number').last() + instance.number = (last_party.number + 1) if last_party else 1 + instance.save(update_fields=["number"]) \ No newline at end of file diff --git a/core/apps/wherehouse/urls.py b/core/apps/wherehouse/urls.py index ca5c75c..c88b4f3 100644 --- a/core/apps/wherehouse/urls.py +++ b/core/apps/wherehouse/urls.py @@ -3,6 +3,7 @@ from django.urls import path, include from core.apps.wherehouse.views import wherehouse as wherehouse_views from core.apps.wherehouse.views import inventory as inventory_views from core.apps.wherehouse.views import invalid_product as invalid_product_views +from core.apps.wherehouse.views import stock_movemend as stock_movemend_views urlpatterns = [ @@ -28,4 +29,10 @@ urlpatterns = [ path('/delete/', invalid_product_views.InvalidProductDeleteApiView.as_view()), ] )), + path('stock_movemend/', include( + [ + path('create/', stock_movemend_views.StockMovemendCreateApiView.as_view()), + path('list/', stock_movemend_views.StockMovemendListApiView.as_view()), + ] + )) ] diff --git a/core/apps/wherehouse/views/stock_movemend.py b/core/apps/wherehouse/views/stock_movemend.py new file mode 100644 index 0000000..fbc42aa --- /dev/null +++ b/core/apps/wherehouse/views/stock_movemend.py @@ -0,0 +1,44 @@ +from rest_framework import generics, parsers +from rest_framework.response import Response + +from core.apps.wherehouse.serializers import stock_movmend as serializers +from core.apps.wherehouse.models import StockMovemend, StockMovmendProduct +from core.apps.accounts.permissions.permissions import HasRolePermission + + +class StockMovemendCreateApiView(generics.GenericAPIView): + serializer_class = serializers.StockMovmendCreateSerializer + queryset = StockMovemend.objects.all() + permission_classes = [HasRolePermission] + required_permissions = [] + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(raise_exception=True): + serializer.save() + return Response( + {'success': True, 'message': 'stock movemend created'}, + status=200 + ) + return Response( + {'success': True, 'error_message': serializer.errors} + ) + + +class StockMovemendListApiView(generics.GenericAPIView): + serializer_class = serializers.StockMovemendListSerializer + queryset = StockMovemend.objects.select_related( + 'wherehouse_to', 'wherehouse_from', 'recipient', 'project_folder', 'project' + ).prefetch_related('movmend_products') + permission_classes = [HasRolePermission] + required_permissions = [] + + def get(self, request): + queryset = self.queryset + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.serializer_class(page, many=True) + return self.get_paginated_response(serializer.data) + serializer = self.serializer_class(queryset, many=True) + return Response(serializer.data, status=200) + \ No newline at end of file