diff --git a/apps/tag/migrations/0037_tagdistribution_dist_identity_and_more.py b/apps/tag/migrations/0037_tagdistribution_dist_identity_and_more.py new file mode 100644 index 0000000..6c878fa --- /dev/null +++ b/apps/tag/migrations/0037_tagdistribution_dist_identity_and_more.py @@ -0,0 +1,54 @@ +# Generated by Django 5.0 on 2026-01-20 07:31 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0060_organization_ownership_code'), + ('tag', '0036_tagdistribution_assigner_org_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='tagdistribution', + name='dist_identity', + field=models.CharField(default='0', max_length=20, null=True, unique=True), + ), + migrations.AddField( + model_name='tagdistribution', + name='is_closed', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='tagbatch', + name='tag', + field=models.ManyToManyField(related_name='batches', to='tag.tag'), + ), + migrations.CreateModel( + name='TagDistributionBatch', + 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)), + ('dist_batch_identity', models.CharField(default='0', max_length=20, null=True, unique=True)), + ('total_tag_count', models.IntegerField(default=0)), + ('is_closed', models.BooleanField(default=False)), + ('assigned_org', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='to_tag_distribution_batch', to='authentication.organization')), + ('assigner_org', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='from_tag_distribution_batch', to='authentication.organization')), + ('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)), + ('distributions', models.ManyToManyField(related_name='tag_distribution_batch', to='tag.tagdistribution')), + ('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, + }, + ), + ] diff --git a/apps/tag/models.py b/apps/tag/models.py index 03fcf67..59c4107 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -65,7 +65,7 @@ class TagBatch(BaseModel): null=True ) - tag = models.ManyToManyField(Tag, related_name='tags') + tag = models.ManyToManyField(Tag, related_name='batches') species_code = models.IntegerField(default=0) serial_from = models.PositiveBigIntegerField(default=0) serial_to = models.PositiveBigIntegerField(default=0) @@ -89,6 +89,7 @@ class TagBatch(BaseModel): class TagDistribution(BaseModel): + dist_identity = models.CharField(max_length=20, default="0", unique=True, null=True) batch = models.ForeignKey( TagBatch, on_delete=models.CASCADE, @@ -110,6 +111,7 @@ class TagDistribution(BaseModel): ) species_code = models.IntegerField(default=0) distributed_number = models.IntegerField(default=0) + is_closed = models.BooleanField(default=False) def __str__(self): return f'{self.id}-{self.distributed_number}-{self.assigned_org.name}' @@ -118,6 +120,31 @@ class TagDistribution(BaseModel): return super(TagDistribution, self).save(*args, **kwargs) +class TagDistributionBatch(BaseModel): + dist_batch_identity = models.CharField(max_length=20, default="0", unique=True, null=True) + assigner_org = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name='from_tag_distribution_batch', + null=True + ) + assigned_org = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name="to_tag_distribution_batch", + null=True + ) + distributions = models.ManyToManyField(TagDistribution, related_name='tag_distribution_batch') + total_tag_count = models.IntegerField(default=0) + is_closed = models.BooleanField(default=False) + + def __str__(self): + return f'{self.id}' + + def save(self, *args, **kwargs): + return super(TagDistributionBatch, self).save(*args, **kwargs) + + class TagAssignment(BaseModel): organization = models.ForeignKey( auth_models.Organization, diff --git a/apps/tag/services/tag_distribution_services.py b/apps/tag/services/tag_distribution_services.py index 6eaf2b0..4e4d405 100644 --- a/apps/tag/services/tag_distribution_services.py +++ b/apps/tag/services/tag_distribution_services.py @@ -1,6 +1,83 @@ +import random + +from django.db import transaction + +from apps.authentication.models import Organization +from apps.tag.exceptions import TagException +from apps.tag.models import Tag, TagBatch, TagDistribution, TagDistributionBatch +from common.generics import generate_unique_code + + class TagDistributionService: """ service of distribute tags in organizations """ - pass + def create_distribution(self, org: Organization = None, data: dict = None): + """ + distribute tags with batch + """ + with transaction.atomic(): + distributions = [] + total_counted_tags = 0 + assigned_org = Organization.objects.get(id=data.get('assigned_org')) + + for distribution in data.get('dists'): + batch_identity = distribution.get('batch_identity', None) + # if batch identity exists distribute tags of batch + if batch_identity: + batch = TagBatch.objects.get(batch_identity=batch_identity) + tags = Tag.objects.filter( + batches__batch_identity=batch_identity, + species_code=distribution.get('species_code'), + status='F' + ) + else: + batch = None + # get tags without batch and only with species code + tags = Tag.objects.filter( + species_code=distribution.get('species_code'), + status='F' + ) + + if tags.count() < distribution.get('count'): + raise TagException( + "تعداد وارد شده از تعداد موجودی این گونه بیشتر میباشد.", # noqa + 403 + ) + + dist = TagDistribution.objects.create( + batch=batch, + assigner_org=org, + assigned_org=assigned_org, + species_code=distribution.get('species_code'), + distributed_number=distribution.get('count'), + dist_identity=generate_unique_code(f"{random.randint(1000, 9999)}"), + ) + + # get counted tag ids and filter by them to update status To Reserve + counted_tags_obj = tags.order_by('create_date')[:int(distribution.get('count'))] + counted_tag_ids = [tag.id for tag in counted_tags_obj] + tags.filter(id__in=counted_tag_ids).update(status='R') + + dist.tag.add(*counted_tags_obj) + distributions.append(dist) + + total_counted_tags += distribution.get('count') + + # create distribution batch + distributions_batch = TagDistributionBatch.objects.create( + assigner_org=org, + assigned_org=assigned_org, + total_tag_count=total_counted_tags, + dist_batch_identity=generate_unique_code(f"{random.randint(1000, 9999)}"), + ) + distributions_batch.distributions.add(*distributions) + + return {'tag_distributions': distributions, 'distributions_batch': distributions_batch} + + def edit_distribution(self): + """ + edit record of distributed tags + """ + pass diff --git a/apps/tag/web/api/v1/api.py b/apps/tag/web/api/v1/api.py index 29d6fc4..ec8540a 100644 --- a/apps/tag/web/api/v1/api.py +++ b/apps/tag/web/api/v1/api.py @@ -16,6 +16,7 @@ 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_distribution_services import TagDistributionService from apps.tag.services.tag_services import TagService from common.helpers import get_organization_by_user from common.liara_tools import upload_to_liara @@ -344,7 +345,13 @@ class TagBatchViewSet(BaseViewSet, SoftDeleteMixin, DynamicSearchMixin, viewsets return Response(status=status.HTTP_200_OK) -class TagDistributionViewSet(BaseViewSet, SoftDeleteMixin, DynamicSearchMixin, viewsets.ModelViewSet): +class TagDistributionViewSet( + BaseViewSet, + SoftDeleteMixin, + DynamicSearchMixin, + viewsets.ModelViewSet, + TagDistributionService +): queryset = tag_models.TagDistribution.objects.all() serializer_class = TagDistributionSerializer filter_backends = [SearchFilter] @@ -369,3 +376,18 @@ class TagDistributionViewSet(BaseViewSet, SoftDeleteMixin, DynamicSearchMixin, v serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) return Response(self.serializer_class(queryset).data) + + def create(self, request, *args, **kwargs): + """ + create tag distributions with batch or without batch in random + """ + org = get_organization_by_user(request.user) + data = request.data.copy() + + distribution_data = self.create_distribution( + org=org, + data=data + ) + + serializer = self.serializer_class(distribution_data.get('tag_distributions'), many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apps/tag/web/api/v1/urls.py b/apps/tag/web/api/v1/urls.py index a286350..1a6d70f 100644 --- a/apps/tag/web/api/v1/urls.py +++ b/apps/tag/web/api/v1/urls.py @@ -4,7 +4,7 @@ from rest_framework.routers import DefaultRouter from .api import ( TagViewSet, TagAssignmentViewSet, - AllocatedTagsViewSet, TagBatchViewSet + AllocatedTagsViewSet, TagBatchViewSet, TagDistributionViewSet ) router = DefaultRouter() @@ -12,6 +12,7 @@ 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') +router.register(r'tag_distribution', TagDistributionViewSet, basename='tag_distribution') urlpatterns = [ path('v1/', include(router.urls)) diff --git a/common/generics.py b/common/generics.py index ae45661..9049ec2 100644 --- a/common/generics.py +++ b/common/generics.py @@ -1,4 +1,5 @@ import base64 +import random from datetime import datetime from functools import lru_cache @@ -56,3 +57,10 @@ def parse_birthdate(jalali_str): gregorian_dt, timezone.get_current_timezone() ) + + +def generate_unique_code(prefix: str): + now = timezone.now() + date_part = now.strftime("%Y%m%d") + rand_part = random.randint(100000, 999999) + return f"{prefix}{date_part}{rand_part}"