add - whole system of inventory entry in organization quotas stat

This commit is contained in:
2025-11-19 15:56:15 +03:30
parent 42c01f3eb5
commit 1bf6950ccb
9 changed files with 152 additions and 34 deletions

View File

@@ -474,6 +474,7 @@ class Quota(BaseModel):
"remaining_weight": stat.first().remaining_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, "quota_distributed": stat.first().total_distributed if stat.exists() else 0,
"been_sold": stat.first().sold_amount 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): def soft_delete(self):

View File

@@ -1,5 +1,6 @@
from apps.product.models import QuotaDistribution, OrganizationQuotaStats from apps.product.models import QuotaDistribution, OrganizationQuotaStats
from apps.product.validators.quota_stats_validator import QuotaStatsValidator from apps.product.validators.quota_stats_validator import QuotaStatsValidator
from apps.warehouse.models import InventoryEntry
class QuotaStatsService: class QuotaStatsService:
@@ -102,3 +103,53 @@ class QuotaStatsService:
assigned_stat.remaining_amount -= distribution.weight assigned_stat.remaining_amount -= distribution.weight
assigned_stat.distributions.remove(distribution) assigned_stat.distributions.remove(distribution)
assigned_stat.save() 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

View File

@@ -33,6 +33,7 @@ class QuotaSerializer(serializers.ModelSerializer):
representation['quota_distributed'] = quota_weight_by_org['quota_distributed'] representation['quota_distributed'] = quota_weight_by_org['quota_distributed']
representation['remaining_weight'] = quota_weight_by_org['remaining_weight'] representation['remaining_weight'] = quota_weight_by_org['remaining_weight']
representation['been_sold'] = quota_weight_by_org['been_sold'] 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( representation['distributions_number_by_me'] = instance.distributions_assigned.filter(
assigner_organization=org assigner_organization=org
).count() ).count()

View File

@@ -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'),
),
]

View File

@@ -19,6 +19,12 @@ class InventoryEntry(BaseModel):
related_name='inventory_entry', related_name='inventory_entry',
null=True null=True
) )
quota = models.ForeignKey(
product_models.Quota,
on_delete=models.CASCADE,
related_name='inventory_entry',
null=True
)
organization = models.ForeignKey( organization = models.ForeignKey(
product_models.Organization, product_models.Organization,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@@ -38,7 +44,7 @@ class InventoryEntry(BaseModel):
# prefix = "POS" # prefix = "POS"
while True: while True:
number_part = ''.join(random.choices(string.digits, k=6)) 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(): if not InventoryEntry.objects.filter(entry_identity=code).exists():
return code return code

View File

@@ -16,24 +16,21 @@ class WarehouseAllocationService:
with transaction.atomic(): with transaction.atomic():
distributions = QuotaDistribution.objects.filter( distributions = QuotaDistribution.objects.filter(
assigned_organization=entry.organization, assigned_organization=entry.organization,
quota=entry.distribution.quota quota=entry.quota
).select_related('quota').order_by('-create_date') ).select_related('quota').order_by('-create_date')
if not distributions.exists(): if not distributions.exists():
raise WareHouseException("توزیعی برای این انبار وجود ندارد", status.HTTP_403_FORBIDDEN) # noqa raise WareHouseException("توزیعی برای این انبار وجود ندارد", status.HTTP_403_FORBIDDEN) # noqa
remaining = entry.weight remaining = entry.weight
for dist in distributions: for dist in distributions:
if remaining <= 0: if remaining <= 0:
break break
stat = OrganizationQuotaStats.objects.get( stat = OrganizationQuotaStats.objects.get(
quota=dist.quota, quota=dist.quota,
organization=dist.assigned_organization organization=dist.assigned_organization
) )
capacity = stat.remaining_amount capacity = stat.remaining_amount
if capacity <= 0: if capacity <= 0:
continue continue
@@ -46,6 +43,7 @@ class WarehouseAllocationService:
) )
stat.inventory_received += allocate_weight stat.inventory_received += allocate_weight
stat.remaining_amount -= allocate_weight
stat.save() stat.save()
remaining -= allocate_weight remaining -= allocate_weight
@@ -55,3 +53,13 @@ class WarehouseAllocationService:
"مقدار وارد شده از انبار بیشتر از مقدار کل سهمیه توزیع داده شده است", # noqa "مقدار وارد شده از انبار بیشتر از مقدار کل سهمیه توزیع داده شده است", # noqa
status.HTTP_400_BAD_REQUEST 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

View File

