diff --git a/apps/core/visibility_registry.py b/apps/core/visibility_registry.py index f041371..4a6b908 100644 --- a/apps/core/visibility_registry.py +++ b/apps/core/visibility_registry.py @@ -9,6 +9,7 @@ VISIBILITY_MAP = { 'inventoryquotasaletransaction': 'seller_organization', 'device': 'assignment__client__organization', 'rancher': 'organization', + 'rancherorganizationlink': 'organization', # noqa # 'deviceactivationcode': 'organization', # 'deviceversion': 'organization', diff --git a/apps/herd/migrations/0021_rancher_organization.py b/apps/herd/migrations/0021_rancher_organization.py new file mode 100644 index 0000000..488b7c3 --- /dev/null +++ b/apps/herd/migrations/0021_rancher_organization.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0 on 2025-12-22 06:12 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0049_alter_bankaccountinformation_account_and_more'), + ('herd', '0020_alter_rancher_ignore_purchase_limit'), + ] + + operations = [ + migrations.AddField( + model_name='rancher', + name='organization', + field=models.ForeignKey(help_text='connect ranchers to their specific Taavoni', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ranchers', to='authentication.organization'), + ), + ] diff --git a/apps/herd/migrations/0022_rancherorganizationlink.py b/apps/herd/migrations/0022_rancherorganizationlink.py new file mode 100644 index 0000000..e5541cd --- /dev/null +++ b/apps/herd/migrations/0022_rancherorganizationlink.py @@ -0,0 +1,35 @@ +# Generated by Django 5.0 on 2025-12-22 07:16 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0049_alter_bankaccountinformation_account_and_more'), + ('herd', '0021_rancher_organization'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='RancherOrganizationLink', + 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)), + ('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)), + ('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='rancher_links', to='authentication.organization')), + ('rancher', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='organization_links', to='herd.rancher')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/herd/models.py b/apps/herd/models.py index 563ef59..1b24f04 100644 --- a/apps/herd/models.py +++ b/apps/herd/models.py @@ -146,3 +146,24 @@ class Rancher(BaseModel): def save(self, *args, **kwargs): return super(Rancher, self).save(*args, **kwargs) + + +class RancherOrganizationLink(BaseModel): + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name='rancher_links', + null=True + ) + rancher = models.ForeignKey( + Rancher, + on_delete=models.CASCADE, + related_name='organization_links', + null=True + ) + + def __str__(self): + return f'rancher: {self.rancher.id} - organization: {self.organization.id}' + + def save(self, *args, **kwargs): + return super(RancherOrganizationLink, self).save(*args, **kwargs) diff --git a/apps/herd/services/rancher_org_link_services.py b/apps/herd/services/rancher_org_link_services.py new file mode 100644 index 0000000..50354cc --- /dev/null +++ b/apps/herd/services/rancher_org_link_services.py @@ -0,0 +1,53 @@ +from django.db.models import Count + +from apps.authentication.models import Organization +from apps.authentication.services.service import get_all_org_child +from apps.herd.models import RancherOrganizationLink + + +class RancherOrganizationService: + """ + different services of ranchers linked to organization + """ + + def orgs_linked_rancher(self, org: Organization = None): + """ + list of organizations with their information of rancher, herd, .... + """ + if org.type.key != 'ADM': + organizations = get_all_org_child(org) + else: + organizations = Organization.objects.filter(type__key='CO') + + linked_qs = RancherOrganizationLink.objects.select_related( + 'rancher', + 'organization' + ).filter(organization__in=organizations, organization__type__key='CO') + + organizations = organizations.annotate( + rancher_count=Count( + 'rancher_links__rancher', + distinct=True + ), + herd_count=Count( + 'rancher_links__rancher__herd', + distinct=True + ), + livestock_count=Count( + 'rancher_links__rancher__herd__live_stock_herd', + distinct=True + ), + ) + + return [ + { + "id": org.id, + "name": org.name, + "province": org.province.name, + "city": org.city.name, + "rancher_count": org.rancher_count, + "herd_count": org.herd_count, + "livestock_count": org.livestock_count, + } + for org in organizations + ] diff --git a/apps/herd/web/api/v1/api.py b/apps/herd/web/api/v1/api.py index e85d2fd..6c4df10 100644 --- a/apps/herd/web/api/v1/api.py +++ b/apps/herd/web/api/v1/api.py @@ -5,13 +5,17 @@ from rest_framework.decorators import action from rest_framework.response import Response from apps.authentication.api.v1.api import UserViewSet +from apps.authentication.models import Organization 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.herd.models import Herd, Rancher +from apps.herd.models import Herd, Rancher, RancherOrganizationLink from apps.herd.services.rancher_dashboard_service import RancherDashboardService -from apps.herd.web.api.v1.serializers import HerdSerializer, RancherSerializer +from apps.herd.services.rancher_org_link_services import RancherOrganizationService +from apps.herd.web.api.v1.serializers import HerdSerializer, RancherSerializer, RancherOrganizationLinkSerializer from apps.livestock.web.api.v1.serializers import LiveStockSerializer +from apps.product.models import OrganizationQuotaStats +from apps.product.services.quota_dashboard_service import QuotaDashboardService from apps.product.web.api.v1.serializers import product_serializers from common.helpers import get_organization_by_user from common.tools import CustomOperations @@ -262,36 +266,105 @@ class RancherViewSet(BaseViewSet, RancherDashboardService, SoftDeleteMixin, view ) return Response(rancher_dashboard_data) + +class RancherOrganizationLinkViewSet( + BaseViewSet, + SoftDeleteMixin, + viewsets.ModelViewSet, + RancherOrganizationService, + QuotaDashboardService, + DynamicSearchMixin +): + queryset = RancherOrganizationLink.objects.select_related('organization', 'rancher') + serializer_class = RancherOrganizationLinkSerializer + search_fields = [ + "rancher__ranching_farm", + "rancher__first_name", + "rancher__last_name", + "rancher__mobile", + "rancher__national_code", + "rancher__birthdate", + "rancher__nationality", + "rancher__address", + "rancher__province__name", + "rancher__city__name", + "organization__name" + ] + @action( methods=['get'], detail=False, + name='org_linked_rancher_list', + url_name='org_linked_rancher_list', + url_path='org_linked_rancher_list', + ) + def org_linked_rancher_list(self, request): + """ + list of organizations with rancher information + """ + org = get_organization_by_user(request.user) + result = self.orgs_linked_rancher(org=org) + + return Response(result) + + @action( + methods=['get'], + detail=True, url_name='org_ranchers', url_path='org_ranchers', name='org_ranchers' ) - def org_ranchers(self, request): + def org_ranchers(self, request, pk=None): """ list of ranchers by organization """ - queryset = self.get_queryset(visibility_by_org_scope=True) + org_id = pk + queryset = self.get_queryset().filter(organization_id=org_id) page = self.paginate_queryset(queryset) if page is not None: # noqa - serializer = product_serializers.IncentivePlanRancherSerializer(page, many=True) + serializer = self.serializer_class(page, many=True) return self.get_paginated_response(serializer.data) return Response(status=status.HTTP_200_OK) @action( methods=['get'], - detail=False, - url_name='org_ranchers_dashboard', - url_path='org_ranchers_dashboard', - name='org_ranchers_dashboard' + detail=True, + url_name='org_ranchers_quota_dashboard', + url_path='org_ranchers_quota_dashboard', + name='org_ranchers_quota_dashboard' ) - def org_ranchers_dashboard(self, request): + def org_ranchers_quota_dashboard(self, request, pk=None): """ - dashboard of ranchers report + dashboard of Org ranchers report by quota """ + org_obj = Organization.objects.get(id=pk) + dashboard_data = self.get_dashboard(self, org=org_obj) - queryset = self.get_queryset(visibility_by_org_scope=True) + return Response(dashboard_data) + + @action( + methods=['get'], + detail=True, + url_name='org_ranchers_product_dashboard', + url_path='org_ranchers_product_dashboard', + name='org_ranchers_product_dashboard' + ) + def org_ranchers_product_dashboard(self, request, pk=None): + """ + dashboard of Org ranchers report by quota + """ + org_obj = Organization.objects.get(id=pk) + + # get organization quota stats and get product ids + org_quota_stat = OrganizationQuotaStats.objects.select_related('organization', 'quota').filter( + organization=org_obj, + quota__is_closed=False + ) + + products = {f'{stat.quota.product.name}': stat.quota.product.id for stat in org_quota_stat} + + dashboard_data = self.get_dashboard_by_product(self, organization=org_obj, products=products) + + return Response(dashboard_data) diff --git a/apps/herd/web/api/v1/serializers.py b/apps/herd/web/api/v1/serializers.py index 81f9f6d..0c74c81 100644 --- a/apps/herd/web/api/v1/serializers.py +++ b/apps/herd/web/api/v1/serializers.py @@ -1,16 +1,17 @@ +from rest_framework import serializers + from apps.authentication.api.v1.serializers.serializer import ( - UserSerializer, OrganizationSerializer, ProvinceSerializer, CitySerializer ) from apps.herd.exception import HerdCapacityException -from apps.herd.models import Herd, Rancher -from rest_framework import serializers +from apps.herd.models import Herd, Rancher, RancherOrganizationLink class HerdSerializer(serializers.ModelSerializer): """ Herd Serializer """ + class Meta: model = Herd fields = '__all__' @@ -49,14 +50,38 @@ class RancherSerializer(serializers.ModelSerializer): representation = super().to_representation(instance) - representation['province'] = { - 'id': instance.province.id, - 'name': instance.province.name - } + if instance.province: + representation['province'] = { + 'id': instance.province.id, + 'name': instance.province.name + } - representation['city'] = { - 'id': instance.city.id, - 'name': instance.city.name - } + if instance.city: + representation['city'] = { + 'id': instance.city.id, + 'name': instance.city.name + } + + return representation + + +class RancherOrganizationLinkSerializer(serializers.ModelSerializer): + class Meta: + model = RancherOrganizationLink + fields = '__all__' + + def to_representation(self, instance): + representation = super().to_representation(instance) + + if instance.rancher: + representation['rancher'] = RancherSerializer(instance.rancher).data + + if instance.organization: + representation['organization'] = { + "id": instance.organization.id, + "name": instance.organization.name, + "province": instance.organization.province.name, + "city": instance.organization.city.name + } return representation diff --git a/apps/herd/web/api/v1/urls.py b/apps/herd/web/api/v1/urls.py index 23731f9..6e745d5 100644 --- a/apps/herd/web/api/v1/urls.py +++ b/apps/herd/web/api/v1/urls.py @@ -1,12 +1,14 @@ from django.urls import path, include from rest_framework import routers -from .api import HerdViewSet, RancherViewSet +from .api import HerdViewSet, RancherViewSet, RancherOrganizationLinkViewSet router = routers.DefaultRouter() router.register('herd', HerdViewSet, basename='herd') router.register('rancher', RancherViewSet, basename='rancher') +router.register('rancher_org_link', RancherOrganizationLinkViewSet, basename='rancher_org_link') + urlpatterns = [ path('api/v1/', include(router.urls)) ] diff --git a/apps/product/migrations/0102_productstats_product_org_stat_type.py b/apps/product/migrations/0102_productstats_product_org_stat_type.py new file mode 100644 index 0000000..5870fba --- /dev/null +++ b/apps/product/migrations/0102_productstats_product_org_stat_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-12-22 06:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0101_quota_edited_pricing_features'), + ] + + operations = [ + migrations.AddField( + model_name='productstats', + name='product_org_stat_type', + field=models.CharField(default='registerer', help_text='registerer or distributioned', max_length=25, null=True), + ), + ]