fix agencie share on pos device - fix validation bug of free product pos

This commit is contained in:
2025-10-05 17:01:21 +03:30
parent f36d767e1c
commit c3f745f5d0
11 changed files with 263 additions and 62 deletions

View File

@@ -0,0 +1,54 @@
# Generated by Django 5.0 on 2025-10-05 08:04
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0079_quotausage_distribution_quotausage_usage_type_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='QuotaFinalPriceTypes',
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)),
('name', models.CharField(max_length=250)),
('en_name', models.CharField(max_length=250, null=True)),
('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)),
('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,
},
),
migrations.CreateModel(
name='QuotaPriceCalculationItems',
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)),
('name', models.CharField(max_length=250)),
('value', 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)),
('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)),
('pricing_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pricing_items', to='product.quotafinalpricetypes')),
('quota', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pricing_items', to='product.quota')),
],
options={
'abstract': False,
},
),
]

View File

@@ -449,6 +449,40 @@ class Quota(BaseModel):
return super(Quota, self).save(*args, **kwargs)
class QuotaFinalPriceTypes(BaseModel):
name = models.CharField(max_length=250)
en_name = models.CharField(max_length=250, null=True)
def __str__(self):
return f'{self.name}'
def save(self, *args, **kwargs):
return super(QuotaFinalPriceTypes, self).save(*args, **kwargs)
class QuotaPriceCalculationItems(BaseModel):
quota = models.ForeignKey(
Quota,
on_delete=models.CASCADE,
related_name='pricing_items',
null=True
)
pricing_type = models.ForeignKey(
'QuotaFinalPriceTypes',
on_delete=models.CASCADE,
related_name='pricing_items',
null=True
)
name = models.CharField(max_length=250)
value = models.PositiveBigIntegerField(default=0)
def __str__(self):
return f'{self.quota.quota_id}-{self.pricing_type.name}-{self.name}'
def save(self, *args, **kwargs):
return super(QuotaPriceCalculationItems, self).save(*args, **kwargs)
class QuotaStats(BaseModel):
quota = models.OneToOneField(
Quota,
@@ -501,10 +535,10 @@ class QuotaUsage(BaseModel):
('incentive', 'INCENTIVE'),
)
usage_type = models.CharField(max_length=150, choices=usage_type_choices, null=True)
def __str__(self):
return f'rancher: {self.rancher.ranching_farm} - plan: {self.incentive_plan.name}'
def save(self, *args, **kwargs):
return super(QuotaUsage, self).save(*args, **kwargs)

View File

@@ -19,8 +19,13 @@ class POSFreeProductSerializer(serializers.ModelSerializer):
product = attrs['product']
organization = attrs['organization']
device = self.context['device']
if self.Meta.model.objects.filter(organization=organization, product=product).exists():
if self.Meta.model.objects.filter(
organization=organization,
product=product,
device=device
).exists():
raise FreePOSProductUniqueCheck()
return attrs

View File

@@ -1,5 +1,5 @@
from apps.product.services.services import quota_live_stock_allocation_info, quota_incentive_plans_info, \
quota_attribute_value
quota_attribute_value, quota_pricing_items_by_type
from apps.herd.services.services import get_rancher_statistics, rancher_quota_weight
from apps.pos_device.services.services import pos_organizations_sharing_information
from rest_framework.exceptions import APIException
@@ -116,20 +116,10 @@ class QuotaDistributionSerializer(serializers.ModelSerializer):
'sharing': pos_organizations_sharing_information(
device,
instance.quota,
distribution=instance
distribution=instance,
owner_org=organization
),
'base_prices': [
{
"text": "قیمت درب کارخانه", # noqa
"name": "base_price_factory",
"value": instance.quota.base_price_factory
},
{
"text": "قیمت درب اتحادیه", # noqa
"name": "base_price_cooperative",
"value": instance.quota.base_price_cooperative
}
]
'base_prices': quota_pricing_items_by_type(instance.quota)
}
if 'rancher' in self.context.keys():

View File

@@ -107,7 +107,7 @@ class POSFreeProductsViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSear
'device': device.id,
})
serializer = product_serializers.POSFreeProductSerializer(data=request.data)
serializer = product_serializers.POSFreeProductSerializer(data=request.data, context={'device': device})
if serializer.is_valid():
serializer.save()

View File