@@ -1,9 +1,11 @@
from django.db.models import Sum 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 django.dispatch import receiver
from apps.product.models import QuotaDistribution from apps.product.models import QuotaDistribution
from .models import InventoryEntry, InventoryQuotaSaleItem 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): 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']) 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) @receiver(post_save, sender=InventoryEntry)
def update_quota_stat_on_entry_soft_delete(sender, instance, **kwargs): def update_quota_stat_on_entry_soft_delete(sender, instance, **kwargs):
if instance.trash: if instance.trash:
pass QuotaStatsService.remove_inventory_entry(instance)
@receiver(post_save, sender=InventoryEntry) # @receiver(post_save, sender=InventoryEntry)
@receiver(post_delete, sender=InventoryEntry) # @receiver(post_delete, sender=InventoryEntry)
def update_distribution_warehouse_entry(sender, instance, **kwargs): # def update_distribution_warehouse_entry(sender, instance, **kwargs):
calculate_warehouse_entry(instance.distribution) # calculate_warehouse_entry(instance.distribution)
warehouse_sold_and_balance(instance.distribution) # warehouse_sold_and_balance(instance.distribution)
@receiver(post_save, sender=InventoryQuotaSaleItem) @receiver(post_save, sender=InventoryQuotaSaleItem)

View File

@@ -10,7 +10,6 @@ from apps.core.api import BaseViewSet
from apps.core.mixins.search_mixin import DynamicSearchMixin from apps.core.mixins.search_mixin import DynamicSearchMixin
from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin
from apps.warehouse import models as warehouse_models 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 apps.warehouse.web.api.v1 import serializers as warehouse_serializers
from common.generics import base64_to_image_file from common.generics import base64_to_image_file
from common.helpers import get_organization_by_user from common.helpers import get_organization_by_user
@@ -71,8 +70,6 @@ class InventoryEntryViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet,
inventory_entry = serializer.save() inventory_entry = serializer.save()
WarehouseAllocationService.allocate(entry=inventory_entry)
# upload document for confirmation entry # upload document for confirmation entry
if 'document' in request.data.keys(): if 'document' in request.data.keys():
self.upload_confirmation_document(request, inventory=inventory_entry.id) self.upload_confirmation_document(request, inventory=inventory_entry.id)

View File

@@ -1,11 +1,9 @@
from django.db import models from rest_framework import serializers, status
from rest_framework import serializers
from apps.product.exceptions import QuotaExpiredTimeException from apps.product.exceptions import QuotaExpiredTimeException
from apps.product.models import OrganizationQuotaStats
from apps.warehouse import models as warehouse_models from apps.warehouse import models as warehouse_models
from apps.warehouse.exceptions import ( from apps.warehouse.exceptions import WareHouseException
InventoryEntryWeightException
)
class InventoryEntrySerializer(serializers.ModelSerializer): class InventoryEntrySerializer(serializers.ModelSerializer):
@@ -16,7 +14,7 @@ class InventoryEntrySerializer(serializers.ModelSerializer):
"create_date", "create_date",
"modify_date", "modify_date",
"organization", "organization",
"distribution", "quota",
"weight", "weight",
"balance", "balance",
"lading_number", "lading_number",
@@ -30,32 +28,49 @@ class InventoryEntrySerializer(serializers.ModelSerializer):
check if inventory entries weight is not more than check if inventory entries weight is not more than
distribution weight & check quota expired time distribution weight & check quota expired time
""" """
quota = attrs['quota']
distribution = attrs['distribution'] org = attrs['organization']
# check for quota expired time # check for quota expired time
if not distribution.quota.is_in_valid_time(): if not quota.is_in_valid_time():
raise QuotaExpiredTimeException() raise QuotaExpiredTimeException()
# total inventory entries weight # total inventory entries weight
total_entered = distribution.inventory_entry.filter(is_confirmed=True).aggregate( # total_entered = distribution.inventory_entry.filter(is_confirmed=True).aggregate(
total=models.Sum('weight') # total=models.Sum('weight')
)['total'] or 0 # )['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 instance exists, for update check weight with distribution weight
if self.instance: if self.instance:
if self.instance.weight == 0: if self.instance.weight == 0:
if total_entered + attrs['weight'] > distribution.weight: if total_entered_weight + attrs['weight'] > remaining_weight_to_enter:
raise InventoryEntryWeightException() raise WareHouseException(
"وزن وارد شده برای ورود به انبار نباید از باقیمانده سهمیه بیشتر باشد", # noqa
status.HTTP_403_FORBIDDEN
)
elif self.instance.weight != 0: elif self.instance.weight != 0:
if total_entered - self.instance.weight + attrs['weight'] > distribution.weight: if total_entered_weight - self.instance.weight + attrs['weight'] > remaining_weight_to_enter:
raise InventoryEntryWeightException() raise WareHouseException(
"وزن وارد شده برای ورود به انبار نباید از باقیمانده سهمیه بیشتر باشد", # noqa
status.HTTP_403_FORBIDDEN
)
# if instance is not exists for create, check entry weight with distribution # if instance is not exists for create, check entry weight with distribution
else: else:
if total_entered + attrs['weight'] > distribution.weight or \ if total_entered_weight + attrs['weight'] > remaining_weight_to_enter:
total_entered + attrs['weight'] > distribution.remaining_weight: # total_entered + attrs['weight'] > distribution.remaining_weight:
raise InventoryEntryWeightException() raise WareHouseException(
"وزن وارد شده برای ورود به انبار نباید از باقیمانده سهمیه بیشتر باشد", # noqa
status.HTTP_403_FORBIDDEN
)
return attrs return attrs