diff --git a/apps/core/visibility_registry.py b/apps/core/visibility_registry.py index 4a6b908..8b3e9bd 100644 --- a/apps/core/visibility_registry.py +++ b/apps/core/visibility_registry.py @@ -10,6 +10,7 @@ VISIBILITY_MAP = { 'device': 'assignment__client__organization', 'rancher': 'organization', 'rancherorganizationlink': 'organization', # noqa + 'tagbatch': 'organization', # noqa # 'deviceactivationcode': 'organization', # 'deviceversion': 'organization', diff --git a/apps/tag/migrations/0028_tagbatch.py b/apps/tag/migrations/0028_tagbatch.py new file mode 100644 index 0000000..12f29c9 --- /dev/null +++ b/apps/tag/migrations/0028_tagbatch.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0 on 2026-01-07 07:13 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('authentication', '0060_organization_ownership_code'), + ('tag', '0027_alter_tag_country_code_alter_tag_ownership_code_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + + ] diff --git a/apps/tag/models.py b/apps/tag/models.py index 025e1b4..773d284 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -29,11 +29,11 @@ class Tag(BaseModel): ) status = models.CharField(max_length=20, default="F") - class Meta: - indexes = [ - models.Index(fields=['ownership_code', 'species_code']), - models.Index(fields=['status']), - ] + # class Meta: + # indexes = [ + # models.Index(fields=['ownership_code', 'species_code']), + # models.Index(fields=['status']), + # ] def __str__(self): return f'{self.id}-{self.tag_code}' @@ -65,12 +65,16 @@ class TagBatch(BaseModel): null=True ) + tag = models.ManyToManyField(Tag, related_name='tags') + species_code = models.IntegerField(default=0) + serial_from = models.PositiveBigIntegerField(default=0) + serial_to = models.PositiveBigIntegerField(default=0) + status = models.CharField( max_length=20, choices=[ ('created', 'CREATED'), ('distributed', 'DISTRIBUTED'), - ('created', 'CREATED'), ], null=True, default='created' @@ -84,23 +88,6 @@ class TagBatch(BaseModel): return super(TagBatch, self).save(*args, **kwargs) -class TagBatchItem(BaseModel): - batch = models.ForeignKey( - TagBatch, - on_delete=models.CASCADE, - related_name='items', - null=True - ) - tag = models.ManyToManyField(Tag, related_name='tags') - species_code = models.IntegerField(default=0) - serial_from = models.PositiveBigIntegerField(default=0) - serial_to = models.PositiveBigIntegerField(default=0) - count = models.IntegerField(default=0) - - def __str__(self): - return f"id:{self.id}-batch:{self.batch.id}-code:{self.species_code}" - - class TagAssignment(BaseModel): organization = models.ForeignKey( auth_models.Organization, diff --git a/apps/tag/services/tag_services.py b/apps/tag/services/tag_services.py index 467b453..a1b276e 100644 --- a/apps/tag/services/tag_services.py +++ b/apps/tag/services/tag_services.py @@ -1,8 +1,10 @@ from django.db.models import Q from django.db.models.aggregates import Count +from apps.authentication.models import Organization from apps.livestock.web.api.v1.serializers import LiveStockSerializer -from apps.tag.models import Tag +from apps.tag.exceptions import TagException +from apps.tag.models import Tag, TagBatch class TagService: @@ -10,6 +12,95 @@ class TagService: Different Services of Livestock Tags """ + def create_tag( + self, + serial_start_range: int = None, + serial_end_range: int = None, + org: Organization = None, + data: dict = None + ): + """ + create livestock tag with batch / batch item + """ + + # create tag batch + request_number = serial_end_range - serial_start_range + batch = TagBatch.objects.create( + organization=org, + request_number=request_number if request_number > 0 else 1, + species_code=data.get('species_code'), + serial_from=serial_start_range, + serial_to=serial_end_range, + status='created', + ) + + tag_list = [] + while serial_start_range <= serial_end_range: + data.update({ + 'serial': str(serial_start_range), + 'ownership_code': org.ownership_code, + 'organization': org, + }) + tag_list.append(Tag(**data)) + + if Tag.objects.filter(serial=serial_start_range, species_code=data.get('species_code')).exists(): + raise TagException(f' پلاک با مشخصات مورد نظر {serial_start_range} وجود دارد ', status_code=403) # noqa + + serial_start_range += 1 + + created_tags = Tag.objects.bulk_create(tag_list) + batch.tag.add(*created_tags) + + return created_tags + + def update_batch_tag( + self, + serial_start_range: int = None, + serial_end_range: int = None, + org: Organization = None, + data: dict = None, + batch_id: int = None, + ): + """ + update livestock tag with batch / batch item + """ + + # update tag batch + request_number = serial_end_range - serial_start_range + batch = TagBatch.objects.get(id=batch_id) + batch.request_number = request_number + batch.species_code = data.get('species_code') + batch.serial_from = serial_start_range + batch.serial_to = serial_end_range + batch.save(update_fields=['request_number', 'species_code', 'serial_from', 'serial_to']) + + # recreate tags for batch + tag_list = [] + while serial_start_range <= serial_end_range: + data.update({ + 'serial': str(serial_start_range), + 'ownership_code': org.ownership_code, + 'organization': org, + }) + tag_list.append(Tag(**data)) + + if Tag.objects.filter( + serial=serial_start_range, + species_code=data.get('species_code') + ).exists() and not batch.tag.filter( + serial=serial_start_range + ).exists(): + raise TagException(f' پلاک با مشخصات مورد نظر {serial_start_range} وجود دارد ', status_code=403) # noqa + + serial_start_range += 1 + + created_tags = Tag.objects.bulk_create(tag_list) + # hard delete of created tags for batch + batch.tag.all().delete() + batch.tag.add(*created_tags) + + return created_tags + def tag_detail(self, by_id: int = None, by_queryset: bool = False): """ get detail of a tag like: livestock, rancher, herd, etc diff --git a/apps/tag/web/api/v1/api.py b/apps/tag/web/api/v1/api.py index 8e2b3bc..514961b 100644 --- a/apps/tag/web/api/v1/api.py +++ b/apps/tag/web/api/v1/api.py @@ -1,6 +1,5 @@ import typing -from django.db import IntegrityError from django.db import transaction from rest_framework import status from rest_framework import viewsets @@ -16,13 +15,14 @@ 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.models import TagBatch 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 + AllocatedTagsSerializer, TagBatchSerializer ) @@ -67,24 +67,38 @@ class TagViewSet(BaseViewSet, TagService, SoftDeleteMixin, DynamicSearchMixin, v """ 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), - '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()) - except IntegrityError as e: # if tag exists before - if 'unique constraint' in e.args[0]: - return Response("tag exists", status.HTTP_406_NOT_ACCEPTABLE) - serial_start_range += 1 + serial_start_range, serial_end_range = request.data.pop('serial_range') # serial_range is like [500, 550] + print(serial_start_range, serial_end_range) + data = request.data.copy() - serializer = self.serializer_class(tag_objects, many=True) + # create tag & batch + created_tags = self.create_tag( + serial_start_range=serial_start_range, + serial_end_range=serial_end_range, + data=data, + org=org + ) + + serializer = self.serializer_class(created_tags, many=True) + return Response(serializer.data, status.HTTP_201_CREATED) + + def update(self, request, pk=None, *args, **kwargs): + """ update tag for livestocks """ # noqa + + org = get_organization_by_user(request.user) # noqa + serial_start_range, serial_end_range = request.data.pop('serial_range') # serial_range is like [500, 550] + data = request.data.copy() + + # create tag & batch + created_tags = self.update_batch_tag( + serial_start_range=serial_start_range, + serial_end_range=serial_end_range, + data=data, + org=org, + batch_id=int(pk) + ) + + serializer = self.serializer_class(created_tags, many=True) return Response(serializer.data, status.HTTP_201_CREATED) @action( @@ -269,3 +283,31 @@ class TagAssignmentViewSet(BaseViewSet, SoftDeleteMixin, DynamicSearchMixin, vie class AllocatedTagsViewSet(SoftDeleteMixin, viewsets.ModelViewSet): queryset = tag_models.AllocatedTags.objects.all() serializer_class = AllocatedTagsSerializer + + +class TagBatchViewSet(BaseViewSet, SoftDeleteMixin, DynamicSearchMixin, viewsets.ModelViewSet): + queryset = TagBatch.objects.all() + serializer_class = TagBatchSerializer + filter_backends = [SearchFilter] + search_fields = [ + "organization__name" + "request_number" + "tag__tag_code" + "species_code" + ] + + def list(self, request, *args, **kwargs): + """ + list of tag batches + """ + + queryset = self.get_queryset(visibility_by_org_scope=True).order_by('-create_date') + + # filter queryset + queryset = self.filter_query(self.filter_queryset(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) diff --git a/apps/tag/web/api/v1/serializers.py b/apps/tag/web/api/v1/serializers.py index e96ee45..e5e86d6 100644 --- a/apps/tag/web/api/v1/serializers.py +++ b/apps/tag/web/api/v1/serializers.py @@ -3,6 +3,7 @@ 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 +from apps.tag.models import TagBatch class TagSerializer(serializers.ModelSerializer): @@ -125,3 +126,20 @@ class AllocatedTagsSerializer(serializers.ModelSerializer): representation['status'] = instance.status return representation + + +class TagBatchSerializer(serializers.ModelSerializer): + class Meta: + model = TagBatch + fields = '__all__' + + def to_representation(self, instance): + representation = super().to_representation(instance) + + representation['tag'] = [{ + 'tag_code': tag.tag_code, + 'species_code': tag.species_code, + 'status': tag.status + } for tag in instance.tag.all()] + + return representation diff --git a/apps/tag/web/api/v1/urls.py b/apps/tag/web/api/v1/urls.py index 4269124..a286350 100644 --- a/apps/tag/web/api/v1/urls.py +++ b/apps/tag/web/api/v1/urls.py @@ -1,15 +1,17 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter + from .api import ( TagViewSet, TagAssignmentViewSet, - AllocatedTagsViewSet + AllocatedTagsViewSet, TagBatchViewSet ) router = DefaultRouter() router.register(r'tag', TagViewSet, basename='tag') router.register(r'tag_assignment', TagAssignmentViewSet, basename='tag_assignment') router.register(r'allocated_tag', AllocatedTagsViewSet, basename='allocated_tag') +router.register(r'tag_batch', TagBatchViewSet, basename='tag_batch') urlpatterns = [ path('v1/', include(router.urls))