diff --git a/apps/product/migrations/0094_organizationquotastats_remaining_amount_and_more.py b/apps/product/migrations/0094_organizationquotastats_remaining_amount_and_more.py new file mode 100644 index 0000000..173276a --- /dev/null +++ b/apps/product/migrations/0094_organizationquotastats_remaining_amount_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0 on 2025-11-16 07:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0093_organizationquotastats'), + ] + + operations = [ + migrations.AddField( + model_name='organizationquotastats', + name='remaining_amount', + field=models.PositiveBigIntegerField(default=0), + ), + migrations.AddField( + model_name='organizationquotastats', + name='sold_amount', + field=models.PositiveBigIntegerField(default=0), + ), + migrations.AddField( + model_name='organizationquotastats', + name='total_distributed', + field=models.PositiveBigIntegerField(default=0), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index ebc3039..e4b9ecc 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -4,6 +4,7 @@ import jdatetime from django.contrib.postgres.fields import ArrayField from django.db import models from django.db import transaction +from django.db.models import Sum from simple_history.models import HistoricalRecords from apps.authentication.models import OrganizationType, Organization @@ -468,7 +469,12 @@ class Quota(BaseModel): quota=self, organization=org ) - return stat.first().total_amount if stat.exists() else None + return { + "quota_weight": stat.first().total_amount if stat.exists() else 0, + "remaining_weight": stat.first().remaining_amount if stat.exists() else 0, + "quota_distributed": stat.first().total_distributed if stat.exists() else 0, + "been_sold": stat.first().sold_amount if stat.exists() else 0, + } def soft_delete(self): self.trash = True @@ -808,6 +814,29 @@ class OrganizationQuotaStats(BaseModel): distributions = models.ManyToManyField(QuotaDistribution) total_amount = models.PositiveBigIntegerField(default=0) + total_distributed = models.PositiveBigIntegerField(default=0) + sold_amount = models.PositiveBigIntegerField(default=0) + remaining_amount = models.PositiveBigIntegerField(default=0) # total - sold + + def update_amount(self): + """ calculate total/sold/remaining """ + from apps.warehouse.models import InventoryQuotaSaleItem + + # calculate total amount of distribution + self.total_distributed = self.distributions.filter().aggregate( + total=Sum('weight') + )['total'] or 0 + + self.sold_amount = InventoryQuotaSaleItem.objects.filter( + quota_distribution__in=self.distributions.all(), + transaction__transaction_status='success' + ).aggregate( + total=Sum('weight') + )['total'] or 0 + + self.remaining_amount = self.total_amount - self.sold_amount + + self.save() def __str__(self): return f"organization: {self.organization} - quota: {self.quota}" diff --git a/apps/product/signals.py b/apps/product/signals.py index d599140..a6b445f 100644 --- a/apps/product/signals.py +++ b/apps/product/signals.py @@ -243,43 +243,24 @@ def update_quota_stats(instance: Quota): ]) -def organization_quota_stats(quota: Quota, distribution: QuotaDistribution = None): +@receiver(post_save, sender=Quota) +def organization_quota_stats(sender, quota: Quota, created: bool, **kwargs): """ set total received distributions for every organization """ - if distribution: - org_quota_stat, created = OrganizationQuotaStats.objects.get_or_create( - quota=quota, - organization=distribution.assigned_organization, - ) + org_quota_stat, created = OrganizationQuotaStats.objects.get_or_create( + quota=quota, + organization=quota.registerer_organization, + ) + org_quota_stat.total_amount = quota.quota_weight + org_quota_stat.save(update_fields=['total_amount']) - # 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) + # delete quota + if quota.trash: + org_quota_stat.soft_delete() - # 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 + # prevent from maximum recursion loop + quota.stat_from_signal = True @receiver([post_save, post_delete], sender=QuotaDistribution) @@ -293,8 +274,6 @@ def update_stats_on_change(sender, instance, **kwargs): 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): @@ -304,3 +283,45 @@ def update_quota_stats_on_change(sender, instance, **kwargs): return organization_quota_stats(instance) + + +@receiver(post_save, sender=QuotaDistribution) +def update_quota_stats_on_distribution(sender, instance: QuotaDistribution, created, **kwargs): + if getattr(instance, 'one_time_loop_flag', False): + return + + if instance.trash: + return + + org = instance.assigned_organization + quota = instance.quota + + stats, _ = OrganizationQuotaStats.objects.get_or_create( + quota=quota, + organization=org, + ) + stats.distributions.add(instance) + stats.update_amount() + instance.one_time_loop_flag = True + + +@receiver(post_save, sender=QuotaDistribution) +def handle_quota_stats_soft_delete_on_distribution(sender, instance: QuotaDistribution, created, **kwargs): + if getattr(instance, 'one_time_loop_flag', False): + return + + if instance.trash: + org = instance.assigned_organization + quota = instance.quota + + stats_qs = OrganizationQuotaStats.objects.filter( + quota=quota, + organization=org, + ) + + if stats_qs: + for stats in stats_qs: + stats.distributions.remove(instance) + stats.update_amount() + + instance.one_time_loop_flag = True diff --git a/apps/product/web/api/v1/serializers/quota_serializers.py b/apps/product/web/api/v1/serializers/quota_serializers.py index 7521f89..ad3f4bb 100644 --- a/apps/product/web/api/v1/serializers/quota_serializers.py +++ b/apps/product/web/api/v1/serializers/quota_serializers.py @@ -15,9 +15,19 @@ class QuotaSerializer(serializers.ModelSerializer): # change quota weight by organization received weight if 'org' in self.context.keys(): - quota_weight_by_org = instance.quota_amount_by_org(self.context['org']) + org = self.context['org'] + quota_weight_by_org = instance.quota_amount_by_org(org) if quota_weight_by_org: - representation['quota_weight'] = quota_weight_by_org + representation['quota_weight'] = quota_weight_by_org['quota_weight'] + representation['quota_distributed'] = quota_weight_by_org['quota_distributed'] + representation['remaining_weight'] = quota_weight_by_org['remaining_weight'] + representation['been_sold'] = quota_weight_by_org['been_sold'] + representation['distributions'] = [{ + "id": dist.id, + "organization": org.name, + "organization_id": org.id, + "weight": dist.weight, + } for dist in instance.distributions_assigned.all()] if isinstance(instance, product_models.Quota): if instance.sale_unit: diff --git a/apps/warehouse/signals.py b/apps/warehouse/signals.py index 1032112..19dabe0 100644 --- a/apps/warehouse/signals.py +++ b/apps/warehouse/signals.py @@ -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 apps.product.models import QuotaDistribution + +from apps.product.models import QuotaDistribution, OrganizationQuotaStats from .models import InventoryEntry, InventoryQuotaSaleItem @@ -67,3 +68,17 @@ def update_distribution_warehouse_sold_and_balance(sender, instance: InventoryQu ) else: print("quota distribution is null - warehouse app signals") + + +@receiver(post_save, sender=InventoryQuotaSaleItem) +def update_quota_stats_on_sale(sender, instance: InventoryQuotaSaleItem, created, **kwargs): + if instance.transaction.transaction_status == 'success': + return + + stats = OrganizationQuotaStats.objects.filter( + organization=instance.quota_distribution.assigned_organization, + quota=instance.quota_distribution.quota + ).first() + + if stats: + stats.update_amounts()