diff --git a/.env.local b/.env.local index 63983c1..13815d0 100644 --- a/.env.local +++ b/.env.local @@ -1,13 +1,13 @@ # Django secrets SECRET_KEY=django-insecure-@0apn-lk85pfw=z00x2ib$w9#rwz8%2v4i_n^^9jz-m9b+y55* -DEBUG=0 +DEBUG=1 ALLOWED_HOSTS=localhost,127.0.0.1,https://rasadyar.net/,https://localhost:9200,https://api.rasadyaar.net,https://api.dam.rasadyaar.net',https://dam.rasadyar.net',http://localhost:3000',http://192.168.88.130:3000',https://rasaddam-front.liara.run',ns0ck4ksk0koks8ksw0ss08g.31.7.78.133.sslip.io' # noqa 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/core/api.py b/apps/core/api.py index f780ebb..27fb448 100644 --- a/apps/core/api.py +++ b/apps/core/api.py @@ -6,6 +6,7 @@ from apps.authentication.services.visibility_services import apply_visibility_fi from apps.authorization.services.role_child import get_all_role_child from apps.core.models import MobileTest, SystemConfig from apps.core.serializers import MobileTestSerializer, SystemConfigSerializer +from apps.core.services.visibility_service import apply_visibility_filter_by_org_type class BaseViewSet(RegionFilterMixin, viewsets.ModelViewSet): @@ -29,6 +30,11 @@ class BaseViewSet(RegionFilterMixin, viewsets.ModelViewSet): queryset = apply_visibility_filter(queryset, org) return queryset + if visibility_by_org_scope: + """ if organization has free visibility by org type, apply visibility filter """ + queryset = apply_visibility_filter_by_org_type(queryset, org) + return queryset + if user_relation.exists(): user_relation = user_relation.first() if not user_relation.role.type.key == 'ADM': diff --git a/apps/herd/migrations/0019_alter_rancher_union_code_alter_rancher_union_name.py b/apps/herd/migrations/0019_alter_rancher_union_code_alter_rancher_union_name.py new file mode 100644 index 0000000..c4b76b0 --- /dev/null +++ b/apps/herd/migrations/0019_alter_rancher_union_code_alter_rancher_union_name.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0 on 2025-11-15 08:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('herd', '0018_rancher_dhi_state'), + ] + + operations = [ + migrations.AlterField( + model_name='rancher', + name='union_code', + field=models.CharField(blank=True, max_length=50, null=True), + ), + migrations.AlterField( + model_name='rancher', + name='union_name', + field=models.CharField(blank=True, max_length=50, null=True), + ), + ] diff --git a/apps/product/migrations/0093_organizationquotastats.py b/apps/product/migrations/0093_organizationquotastats.py new file mode 100644 index 0000000..9b2a7b5 --- /dev/null +++ b/apps/product/migrations/0093_organizationquotastats.py @@ -0,0 +1,37 @@ +# Generated by Django 5.0 on 2025-11-15 08:46 + +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'), + ('product', '0092_remove_historicalquotadistribution_free_sale_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='OrganizationQuotaStats', + 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)), + ('total_amount', models.PositiveBigIntegerField(default=0)), + ('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(to='product.quotadistribution')), + ('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='org_quota_stats', to='authentication.organization')), + ('quota', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='org_quota_stats', to='product.quota')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index af903f0..ebc3039 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -463,6 +463,13 @@ class Quota(BaseModel): return persian_date.month in self.sale_license + def quota_amount_by_org(self, org: Organization): + stat = OrganizationQuotaStats.objects.filter( + quota=self, + organization=org + ) + return stat.first().total_amount if stat.exists() else None + def soft_delete(self): self.trash = True self.save(update_fields=['trash']) @@ -783,3 +790,27 @@ class QuotaDistribution(BaseModel): if not self.distribution_id: self.distribution_id = self.generate_distribution_id() return super(QuotaDistribution, self).save(*args, **kwargs) + + +class OrganizationQuotaStats(BaseModel): + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name='org_quota_stats', + null=True + ) + quota = models.ForeignKey( + Quota, + on_delete=models.CASCADE, + related_name='org_quota_stats', + null=True + ) + + distributions = models.ManyToManyField(QuotaDistribution) + total_amount = models.PositiveBigIntegerField(default=0) + + def __str__(self): + return f"organization: {self.organization} - quota: {self.quota}" + + def save(self, *args, **kwargs): + return super(OrganizationQuotaStats, self).save(*args, **kwargs) diff --git a/apps/product/signals.py b/apps/product/signals.py index 850e443..d599140 100644 --- a/apps/product/signals.py +++ b/apps/product/signals.py @@ -12,6 +12,7 @@ from apps.warehouse.models import ( from common.helpers import get_organization_by_user from .models import ( QuotaDistribution, + OrganizationQuotaStats, Quota, Product, ProductStats, @@ -242,15 +243,64 @@ def update_quota_stats(instance: Quota): ]) +def organization_quota_stats(quota: Quota, distribution: QuotaDistribution = None): + """ + set total received distributions for every organization + """ + if distribution: + org_quota_stat, created = OrganizationQuotaStats.objects.get_or_create( + quota=quota, + organization=distribution.assigned_organization, + ) + + # delete distribution + # decrease org stat total amount after remove distribution + if distribution.trash: + org_quota_stat.total_amount -= distribution.weight + org_quota_stat.save(update_fields=["total_amount"]) + else: + # if stat was created before or total amount is 0 + if not created or org_quota_stat.total_amount == 0: + org_quota_stat.total_amount += distribution.weight + org_quota_stat.save(update_fields=["total_amount"]) + org_quota_stat.distributions.add(distribution) + + # prevent from maximum recursion loop + distribution.stat_from_signal = True + else: + org_quota_stat, created = OrganizationQuotaStats.objects.get_or_create( + quota=quota, + organization=quota.registerer_organization, + total_amount=quota.quota_weight + ) + + # delete quota + if quota.trash: + org_quota_stat.soft_delete() + + # prevent from maximum recursion loop + quota.stat_from_signal = True + + @receiver([post_save, post_delete], sender=QuotaDistribution) @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_quota_stats(instance.quota) - # elif sender == InventoryQuotaSaleTransaction: - # if instance.quota_distribution: - # update_product_stats(instance.quota_distribution.quota.product, instance.quota_distribution) - # update_quota_stats(instance.quota_distribution.quota) - # else: - # print("quota distribution is null - product app signal") + + # if _from_signal=True prevent from maximum recursion loop + if getattr(instance, 'stat_from_signal', False): + return + + organization_quota_stats(instance.quota, instance) + + +@receiver([post_save, post_delete], sender=Quota) +def update_quota_stats_on_change(sender, instance, **kwargs): + if sender == Quota: + # if _from_signal=True prevent from maximum recursion loop + if getattr(instance, 'stat_from_signal', False): + return + + organization_quota_stats(instance) diff --git a/apps/product/web/api/v1/serializers/quota_distribution_serializers.py b/apps/product/web/api/v1/serializers/quota_distribution_serializers.py index 69edcc6..5209239 100644 --- a/apps/product/web/api/v1/serializers/quota_distribution_serializers.py +++ b/apps/product/web/api/v1/serializers/quota_distribution_serializers.py @@ -87,9 +87,6 @@ class QuotaDistributionSerializer(serializers.ModelSerializer): if not self.instance.parent_distribution: if amount + total > self.instance.quota.quota_weight: raise APIException("وزن وارد شده بیشتر از وزن باقیمانده است", code=403) # noqa - # else: - # if amount > self.instance.parent_distribution.remaining_weight: - # raise QuotaWeightException() return data diff --git a/apps/product/web/api/v1/serializers/quota_serializers.py b/apps/product/web/api/v1/serializers/quota_serializers.py index 500f72b..db430e3 100644 --- a/apps/product/web/api/v1/serializers/quota_serializers.py +++ b/apps/product/web/api/v1/serializers/quota_serializers.py @@ -10,8 +10,15 @@ class QuotaSerializer(serializers.ModelSerializer): model = product_models.Quota fields = '__all__' - def to_representation(self, instance): + def to_representation(self, instance: product_models.Quota): representation = super().to_representation(instance) + + # change quota weight by organization received weight + if self.context['org']: + quota_weight_by_org = instance.quota_amount_by_org(self.context['org']) + if quota_weight_by_org: + representation['quota_weight'] = quota_weight_by_org + if isinstance(instance, product_models.Quota): if instance.sale_unit: representation['sale_unit'] = product_serializers.SaleUnitSerializer( diff --git a/apps/product/web/api/v1/viewsets/quota_api.py b/apps/product/web/api/v1/viewsets/quota_api.py index 1a46752..0e11d8f 100644 --- a/apps/product/web/api/v1/viewsets/quota_api.py +++ b/apps/product/web/api/v1/viewsets/quota_api.py @@ -355,7 +355,7 @@ class QuotaViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, DynamicS queryset.order_by('-modify_date') ) if page is not None: # noqa - serializer = self.get_serializer(page, many=True) + serializer = self.get_serializer(page, many=True, context={'org': get_organization_by_user(request.user)}) return self.get_paginated_response(serializer.data) @action( diff --git a/apps/product/web/api/v1/viewsets/quota_distribution_api.py b/apps/product/web/api/v1/viewsets/quota_distribution_api.py index 6fe6823..2351f41 100644 --- a/apps/product/web/api/v1/viewsets/quota_distribution_api.py +++ b/apps/product/web/api/v1/viewsets/quota_distribution_api.py @@ -65,6 +65,10 @@ class QuotaDistributionViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewS if serializer.is_valid(): distribution = serializer.save() + # add this organization to quota assigned_organizations + # this org is received a distribution from quota + distribution.quota.assigned_organizations.add(distribution.assigned_organization) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN)