From 87dc1f6eecf0830b09816b44b1e11cb5a1bed70c Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Tue, 6 Jan 2026 15:15:10 +0330 Subject: [PATCH] import - pelak kobi system --> list, detail, create,.... --- .env.local | 2 +- .../0058_organization_ownership_code.py | 18 ++ ...0059_remove_organization_ownership_code.py | 17 ++ .../0060_organization_ownership_code.py | 18 ++ apps/authentication/models.py | 1 + apps/herd/web/api/v1/serializers.py | 1 - apps/livestock/models.py | 2 +- apps/tag/exceptions.py | 19 +- ..._code_alter_tag_ownership_code_and_more.py | 38 ++++ apps/tag/models.py | 31 ++- apps/tag/services/__init__.py | 0 apps/tag/services/tag_services.py | 49 ++++ apps/tag/web/api/v1/api.py | 212 ++++++++---------- apps/tag/web/api/v1/serializers.py | 22 +- 14 files changed, 282 insertions(+), 148 deletions(-) create mode 100644 apps/authentication/migrations/0058_organization_ownership_code.py create mode 100644 apps/authentication/migrations/0059_remove_organization_ownership_code.py create mode 100644 apps/authentication/migrations/0060_organization_ownership_code.py create mode 100644 apps/tag/migrations/0027_alter_tag_country_code_alter_tag_ownership_code_and_more.py create mode 100644 apps/tag/services/__init__.py create mode 100644 apps/tag/services/tag_services.py diff --git a/.env.local b/.env.local index 3402c15..13815d0 100644 --- a/.env.local +++ b/.env.local @@ -7,7 +7,7 @@ ENV_NAME=DEV # Database secrets DB_HOST=31.7.78.133 DB_PORT=14352 -DB_NAME=Production +DB_NAME=Development DB_USERNAME=postgres DB_PASSWORD=pfLIVXupbDetvFMt2gUvxLXUL9b4HIOHaPcKXsBEZ1i8zl0iLUjmhUfXlGfJKcTV diff --git a/apps/authentication/migrations/0058_organization_ownership_code.py b/apps/authentication/migrations/0058_organization_ownership_code.py new file mode 100644 index 0000000..c7f2919 --- /dev/null +++ b/apps/authentication/migrations/0058_organization_ownership_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2026-01-05 07:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0057_organization_purchase_policy'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='ownership_code', + field=models.IntegerField(default=0), + ), + ] diff --git a/apps/authentication/migrations/0059_remove_organization_ownership_code.py b/apps/authentication/migrations/0059_remove_organization_ownership_code.py new file mode 100644 index 0000000..13d9adf --- /dev/null +++ b/apps/authentication/migrations/0059_remove_organization_ownership_code.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0 on 2026-01-05 07:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0058_organization_ownership_code'), + ] + + operations = [ + migrations.RemoveField( + model_name='organization', + name='ownership_code', + ), + ] diff --git a/apps/authentication/migrations/0060_organization_ownership_code.py b/apps/authentication/migrations/0060_organization_ownership_code.py new file mode 100644 index 0000000..fe83fbc --- /dev/null +++ b/apps/authentication/migrations/0060_organization_ownership_code.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2026-01-05 07:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0059_remove_organization_ownership_code'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='ownership_code', + field=models.IntegerField(default=0), + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 7feca37..3644dd1 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -147,6 +147,7 @@ class Organization(BaseModel): has_pos = models.BooleanField(default=False) additional_data = models.JSONField(default=dict) service_area = models.ManyToManyField(City, related_name='service_area') + ownership_code = models.IntegerField(default=0) PURCHASE_POLICIES = ( ('INTERNAL_ONLY', 'Internal Only'), diff --git a/apps/herd/web/api/v1/serializers.py b/apps/herd/web/api/v1/serializers.py index 1de4c0c..1e302d0 100644 --- a/apps/herd/web/api/v1/serializers.py +++ b/apps/herd/web/api/v1/serializers.py @@ -30,7 +30,6 @@ class HerdSerializer(serializers.ModelSerializer): if isinstance(instance, Herd): if instance.owner: representation['owner'] = instance.owner.id - representation['cooperative'] = OrganizationSerializer(instance.cooperative).data representation['province'] = ProvinceSerializer(instance.province).data representation['city'] = CitySerializer(instance.city).data if instance.contractor: diff --git a/apps/livestock/models.py b/apps/livestock/models.py index ecb71d7..310eccd 100644 --- a/apps/livestock/models.py +++ b/apps/livestock/models.py @@ -95,7 +95,7 @@ class LiveStock(BaseModel): archive = models.BooleanField(default=False) def __str__(self): - return f'{self.type.name}-{self.species.name}' + return f'{self.type.name if self.type else ''}-{self.species.name if self.species else ''}' def save(self, *args, **kwargs): return super(LiveStock, self).save(*args, **kwargs) diff --git a/apps/tag/exceptions.py b/apps/tag/exceptions.py index 5641803..95c1e33 100644 --- a/apps/tag/exceptions.py +++ b/apps/tag/exceptions.py @@ -1,8 +1,25 @@ -from rest_framework.exceptions import APIException from rest_framework import status +from rest_framework.exceptions import APIException class SpeciesNumberCheckException(APIException): status_code = status.HTTP_403_FORBIDDEN default_detail = 'Entered species number is more than user free tags' default_code = 'more than free tags' + + +class TagException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "خطا در اطلاعات پلاک" # noqa + default_code = 'error' + + def __init__(self, message=None, status_code=None, code=None): + if status_code is not None: + self.status_code = status_code + + detail = { + "message": message, + "status_code": status_code + } + + super().__init__(detail) diff --git a/apps/tag/migrations/0027_alter_tag_country_code_alter_tag_ownership_code_and_more.py b/apps/tag/migrations/0027_alter_tag_country_code_alter_tag_ownership_code_and_more.py new file mode 100644 index 0000000..1f2eab3 --- /dev/null +++ b/apps/tag/migrations/0027_alter_tag_country_code_alter_tag_ownership_code_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.0 on 2026-01-05 07:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0026_temporarytags_sync_status'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='country_code', + field=models.IntegerField(default=364, help_text='it is static 364, iran country code'), + ), + migrations.AlterField( + model_name='tag', + name='ownership_code', + field=models.IntegerField(default=0, help_text='ownership code of organizations like: 51, 52, etc'), + ), + migrations.AlterField( + model_name='tag', + name='serial', + field=models.CharField(help_text='7 digit number as serial on tag code', max_length=8), + ), + migrations.AlterField( + model_name='tag', + name='species_code', + field=models.IntegerField(default=0, help_text='code of livestock type like: 1->sheep'), + ), + migrations.AlterField( + model_name='tag', + name='static_code', + field=models.IntegerField(default=0, help_text='always a static number'), + ), + ] diff --git a/apps/tag/models.py b/apps/tag/models.py index 7a0cbfd..8440739 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -1,21 +1,19 @@ import random -from crum import get_current_user from django.db import models from jdatetime import datetime from apps.authentication import models as auth_models -from apps.authorization import models as authoriz_models from apps.core.models import BaseModel from apps.tag.tools import tag_code_serial_scanning class Tag(BaseModel): - country_code = models.IntegerField(default=364) - static_code = models.IntegerField(default=0) - ownership_code = models.IntegerField(default=0) - species_code = models.IntegerField(default=0) - serial = models.CharField(max_length=8) + country_code = models.IntegerField(default=364, help_text='it is static 364, iran country code') + static_code = models.IntegerField(default=0, help_text='always a static number') + ownership_code = models.IntegerField(default=0, help_text='ownership code of organizations like: 51, 52, etc') + species_code = models.IntegerField(default=0, help_text='code of livestock type like: 1->sheep') + serial = models.CharField(max_length=8, help_text='7 digit number as serial on tag code') tag_code = models.CharField(max_length=20, unique=True, null=True) organization = models.ForeignKey( auth_models.Organization, @@ -25,10 +23,17 @@ class Tag(BaseModel): ) status_choices = ( ('F', 'Free'), - ('A', 'Assigned') + ('R', 'Reserved'), + ('A', 'Assigned'), ) status = models.CharField(max_length=20, default="F") + class Meta: + indexes = [ + models.Index(fields=['ownership_code', 'species_code']), + models.Index(fields=['status']), + ] + def __str__(self): return f'{self.id}-{self.tag_code}' @@ -43,16 +48,6 @@ class Tag(BaseModel): f"{self.ownership_code}" \ f"{self.species_code}" \ f"{self.serial}" - if not self.organization: - # set user organization for tag - user = get_current_user() - self.organization = ( - authoriz_models.UserRelations.objects.select_related( - 'organization' - ).get( - user=user - )).organization - super(Tag, self).save(*args, **kwargs) diff --git a/apps/tag/services/__init__.py b/apps/tag/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tag/services/tag_services.py b/apps/tag/services/tag_services.py new file mode 100644 index 0000000..467b453 --- /dev/null +++ b/apps/tag/services/tag_services.py @@ -0,0 +1,49 @@ +from django.db.models import Q +from django.db.models.aggregates import Count + +from apps.livestock.web.api.v1.serializers import LiveStockSerializer +from apps.tag.models import Tag + + +class TagService: + """ + Different Services of Livestock Tags + """ + + def tag_detail(self, by_id: int = None, by_queryset: bool = False): + """ + get detail of a tag like: livestock, rancher, herd, etc + """ + + if by_id: + tag = Tag.objects.prefetch_related( + 'livestock_tag' + ).get(id=by_id) + + livestock = tag.livestock_tag.first() + + result = LiveStockSerializer(livestock).data + else: + result = dict() + + return result + + def tag_dashboard(self): + """ + dashboard of tags page + """ + + queryset = Tag.objects.all() + response = queryset.aggregate( + count=Count('id'), + cow_count=Count('id', filter=Q(species_code=1)), + buffalo_count=Count('id', filter=Q(species_code=2)), + camel_count=Count('id', filter=Q(species_code=3)), + sheep_count=Count('id', filter=Q(species_code=4)), + goat_count=Count('id', filter=Q(species_code=5)), + free_count=Count('id', filter=Q(status='F')), + assign_count=Count('id', filter=Q(status='A')), + + ) + + return response diff --git a/apps/tag/web/api/v1/api.py b/apps/tag/web/api/v1/api.py index d5496bb..8e2b3bc 100644 --- a/apps/tag/web/api/v1/api.py +++ b/apps/tag/web/api/v1/api.py @@ -1,53 +1,81 @@ -from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin -from rest_framework import viewsets -from apps.tag import models as tag_models +import typing + +from django.db import IntegrityError +from django.db import transaction from rest_framework import status +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.exceptions import APIException +from rest_framework.filters import SearchFilter from rest_framework.response import Response + +from apps.authentication.api.v1.api import GeneralOTPViewSet +from apps.authorization import models as authorize_models +from apps.core.api import BaseViewSet +from apps.core.mixins.search_mixin import DynamicSearchMixin +from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin +from apps.tag import exceptions as tag_exceptions +from apps.tag import models as tag_models +from apps.tag.services.tag_services import TagService +from common.helpers import get_organization_by_user +from common.liara_tools import upload_to_liara from .serializers import ( TagSerializer, TagAssignmentSerializer, AllocatedTagsSerializer ) -from rest_framework.decorators import action -from django.db import transaction -from rest_framework.exceptions import APIException -from apps.tag import permissions as tag_permissions -from apps.authorization import models as authorize_models -from apps.authentication.api.v1.api import GeneralOTPViewSet -from apps.tag.tools import tag_code_serial_scanning -from apps.tag import exceptions as tag_exceptions -from common.helpers import detect_file_extension -from common.liara_tools import upload_to_liara -from django.db import IntegrityError -import typing -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 TagViewSet(SoftDeleteMixin, viewsets.ModelViewSet): +class TagViewSet(BaseViewSet, TagService, SoftDeleteMixin, DynamicSearchMixin, viewsets.ModelViewSet): """ Tag View Set """ queryset = tag_models.Tag.objects.all() serializer_class = TagSerializer + filter_backends = [SearchFilter] + search_fields = [ + 'serial', + 'tag_code', + 'organization__name', + 'organization__type__key', + 'livestock_tag__herd__rancher__ranching_farm', + 'livestock_tag__herd__rancher__first_name', + 'livestock_tag__herd__rancher__last_name', + 'livestock_tag__herd__rancher__mobile', + 'livestock_tag__herd__rancher__national_code', + ] + + def list(self, request, *args, **kwargs): + """ + list of tags with filter & search + """ + param = self.request.query_params # noqa + queryset = self.get_queryset(visibility_by_org_scope=True).order_by('-create_date') + + if 'status' in param.keys(): + queryset = queryset.filter(status=param.get('status', None)) + + # filter queryset + queryset = self.filter_query(queryset) + + page = self.paginate_queryset(queryset) + if page is not None: # noqa + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + return Response(self.serializer_class(queryset).data) @transaction.atomic def create(self, request: object, *args: list, **kwargs: dict) -> typing.Any: """ Create tag for livestocks """ # noqa + + org = get_organization_by_user(request.user) # noqa tag_objects = [] serial_start_range, serial_end_range = request.data['serial_range'] # serial_range is like [500, 550] while serial_start_range <= serial_end_range: try: - request.data.update({'serial': str(serial_start_range)}) + request.data.update({ + 'serial': str(serial_start_range), + 'ownership_code': org.ownership_code, + 'organization': org, + }) serializer = self.serializer_class(data=request.data) if serializer.is_valid(raise_exception=True): tag_objects.append(serializer.save()) @@ -59,6 +87,35 @@ class TagViewSet(SoftDeleteMixin, viewsets.ModelViewSet): serializer = self.serializer_class(tag_objects, many=True) return Response(serializer.data, status.HTTP_201_CREATED) + @action( + methods=['get'], + detail=True, + url_path='tag_detail', + url_name='tag_detail', + name='tag_detail', + ) + def get_tag_detail(self, request, pk=None): + """ + show detail of a tag + """ + response = self.tag_detail(by_id=pk) + return Response(response) + + @action( + methods=['get'], + detail=False, + url_path='tag_dashboard', + url_name='tag_dashboard', + name='tag_dashboard' + ) + def get_tag_dashboard(self, request): + """ + dashboard of tags page + """ + + response = self.tag_dashboard() + return Response(response) + @action( methods=['get'], detail=False, @@ -77,39 +134,8 @@ class TagViewSet(SoftDeleteMixin, viewsets.ModelViewSet): except APIException as e: return Response(e, status.HTTP_204_NO_CONTENT) - @action( - methods=['put'], - detail=True, - url_path='trash', - url_name='trash', - name='trash', - ) - @transaction.atomic - def trash(self, request, pk=None): - """ Sent Tag 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 Tag 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 TagAssignmentViewSet(SoftDeleteMixin, viewsets.ModelViewSet): +class TagAssignmentViewSet(BaseViewSet, SoftDeleteMixin, DynamicSearchMixin, viewsets.ModelViewSet): """ assignment of tags """ queryset = tag_models.TagAssignment.objects.all() user_relations_queryset = authorize_models.UserRelations.objects.all() @@ -239,69 +265,7 @@ class TagAssignmentViewSet(SoftDeleteMixin, viewsets.ModelViewSet): else: return Response(check_response.status_code, status=status.HTTP_403_FORBIDDEN) - @action( - methods=['put'], - detail=True, - url_path='trash', - url_name='trash', - name='trash', - ) - @transaction.atomic - def trash(self, request, pk=None): - """ Sent TagAssigment 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 TagAssignment 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 AllocatedTagsViewSet(SoftDeleteMixin, viewsets.ModelViewSet): queryset = tag_models.AllocatedTags.objects.all() serializer_class = AllocatedTagsSerializer - - @action( - methods=['put'], - detail=True, - url_path='trash', - url_name='trash', - name='trash', - ) - @transaction.atomic - def trash(self, request, pk=None): - """ Sent AllocatedTag 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 AllocatedTag 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) diff --git a/apps/tag/web/api/v1/serializers.py b/apps/tag/web/api/v1/serializers.py index c4c8134..e96ee45 100644 --- a/apps/tag/web/api/v1/serializers.py +++ b/apps/tag/web/api/v1/serializers.py @@ -1,6 +1,8 @@ -from apps.authentication.api.v1.serializers import serializer as auth_serializers from rest_framework import serializers + +from apps.authentication.api.v1.serializers import serializer as auth_serializers from apps.tag import models as tag_models +from apps.tag.exceptions import TagException class TagSerializer(serializers.ModelSerializer): @@ -25,6 +27,18 @@ class TagSerializer(serializers.ModelSerializer): } } + def validate(self, attrs): + """ + Some validation on tags serialization + """ + species_code = attrs.get('species_code', None) + serial_range = attrs.get('serial_range', None) + + if self.Meta.model.objects.filter(species_code=species_code, serial__in=serial_range).exists(): + raise TagException(f'پلاک با سریال مد نظر از قبل وجود دارد', status_code=403) # noqa + + return attrs + def update(self, instance, validated_data): """ update tag information """ @@ -58,7 +72,11 @@ class TagSerializer(serializers.ModelSerializer): """ Customize output of serializer """ representation = super().to_representation(instance) if isinstance(instance, tag_models.Tag): - representation['organization'] = auth_serializers.OrganizationSerializer(instance.organization).data + if instance.organization: + representation['organization'] = { + 'name': instance.organization.name, + 'id': instance.organization.id + } return representation