add - WarehouseAllocationService

This commit is contained in:
2025-11-18 16:51:34 +03:30
parent 23e927a631
commit 4bf900a1e2
7 changed files with 152 additions and 2 deletions

View File

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

View File

@@ -103,7 +103,7 @@ class QuotaDistributionSerializer(serializers.ModelSerializer):
'id': instance.assigned_organization.id, 'id': instance.assigned_organization.id,
# if distributor is 0 , organization has not any distribute # if distributor is 0 , organization has not any distribute
'is_distributor': instance.quota.distributions_assigned.filter( 'is_distributor': instance.quota.distributions_assigned.filter(
assigner_organization=instance.assigner_organization assigner_organization=instance.assigned_organization
).count() ).count()
} }

View File

@@ -1,5 +1,22 @@
from rest_framework.exceptions import APIException
from rest_framework import status 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): class InventoryEntryWeightException(APIException):

View File

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

View File

@@ -59,6 +59,25 @@ class InventoryEntry(BaseModel):
super(InventoryEntry, self).save(*args, **kwargs) 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): class InventoryQuotaSaleTransaction(BaseModel):
rancher = models.ForeignKey( rancher = models.ForeignKey(
Rancher, Rancher,

View File

@@ -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
)

View File

@@ -10,6 +10,7 @@ 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
@@ -70,6 +71,8 @@ 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)