add & fix - transaction dashboard / edit distribution with stat validation bug
This commit is contained in:
@@ -81,12 +81,14 @@ def get_rancher_statistic_by_herd(rancher: Rancher = None) -> typing.Any:
|
|||||||
def rancher_quota_weight(
|
def rancher_quota_weight(
|
||||||
rancher: Rancher,
|
rancher: Rancher,
|
||||||
inventory_entry: InventoryEntry = None,
|
inventory_entry: InventoryEntry = None,
|
||||||
distribution: QuotaDistribution = None
|
distribution: QuotaDistribution = None,
|
||||||
|
quota: Quota = None
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
:param rancher: Rancher instance
|
:param rancher: Rancher instance
|
||||||
:param inventory_entry: InventoryEntry instance
|
:param inventory_entry: InventoryEntry instance
|
||||||
:param distribution: QuotaDistribution instance
|
:param distribution: QuotaDistribution instance
|
||||||
|
:param quota: Quota instance
|
||||||
:return: dict {total, by_type}
|
:return: dict {total, by_type}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ def rancher_quota_weight(
|
|||||||
elif distribution:
|
elif distribution:
|
||||||
quota: Quota = distribution.quota
|
quota: Quota = distribution.quota
|
||||||
else:
|
else:
|
||||||
quota: Quota = Quota()
|
quota: Quota = quota
|
||||||
|
|
||||||
# list of quota live stock allocations
|
# list of quota live stock allocations
|
||||||
allocations = list(quota.livestock_allocations.all().select_related('livestock_type'))
|
allocations = list(quota.livestock_allocations.all().select_related('livestock_type'))
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from apps.core.models import SystemConfig
|
||||||
|
from apps.herd.services.services import get_rancher_statistic_by_herd, rancher_quota_weight
|
||||||
from apps.livestock.web.api.v1.serializers import LiveStockTypeSerializer
|
from apps.livestock.web.api.v1.serializers import LiveStockTypeSerializer
|
||||||
|
from apps.pos_device.services.services import pos_organizations_sharing_information
|
||||||
from apps.product import models as product_models
|
from apps.product import models as product_models
|
||||||
|
from apps.product.services.services import quota_live_stock_allocation_info, quota_incentive_plans_info, \
|
||||||
|
quota_attribute_value, quota_pricing_items_by_type
|
||||||
from apps.product.web.api.v1.serializers import product_serializers
|
from apps.product.web.api.v1.serializers import product_serializers
|
||||||
|
|
||||||
|
|
||||||
@@ -239,4 +244,81 @@ class QuotaLiveStockAgeLimitationSerializer(serializers.ModelSerializer):
|
|||||||
class OrganizationQuotaStatsSerializer(serializers.ModelSerializer):
|
class OrganizationQuotaStatsSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = product_models.OrganizationQuotaStats
|
model = product_models.OrganizationQuotaStats
|
||||||
fields = '__all__'
|
fields = [
|
||||||
|
'id',
|
||||||
|
'create_date',
|
||||||
|
'modify_date',
|
||||||
|
'creator_info',
|
||||||
|
'modifier_info',
|
||||||
|
]
|
||||||
|
|
||||||
|
def to_representation(self, instance: product_models.OrganizationQuotaStats):
|
||||||
|
""" Custom output of serializer """
|
||||||
|
|
||||||
|
representation = super().to_representation(instance)
|
||||||
|
|
||||||
|
organization = self.context['organization']
|
||||||
|
rancher = self.context['rancher']
|
||||||
|
device = self.context['device']
|
||||||
|
|
||||||
|
representation['weight'] = instance.total_amount
|
||||||
|
representation['remaining_weight'] = instance.remaining_amount
|
||||||
|
representation['distributed'] = instance.total_distributed
|
||||||
|
representation['warehouse_entry'] = instance.inventory_received
|
||||||
|
representation['warehouse_balance'] = instance.inventory_entry_balance
|
||||||
|
representation['been_sold'] = instance.sold_amount
|
||||||
|
representation['pre_sale_balance'] = instance.quota.pre_sale_balance
|
||||||
|
representation['free_sale_balance'] = instance.quota.free_sale_balance
|
||||||
|
|
||||||
|
representation['free_sale'] = instance.quota.free_sale
|
||||||
|
representation['pre_sale'] = instance.quota.pre_sale
|
||||||
|
|
||||||
|
if instance.quota:
|
||||||
|
representation['quota'] = {
|
||||||
|
'quota_identity': instance.quota.quota_id,
|
||||||
|
'quota_weight': instance.quota.quota_weight,
|
||||||
|
'pos_sale_type': instance.quota.pos_sale_type,
|
||||||
|
'quota_livestock_allocations': quota_live_stock_allocation_info(
|
||||||
|
instance.quota
|
||||||
|
),
|
||||||
|
'quota_incentive_plans': quota_incentive_plans_info(instance.quota, rancher),
|
||||||
|
'quota_sale_license': instance.quota.sale_license,
|
||||||
|
'has_sale_license': instance.quota.is_in_valid_time(),
|
||||||
|
}
|
||||||
|
|
||||||
|
sale_limitation = SystemConfig.get(
|
||||||
|
"IGNORE_ALL_RANCHER_PURCHASE_LIMITS",
|
||||||
|
quota_identity=instance.quota.quota_id
|
||||||
|
)
|
||||||
|
representation['product'] = {
|
||||||
|
'image': instance.quota.product.img,
|
||||||
|
'name': instance.quota.product.name,
|
||||||
|
'id': instance.quota.product.id,
|
||||||
|
'free_sale_for_all': sale_limitation if sale_limitation else False,
|
||||||
|
'free_sale_for_this_rancher': rancher.ignore_purchase_limit
|
||||||
|
}
|
||||||
|
|
||||||
|
sharing_list = pos_organizations_sharing_information(
|
||||||
|
device,
|
||||||
|
instance.quota,
|
||||||
|
owner_org=organization
|
||||||
|
)
|
||||||
|
|
||||||
|
representation['pricing'] = { # noqa
|
||||||
|
'main_account_sheba': "IR" + organization.bank_information.first().sheba,
|
||||||
|
'pricing_attributes': quota_attribute_value(instance.quota),
|
||||||
|
'sharing': sharing_list,
|
||||||
|
'base_prices': quota_pricing_items_by_type(instance.quota, sharing=sharing_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'rancher' in self.context.keys():
|
||||||
|
# rancher herd & live stock statistics
|
||||||
|
livestock_counts_list, livestock_counts_dict = get_rancher_statistic_by_herd(rancher)
|
||||||
|
representation['rancher_statistics'] = livestock_counts_list
|
||||||
|
|
||||||
|
# rancher live stock statistics by quota distributions
|
||||||
|
representation['rancher_quota_weight_statistics'] = rancher_quota_weight(
|
||||||
|
rancher, quota=instance.quota
|
||||||
|
)
|
||||||
|
|
||||||
|
return representation
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ class OrganizationQuotaStatsViewSet(viewsets.ModelViewSet, DynamicSearchMixin, P
|
|||||||
Q(quota__pre_sale=True) | Q(quota__free_sale=True) | Q(inventory_received__gt=0)
|
Q(quota__pre_sale=True) | Q(quota__free_sale=True) | Q(inventory_received__gt=0)
|
||||||
)
|
)
|
||||||
).order_by('-create_date')
|
).order_by('-create_date')
|
||||||
|
print(quotas)
|
||||||
# check quota distributions for rancher
|
# check quota distributions for rancher
|
||||||
# available_distributions = [
|
# available_distributions = [
|
||||||
# distribution for distribution in distributions if (
|
# distribution for distribution in distributions if (
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ def update_product_stats(instance: Product, distribution: QuotaDistribution = No
|
|||||||
QuotaStatsValidator.validate_assigner_has_enough(
|
QuotaStatsValidator.validate_assigner_has_enough(
|
||||||
organization,
|
organization,
|
||||||
distribution.quota,
|
distribution.quota,
|
||||||
distribution.weight
|
distribution.weight,
|
||||||
|
allow_zero=True
|
||||||
)
|
)
|
||||||
if ProductStats.objects.filter(
|
if ProductStats.objects.filter(
|
||||||
organization=organization,
|
organization=organization,
|
||||||
|
|||||||
@@ -50,14 +50,14 @@ class QuotaStatsValidator:
|
|||||||
if amount < 0:
|
if amount < 0:
|
||||||
raise DistributionException("مقدار وزن نباید منفی باشد", status.HTTP_403_FORBIDDEN) # noqa
|
raise DistributionException("مقدار وزن نباید منفی باشد", status.HTTP_403_FORBIDDEN) # noqa
|
||||||
|
|
||||||
if remaining < amount and not allow_zero:
|
if remaining + amount < amount and not allow_zero:
|
||||||
if update_operation:
|
if update_operation:
|
||||||
raise DistributionException(
|
raise DistributionException(
|
||||||
"مقدار وزن ویرایش شده از وزن باقیمانده بیشتر است", # noqa
|
"مقدار وزن ویرایش شده از وزن باقیمانده بیشتر است", # noqa
|
||||||
status.HTTP_403_FORBIDDEN
|
status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
raise DistributionException(
|
raise DistributionException(
|
||||||
f"تخصیص مقدار {amount} بیشتر از مقدار باقیمانده {remaining} برای سازمان {assigner_org.name} میباشد.",
|
f"تخصیص مقدار {amount} بیشتر از مقدار باقیمانده {remaining + amount} برای سازمان {assigner_org.name} میباشد.",
|
||||||
# noqa
|
# noqa
|
||||||
status.HTTP_403_FORBIDDEN
|
status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
|
|||||||
92
apps/warehouse/services/transaction_dashboard_service.py
Normal file
92
apps/warehouse/services/transaction_dashboard_service.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django.db.models import Sum, Count, Case, When, Q, Value
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
from apps.warehouse.models import InventoryQuotaSaleTransaction, InventoryQuotaSaleItem
|
||||||
|
|
||||||
|
|
||||||
|
class TransactionDashboardService:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_dashboard(org):
|
||||||
|
transactions = InventoryQuotaSaleTransaction.objects.filter(
|
||||||
|
seller_organization=org
|
||||||
|
)
|
||||||
|
|
||||||
|
transaction_stats = transactions.aggregate(
|
||||||
|
total_transactions=Count("id"),
|
||||||
|
success_transactions=Count("id", filter=Q(transaction_status="success")),
|
||||||
|
failed_transactions=Count("id", filter=Q(transaction_status="failed")),
|
||||||
|
waiting_transactions=Count("id", filter=Q(transaction_status="waiting")),
|
||||||
|
total_amount=Coalesce(Sum("price_paid"), 0),
|
||||||
|
total_weight=Coalesce(Sum("weight"), 0),
|
||||||
|
unique_ranchers=Count("rancher", distinct=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
items = InventoryQuotaSaleItem.objects.filter(
|
||||||
|
transaction__seller_organization=org
|
||||||
|
).select_related("gov_product", "free_product")
|
||||||
|
|
||||||
|
products_stats = items.values(
|
||||||
|
product_id=Case(
|
||||||
|
When(gov_product__isnull=False, then="gov_product_id"),
|
||||||
|
When(free_product__isnull=False, then="free_product_id"),
|
||||||
|
),
|
||||||
|
product_name=Case(
|
||||||
|
When(gov_product__isnull=False, then="gov_product__name"),
|
||||||
|
When(free_product__isnull=False, then="free_product__product__name"),
|
||||||
|
),
|
||||||
|
product_type=Case(
|
||||||
|
When(gov_product__isnull=False, then=Value("gov")),
|
||||||
|
When(free_product__isnull=False, then=Value("free")),
|
||||||
|
)
|
||||||
|
).annotate(
|
||||||
|
total_sales=Count("id"),
|
||||||
|
total_weight=Coalesce(Sum("weight"), 0),
|
||||||
|
# total_price=Coalesce(Sum("total_price"), 0),
|
||||||
|
# avg_unit_price=Coalesce(Sum("total_price") / Sum("weight"), 0),
|
||||||
|
success_sales=Count("id", filter=Q(transaction__transaction_status="success")),
|
||||||
|
failed_sales=Count("id", filter=Q(transaction__transaction_status="failed")),
|
||||||
|
waiting_sales=Count("id", filter=Q(transaction__transaction_status="waiting")),
|
||||||
|
card_payments=Count("id", filter=Q(transaction__price_type="card")),
|
||||||
|
cash_payments=Count("id", filter=Q(transaction__price_type="cash")),
|
||||||
|
check_payments=Count("id", filter=Q(transaction__price_type="check")),
|
||||||
|
credit_payments=Count("id", filter=Q(transaction__price_type="credit")),
|
||||||
|
extra_items=Count("id", filter=Q(is_extra=True)),
|
||||||
|
pre_sale_items=Count("id", filter=Q(is_pre_sale=True)),
|
||||||
|
).order_by("-total_sales")
|
||||||
|
|
||||||
|
# calculate sum of item share percentage by product
|
||||||
|
items_by_product = defaultdict(list)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
pid = item.gov_product_id or item.free_product_id
|
||||||
|
items_by_product[pid].append(item)
|
||||||
|
|
||||||
|
for product in products_stats:
|
||||||
|
pid = product["product_id"]
|
||||||
|
share_totals = defaultdict(lambda: {"total_price": 0, "count": 0})
|
||||||
|
|
||||||
|
for item in items_by_product.get(pid, []):
|
||||||
|
if item.item_share:
|
||||||
|
for share in item.item_share:
|
||||||
|
# share: {"name": ..., "price": ..., "shaba": ...}
|
||||||
|
name = share.get("name")
|
||||||
|
price = share.get("price", 0)
|
||||||
|
|
||||||
|
share_totals[name]["total_price"] += price
|
||||||
|
share_totals[name]["count"] += 1
|
||||||
|
|
||||||
|
product["item_share_stats"] = sorted(
|
||||||
|
[
|
||||||
|
{"name": name, **val}
|
||||||
|
for name, val in share_totals.items()
|
||||||
|
],
|
||||||
|
key=lambda x: -x["total_price"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"transaction_summary": transaction_stats,
|
||||||
|
"product_summary": list(products_stats),
|
||||||
|
}
|
||||||
@@ -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.transaction_dashboard_service import TransactionDashboardService
|
||||||
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
|
||||||
@@ -158,7 +159,13 @@ class InventoryEntryViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet,
|
|||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class InventoryQuotaSaleTransactionViewSet(BaseViewSet, SoftDeleteMixin, DynamicSearchMixin, viewsets.ModelViewSet):
|
class InventoryQuotaSaleTransactionViewSet(
|
||||||
|
BaseViewSet,
|
||||||
|
SoftDeleteMixin,
|
||||||
|
TransactionDashboardService,
|
||||||
|
DynamicSearchMixin,
|
||||||
|
viewsets.ModelViewSet,
|
||||||
|
):
|
||||||
queryset = warehouse_models.InventoryQuotaSaleTransaction.objects.all()
|
queryset = warehouse_models.InventoryQuotaSaleTransaction.objects.all()
|
||||||
serializer_class = warehouse_serializers.InventoryQuotaSaleTransactionSerializer
|
serializer_class = warehouse_serializers.InventoryQuotaSaleTransactionSerializer
|
||||||
filter_backends = [filters.SearchFilter]
|
filter_backends = [filters.SearchFilter]
|
||||||
@@ -212,6 +219,23 @@ class InventoryQuotaSaleTransactionViewSet(BaseViewSet, SoftDeleteMixin, Dynamic
|
|||||||
serializer = self.get_serializer(page, many=True)
|
serializer = self.get_serializer(page, many=True)
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@action(
|
||||||
|
methods=['get'],
|
||||||
|
detail=False,
|
||||||
|
url_path='transactions_dashboard',
|
||||||
|
url_name='transactions_dashboard',
|
||||||
|
name='transactions_dashboard'
|
||||||
|
)
|
||||||
|
def transactions_dashboard(self, request):
|
||||||
|
"""
|
||||||
|
dashboard of full detail of all my transactions
|
||||||
|
"""
|
||||||
|
|
||||||
|
org = get_organization_by_user(request.user)
|
||||||
|
transaction_dashboard_data = self.get_dashboard(org)
|
||||||
|
|
||||||
|
return Response(transaction_dashboard_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class InventoryQuotaSaleItemViewSet(SoftDeleteMixin, BaseViewSet, viewsets.ModelViewSet):
|
class InventoryQuotaSaleItemViewSet(SoftDeleteMixin, BaseViewSet, viewsets.ModelViewSet):
|
||||||
queryset = warehouse_models.InventoryQuotaSaleItem.objects.all()
|
queryset = warehouse_models.InventoryQuotaSaleItem.objects.all()
|
||||||
|
|||||||
Reference in New Issue
Block a user