quota stat by organization - v1
This commit is contained in:
@@ -1,13 +1,13 @@
|
|||||||
# Django secrets
|
# Django secrets
|
||||||
SECRET_KEY=django-insecure-@0apn-lk85pfw=z00x2ib$w9#rwz8%2v4i_n^^9jz-m9b+y55*
|
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
|
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
|
ENV_NAME=DEV
|
||||||
|
|
||||||
# Database secrets
|
# Database secrets
|
||||||
DB_HOST=31.7.78.133
|
DB_HOST=31.7.78.133
|
||||||
DB_PORT=14352
|
DB_PORT=14352
|
||||||
DB_NAME=Production
|
DB_NAME=Development
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=pfLIVXupbDetvFMt2gUvxLXUL9b4HIOHaPcKXsBEZ1i8zl0iLUjmhUfXlGfJKcTV
|
DB_PASSWORD=pfLIVXupbDetvFMt2gUvxLXUL9b4HIOHaPcKXsBEZ1i8zl0iLUjmhUfXlGfJKcTV
|
||||||
|
|
||||||
|
|||||||
@@ -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.authorization.services.role_child import get_all_role_child
|
||||||
from apps.core.models import MobileTest, SystemConfig
|
from apps.core.models import MobileTest, SystemConfig
|
||||||
from apps.core.serializers import MobileTestSerializer, SystemConfigSerializer
|
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):
|
class BaseViewSet(RegionFilterMixin, viewsets.ModelViewSet):
|
||||||
@@ -29,6 +30,11 @@ class BaseViewSet(RegionFilterMixin, viewsets.ModelViewSet):
|
|||||||
queryset = apply_visibility_filter(queryset, org)
|
queryset = apply_visibility_filter(queryset, org)
|
||||||
return queryset
|
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():
|
if user_relation.exists():
|
||||||
user_relation = user_relation.first()
|
user_relation = user_relation.first()
|
||||||
if not user_relation.role.type.key == 'ADM':
|
if not user_relation.role.type.key == 'ADM':
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
37
apps/product/migrations/0093_organizationquotastats.py
Normal file
37
apps/product/migrations/0093_organizationquotastats.py
Normal 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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -463,6 +463,13 @@ class Quota(BaseModel):
|
|||||||
|
|
||||||
return persian_date.month in self.sale_license
|
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):
|
def soft_delete(self):
|
||||||
self.trash = True
|
self.trash = True
|
||||||
self.save(update_fields=['trash'])
|
self.save(update_fields=['trash'])
|
||||||
@@ -783,3 +790,27 @@ class QuotaDistribution(BaseModel):
|
|||||||
if not self.distribution_id:
|
if not self.distribution_id:
|
||||||
self.distribution_id = self.generate_distribution_id()
|
self.distribution_id = self.generate_distribution_id()
|
||||||
return super(QuotaDistribution, self).save(*args, **kwargs)
|
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)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from apps.warehouse.models import (
|
|||||||
from common.helpers import get_organization_by_user
|
from common.helpers import get_organization_by_user
|
||||||
from .models import (
|
from .models import (
|
||||||
QuotaDistribution,
|
QuotaDistribution,
|
||||||
|
OrganizationQuotaStats,
|
||||||
Quota,
|
Quota,
|
||||||
Product,
|
Product,
|
||||||
ProductStats,
|
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=QuotaDistribution)
|
||||||
@receiver([post_save, post_delete], sender=InventoryQuotaSaleTransaction)
|
@receiver([post_save, post_delete], sender=InventoryQuotaSaleTransaction)
|
||||||
def update_stats_on_change(sender, instance, **kwargs):
|
def update_stats_on_change(sender, instance, **kwargs):
|
||||||
if sender == QuotaDistribution:
|
if sender == QuotaDistribution:
|
||||||
update_product_stats(instance.quota.product, instance)
|
update_product_stats(instance.quota.product, instance)
|
||||||
update_quota_stats(instance.quota)
|
update_quota_stats(instance.quota)
|
||||||
# elif sender == InventoryQuotaSaleTransaction:
|
|
||||||
# if instance.quota_distribution:
|
# if _from_signal=True prevent from maximum recursion loop
|
||||||
# update_product_stats(instance.quota_distribution.quota.product, instance.quota_distribution)
|
if getattr(instance, 'stat_from_signal', False):
|
||||||
# update_quota_stats(instance.quota_distribution.quota)
|
return
|
||||||
# else:
|
|
||||||
# print("quota distribution is null - product app signal")
|
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)
|
||||||
|
|||||||
@@ -87,9 +87,6 @@ class QuotaDistributionSerializer(serializers.ModelSerializer):
|
|||||||
if not self.instance.parent_distribution:
|
if not self.instance.parent_distribution:
|
||||||
if amount + total > self.instance.quota.quota_weight:
|
if amount + total > self.instance.quota.quota_weight:
|
||||||
raise APIException("وزن وارد شده بیشتر از وزن باقیمانده است", code=403) # noqa
|
raise APIException("وزن وارد شده بیشتر از وزن باقیمانده است", code=403) # noqa
|
||||||
# else:
|
|
||||||
# if amount > self.instance.parent_distribution.remaining_weight:
|
|
||||||
# raise QuotaWeightException()
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,15 @@ class QuotaSerializer(serializers.ModelSerializer):
|
|||||||
model = product_models.Quota
|
model = product_models.Quota
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance: product_models.Quota):
|
||||||
representation = super().to_representation(instance)
|
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 isinstance(instance, product_models.Quota):
|
||||||
if instance.sale_unit:
|
if instance.sale_unit:
|
||||||
representation['sale_unit'] = product_serializers.SaleUnitSerializer(
|
representation['sale_unit'] = product_serializers.SaleUnitSerializer(
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ class QuotaViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, DynamicS
|
|||||||
queryset.order_by('-modify_date')
|
queryset.order_by('-modify_date')
|
||||||
)
|
)
|
||||||
if page is not None: # noqa
|
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)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
@action(
|
@action(
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ class QuotaDistributionViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewS
|
|||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
distribution = serializer.save()
|
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.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN)
|
return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user