From 6491c53306b0d639cb2d81db2670c22723059c85 Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Sun, 7 Dec 2025 15:50:35 +0330 Subject: [PATCH] import - quota dashboard & some changes in product quota stat --- apps/authentication/api/v1/api.py | 3 + apps/authorization/api/v1/api.py | 3 +- apps/product/models.py | 6 + .../services/quota_dashboard_service.py | 50 ++++ apps/product/signals.py | 257 +++++++++--------- apps/product/web/api/v1/viewsets/quota_api.py | 33 ++- 6 files changed, 224 insertions(+), 128 deletions(-) create mode 100644 apps/product/services/quota_dashboard_service.py diff --git a/apps/authentication/api/v1/api.py b/apps/authentication/api/v1/api.py index 563be56..2b1d14e 100644 --- a/apps/authentication/api/v1/api.py +++ b/apps/authentication/api/v1/api.py @@ -212,6 +212,8 @@ class OrganizationTypeViewSet(BaseViewSet, SoftDeleteMixin, ModelViewSet): """ Crud operations for Organization Type model """ # queryset = OrganizationType.objects.all() serializer_class = OrganizationTypeSerializer + filter_backends = [filters.SearchFilter] + search_fields = ['name'] def list(self, request, *args, **kwargs): """ all organization type """ @@ -226,6 +228,7 @@ class OrganizationTypeViewSet(BaseViewSet, SoftDeleteMixin, ModelViewSet): elif org_type_field == 'CO': queryset = self.get_queryset().filter(org_type_field=org_type_field).order_by('-modify_date') + queryset = self.filter_queryset(queryset) # noqa page = self.paginate_queryset(queryset.order_by('-create_date')) # paginate queryset # noqa if page is not None: # noqa serializer = self.serializer_class(page, many=True) diff --git a/apps/authorization/api/v1/api.py b/apps/authorization/api/v1/api.py index 11b39b0..69ba46a 100644 --- a/apps/authorization/api/v1/api.py +++ b/apps/authorization/api/v1/api.py @@ -113,7 +113,8 @@ class RoleViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet): def list(self, request, *args, **kwargs): """ all roles """ - role = self.paginate_queryset(self.get_queryset().order_by('-modify_date')) + queryset = self.filter_queryset(self.get_queryset().order_by('-modify_date')) + role = self.paginate_queryset(queryset) if role is not None: # noqa serializer = self.get_serializer(role, many=True) return self.get_paginated_response(serializer.data) diff --git a/apps/product/models.py b/apps/product/models.py index af1c6cb..81cf934 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -154,6 +154,12 @@ class ProductStats(BaseModel): related_name='product_stats', null=True ) + product_org_stat_type = models.CharField( + max_length=25, + null=True, + default='registerer', + help_text='registerer or distributioned' # noqa + ) quotas_number = models.PositiveBigIntegerField(default=0) sale_unit = models.CharField(max_length=25, null=True) active_quotas_weight = models.PositiveBigIntegerField(default=0) diff --git a/apps/product/services/quota_dashboard_service.py b/apps/product/services/quota_dashboard_service.py new file mode 100644 index 0000000..3ceee1d --- /dev/null +++ b/apps/product/services/quota_dashboard_service.py @@ -0,0 +1,50 @@ +from django.db.models import Sum, Count +from django.db.models.functions import Coalesce + +from apps.authentication.models import Organization +from apps.authentication.services.service import get_all_org_child +from apps.core.services.filter.search import DynamicSearchService +from apps.product.models import OrganizationQuotaStats + + +class QuotaDashboardService: + """ + dashboard of quota information + """ + + @staticmethod + def get_dashboard(self, org: Organization, start_date: str = None, end_date: str = None, + search_fields: list[str] = None): + orgs_child = get_all_org_child(org=org) + orgs_child.append(org) + + if org.type.key == 'ADM': + org_quota_stats = OrganizationQuotaStats.objects.all() + else: + org_quota_stats = OrganizationQuotaStats.objects.filter( + organization__in=orgs_child + ) + + # filter queryset (transactions & items) by date + if start_date and end_date: + org_quota_stats = DynamicSearchService( + queryset=org_quota_stats, + start=start_date, + end=end_date, + date_field="create_date", + search_fields=search_fields + ).apply() + + org_quota_stats = org_quota_stats.aggregate( + total_quotas=Count("quota", distinct=True), + total_distributed=Coalesce(Sum("total_distributed", ), 0), + remaining_amount=Coalesce(Sum("remaining_amount", ), 0), + inventory_received=Coalesce(Sum("inventory_received", ), 0), + sold_amount=Coalesce(Sum("sold_amount", ), 0), + total_amount=Coalesce(Sum("total_amount", ), 0), + inventory_entry_balance=Coalesce(Sum("inventory_entry_balance", ), 0), + ) + + return { + "quotas_summary": org_quota_stats, + } diff --git a/apps/product/signals.py b/apps/product/signals.py index a3a5d77..2b09839 100644 --- a/apps/product/signals.py +++ b/apps/product/signals.py @@ -14,7 +14,6 @@ from common.helpers import get_organization_by_user from .models import ( QuotaDistribution, Quota, - Product, ProductStats, QuotaStats, OrganizationQuotaStats ) @@ -75,144 +74,150 @@ def update_quota_remaining(sender, instance, **kwargs): remaining_distribution_weight(instance) -def update_product_stats(instance: Product, distribution: QuotaDistribution = None): +def update_product_stats(instance: OrganizationQuotaStats): """ update all stats of product """ user = get_current_user() # get user object if not isinstance(user, AnonymousUser): organization = get_organization_by_user(user) - QuotaStatsValidator.validate_assigner_has_enough( - organization, - distribution.quota, - distribution.weight, - allow_zero=True + # QuotaStatsValidator.validate_assigner_has_enough( + # organization, + # distribution.quota, + # distribution.weight, + # allow_zero=True + # ) + + stat, created = ProductStats.objects.get_or_create( + organization=organization, + product=instance, + sale_unit=instance.quota.sale_unit.unit ) - if ProductStats.objects.filter( - organization=organization, - product=instance, - sale_unit=distribution.quota.sale_unit.unit - ): - stat = instance.stats.get( - organization=organization, - product=instance, - sale_unit=distribution.quota.sale_unit.unit - ) - else: - stat = ProductStats.objects.create( - product=instance, - organization=organization, - sale_unit=distribution.quota.sale_unit.unit - ) # number of quotas - quota = Quota.objects.filter( - Q( - distributions_assigned__in=QuotaDistribution.objects.filter( - Q(assigned_organization=organization) | - Q(assigner_organization=organization) & - Q(parent_distribution__isnull=True) - - ) - ) | - Q(registerer_organization=organization), - product=instance - ).distinct() - - quotas_count = quota.count() # noqa - - total_quotas_weight = quota.aggregate( # noqa - total=models.Sum('quota_weight') - )['total'] or 0 - - # total weight of product that assigned in quota - active_quotas_weight = quota.filter(is_closed=False).aggregate( - total=models.Sum('quota_weight') - )['total'] or 0 - - closed_quotas_weight = quota.filter(is_closed=True).aggregate( # noqa - total=models.Sum('quota_weight') - )['total'] or 0 - - # total remaining weight of product quotas - total_remaining_quotas_weight = quota.filter(is_closed=False).aggregate( # noqa - total=models.Sum('remaining_weight') - )['total'] or 0 - - received_distribution_weight = QuotaDistribution.objects.filter( - quota__product_id=instance.id, - quota__is_closed=False, - quota__sale_unit=distribution.quota.sale_unit, - assigned_organization=organization, - parent_distribution__isnull=True + org_quota_stat = OrganizationQuotaStats.objects.filter( + organization=organization, + quota__product=instance.quota.product, ) - received_distribution_number = received_distribution_weight.count() + quotas_count = org_quota_stat.count() # noqa - received_distribution_weight = received_distribution_weight.aggregate( - total_weight=models.Sum('weight') - )['total_weight'] or 0 - - # product total distributed weight from quota - given_distribution_weight = QuotaDistribution.objects.filter( - quota__product_id=instance.id, - quota__is_closed=False, - quota__sale_unit=distribution.quota.sale_unit, - assigner_organization=organization, - parent_distribution__isnull=True + product_stat_data = org_quota_stat.aggregate( + total_quotas_weight=models.Sum('total_amount'), + active_quotas_weight=models.Sum('active_quotas_weight', filter=Q(quota__is_closed=False)), + closed_quotas_weight=models.Sum('closed_quotas_weight', filter=Q(quota__is_closed=True)), + total_remaining_quotas_weight=models.Sum('remaining_amount'), + total_distributed=models.Sum('quota_distributed'), + total_inventory_in=models.Sum('total_inventory_in'), + total_sold=models.Sum('total_sold'), ) + # quota = Quota.objects.filter( + # Q( + # distributions_assigned__in=QuotaDistribution.objects.filter( + # Q(assigned_organization=organization) | + # Q(assigner_organization=organization) & + # Q(parent_distribution__isnull=True) + # + # ) + # ) | + # Q(registerer_organization=organization), + # product=instance + # ).distinct() + # + # quotas_count = quota.count() # noqa + # + # total_quotas_weight = quota.aggregate( # noqa + # total=models.Sum('quota_weight') + # )['total'] or 0 + # + # # total weight of product that assigned in quota + # active_quotas_weight = quota.filter(is_closed=False).aggregate( + # total=models.Sum('quota_weight') + # )['total'] or 0 + # + # closed_quotas_weight = quota.filter(is_closed=True).aggregate( # noqa + # total=models.Sum('quota_weight') + # )['total'] or 0 + # + # # total remaining weight of product quotas + # total_remaining_quotas_weight = quota.filter(is_closed=False).aggregate( # noqa + # total=models.Sum('remaining_weight') + # )['total'] or 0 + # + # received_distribution_weight = QuotaDistribution.objects.filter( + # quota__product_id=instance.id, + # quota__is_closed=False, + # quota__sale_unit=distribution.quota.sale_unit, + # assigned_organization=organization, + # parent_distribution__isnull=True + # ) + # + # received_distribution_number = received_distribution_weight.count() + # + # received_distribution_weight = received_distribution_weight.aggregate( + # total_weight=models.Sum('weight') + # )['total_weight'] or 0 + # + # # product total distributed weight from quota + # given_distribution_weight = QuotaDistribution.objects.filter( + # quota__product_id=instance.id, + # quota__is_closed=False, + # quota__sale_unit=distribution.quota.sale_unit, + # assigner_organization=organization, + # parent_distribution__isnull=True + # ) + # + # given_distribution_number = given_distribution_weight.count() + # given_distribution_weight = given_distribution_weight.aggregate( + # total_weight=models.Sum('weight') + # )['total_weight'] or 0 + # + # if received_distribution_weight > 0: + # distribution_weight_balance = received_distribution_weight - given_distribution_weight + # else: + # distribution_weight_balance = given_distribution_weight + # + # # total sold of product from quota + # total_sold = QuotaDistribution.objects.filter( + # quota__product_id=instance.id, + # quota__is_closed=False, + # quota__sale_unit=distribution.quota.sale_unit, + # assigned_organization=organization + # ).aggregate(total_sold=models.Sum('been_sold'))['total_sold'] or 0 + # + # # total entry from product to inventory + # total_warehouse_entry = QuotaDistribution.objects.filter( + # quota__product_id=instance.id, + # quota__is_closed=False, + # quota__sale_unit=distribution.quota.sale_unit, + # assigned_organization=organization + # ).aggregate(total_entry=models.Sum('warehouse_entry'))['total_entry'] or 0 - given_distribution_number = given_distribution_weight.count() - given_distribution_weight = given_distribution_weight.aggregate( - total_weight=models.Sum('weight') - )['total_weight'] or 0 - - if received_distribution_weight > 0: - distribution_weight_balance = received_distribution_weight - given_distribution_weight - else: - distribution_weight_balance = given_distribution_weight - - # total sold of product from quota - total_sold = QuotaDistribution.objects.filter( - quota__product_id=instance.id, - quota__is_closed=False, - quota__sale_unit=distribution.quota.sale_unit, - assigned_organization=organization - ).aggregate(total_sold=models.Sum('been_sold'))['total_sold'] or 0 - - # total entry from product to inventory - total_warehouse_entry = QuotaDistribution.objects.filter( - quota__product_id=instance.id, - quota__is_closed=False, - quota__sale_unit=distribution.quota.sale_unit, - assigned_organization=organization - ).aggregate(total_entry=models.Sum('warehouse_entry'))['total_entry'] or 0 - - stat.quotas_number = quotas_count - stat.active_quotas_weight = active_quotas_weight - stat.closed_quotas_weight = closed_quotas_weight - stat.total_quota_weight = total_quotas_weight - stat.total_quota_remaining = total_remaining_quotas_weight - stat.total_remaining_distribution_weight = distribution_weight_balance - stat.received_distribution_weight = received_distribution_weight - stat.given_distribution_weight = given_distribution_weight - stat.received_distribution_number = received_distribution_number - stat.given_distribution_number = given_distribution_number - stat.total_warehouse_entry = total_warehouse_entry - stat.total_sold = total_sold - stat.save(update_fields=[ - "quotas_number", - "active_quotas_weight", - "closed_quotas_weight", - "total_quota_weight", - "total_quota_remaining", - "total_remaining_distribution_weight", - "received_distribution_weight", - "given_distribution_weight", - "received_distribution_number", - "total_warehouse_entry", - "total_sold", - ]) + # stat.quotas_number = quotas_count + # stat.active_quotas_weight = active_quotas_weight + # stat.closed_quotas_weight = closed_quotas_weight + # stat.total_quota_weight = total_quotas_weight + # stat.total_quota_remaining = total_remaining_quotas_weight + # stat.total_remaining_distribution_weight = distribution_weight_balance + # stat.received_distribution_weight = received_distribution_weight + # stat.given_distribution_weight = given_distribution_weight + # stat.received_distribution_number = received_distribution_number + # stat.given_distribution_number = given_distribution_number + # stat.total_warehouse_entry = total_warehouse_entry + # stat.total_sold = total_sold + # stat.save(update_fields=[ + # "quotas_number", + # "active_quotas_weight", + # "closed_quotas_weight", + # "total_quota_weight", + # "total_quota_remaining", + # "total_remaining_distribution_weight", + # "received_distribution_weight", + # "given_distribution_weight", + # "received_distribution_number", + # "total_warehouse_entry", + # "total_sold", + # ]) def update_quota_stats(instance: Quota): @@ -254,7 +259,7 @@ def update_quota_stats(instance: Quota): @receiver([post_save, post_delete], sender=InventoryQuotaSaleTransaction) def update_stats_on_change(sender, instance, **kwargs): if sender == QuotaDistribution: - update_product_stats(instance.quota.product, instance) + # update_product_stats(instance.quota.product, instance) update_quota_stats(instance.quota) # if _from_signal=True prevent from maximum recursion loop diff --git a/apps/product/web/api/v1/viewsets/quota_api.py b/apps/product/web/api/v1/viewsets/quota_api.py index 1844663..410eb68 100644 --- a/apps/product/web/api/v1/viewsets/quota_api.py +++ b/apps/product/web/api/v1/viewsets/quota_api.py @@ -15,6 +15,7 @@ from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin from apps.core.pagination import CustomPageNumberPagination from apps.product import models as product_models from apps.product.exceptions import QuotaExpiredTimeException +from apps.product.services.quota_dashboard_service import QuotaDashboardService from apps.product.web.api.v1.serializers import quota_distribution_serializers from apps.product.web.api.v1.serializers import quota_serializers from apps.product.web.api.v1.viewsets import product_api @@ -35,7 +36,8 @@ def delete(queryset, pk): obj.delete() -class QuotaViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin): # noqa +class QuotaViewSet(BaseViewSet, SoftDeleteMixin, QuotaDashboardService, viewsets.ModelViewSet, + DynamicSearchMixin, ): # noqa """ apis for product quota """ queryset = product_models.Quota.objects.all() @@ -49,6 +51,8 @@ class QuotaViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, DynamicS "sale_type", "sale_unit__unit", "group", + "created_by", + "modified_by" ] @transaction.atomic @@ -351,6 +355,33 @@ class QuotaViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, DynamicS quota.save() return Response(status=status.HTTP_200_OK) + @action( + methods=['get'], + detail=False, + url_path='quotas_dashboard', + url_name='quotas_dashboard', + name='quotas_dashboard' + ) + def quotas_dashboard(self, request): + """ + dashboard of all quotas & their information + """ + + query_param = self.request.query_params # noqa + + start_date = query_param.get('start') if 'start' in query_param.keys() else None + end_date = query_param.get('end') if 'end' in query_param.keys() else None + + quota_dashboard = self.get_dashboard( + self, + org=get_organization_by_user(request.user), + start_date=start_date, + end_date=end_date, + search_fields=self.search_fields, + ) + + return Response(quota_dashboard) + @action( methods=['get'], detail=False,