From 4bf900a1e2aeb4f96df21ebe30ced0782a32c8a1 Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Tue, 18 Nov 2025 16:51:34 +0330 Subject: [PATCH] add - WarehouseAllocationService --- ...ganizationquotastats_inventory_received.py | 18 ++++++ .../quota_distribution_serializers.py | 2 +- apps/warehouse/exceptions.py | 19 ++++++- .../0042_inventoryentryallocation.py | 36 ++++++++++++ apps/warehouse/models.py | 19 +++++++ .../services/warehouse_allocation_service.py | 57 +++++++++++++++++++ apps/warehouse/web/api/v1/api.py | 3 + 7 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 apps/product/migrations/0096_organizationquotastats_inventory_received.py create mode 100644 apps/warehouse/migrations/0042_inventoryentryallocation.py create mode 100644 apps/warehouse/services/warehouse_allocation_service.py diff --git a/apps/product/migrations/0096_organizationquotastats_inventory_received.py b/apps/product/migrations/0096_organizationquotastats_inventory_received.py new file mode 100644 index 0000000..39d63be --- /dev/null +++ b/apps/product/migrations/0096_organizationquotastats_inventory_received.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-11-18 12:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0095_organizationquotastats_stat_type'), + ] + + operations = [ + migrations.AddField( + model_name='organizationquotastats', + name='inventory_received', + field=models.PositiveBigIntegerField(default=0), + ), + ] diff --git a/apps/product/web/api/v1/serializers/quota_distribution_serializers.py b/apps/product/web/api/v1/serializers/quota_distribution_serializers.py index 50b1275..f301a7f 100644 --- a/apps/product/web/api/v1/serializers/quota_distribution_serializers.py +++ b/apps/product/web/api/v1/serializers/quota_distribution_serializers.py @@ -103,7 +103,7 @@ class QuotaDistributionSerializer(serializers.ModelSerializer): 'id': instance.assigned_organization.id, # if distributor is 0 , organization has not any distribute 'is_distributor': instance.quota.distributions_assigned.filter( - assigner_organization=instance.assigner_organization + assigner_organization=instance.assigned_organization ).count() } diff --git a/apps/warehouse/exceptions.py b/apps/warehouse/exceptions.py index 2bcf795..db4e504 100644 --- a/apps/warehouse/exceptions.py +++ b/apps/warehouse/exceptions.py @@ -1,5 +1,22 @@ -from rest_framework.exceptions import APIException from rest_framework import status +from rest_framework.exceptions import APIException + + +class WareHouseException(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "مقدار ورد شده از انبار بیشتر از موجودی انبار میباشد" # noqa + default_code = 'error' + + def __init__(self, message=None, code=None, status_code=None): + if status_code is not None: + self.status_code = status_code + + detail = { + "message": message, + "status_code": status_code + } + + super().__init__(detail) class InventoryEntryWeightException(APIException): diff --git a/apps/warehouse/migrations/0042_inventoryentryallocation.py b/apps/warehouse/migrations/0042_inventoryentryallocation.py new file mode 100644 index 0000000..dad599d --- /dev/null +++ b/apps/warehouse/migrations/0042_inventoryentryallocation.py @@ -0,0 +1,36 @@ +# Generated by Django 5.0 on 2025-11-18 13:20 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0096_organizationquotastats_inventory_received'), + ('warehouse', '0041_alter_inventoryquotasaletransaction_payer_cart_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='InventoryEntryAllocation', + 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)), + ('weight', 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)), + ('distribution', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='product.quotadistribution')), + ('inventory_entry', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='warehouse.inventoryentry')), + ('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)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/warehouse/models.py b/apps/warehouse/models.py index 04f443d..70eeafa 100644 --- a/apps/warehouse/models.py +++ b/apps/warehouse/models.py @@ -59,6 +59,25 @@ class InventoryEntry(BaseModel): super(InventoryEntry, self).save(*args, **kwargs) +class InventoryEntryAllocation(BaseModel): + inventory_entry = models.ForeignKey( + InventoryEntry, + on_delete=models.CASCADE, + related_name='allocations', + null=True + ) + distribution = models.ForeignKey( + product_models.QuotaDistribution, + on_delete=models.CASCADE, + related_name='allocations', + null=True + ) + weight = models.PositiveBigIntegerField(default=0) + + def __str__(self): + return f"{self.weight} -> Distribution {self.distribution.id}" + + class InventoryQuotaSaleTransaction(BaseModel): rancher = models.ForeignKey( Rancher, diff --git a/apps/warehouse/services/warehouse_allocation_service.py b/apps/warehouse/services/warehouse_allocation_service.py new file mode 100644 index 0000000..53c0f03 --- /dev/null +++ b/apps/warehouse/services/warehouse_allocation_service.py @@ -0,0 +1,57 @@ +from django.db import transaction +from rest_framework import status + +from apps.product.models import QuotaDistribution, OrganizationQuotaStats +from apps.warehouse.exceptions import WareHouseException +from apps.warehouse.models import InventoryEntry, InventoryEntryAllocation + + +class WarehouseAllocationService: + + @staticmethod + def allocate(entry: InventoryEntry): + """ + Auto allocate entry.total_weight between multiple distributions + """ + with transaction.atomic(): + distributions = QuotaDistribution.objects.filter( + assigned_organization=entry.organization, + quota=entry.distribution.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 + + allocate_weight = min(remaining, capacity) + + InventoryEntryAllocation.objects.create( + distribution=dist, + entry=entry, + weight=allocate_weight + ) + + stat.inventory_received += allocate_weight + stat.save() + + remaining -= allocate_weight + + if remaining > 0: + raise WareHouseException( + "مقدار ورد شده از انبار بیشتر از مقدار کل سهمیه توزیع داده شده است", + status.HTTP_400_BAD_REQUEST + ) diff --git a/apps/warehouse/web/api/v1/api.py b/apps/warehouse/web/api/v1/api.py index 4d734f7..6cb699f 100644 --- a/apps/warehouse/web/api/v1/api.py +++ b/apps/warehouse/web/api/v1/api.py @@ -10,6 +10,7 @@ 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 @@ -70,6 +71,8 @@ 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)