quota stat by organization - v1

This commit is contained in:
2025-11-15 16:04:41 +03:30
parent afc9def177
commit ec6637569d
10 changed files with 168 additions and 13 deletions

View File

@@ -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

View File

@@ -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':

View File

@@ -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),
),
]

View File

@@ -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,
},
),
]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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(

View File

@@ -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(

View File

@@ -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)