diff --git a/core/apps/wherehouse/admin/__init__.py b/core/apps/wherehouse/admin/__init__.py index 0a68aae..bf67400 100644 --- a/core/apps/wherehouse/admin/__init__.py +++ b/core/apps/wherehouse/admin/__init__.py @@ -1,3 +1,4 @@ from .wherehouse import * from .inventory import * -from .stock_movement import * \ No newline at end of file +from .stock_movement import * +from .invalid_product import * \ No newline at end of file diff --git a/core/apps/wherehouse/admin/invalid_product.py b/core/apps/wherehouse/admin/invalid_product.py new file mode 100644 index 0000000..f2fc327 --- /dev/null +++ b/core/apps/wherehouse/admin/invalid_product.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from core.apps.wherehouse.models import InvalidProduct + + +@admin.register(InvalidProduct) +class InvalidProductAdmin(admin.ModelAdmin): + list_display = ['inventory', 'project_folder', 'amount', 'status'] \ No newline at end of file diff --git a/core/apps/wherehouse/admin/inventory.py b/core/apps/wherehouse/admin/inventory.py index d534a0d..d182489 100644 --- a/core/apps/wherehouse/admin/inventory.py +++ b/core/apps/wherehouse/admin/inventory.py @@ -5,5 +5,5 @@ from core.apps.wherehouse.models.inventory import Inventory @admin.register(Inventory) class InventoryAdmin(admin.ModelAdmin): - list_display = ['quantity', 'wherehouse', 'product'] - \ No newline at end of file + list_display = ['id', 'wherehouse', 'is_invalid'] + list_filter = ['wherehouse', 'is_invalid'] \ No newline at end of file diff --git a/core/apps/wherehouse/migrations/0005_invalidproduct.py b/core/apps/wherehouse/migrations/0005_invalidproduct.py new file mode 100644 index 0000000..24ef242 --- /dev/null +++ b/core/apps/wherehouse/migrations/0005_invalidproduct.py @@ -0,0 +1,40 @@ +# Generated by Django 5.2.4 on 2025-08-26 17:20 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0016_estimatework_employee_estimatework_end_date_and_more'), + ('wherehouse', '0004_inventory_project_inventory_project_folder'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='InvalidProduct', + 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)), + ('amount', models.PositiveIntegerField()), + ('status', models.CharField(choices=[('BROKEN', 'singan'), ('LOST', 'yoqolgan'), ('OTHER', 'boshqa')], default='other', max_length=20)), + ('created_date', models.DateField(blank=True, null=True)), + ('expiry_date', models.DateField(blank=True, null=True)), + ('comment', models.DateField(blank=True, null=True)), + ('file', models.FileField(blank=True, null=True, upload_to='invalid_product/files/')), + ('inventory', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invalid_products', to='wherehouse.inventory')), + ('project_folder', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invalid_products', to='projects.projectfolder')), + ('witnesses', models.ManyToManyField(related_name='invalid_products', to=settings.AUTH_USER_MODEL)), + ('work', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invalid_products', to='projects.estimatework')), + ], + options={ + 'verbose_name': 'yaroqsiz maxsulot', + 'verbose_name_plural': 'yaroqsiz maxsulotlar', + }, + ), + ] diff --git a/core/apps/wherehouse/migrations/0006_inventory_is_invalid.py b/core/apps/wherehouse/migrations/0006_inventory_is_invalid.py new file mode 100644 index 0000000..9b9eb24 --- /dev/null +++ b/core/apps/wherehouse/migrations/0006_inventory_is_invalid.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-08-27 09:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wherehouse', '0005_invalidproduct'), + ] + + operations = [ + migrations.AddField( + model_name='inventory', + name='is_invalid', + field=models.BooleanField(default=False), + ), + ] diff --git a/core/apps/wherehouse/models/__init__.py b/core/apps/wherehouse/models/__init__.py index e75ed97..8ecabb3 100644 --- a/core/apps/wherehouse/models/__init__.py +++ b/core/apps/wherehouse/models/__init__.py @@ -1,3 +1,4 @@ from .inventory import * from .wherehouse import * -from .stock_movemend import * \ No newline at end of file +from .stock_movemend import * +from .invalid_product import * \ No newline at end of file diff --git a/core/apps/wherehouse/models/invalid_product.py b/core/apps/wherehouse/models/invalid_product.py new file mode 100644 index 0000000..d8f129e --- /dev/null +++ b/core/apps/wherehouse/models/invalid_product.py @@ -0,0 +1,43 @@ +from django.db import models + +# shared +from core.apps.shared.models import BaseModel +# wherehouse +from core.apps.wherehouse.models import WhereHouse, Inventory +# projects +from core.apps.projects.models import ProjectFolder, EstimateWork +# accounts +from core.apps.accounts.models import User + + +class InvalidProduct(BaseModel): + STATUS = ( + ('BROKEN', 'singan'), + ('LOST', 'yoqolgan'), + ('OTHER', 'boshqa'), + ) + + # relationship + inventory = models.ForeignKey(Inventory, on_delete=models.CASCADE, related_name='invalid_products') + project_folder = models.ForeignKey( + ProjectFolder, on_delete=models.CASCADE, related_name='invalid_products' + ) + witnesses = models.ManyToManyField(User, related_name='invalid_products') + work = models.ForeignKey( + EstimateWork, on_delete=models.SET_NULL, null=True, blank=True, related_name='invalid_products' + ) + # required + amount = models.PositiveIntegerField() + status = models.CharField(max_length=20, choices=STATUS, default='other') + # optional + created_date = models.DateField(null=True, blank=True) + expiry_date = models.DateField(null=True, blank=True) + comment = models.DateField(null=True, blank=True) + file = models.FileField(null=True, blank=True, upload_to='invalid_product/files/') + + def __str__(self): + return f'{self.amount} ta maxsulot yaroqsiz' + + class Meta: + verbose_name = 'yaroqsiz maxsulot' + verbose_name_plural = 'yaroqsiz maxsulotlar' \ No newline at end of file diff --git a/core/apps/wherehouse/models/inventory.py b/core/apps/wherehouse/models/inventory.py index 8233d5f..0936fc1 100644 --- a/core/apps/wherehouse/models/inventory.py +++ b/core/apps/wherehouse/models/inventory.py @@ -26,6 +26,7 @@ class Inventory(BaseModel): Project, on_delete=models.SET_NULL, null=True, blank=True, related_name='inventories' ) + is_invalid = models.BooleanField(default=False) def __str__(self): return f'{self.product} in {self.wherehouse}' diff --git a/core/apps/wherehouse/serializers/invalid_product.py b/core/apps/wherehouse/serializers/invalid_product.py new file mode 100644 index 0000000..e7e0ff7 --- /dev/null +++ b/core/apps/wherehouse/serializers/invalid_product.py @@ -0,0 +1,76 @@ +from django.db import transaction + +from rest_framework import serializers + +from core.apps.wherehouse.models import InvalidProduct, Inventory +from core.apps.projects.models import ProjectFolder, EstimateWork +from core.apps.wherehouse.serializers.inventory import InventoryListSerializer +from core.apps.accounts.serializers.user import UserListSerializer + + +class InvalidProductCreateSerializer(serializers.Serializer): + inventory_id = serializers.UUIDField() + project_folder_id = serializers.UUIDField() + witnesses_ids = serializers.ListField(child=serializers.UUIDField()) + work_id = serializers.UUIDField(required=False) + amount = serializers.IntegerField() + status = serializers.ChoiceField(choices=InvalidProduct.STATUS) + created_date = serializers.DateField(required=False) + expiry_date = serializers.DateField(required=False) + comment = serializers.CharField(required=False) + file = serializers.FileField(required=False) + + def validate(self, attrs): + inventory = Inventory.objects.filter(id=attrs['inventory_id']).first() + if not inventory: + raise serializers.ValidationError("Inventory not found") + project_folder = ProjectFolder.objects.filter(id=attrs['project_folder_id']).first() + if not project_folder: + raise serializers.ValidationError("Project Folder not found") + if attrs.get('work_id'): + work = EstimateWork.objects.filter(id=attrs['work_id']).first() + if not work: + raise serializers.ValidationError("Work not found") + attrs['work'] = work + attrs['inventory'] = inventory + attrs['project_folder'] = project_folder + return super().validate(attrs) + + def create(self, validated_data): + with transaction.atomic(): + witnesses_ids = validated_data.pop('witnesses_ids') + invalid_product = InvalidProduct.objects.create( + inventory=validated_data.get('inventory'), + project_folder=validated_data.get('project_folder'), + work=validated_data.get('work'), + amount=validated_data.get('amount'), + status=validated_data.get('status'), + created_date=validated_data.get('created_date'), + expiry_date=validated_data.get('expiry_date'), + comment=validated_data.get('comment'), + file=validated_data.get('file'), + ) + invalid_product.witnesses.set(witnesses_ids) + invalid_product.inventory.is_invalid = True + invalid_product.inventory.save() + invalid_product.save() + return invalid_product + + +class InvliadProductListSerializer(serializers.ModelSerializer): + inventory = InventoryListSerializer() + project_folder = serializers.SerializerMethodField(method_name='get_folder') + witnesses = UserListSerializer(many=True) + + class Meta: + model = InvalidProduct + fields = [ + 'id', 'status', 'inventory', 'project_folder', 'witnesses', 'work', 'amount', + 'created_date', 'expiry_date', 'comment', 'file' + ] + + def get_folder(self, obj): + return { + 'id': obj.project_folder.id, + 'name': obj.project_folder.name, + } \ No newline at end of file diff --git a/core/apps/wherehouse/urls.py b/core/apps/wherehouse/urls.py index 96cc523..d9e2644 100644 --- a/core/apps/wherehouse/urls.py +++ b/core/apps/wherehouse/urls.py @@ -2,6 +2,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 urlpatterns = [ @@ -19,4 +20,10 @@ urlpatterns = [ path('list/', inventory_views.InventoryListApiView.as_view()), ] )), -] + path('invalid_product/', include( + [ + path('create/', invalid_product_views.InvalidProductCreateApiView.as_view()), + path('list/', invalid_product_views.InvalidProductListApiView.as_view()), + ] + )), +] diff --git a/core/apps/wherehouse/views/invalid_product.py b/core/apps/wherehouse/views/invalid_product.py new file mode 100644 index 0000000..f0c2ad0 --- /dev/null +++ b/core/apps/wherehouse/views/invalid_product.py @@ -0,0 +1,45 @@ +from rest_framework import generics, parsers +from rest_framework.response import Response + +from core.apps.accounts.permissions.permissions import HasRolePermission +from core.apps.wherehouse.serializers import invalid_product as serializers +from core.apps.wherehouse.models import InvalidProduct + + +class InvalidProductCreateApiView(generics.GenericAPIView): + serializer_class = serializers.InvalidProductCreateSerializer + queryset = InvalidProduct.objects.all() + permission_classes = [HasRolePermission] + required_permissions = [] + parser_classes = [parsers.FormParser, parsers.MultiPartParser] + + 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': 'invalid product created'}, + status=201 + ) + return Response( + {'success': True, 'error_message': serializer.errors}, + status=400 + ) + + +class InvalidProductListApiView(generics.GenericAPIView): + serializer_class = serializers.InvliadProductListSerializer + queryset = InvalidProduct.objects.select_related( + 'inventory', 'project_folder', 'work' + ).prefetch_related('witnesses') + permission_classes = [HasRolePermission] + required_permissions = [] + + def get(self, request): + invalid_products = self.queryset + page = self.paginate_queryset(invalid_products) + if page is not None: + serializer = self.serializer_class(page, many=True) + return self.get_paginated_response(serializer.data) + serializer = self.serializer_class(invalid_products, many=True) + return Response(serializer.data, status=200) \ No newline at end of file diff --git a/core/apps/wherehouse/views/inventory.py b/core/apps/wherehouse/views/inventory.py index 38ace48..f42d130 100644 --- a/core/apps/wherehouse/views/inventory.py +++ b/core/apps/wherehouse/views/inventory.py @@ -12,7 +12,7 @@ from core.apps.accounts.permissions.permissions import HasRolePermission class InventoryListApiView(generics.GenericAPIView): serializer_class = serializers.InventoryListSerializer - queryset = Inventory.objects.select_related('product', 'unity') + queryset = Inventory.objects.select_related('product', 'unity').exclude(is_invalid=True) permissions_classes = [HasRolePermission] required_permissions = ['wherehouse'] filter_backends = [DjangoFilterBackend, filters.SearchFilter] diff --git a/manage.py b/manage.py index 6361598..7aed48d 100755 --- a/manage.py +++ b/manage.py @@ -20,3 +20,4 @@ def main(): if __name__ == '__main__': main() +