@@ -1,6 +1,14 @@
from apps.product.models import Quota, QuotaLivestockAllocation
from collections import defaultdict
from apps.product.models import (
Quota,
QuotaLivestockAllocation,
QuotaPriceCalculationItems,
QuotaFinalPriceTypes
)
from apps.warehouse.models import InventoryEntry
from apps.herd.models import Rancher
from django.db.models import Sum
import typing
@@ -86,3 +94,42 @@ def quota_attribute_value(quota: Quota) -> typing.Any:
} for attr in attributes]
return attribute_values_list
def quota_pricing_items_by_type(quota: Quota) -> typing.Any:
"""
information of quota pricing items by final price type
Optimized: fetch all pricing items once, group by pricing_type
"""
# مرحله ۱: همه‌ی آیتم‌های مربوط به این quota رو یکجا بگیر
items = (
QuotaPriceCalculationItems.objects
.filter(quota=quota)
.select_related("pricing_type")
.values("pricing_type_id", "pricing_type__en_name", "pricing_type__name", "name", "value")
)
# مرحله ۲: گروه‌بندی آیتم‌ها بر اساس pricing_type
grouped = defaultdict(list)
for item in items:
key = item["pricing_type__en_name"]
grouped[key].append({
"name": item["name"],
"value": item["value"]
})
# مرحله ۳: جمع کل هر گروه
result = []
for en_name, group_items in grouped.items():
total_price = sum(i["value"] for i in group_items if i["value"])
fa_name = next(
(i["pricing_type__name"] for i in items if i["pricing_type__en_name"] == en_name),
en_name
)
result.append({
en_name: group_items,
f"{en_name}_fa": fa_name,
f"{en_name}_total_price": total_price,
})
return result

View File

@@ -259,3 +259,15 @@ class QuotaLiveStockAgeLimitationSerializer(serializers.ModelSerializer):
instance.save()
return instance
class QuotaPriceCalculationPriceItemsSerializer(serializers.ModelSerializer):
class Meta:
model = product_models.QuotaPriceCalculationItems
fields = '__all__'
class QuotaFinalPriceTypeSerializer(serializers.ModelSerializer):
class Meta:
model = product_models.QuotaFinalPriceTypes
fields = '__all__'

View File

@@ -17,6 +17,8 @@ router.register(r'incentive_plan', product_api.IncentivePlanViewSet, basename='i
router.register(r'rancher_incentive_plan', product_api.IncentivePlanRancherViewSet, basename='rancher_incentive_plan')
router.register(r'stats', product_api.ProductStatsViewSet, basename='stats')
router.register(r'quota', quota_api.QuotaViewSet, basename='quota')
router.register(r'quota_pricing_items', quota_api.QuotaPriceCalculationItemsViewSet, basename='quota_pricing_items')
router.register(r'quota_final_price_type', quota_api.QuotaFinalPriceTypeViewSet, basename='quota_final_price_type')
router.register(r'quota_distribution', distribution_apis.QuotaDistributionViewSet, basename='quota_distribution')
urlpatterns = [

View File

@@ -128,13 +128,26 @@ class QuotaViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin):
)
livestock_age_limits.append(age_limit_creation_object)
# create quota price calculation items for final price types
price_calculation_items = []
if 'price_calculation_items' in request.data.keys():
for price_item in request.data['price_calculation_items']:
price_item.update({'quota': quota.id})
price_item_creation_object = CustomOperations().custom_create(
request=request,
view=QuotaPriceCalculationItemsViewSet(),
data=price_item
)
price_calculation_items.append(price_item_creation_object)
data = {
'quota': serializer.data,
'incentive_plan': plans_list, # noqa
'attribute_values': attributes_value_list,
'broker_values': broker_data_list,
'live_stock_allocations': allocations_list,
'livestock_age_limitations': livestock_age_limits
'livestock_age_limitations': livestock_age_limits,
'price_calculation_items': price_calculation_items
}
# call save method to generate id & calculate quota final price
@@ -241,13 +254,30 @@ class QuotaViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin):
)
livestock_age_limits.append(age_limit_creation_object)
# create quota price calculation items for final price types
price_calculation_items = []
if 'price_calculation_items' in request.data.keys():
# remove live stock age limit relations
quota.pricing_items.all().delete()
for price_item in request.data['price_calculation_items']:
price_item.update({'quota': quota.id})
price_item_creation_object = CustomOperations().custom_create(
request=request,
view=QuotaPriceCalculationItemsViewSet(),
data=price_item
)
price_calculation_items.append(price_item_creation_object)
data = {
'quota': serializer.data,
'incentive_plan': plans_list, # noqa
'attribute_values': attributes_value_list,
'broker_values': broker_data_list,
'live_stock_allocations': allocations_list,
'livestock_age_limitations': livestock_age_limits
'livestock_age_limitations': livestock_age_limits,
'price_calculation_items': price_calculation_items
}
# call save method to generate id & calculate quota final price
@@ -602,3 +632,13 @@ class QuotaLiveStockAgeLimitation(SoftDeleteMixin, viewsets.ModelViewSet):
return Response(status=status.HTTP_200_OK)
except APIException as e:
return Response(e, status=status.HTTP_204_NO_CONTENT)
class QuotaPriceCalculationItemsViewSet(viewsets.ModelViewSet):
queryset = product_models.QuotaPriceCalculationItems.objects.all().select_related('quota', 'pricing_type')
serializer_class = quota_serializers.QuotaPriceCalculationPriceItemsSerializer
class QuotaFinalPriceTypeViewSet(viewsets.ModelViewSet):
queryset = product_models.QuotaFinalPriceTypes.objects.all()
serializer_class = quota_serializers.QuotaFinalPriceTypeSerializer