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

@@ -1,4 +1,10 @@
from apps.product.models import Quota, QuotaDistribution
from apps.product.models import (
Quota,
QuotaDistribution,
Organization,
QuotaPriceCalculationItems,
QuotaFinalPriceTypes
)
from apps.pos_device.models import Device, StakeHolderShareAmount
import typing
@@ -6,7 +12,8 @@ import typing
def pos_organizations_sharing_information(
device: Device,
quota: Quota = None,
distribution: QuotaDistribution = None
distribution: QuotaDistribution = None,
owner_org: Organization = None
) -> typing.Any:
"""
pos sharing organizations' information,
@@ -14,7 +21,10 @@ def pos_organizations_sharing_information(
"""
stake_holders = device.stake_holders.select_related('broker', 'organization').filter(default=False)
sharing_information_list = [{
sharing_information_list = []
for item in stake_holders:
if item.broker and not owner_org.type.name == 'AGC': # if stakeholder is not an agency, it is a broker
sharing_information_list.append({
"organization_name": item.organization.name,
"bank_account": {
"credit_card": item.organization.bank_information.first().card,
@@ -24,11 +34,41 @@ def pos_organizations_sharing_information(
"broker": item.broker.name if item.broker else None,
"amount": quota.broker_values.filter(
broker=item.broker
).first().value if quota and item.broker else (
item.holders_share_amount.filter(quota_distribution=distribution).first().share_amount
if item.holders_share_amount.filter(quota_distribution=distribution).exists() else None
),
).first().value if quota and item.broker else None,
# """
# if we will need to get agencies share amount, we can use this bellow code
#
# # item.holders_share_amount.filter(quota_distribution=distribution).first().share_amount
# # if item.holders_share_amount.filter(quota_distribution=distribution).exists() else None
# """
"default_account": item.default
} for item in stake_holders]
})
# if device owner is an agency organization
if owner_org.type.name == 'AGC':
sharing_information_list.append({
"organization_name": owner_org.parent_organization.name,
"bank_account": {
"credit_card": owner_org.parent_organization.bank_information.first().card,
"sheba": owner_org.parent_organization.bank_information.first().sheba,
"account": owner_org.parent_organization.bank_information.first().account,
} if owner_org.parent_organization.bank_information.exists() else {},
"amount": quota.pricing_items.get(
name='base_price'
) if quota.pricing_items.filter(
name='base_price'
) else None,
# """
# if we will need to get agencies share amount, we can use this bellow code
#
# # item.holders_share_amount.filter(quota_distribution=distribution).first().share_amount
# # if item.holders_share_amount.filter(quota_distribution=distribution).exists() else None
# """
"default_account": item.default
})
return sharing_information_list

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,

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

View File

@@ -141,29 +141,6 @@ class InventoryQuotaSaleTransactionViewSet(viewsets.ModelViewSet, DynamicSearchM
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN)
@action(
methods=['get'],
detail=False,
url_path='transactions',
url_name='transactions',
name='transactions',
)
@transaction.atomic
def transactions(self, request):
""" pos transactions list """
# get device object
device = self.get_pos_device()
queryset = self.queryset.filter(pos_device=device)
queryset = self.filter_query(queryset)
# paginate & response
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
class QuotaPreSaleItemViewSet(viewsets.ModelViewSet, DynamicSearchMixin, POSDeviceMixin):
queryset = warehouse_models.QuotaPreSaleItem.objects.all().select_related(