diff --git a/Rasaddam_Backend/settings.py b/Rasaddam_Backend/settings.py index 07d71e7..e99c2fc 100644 --- a/Rasaddam_Backend/settings.py +++ b/Rasaddam_Backend/settings.py @@ -66,6 +66,7 @@ INSTALLED_APPS = [ 'apps.warehouse.apps.WarehouseConfig', 'apps.search.apps.SearchConfig', 'apps.log.apps.LogConfig', + 'apps.product.apps.ProductConfig', 'rest_captcha', 'captcha', 'drf_yasg' @@ -155,8 +156,8 @@ REST_FRAMEWORK = { 'rest_framework.authentication.BasicAuthentication', ), 'EXCEPTION_HANDLER': 'apps.core.error_handler.custom_exception_handler', - "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", - "PAGE_SIZE": 25, + "DEFAULT_PAGINATION_CLASS": 'apps.core.pagination.CustomPageNumberPagination', + "PAGE_SIZE": 20, 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' } diff --git a/Rasaddam_Backend/urls.py b/Rasaddam_Backend/urls.py index e9cd972..f065616 100644 --- a/Rasaddam_Backend/urls.py +++ b/Rasaddam_Backend/urls.py @@ -37,5 +37,6 @@ urlpatterns = [ path('livestock/', include('apps.livestock.urls')), path('tag/', include('apps.tag.urls')), path('search/', include('apps.search.urls')), + path('product/', include('apps.product.urls')), path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), ] diff --git a/apps/authorization/api/v1/serializers.py b/apps/authorization/api/v1/serializers.py index 3d89554..09e9215 100644 --- a/apps/authorization/api/v1/serializers.py +++ b/apps/authorization/api/v1/serializers.py @@ -18,6 +18,7 @@ class PageSerializer(serializers.ModelSerializer): class Meta: model = Page fields = [ + 'id', 'name', 'code' ] diff --git a/apps/core/error_handler.py b/apps/core/error_handler.py index af68f1e..b87b648 100644 --- a/apps/core/error_handler.py +++ b/apps/core/error_handler.py @@ -7,8 +7,9 @@ def custom_exception_handler(exc, context): if response is not None: response.data['status_code'] = response.status_code - response.data['message'] = response.data.get('detail', str(exc)) - del response.data['detail'] + if response.data.get('detail'): + response.data['message'] = response.data.get('detail', str(exc)) + del response.data['detail'] else: response = JsonResponse({'message': str(exc), 'status_code': 500}) response.status_code = 500 diff --git a/apps/core/pagination.py b/apps/core/pagination.py new file mode 100644 index 0000000..cb301dc --- /dev/null +++ b/apps/core/pagination.py @@ -0,0 +1,7 @@ +from rest_framework.pagination import PageNumberPagination + + +class CustomPageNumberPagination(PageNumberPagination): + page_size = 20 # default + page_size_query_param = 'page_size' # set from client + max_page_size = 100 # maximum items to show diff --git a/apps/product/__init__.py b/apps/product/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/admin.py b/apps/product/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/product/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/product/apps.py b/apps/product/apps.py new file mode 100644 index 0000000..cb72851 --- /dev/null +++ b/apps/product/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProductConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.product' diff --git a/apps/product/management/__init__.py b/apps/product/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/management/commands/__init__.py b/apps/product/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/management/commands/command.py b/apps/product/management/commands/command.py new file mode 100644 index 0000000..c68face --- /dev/null +++ b/apps/product/management/commands/command.py @@ -0,0 +1 @@ +# Your custom management commands go here. diff --git a/apps/product/migrations/0001_initial.py b/apps/product/migrations/0001_initial.py new file mode 100644 index 0000000..4bddedc --- /dev/null +++ b/apps/product/migrations/0001_initial.py @@ -0,0 +1,56 @@ +# Generated by Django 5.0 on 2025-06-03 12:19 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ReferenceProduct', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_date', models.DateTimeField(auto_now_add=True)), + ('modify_date', models.DateTimeField(auto_now=True)), + ('creator_info', models.CharField(max_length=100, null=True)), + ('modifier_info', models.CharField(max_length=100, null=True)), + ('trash', models.BooleanField(default=False)), + ('name', models.CharField(default='empty', max_length=250)), + ('type', models.CharField(choices=[('F', 'Free'), ('G', 'Governmental')], max_length=3)), + ('img', models.CharField(default='empty', max_length=100)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_date', models.DateTimeField(auto_now_add=True)), + ('modify_date', models.DateTimeField(auto_now=True)), + ('creator_info', models.CharField(max_length=100, null=True)), + ('modifier_info', models.CharField(max_length=100, null=True)), + ('trash', models.BooleanField(default=False)), + ('name', models.CharField(default='empty', max_length=250)), + ('type', models.CharField(choices=[('F', 'Free'), ('G', 'Governmental')], max_length=3)), + ('img', models.CharField(default='empty', max_length=100)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)), + ('reference', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reference_product', to='product.referenceproduct')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/product/migrations/0002_attribute_attributevalue.py b/apps/product/migrations/0002_attribute_attributevalue.py new file mode 100644 index 0000000..0f08232 --- /dev/null +++ b/apps/product/migrations/0002_attribute_attributevalue.py @@ -0,0 +1,54 @@ +# Generated by Django 5.0 on 2025-06-03 13:14 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Attribute', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_date', models.DateTimeField(auto_now_add=True)), + ('modify_date', models.DateTimeField(auto_now=True)), + ('creator_info', models.CharField(max_length=100, null=True)), + ('modifier_info', models.CharField(max_length=100, null=True)), + ('trash', models.BooleanField(default=False)), + ('name', models.CharField(default='empty', max_length=100)), + ('type', models.CharField(default='empty', help_text='type of attribute like: calculate product by kilogram', max_length=255)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)), + ('reference_product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reference_attribute', to='product.referenceproduct')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='AttributeValue', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_date', models.DateTimeField(auto_now_add=True)), + ('modify_date', models.DateTimeField(auto_now=True)), + ('creator_info', models.CharField(max_length=100, null=True)), + ('modifier_info', models.CharField(max_length=100, null=True)), + ('trash', models.BooleanField(default=False)), + ('value', models.IntegerField(default=0)), + ('attribute', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attribute_value', to='product.attribute')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_createddby', to=settings.AUTH_USER_MODEL)), + ('modified_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_modifiedby', to=settings.AUTH_USER_MODEL)), + ('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_attribute_value', to='product.product')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/product/migrations/__init__.py b/apps/product/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/mobile/api/__init__.py b/apps/product/mobile/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/mobile/api/v1/__init__.py b/apps/product/mobile/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/mobile/api/v1/serializers.py b/apps/product/mobile/api/v1/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/mobile/api/v1/urls.py b/apps/product/mobile/api/v1/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/mobile/api/v1/views.py b/apps/product/mobile/api/v1/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/mobile/tests/test_common_services.py b/apps/product/mobile/tests/test_common_services.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/models.py b/apps/product/models.py new file mode 100644 index 0000000..8de36a2 --- /dev/null +++ b/apps/product/models.py @@ -0,0 +1,94 @@ +from django.db import models +from apps.core.models import BaseModel + + +# Create your models here. + +class ReferenceProduct(BaseModel): + """ Reference product - like: rice """ + + name = models.CharField(max_length=250, default='empty') # noqa + type_choices = ( + ('F', 'Free'), # free product + ('G', 'Governmental') # government product + ) + type = models.CharField(max_length=3, choices=type_choices) + img = models.CharField(max_length=100, default='empty') + + def __str__(self): + return f'name: {self.name} - type: {self.type}' + + def save(self, *args, **kwargs): + super(ReferenceProduct, self).save(*args, **kwargs) + + +class Product(BaseModel): + """ Child of reference product - like: brown rice """ + name = models.CharField(max_length=250, default='empty') # noqa + type_choices = ( + ('F', 'Free'), # free product + ('G', 'Governmental') # + ) + type = models.CharField(max_length=3, choices=type_choices) + img = models.CharField(max_length=100, default='empty') + reference = models.ForeignKey( + ReferenceProduct, + on_delete=models.CASCADE, + related_name='reference_product', + null=True + ) + + def __str__(self): + return f'name: {self.name} - type: {self.type}' + + def save(self, *args, **kwargs): + super(Product, self).save(*args, **kwargs) + + +class Attribute(BaseModel): + """ + every reference product have multiple attributes + """ + reference_product = models.ForeignKey( + ReferenceProduct, + on_delete=models.CASCADE, + related_name='reference_attribute', + null=True + ) + name = models.CharField(max_length=100, default='empty') + type = models.CharField( + max_length=255, + default='empty', + help_text='type of attribute like: calculate product by kilogram' + ) + + def __str__(self): + return f'{self.reference_product.name} - {self.name}' + + def save(self, *args, **kwargs): + return super(Attribute, self).save(*args, **kwargs) + + +class AttributeValue(BaseModel): + """ + every child product should have attribute value for + reference product attribute + """ + product = models.ForeignKey( + Product, + on_delete=models.CASCADE, + related_name='product_attribute_value', + null=True + ) + attribute = models.ForeignKey( + Attribute, + on_delete=models.CASCADE, + related_name='attribute_value' + ) + value = models.IntegerField(default=0) + + def __str__(self): + return f'{self.product.name} - {self.attribute.name} - {self.value}' + + def save(self, *args, **kwargs): + return super(AttributeValue, self).save(*args, **kwargs) diff --git a/apps/product/permissions.py b/apps/product/permissions.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/pos/api/__init__.py b/apps/product/pos/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/pos/api/v1/__init__.py b/apps/product/pos/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/pos/api/v1/serializers.py b/apps/product/pos/api/v1/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/pos/api/v1/urls.py b/apps/product/pos/api/v1/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/pos/api/v1/views.py b/apps/product/pos/api/v1/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/pos/tests/test_common_services.py b/apps/product/pos/tests/test_common_services.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/services.py b/apps/product/services.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/tests.py b/apps/product/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/product/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/product/tools.py b/apps/product/tools.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/urls.py b/apps/product/urls.py new file mode 100644 index 0000000..3afe724 --- /dev/null +++ b/apps/product/urls.py @@ -0,0 +1,5 @@ +from django.urls import path, include + +urlpatterns = [ + path('web/api/', include('apps.product.web.api.v1.urls')) +] diff --git a/apps/product/views.py b/apps/product/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/product/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/product/web/api/__init__.py b/apps/product/web/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/web/api/v1/__init__.py b/apps/product/web/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/web/api/v1/api.py b/apps/product/web/api/v1/api.py new file mode 100644 index 0000000..5df632c --- /dev/null +++ b/apps/product/web/api/v1/api.py @@ -0,0 +1,93 @@ +from apps.product.web.api.v1 import serializers as product_serializers +from rest_framework.exceptions import APIException +from apps.product import models as product_models +from rest_framework.response import Response +from rest_framework.decorators import action +from rest_framework import viewsets +from rest_framework import status +from django.db import transaction + + +def trash(queryset, pk): + """ sent object to trash """ + obj = queryset.get(id=pk) + obj.trash = True + obj.save() + + +def delete(queryset, pk): + """ full delete object """ + obj = queryset.get(id=pk) + obj.delete() + + +class ProductViewSet(viewsets.ModelViewSet): + queryset = product_models.Product.objects.all() + serializer_class = product_serializers.ProductSerializer + + @action( + methods=['put'], + detail=True, + url_path='trash', + url_name='trash', + name='trash', + ) + @transaction.atomic + def trash(self, request, pk=None): + """ Sent product to trash """ + try: + trash(self.queryset, pk) + except APIException as e: + return Response(e, status.HTTP_204_NO_CONTENT) + + @action( + methods=['post'], + detail=True, + url_name='delete', + url_path='delete', + name='delete' + ) + @transaction.atomic + def delete(self, request, pk=None): + """ Full delete of product object """ + try: + delete(self.queryset, pk) + return Response(status=status.HTTP_200_OK) + except APIException as e: + return Response(e, status=status.HTTP_204_NO_CONTENT) + + +class ReferenceProductViewSet(viewsets.ModelViewSet): + queryset = product_models.ReferenceProduct.objects.all() + serializer_class = product_serializers.ReferenceProductSerializer + + @action( + methods=['put'], + detail=True, + url_path='trash', + url_name='trash', + name='trash', + ) + @transaction.atomic + def trash(self, request, pk=None): + """ Sent product to trash """ + try: + trash(self.queryset, pk) + except APIException as e: + return Response(e, status.HTTP_204_NO_CONTENT) + + @action( + methods=['post'], + detail=True, + url_name='delete', + url_path='delete', + name='delete' + ) + @transaction.atomic + def delete(self, request, pk=None): + """ Full delete of product object """ + try: + delete(self.queryset, pk) + return Response(status=status.HTTP_200_OK) + except APIException as e: + return Response(e, status=status.HTTP_204_NO_CONTENT) \ No newline at end of file diff --git a/apps/product/web/api/v1/serializers.py b/apps/product/web/api/v1/serializers.py new file mode 100644 index 0000000..5695bdb --- /dev/null +++ b/apps/product/web/api/v1/serializers.py @@ -0,0 +1,27 @@ +from rest_framework import serializers +from apps.product import models as product_models + + +class ReferenceProductSerializer(serializers.ModelSerializer): + """ Serializer of reference product """ + + class Meta: + model = product_models.ReferenceProduct + fields = '__all__' + + +class ProductSerializer(serializers.ModelSerializer): + """ Serializer of product """ + + class Meta: + model = product_models.Product + fields = '__all__' + + def to_representation(self, instance): + """ Custom output of product serializer """ + + representation = super().to_representation(instance) + if instance.reference: + representation['reference'] = ReferenceProductSerializer(instance.reference).data + + return representation diff --git a/apps/product/web/api/v1/urls.py b/apps/product/web/api/v1/urls.py new file mode 100644 index 0000000..9cba8b9 --- /dev/null +++ b/apps/product/web/api/v1/urls.py @@ -0,0 +1,11 @@ +from apps.product.web.api.v1 import api as api_views +from rest_framework.routers import DefaultRouter +from django.urls import path, include + +router = DefaultRouter() +router.register(r'product', api_views.ProductViewSet, basename='product') +router.register(r'reference', api_views.ReferenceProductViewSet, basename='reference') + +urlpatterns = [ + path('v1/', include(router.urls)) +] diff --git a/apps/product/web/api/v1/views.py b/apps/product/web/api/v1/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/product/web/tests/test_common_services.py b/apps/product/web/tests/test_common_services.py new file mode 100644 index 0000000..e69de29 diff --git a/ss.png b/ss.png deleted file mode 100644 index 6576921..0000000 Binary files a/ss.png and /dev/null differ