from django.db import models from django.db.models import Sum from django.db.models.signals import post_save, post_delete, post_init from django.dispatch import receiver from apps.product.validators.quota_stats_validator import QuotaStatsValidator from apps.warehouse.models import ( InventoryQuotaSaleTransaction, InventoryEntry ) from .models import ( QuotaDistribution, Quota, QuotaStats, OrganizationQuotaStats ) from .services.quota_stat_service import QuotaStatsService def recalculate_remaining_amount(quota): """ calculate remaining weight from distribution """ total_distributed = quota.distributions_assigned.filter( parent_distribution__isnull=True, assigner_organization=quota.registerer_organization, trash=False, ).aggregate( total=Sum('weight') )['total'] or 0 quota.remaining_weight = quota.quota_weight - total_distributed quota.quota_distributed = total_distributed quota.save(update_fields=["remaining_weight", "quota_distributed"]) def remaining_distribution_weight(instance: QuotaDistribution): """ calculate remaining & distributed weight from distribution """ total_children_weight = instance.children.filter(trash=False).aggregate( total=Sum('weight') )['total'] or 0 # total warehouse inventory entry total_entry = instance.inventory_entry.filter(trash=False).aggregate( total=Sum('weight') )['total'] or 0 instance.remaining_weight = instance.weight - total_children_weight - total_entry instance.distributed = total_children_weight instance._from_signal = True instance.save(update_fields=['remaining_weight', 'distributed']) if instance.parent_distribution: remaining_distribution_weight(instance.parent_distribution) @receiver(post_save, sender=QuotaDistribution) @receiver(post_delete, sender=QuotaDistribution) def update_quota_remaining(sender, instance, **kwargs): recalculate_remaining_amount(instance.quota) # if _from_signal=True prevent from maximum recursion loop if getattr(instance, '_from_signal', False): return # when delete object, prevent from update fields error # if kwargs.get('signal') == post_delete: if instance.trash: if instance.parent_distribution: remaining_distribution_weight(instance.parent_distribution) else: remaining_distribution_weight(instance) # def update_product_stats(instance: OrganizationQuotaStats): # """ update all stats of product """ # # user = get_current_user() # get user object # if not isinstance(user, AnonymousUser): # organization = get_organization_by_user(user) # # QuotaStatsValidator.validate_assigner_has_enough( # # organization, # # distribution.quota, # # distribution.weight, # # allow_zero=True # # ) # # stat, created = ProductStats.objects.get_or_create( # organization=organization, # product=instance, # sale_unit=instance.quota.sale_unit.unit # ) # # # number of quotas # # org_quota_stat = OrganizationQuotaStats.objects.filter( # organization=organization, # quota__product=instance.quota.product, # ) # # quotas_count = org_quota_stat.count() # noqa # # product_stat_data = org_quota_stat.aggregate( # total_quotas_weight=models.Sum('total_amount'), # active_quotas_weight=models.Sum('active_quotas_weight', filter=Q(quota__is_closed=False)), # closed_quotas_weight=models.Sum('closed_quotas_weight', filter=Q(quota__is_closed=True)), # total_remaining_quotas_weight=models.Sum('remaining_amount'), # total_distributed=models.Sum('quota_distributed'), # total_warehouse_entry=models.Sum('inventory_received'), # total_sold=models.Sum('total_sold'), # ) # # quota = Quota.objects.filter( # # Q( # # distributions_assigned__in=QuotaDistribution.objects.filter( # # Q(assigned_organization=organization) | # # Q(assigner_organization=organization) & # # Q(parent_distribution__isnull=True) # # # # ) # # ) | # # Q(registerer_organization=organization), # # product=instance # # ).distinct() # # # # quotas_count = quota.count() # noqa # # # # total_quotas_weight = quota.aggregate( # noqa # # total=models.Sum('quota_weight') # # )['total'] or 0 # # # # # total weight of product that assigned in quota # # active_quotas_weight = quota.filter(is_closed=False).aggregate( # # total=models.Sum('quota_weight') # # )['total'] or 0 # # # # closed_quotas_weight = quota.filter(is_closed=True).aggregate( # noqa # # total=models.Sum('quota_weight') # # )['total'] or 0 # # # # # total remaining weight of product quotas # # total_remaining_quotas_weight = quota.filter(is_closed=False).aggregate( # noqa # # total=models.Sum('remaining_weight') # # )['total'] or 0 # # # # received_distribution_weight = QuotaDistribution.objects.filter( # # quota__product_id=instance.id, # # quota__is_closed=False, # # quota__sale_unit=distribution.quota.sale_unit, # # assigned_organization=organization, # # parent_distribution__isnull=True # # ) # # # # received_distribution_number = received_distribution_weight.count() # # # # received_distribution_weight = received_distribution_weight.aggregate( # # total_weight=models.Sum('weight') # # )['total_weight'] or 0 # # # # # product total distributed weight from quota # # given_distribution_weight = QuotaDistribution.objects.filter( # # quota__product_id=instance.id, # # quota__is_closed=False, # # quota__sale_unit=distribution.quota.sale_unit, # # assigner_organization=organization, # # parent_distribution__isnull=True # # ) # # # # given_distribution_number = given_distribution_weight.count() # # given_distribution_weight = given_distribution_weight.aggregate( # # total_weight=models.Sum('weight') # # )['total_weight'] or 0 # # # # if received_distribution_weight > 0: # # distribution_weight_balance = received_distribution_weight - given_distribution_weight # # else: # # distribution_weight_balance = given_distribution_weight # # # # # total sold of product from quota # # total_sold = QuotaDistribution.objects.filter( # # quota__product_id=instance.id, # # quota__is_closed=False, # # quota__sale_unit=distribution.quota.sale_unit, # # assigned_organization=organization # # ).aggregate(total_sold=models.Sum('been_sold'))['total_sold'] or 0 # # # # # total entry from product to inventory # # total_warehouse_entry = QuotaDistribution.objects.filter( # # quota__product_id=instance.id, # # quota__is_closed=False, # # quota__sale_unit=distribution.quota.sale_unit, # # assigned_organization=organization # # ).aggregate(total_entry=models.Sum('warehouse_entry'))['total_entry'] or 0 # # # stat.quotas_number = quotas_count # # stat.active_quotas_weight = active_quotas_weight # # stat.closed_quotas_weight = closed_quotas_weight # # stat.total_quota_weight = total_quotas_weight # # stat.total_quota_remaining = total_remaining_quotas_weight # # stat.total_remaining_distribution_weight = distribution_weight_balance # # stat.received_distribution_weight = received_distribution_weight # # stat.given_distribution_weight = given_distribution_weight # # stat.received_distribution_number = received_distribution_number # # stat.given_distribution_number = given_distribution_number # # stat.total_warehouse_entry = total_warehouse_entry # # stat.total_sold = total_sold # # stat.save(update_fields=[ # # "quotas_number", # # "active_quotas_weight", # # "closed_quotas_weight", # # "total_quota_weight", # # "total_quota_remaining", # # "total_remaining_distribution_weight", # # "received_distribution_weight", # # "given_distribution_weight", # # "received_distribution_number", # # "total_warehouse_entry", # # "total_sold", # # ]) def update_quota_stats(instance: Quota): """ update all stats of quota """ if hasattr(instance, 'stats'): stat = instance.stats else: stat = QuotaStats.objects.create(quota=instance) total_distributed = instance.quota_distributed total_remaining = instance.remaining_weight # total entry to inventory from quota total_inventory = InventoryEntry.objects.filter( distribution__quota=instance ).aggregate( total_inventory=models.Sum('weight') )['total_inventory'] or 0 # total sale of distributions from quota total_sale = instance.distributions_assigned.filter( quota__is_closed=False ).aggregate(total_sale=models.Sum('been_sold'))['total_sale'] or 0 stat.total_distributed = total_distributed stat.remaining = total_remaining stat.total_inventory = total_inventory stat.total_sale = total_sale stat.save(update_fields=[ "total_distributed", "remaining", "total_inventory", "total_sale", ]) @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) # if _from_signal=True prevent from maximum recursion loop if getattr(instance, 'stat_from_signal', False): return @receiver(post_init, sender=QuotaDistribution) def store_original_weight(sender, instance, **kwargs): pass @receiver(post_init, sender=QuotaDistribution) def distribution_pre_save(sender, instance, **kwargs): if instance.pk: instance._is_update = True instance._old_weight = instance.weight else: instance._is_update = False @receiver(post_save, sender=QuotaDistribution) def update_stats_after_save(sender, instance, created, **kwargs): if getattr(instance, 'stat_from_signal', False): return if instance.trash: return if not instance._is_update: # noqa QuotaStatsService.apply_distribution(instance) else: QuotaStatsService.update_distribution(instance, instance._old_weight) # noqa instance.stat_from_signal = True @receiver(post_save, sender=QuotaDistribution) def delete_stats_after_soft_delete(sender, instance, **kwargs): if getattr(instance, 'stat_from_signal', False): return if instance.trash: QuotaStatsService.delete_distribution(instance) instance.stat_from_signal = True @receiver(post_save, sender=Quota) def organization_quota_stats(sender, instance: Quota, created: bool, **kwargs): """ set total received distributions for every organization """ from apps.warehouse.models import InventoryQuotaSaleItem if getattr(instance, 'stat_from_signal', False): return org_quota_stat, created = OrganizationQuotaStats.objects.get_or_create( quota=instance, organization=instance.registerer_organization, stat_type='quota' ) org_quota_stat.total_amount = instance.quota_weight org_quota_stat.total_distributed = instance.quota_distributed org_quota_stat.sold_amount = InventoryQuotaSaleItem.objects.filter( quota_distribution__quota=instance, transaction__transaction_status='success' ).aggregate( total=Sum('weight') )['total'] or 0 org_quota_stat.remaining_amount = org_quota_stat.total_amount - org_quota_stat.total_distributed # validate total_amount & distributed with remaining in update QuotaStatsValidator.validate_quota_weight_update(org_quota_stat.remaining_amount) org_quota_stat.save(update_fields=['total_amount', 'total_distributed', 'sold_amount', 'remaining_amount']) # delete quota if instance.trash: org_quota_stat.soft_delete() # prevent from maximum recursion loop instance.stat_from_signal = True # # # @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(main_quota=True) # 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(main_quota=True) # # instance.one_time_loop_flag = True