organization limit for quota - get org childs

This commit is contained in:
2025-07-16 12:21:29 +03:30
parent a70d451ca1
commit a6b29ad28d
47 changed files with 375 additions and 11 deletions

View File

@@ -24,3 +24,14 @@ class QuotaExpiredTimeException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "زمان مجوز این سهمیه به پایان رسیده است" # noqa
default_code = 'error'
class QuotaLimitByOrganizationException(APIException):
"""
if limitation of quota by organization is true,
distribution should be done in organizations limits
"""
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "سازمان انتخاب شده در بین سامان های انتخاب شده برای توزیع سهمیه وجود ندارد" # noqa
default_code = 'error'

View File

@@ -0,0 +1,69 @@
# Generated by Django 5.0 on 2025-07-16 07:41
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0026_organizationstats'),
('product', '0047_quotalivestockagelimitation_livestock_type'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='quota',
name='has_organization_limit',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='quota',
name='limit_by_organizations',
field=models.ManyToManyField(null=True, related_name='quota_limits', to='authentication.organization'),
),
migrations.CreateModel(
name='ProductStats',
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_quota', models.PositiveBigIntegerField(default=0)),
('total_remaining', models.PositiveBigIntegerField(default=0)),
('total_sold', models.PositiveBigIntegerField(default=0)),
('total_transactions', 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)),
('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)),
('product', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stats', to='product.product')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='QuotaStats',
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_distributed', models.PositiveBigIntegerField(default=0)),
('remaining', models.PositiveBigIntegerField(default=0)),
('total_inventory', models.PositiveBigIntegerField(default=0)),
('total_sale', 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)),
('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)),
('quota', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stats', to='product.quota')),
],
options={
'abstract': False,
},
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.0 on 2025-07-16 07:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0026_organizationstats'),
('product', '0048_quota_has_organization_limit_and_more'),
]
operations = [
migrations.AlterField(
model_name='quota',
name='limit_by_organizations',
field=models.ManyToManyField(related_name='quota_limits', to='authentication.organization'),
),
]

View File

@@ -134,6 +134,25 @@ class Product(BaseModel):
super(Product, self).save(*args, **kwargs)
class ProductStats(BaseModel):
product = models.OneToOneField(
Product,
on_delete=models.CASCADE,
related_name='stats',
null=True
)
total_quota = models.PositiveBigIntegerField(default=0)
total_remaining = models.PositiveBigIntegerField(default=0)
total_sold = models.PositiveBigIntegerField(default=0)
total_transactions = models.PositiveBigIntegerField(default=0)
def __str__(self):
return f'Product: {self.product.name}-{self.product.id} stats'
def save(self, *args, **kwargs):
return super(ProductStats).save(*args, **kwargs)
class Attribute(BaseModel):
"""
every reference product have multiple attributes
@@ -316,6 +335,8 @@ class Quota(BaseModel):
)
has_distribution_limit = models.BooleanField(default=False)
distribution_mode = ArrayField(base_field=models.IntegerField(), blank=True, null=True)
has_organization_limit = models.BooleanField(default=False)
limit_by_organizations = models.ManyToManyField(Organization, related_name='quota_limits')
base_price_factory = models.DecimalField(max_digits=12, decimal_places=2)
base_price_cooperative = models.DecimalField(max_digits=12, decimal_places=2)
final_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
@@ -371,6 +392,25 @@ class Quota(BaseModel):
return super(Quota, self).save(*args, **kwargs)
class QuotaStats(BaseModel):
quota = models.OneToOneField(
Quota,
on_delete=models.CASCADE,
related_name='stats',
null=True,
)
total_distributed = models.PositiveBigIntegerField(default=0)
remaining = models.PositiveBigIntegerField(default=0)
total_inventory = models.PositiveBigIntegerField(default=0)
total_sale = models.PositiveBigIntegerField(default=0)
def __str__(self):
return f'Quota: {self.quota.quota_id} stats'
def save(self, *args, **kwargs):
return super(QuotaStats, self).save(*args, **kwargs)
class QuotaIncentiveAssignment(BaseModel):
""" assign incentive plan to quota """

View File

@@ -1,7 +1,8 @@
from django.db.models import Sum
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from .models import QuotaDistribution, Quota
from .models import QuotaDistribution, Quota, Product
from apps.warehouse.models import InventoryQuotaSaleTransaction
def recalculate_remaining_amount(quota):
@@ -18,3 +19,18 @@ def recalculate_remaining_amount(quota):
@receiver(post_delete, sender=QuotaDistribution)
def update_quota_remaining(sender, instance, **kwargs):
recalculate_remaining_amount(instance.quota)
def update_product_stats(instance: Product):
pass
def update_quota_stats(instance: Quota):
pass
@receiver([post_save, post_delete], sender=QuotaDistribution)
@receiver([post_save, post_delete], sender=InventoryQuotaSaleTransaction)
def update_stats_on_change(sender, instance, **kwargs):
update_product_stats(instance)
update_quota_stats(instance)

View File

@@ -30,6 +30,12 @@ class ProductSerializer(serializers.ModelSerializer):
return representation
class ProductStatsSerializer(serializers.ModelSerializer):
class Meta:
model = product_models.ProductStats
fields = '__all__'
class AttributeSerializer(serializers.ModelSerializer):
""" serialize attributes of reference product """
@@ -61,7 +67,6 @@ class AttributeValueSerializer(serializers.ModelSerializer):
fields = '__all__'
def update(self, instance, validated_data):
instance.quota = validated_data.get('quota', instance.quota)
instance.attribute = validated_data.get('attribute', instance.attribute)
instance.value = validated_data.get('value', instance.value)

View File

@@ -5,7 +5,8 @@ from django.db import models
from apps.product.exceptions import (
QuotaWeightException,
QuotaClosedException,
QuotaExpiredTimeException
QuotaExpiredTimeException,
QuotaLimitByOrganizationException
)
@@ -21,12 +22,15 @@ class QuotaDistributionSerializer(serializers.ModelSerializer):
def validate(self, data):
"""
to validate if distribution weight
@ to validate if distribution weight
more than quota weight raise exception
or if quota is closed raise exception
@ if quota is closed raise exception
@ if quota has organization limit, before distribution
check assigned organization
"""
quota = data['quota']
assigned_organization = data['assigned_organization']
amount = data['weight']
instance_id = self.instance.id if self.instance else None
@@ -38,6 +42,9 @@ class QuotaDistributionSerializer(serializers.ModelSerializer):
if quota.is_closed:
raise QuotaClosedException()
if assigned_organization not in quota.limit_by_organizations.all():
raise QuotaLimitByOrganizationException()
# total quota distributions weight
total = product_models.QuotaDistribution.objects.filter(
quota=quota

View File

@@ -59,6 +59,8 @@ class QuotaSerializer(serializers.ModelSerializer):
instance.group = validated_data.get('group', instance.group)
instance.has_distribution_limit = validated_data.get('has_distribution_limit', instance.has_distribution_limit)
instance.distribution_mode = validated_data.get('distribution_mode', instance.distribution_mode)
instance.has_organization_limit = validated_data.get('has_organization_limit', instance.has_organization_limit)
instance.limit_by_organizations = validated_data.get('limit_by_organizations', instance.limit_by_organizations)
instance.base_price_factory = validated_data.get('base_price_factory', instance.base_price_factory)
instance.base_price_cooperative = validated_data.get('base_price_cooperative', instance.base_price_cooperative)
instance.final_price = validated_data.get('final_price', instance.final_price)
@@ -74,6 +76,12 @@ class QuotaSerializer(serializers.ModelSerializer):
return instance
class QuotaStatsSerializer(serializers.ModelSerializer):
class Meta:
model = product_models.QuotaStats
fields = '__all__'
class QuotaIncentiveAssignmentSerializer(serializers.ModelSerializer):
class Meta:
model = product_models.QuotaIncentiveAssignment