diff --git a/apps/product/models.py b/apps/product/models.py index 994bb04..756045c 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -474,6 +474,7 @@ class Quota(BaseModel): "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, + "inventory_received": stat.first().inventory_received if stat.exists() else 0, } def soft_delete(self): diff --git a/apps/product/services/quota_stat_service.py b/apps/product/services/quota_stat_service.py index 9c1582a..99487d9 100644 --- a/apps/product/services/quota_stat_service.py +++ b/apps/product/services/quota_stat_service.py @@ -1,5 +1,6 @@ from apps.product.models import QuotaDistribution, OrganizationQuotaStats from apps.product.validators.quota_stats_validator import QuotaStatsValidator +from apps.warehouse.models import InventoryEntry class QuotaStatsService: @@ -102,3 +103,53 @@ class QuotaStatsService: assigned_stat.remaining_amount -= distribution.weight assigned_stat.distributions.remove(distribution) assigned_stat.save() + + @staticmethod + def apply_inventory_entry(entry: InventoryEntry, created): + quota = entry.quota + weight = entry.weight + + if not created: + old_weight = entry._old_weight # noqa + diff = weight - old_weight + else: + diff = weight + print("0000", diff) + QuotaStatsService._propagate_inventory( + organization=entry.organization, + quota=quota, + diff=diff + ) + + @staticmethod + def remove_inventory_entry(entry: InventoryEntry): + quota = entry.quota + weight = entry.weight + + QuotaStatsService._propagate_inventory( + organization=entry.organization, + quota=quota, + diff=-weight + ) + + @staticmethod + def _propagate_inventory(organization, quota, diff): + org = organization + + while org: + stat = OrganizationQuotaStats.objects.filter( + organization=org, + quota=quota + ).first() + if stat: + print(stat.id) + print(org.id, quota.id) + stat.inventory_received = (stat.inventory_received or 0) + diff + print(stat.remaining_amount) + stat.remaining_amount = stat.remaining_amount - diff + if stat.inventory_received < 0: + stat.inventory_received = 0 + + stat.save(update_fields=['inventory_received', 'remaining_amount']) + + org = org.parent_organization diff --git a/apps/product/web/api/v1/serializers/quota_serializers.py b/apps/product/web/api/v1/serializers/quota_serializers.py index 1c8dd9e..c11a03b 100644 --- a/apps/product/web/api/v1/serializers/quota_serializers.py +++ b/apps/product/web/api/v1/serializers/quota_serializers.py @@ -33,6 +33,7 @@ class QuotaSerializer(serializers.ModelSerializer): 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['inventory_received'] = quota_weight_by_org['inventory_received'] representation['distributions_number_by_me'] = instance.distributions_assigned.filter( assigner_organization=org ).count() diff --git a/apps/warehouse/migrations/0043_inventoryentry_quota.py b/apps/warehouse/migrations/0043_inventoryentry_quota.py new file mode 100644 index 0000000..e4aeec6 --- /dev/null +++ b/apps/warehouse/migrations/0043_inventoryentry_quota.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0 on 2025-11-19 10:14 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0096_organizationquotastats_inventory_received'), + ('warehouse', '0042_inventoryentryallocation'), + ] + + operations = [ + migrations.AddField( + model_name='inventoryentry', + name='quota', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_entry', to='product.quota'), + ), + ] diff --git a/apps/warehouse/models.py b/apps/warehouse/models.py index 70eeafa..c18cd50 100644 --- a/apps/warehouse/models.py +++ b/apps/warehouse/models.py @@ -19,6 +19,12 @@ class InventoryEntry(BaseModel): related_name='inventory_entry', null=True ) + quota = models.ForeignKey( + product_models.Quota, + on_delete=models.CASCADE, + related_name='inventory_entry', + null=True + ) organization = models.ForeignKey( product_models.Organization, on_delete=models.CASCADE, @@ -38,7 +44,7 @@ class InventoryEntry(BaseModel): # prefix = "POS" while True: number_part = ''.join(random.choices(string.digits, k=6)) - code = f"{self.distribution.quota.quota_id}{number_part}" + code = f"{self.quota.quota_id}{number_part}" if not InventoryEntry.objects.filter(entry_identity=code).exists(): return code diff --git a/apps/warehouse/services/warehouse_allocation_service.py b/apps/warehouse/services/warehouse_allocation_service.py index 44b5c9e..dc9da93 100644 --- a/apps/warehouse/services/warehouse_allocation_service.py +++ b/apps/warehouse/services/warehouse_allocation_service.py @@ -16,24 +16,21 @@ class WarehouseAllocationService: with transaction.atomic(): distributions = QuotaDistribution.objects.filter( assigned_organization=entry.organization, - quota=entry.distribution.quota + quota=entry.quota ).select_related('quota').order_by('-create_date') if not distributions.exists(): raise WareHouseException("توزیعی برای این انبار وجود ندارد", status.HTTP_403_FORBIDDEN) # noqa remaining = entry.weight - for dist in distributions: if remaining <= 0: break - stat = OrganizationQuotaStats.objects.get( quota=dist.quota, organization=dist.assigned_organization ) capacity = stat.remaining_amount - if capacity <= 0: continue @@ -46,6 +43,7 @@ class WarehouseAllocationService: ) stat.inventory_received += allocate_weight + stat.remaining_amount -= allocate_weight stat.save() remaining -= allocate_weight @@ -55,3 +53,13 @@ class WarehouseAllocationService: "مقدار وارد شده از انبار بیشتر از مقدار کل سهمیه توزیع داده شده است", # noqa status.HTTP_400_BAD_REQUEST ) + + org = entry.organization.parent_organization + while org: + stat = OrganizationQuotaStats.objects.get( + quota=entry.quota, + organization=org + ) + stat.inventory_received += entry.weight + stat.save() + org = org.parent_organization diff --git a/apps/warehouse/signals.py b/apps/warehouse/signals.py index 7b68f2e..6a3cce8 100644 --- a/apps/warehouse/signals.py +++ b/apps/warehouse/signals.py @@ -1,9 +1,11 @@ from django.db.models import Sum -from django.db.models.signals import post_save, post_delete +from django.db.models.signals import post_save, post_delete, post_init from django.dispatch import receiver from apps.product.models import QuotaDistribution from .models import InventoryEntry, InventoryQuotaSaleItem +from .services.warehouse_allocation_service import WarehouseAllocationService +from ..product.services.quota_stat_service import QuotaStatsService def calculate_warehouse_entry(quota_distribution): @@ -50,17 +52,34 @@ def warehouse_sold_and_balance(quota_distribution: QuotaDistribution): quota_distribution.save(update_fields=['been_sold', 'warehouse_balance', 'free_sale_balance', 'pre_sale_balance']) +@receiver(post_init, sender=InventoryEntry) +def inventory_entry_pre_save(sender, instance: InventoryEntry, **kwargs): + if instance.pk: + instance._is_update = True + instance._old_weight = instance.weight + else: + instance._is_update = False + + +@receiver(post_save, sender=InventoryEntry) +def update_quota_stat_on_entry_update(sender, instance: InventoryEntry, created, **kwargs): + if instance._is_update: # noqa + QuotaStatsService.apply_inventory_entry(instance, created=False) + else: + WarehouseAllocationService.allocate(entry=instance) + + @receiver(post_save, sender=InventoryEntry) def update_quota_stat_on_entry_soft_delete(sender, instance, **kwargs): if instance.trash: - pass + QuotaStatsService.remove_inventory_entry(instance) -@receiver(post_save, sender=InventoryEntry) -@receiver(post_delete, sender=InventoryEntry) -def update_distribution_warehouse_entry(sender, instance, **kwargs): - calculate_warehouse_entry(instance.distribution) - warehouse_sold_and_balance(instance.distribution) +# @receiver(post_save, sender=InventoryEntry) +# @receiver(post_delete, sender=InventoryEntry) +# def update_distribution_warehouse_entry(sender, instance, **kwargs): +# calculate_warehouse_entry(instance.distribution) +# warehouse_sold_and_balance(instance.distribution) @receiver(post_save, sender=InventoryQuotaSaleItem) diff --git a/apps/warehouse/web/api/v1/api.py b/apps/warehouse/web/api/v1/api.py index 6cb699f..4d734f7 100644 --- a/apps/warehouse/web/api/v1/api.py +++ b/apps/warehouse/web/api/v1/api.py @@ -10,7 +10,6 @@ from apps.core.api import BaseViewSet from apps.core.mixins.search_mixin import DynamicSearchMixin from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin from apps.warehouse import models as warehouse_models -from apps.warehouse.services.warehouse_allocation_service import WarehouseAllocationService from apps.warehouse.web.api.v1 import serializers as warehouse_serializers from common.generics import base64_to_image_file from common.helpers import get_organization_by_user @@ -71,8 +70,6 @@ class InventoryEntryViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, inventory_entry = serializer.save() - WarehouseAllocationService.allocate(entry=inventory_entry) - # upload document for confirmation entry if 'document' in request.data.keys(): self.upload_confirmation_document(request, inventory=inventory_entry.id) diff --git a/apps/warehouse/web/api/v1/serializers.py b/apps/warehouse/web/api/v1/serializers.py index c56e847..fbc52cc 100644 --- a/apps/warehouse/web/api/v1/serializers.py +++ b/apps/warehouse/web/api/v1/serializers.py @@ -1,11 +1,9 @@ -from django.db import models -from rest_framework import serializers +from rest_framework import serializers, status from apps.product.exceptions import QuotaExpiredTimeException +from apps.product.models import OrganizationQuotaStats from apps.warehouse import models as warehouse_models -from apps.warehouse.exceptions import ( - InventoryEntryWeightException -) +from apps.warehouse.exceptions import WareHouseException class InventoryEntrySerializer(serializers.ModelSerializer): @@ -16,7 +14,7 @@ class InventoryEntrySerializer(serializers.ModelSerializer): "create_date", "modify_date", "organization", - "distribution", + "quota", "weight", "balance", "lading_number", @@ -30,32 +28,49 @@ class InventoryEntrySerializer(serializers.ModelSerializer): check if inventory entries weight is not more than distribution weight & check quota expired time """ - - distribution = attrs['distribution'] + quota = attrs['quota'] + org = attrs['organization'] # check for quota expired time - if not distribution.quota.is_in_valid_time(): + if not quota.is_in_valid_time(): raise QuotaExpiredTimeException() # total inventory entries weight - total_entered = distribution.inventory_entry.filter(is_confirmed=True).aggregate( - total=models.Sum('weight') - )['total'] or 0 + # total_entered = distribution.inventory_entry.filter(is_confirmed=True).aggregate( + # total=models.Sum('weight') + # )['total'] or 0 + + org_quota_stat = OrganizationQuotaStats.objects.get( + organization=org, + quota=quota + ) + + total_entered_weight = org_quota_stat.inventory_received + remaining_weight_to_enter = org_quota_stat.remaining_amount # if instance exists, for update check weight with distribution weight if self.instance: if self.instance.weight == 0: - if total_entered + attrs['weight'] > distribution.weight: - raise InventoryEntryWeightException() + if total_entered_weight + attrs['weight'] > remaining_weight_to_enter: + raise WareHouseException( + "وزن وارد شده برای ورود به انبار نباید از باقیمانده سهمیه بیشتر باشد", # noqa + status.HTTP_403_FORBIDDEN + ) elif self.instance.weight != 0: - if total_entered - self.instance.weight + attrs['weight'] > distribution.weight: - raise InventoryEntryWeightException() + if total_entered_weight - self.instance.weight + attrs['weight'] > remaining_weight_to_enter: + raise WareHouseException( + "وزن وارد شده برای ورود به انبار نباید از باقیمانده سهمیه بیشتر باشد", # noqa + status.HTTP_403_FORBIDDEN + ) # if instance is not exists for create, check entry weight with distribution else: - if total_entered + attrs['weight'] > distribution.weight or \ - total_entered + attrs['weight'] > distribution.remaining_weight: - raise InventoryEntryWeightException() + if total_entered_weight + attrs['weight'] > remaining_weight_to_enter: + # total_entered + attrs['weight'] > distribution.remaining_weight: + raise WareHouseException( + "وزن وارد شده برای ورود به انبار نباید از باقیمانده سهمیه بیشتر باشد", # noqa + status.HTTP_403_FORBIDDEN + ) return attrs