diff --git a/apps/pos_device/services/services.py b/apps/pos_device/services/services.py index 7178c33..26237ac 100644 --- a/apps/pos_device/services/services.py +++ b/apps/pos_device/services/services.py @@ -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,21 +21,54 @@ def pos_organizations_sharing_information( """ stake_holders = device.stake_holders.select_related('broker', 'organization').filter(default=False) - sharing_information_list = [{ - "organization_name": item.organization.name, - "bank_account": { - "credit_card": item.organization.bank_information.first().card, - "sheba": item.organization.bank_information.first().sheba, - "account": item.organization.bank_information.first().account, - } if item.organization.bank_information.exists() else {}, - "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 - ), - "default_account": item.default - } for item in stake_holders] + 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, + "sheba": item.organization.bank_information.first().sheba, + "account": item.organization.bank_information.first().account, + } if item.organization.bank_information.exists() else {}, + "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 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 + }) + + # 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 diff --git a/apps/product/migrations/0080_quotafinalpricetypes_quotapricecalculationitems.py b/apps/product/migrations/0080_quotafinalpricetypes_quotapricecalculationitems.py new file mode 100644 index 0000000..ca0be87 --- /dev/null +++ b/apps/product/migrations/0080_quotafinalpricetypes_quotapricecalculationitems.py @@ -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, + }, + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index a26ed75..46fe966 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -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) diff --git a/apps/product/pos/api/v1/serializers/product_serializers.py b/apps/product/pos/api/v1/serializers/product_serializers.py index c958006..d443798 100644 --- a/apps/product/pos/api/v1/serializers/product_serializers.py +++ b/apps/product/pos/api/v1/serializers/product_serializers.py @@ -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 diff --git a/apps/product/pos/api/v1/serializers/quota_distribution_serializers.py b/apps/product/pos/api/v1/serializers/quota_distribution_serializers.py index e07aea6..0d70b5a 100644 --- a/apps/product/pos/api/v1/serializers/quota_distribution_serializers.py +++ b/apps/product/pos/api/v1/serializers/quota_distribution_serializers.py @@ -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(): diff --git a/apps/product/pos/api/v1/viewsets/product_api.py b/apps/product/pos/api/v1/viewsets/product_api.py index 8c6edd9..ccbb6ae 100644 --- a/apps/product/pos/api/v1/viewsets/product_api.py +++ b/apps/product/pos/api/v1/viewsets/product_api.py @@ -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() diff --git a/apps/product/services/services.py b/apps/product/services/services.py index 4c7cdd9..d965e9a 100644 --- a/apps/product/services/services.py +++ b/apps/product/services/services.py @@ -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 diff --git a/apps/product/web/api/v1/serializers/quota_serializers.py b/apps/product/web/api/v1/serializers/quota_serializers.py index 614c170..590214a 100644 --- a/apps/product/web/api/v1/serializers/quota_serializers.py +++ b/apps/product/web/api/v1/serializers/quota_serializers.py @@ -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__' diff --git a/apps/product/web/api/v1/urls.py b/apps/product/web/api/v1/urls.py index 2282430..67b01f8 100644 --- a/apps/product/web/api/v1/urls.py +++ b/apps/product/web/api/v1/urls.py @@ -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 = [ diff --git a/apps/product/web/api/v1/viewsets/quota_api.py b/apps/product/web/api/v1/viewsets/quota_api.py index 7a70c69..bccf40c 100644 --- a/apps/product/web/api/v1/viewsets/quota_api.py +++ b/apps/product/web/api/v1/viewsets/quota_api.py @@ -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 diff --git a/apps/warehouse/pos/api/v1/api.py b/apps/warehouse/pos/api/v1/api.py index 3183837..d947661 100644 --- a/apps/warehouse/pos/api/v1/api.py +++ b/apps/warehouse/pos/api/v1/api.py @@ -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(