fix - quota exception in distribution limit & sale licence and visibility free by org

This commit is contained in:
2025-11-02 10:59:57 +03:30
parent cca24d6f6a
commit 3b2dc4c522
10 changed files with 160 additions and 37 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0 on 2025-11-02 07:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0043_alter_user_options_alter_user_managers'),
]
operations = [
migrations.AddField(
model_name='organization',
name='free_visibility_by_scope',
field=models.BooleanField(default=False),
),
]

View File

@@ -116,6 +116,7 @@ class Organization(BaseModel):
('CI', 'City')
)
field_of_activity = models.CharField(max_length=2, choices=activity_fields, default="")
free_visibility_by_scope = models.BooleanField(default=False)
company_code = models.CharField(max_length=30, default="")
province = models.ForeignKey(
Province,

View File

@@ -0,0 +1,44 @@
import typing
from django.db.models import Q
from apps.authentication.models import Organization
from apps.core.visibility_registry import VISIBILITY_MAP
def get_visible_organizations(org: Organization) -> typing.Any:
"""
get visible organizations
"""
if org.free_visibility_by_scope:
if org.field_of_activity == 'CO':
return Organization.objects.all()
elif org.field_of_activity == 'CI':
return Organization.objects.filter(city=org.city)
elif org.field_of_activity == 'PR':
return Organization.objects.filter(province=org.province)
return Organization.objects.filter(id=org.id)
def apply_visibility_filter(queryset, org):
model_name = queryset.model.__name__.lower()
org_fields = VISIBILITY_MAP.get(model_name)
if not org_fields:
return queryset
visible_orgs = get_visible_organizations(org)
org_ids = visible_orgs.values_list('id', flat=True)
if isinstance(org_fields, str):
return queryset.filter(**{f"{org_fields}__in": org_ids})
q_obj = Q()
for field in org_fields:
q_obj |= Q(**{f"{field}__in": org_ids})
return queryset.filter(q_obj)

View File

@@ -2,6 +2,7 @@ from rest_framework import viewsets
from apps.authentication.mixins.region_filter import RegionFilterMixin, get_organization_by_user
from apps.authentication.services.service import get_all_org_child
from apps.authentication.services.visibility_services import apply_visibility_filter
from apps.authorization.services.role_child import get_all_role_child
from apps.core.models import MobileTest, SystemConfig
from apps.core.serializers import MobileTestSerializer, SystemConfigSerializer
@@ -13,7 +14,7 @@ class BaseViewSet(RegionFilterMixin, viewsets.ModelViewSet):
It applies region-based filtering automatically to GET (list) requests.
"""
def get_queryset(self, show_my_org: bool = None):
def get_queryset(self, show_my_org: bool = None, visibility_by_org_scope: bool = None):
queryset = super().get_queryset()
request = self.request
user = request.user
@@ -23,6 +24,11 @@ class BaseViewSet(RegionFilterMixin, viewsets.ModelViewSet):
if self.request.method.lower() == 'get' and not self.kwargs.get('pk'):
queryset = self.filter_by_region(queryset, org=True)
if visibility_by_org_scope:
""" if organization has free visibility by scope, apply visibility filter """
queryset = apply_visibility_filter(queryset, org)
return queryset
if user_relation.exists():
user_relation = user_relation.first()
if not user_relation.role.type.key == 'ADM':

View File

@@ -0,0 +1,16 @@
VISIBILITY_MAP = {
'userrelations': 'organization',
'organization': 'id',
'quota': ['registerer_organization', 'assigned_organizations'],
'quotadistribution': ['assigner_organization', 'assigned_organization'],
'inventoryentry': 'organization',
'inventoryquotasaletransaction': 'organization',
'device': 'organization',
# 'deviceactivationcode': 'organization',
# 'deviceversion': 'organization',
# 'posclient': 'organization',
# 'deviceassignment': 'organization',
# 'stakeholders': 'organization',
# 'stakeholdershareamount': 'registering_organization',
# 'posfreeproducts': 'organization',
}

View File

@@ -38,7 +38,15 @@ class QuotaExpiredTimeException(APIException):
"""if quota allowed time for distribute, sale, etc. is expired"""
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "زمان مجوز این سهمیه به پایان رسیده است" # noqa
default_detail = "این سهمیه دارای محدودیت توزیع میباشد" # noqa
default_code = 'error'
class QuotaSaleTimeException(APIException):
"""if quota allowed time for distribute, sale, etc. is expired"""
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "اين سهمیه دارای محدودیت مجوز فروش میباشد" # noqa
default_code = 'error'

View File

@@ -446,6 +446,13 @@ class Quota(BaseModel):
now = datetime.now()
persian_date = jdatetime.datetime.fromgregorian(datetime=now)
return persian_date.month in self.distribution_mode
def is_in_sale_licence_time(self):
""" check if quota allowed time for sale and... is expired """
now = datetime.now()
persian_date = jdatetime.datetime.fromgregorian(datetime=now)
return persian_date.month in self.sale_license
def soft_delete(self):

View File

@@ -1,22 +1,24 @@
from apps.product.web.api.v1.serializers import quota_distribution_serializers
from apps.product.services.services import get_products_in_warehouse
from apps.product.web.api.v1.serializers import quota_serializers
from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin
from apps.product.exceptions import QuotaExpiredTimeException
from apps.core.mixins.search_mixin import DynamicSearchMixin
from apps.core.pagination import CustomPageNumberPagination
from apps.product.web.api.v1.viewsets import product_api
from common.helpers import get_organization_by_user
from rest_framework.exceptions import APIException
from apps.product import models as product_models
from rest_framework.response import Response
from rest_framework.decorators import action
from common.tools import CustomOperations
from rest_framework import viewsets, filters
from rest_framework import status
from datetime import datetime
from django.db import transaction
from django.db.models import Q
from datetime import datetime
from rest_framework import status
from rest_framework import viewsets, filters
from rest_framework.decorators import action
from rest_framework.exceptions import APIException
from rest_framework.response import Response
from apps.core.api import BaseViewSet
from apps.core.mixins.search_mixin import DynamicSearchMixin
from apps.core.mixins.soft_delete_mixin import SoftDeleteMixin
from apps.core.pagination import CustomPageNumberPagination
from apps.product import models as product_models
from apps.product.exceptions import QuotaExpiredTimeException
from apps.product.web.api.v1.serializers import quota_distribution_serializers
from apps.product.web.api.v1.serializers import quota_serializers
from apps.product.web.api.v1.viewsets import product_api
from common.helpers import get_organization_by_user
from common.tools import CustomOperations
def trash(queryset, pk): # noqa
@@ -32,7 +34,7 @@ def delete(queryset, pk):
obj.delete()
class QuotaViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin): # noqa
class QuotaViewSet(BaseViewSet, SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin): # noqa
""" apis for product quota """
queryset = product_models.Quota.objects.all()
@@ -344,18 +346,14 @@ class QuotaViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin):
def active_quotas(self, request):
""" list of organization active quotas """
queryset = self.filter_query(self.queryset) # return by search param or all objects
organization = get_organization_by_user(request.user)
queryset = self.filter_query(
self.get_queryset(visibility_by_org_scope=True)) # return by search param or all objects
# paginate queryset
page = self.paginate_queryset(
queryset.filter(
Q(registerer_organization=organization),
Q(is_closed=False)
).order_by('-modify_date')
queryset.order_by('-modify_date')
)
if page is not None:
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
@@ -370,18 +368,14 @@ class QuotaViewSet(SoftDeleteMixin, viewsets.ModelViewSet, DynamicSearchMixin):
def closed_quotas(self, request):
""" list of organization closed quotas """
queryset = self.filter_query(self.queryset) # return by search param or all objects
organization = get_organization_by_user(request.user)
queryset = self.filter_query(
self.get_queryset(visibility_by_org_scope=True)) # return by search param or all objects
# paginate queryset
page = self.paginate_queryset(
queryset.filter(
Q(registerer_organization=organization),
Q(is_closed=True)
).order_by('-modify_date')
queryset.order_by('-modify_date')
)
if page is not None:
if page is not None: # noqa
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)

View File

@@ -9,7 +9,7 @@ from apps.herd.services.services import get_rancher_statistics, rancher_quota_we
from apps.pos_device.models import POSFreeProducts
from apps.pos_device.pos.api.v1.serializers.device import DeviceSerializer
from apps.pos_device.services.services import pos_organizations_sharing_information
from apps.product.exceptions import DistributionWeightException
from apps.product.exceptions import DistributionWeightException, QuotaSaleTimeException
from apps.product.models import (
QuotaDistribution,
Product
@@ -251,6 +251,10 @@ class InventoryQuotaSaleTransactionSerializer(serializers.ModelSerializer):
for item in items:
if 'quota_distribution' in item.keys():
distribution = QuotaDistribution.objects.get(id=item.get('quota_distribution'))
# if quota has not been in sale time
if not distribution.quota.is_in_sale_licence_time():
raise QuotaSaleTimeException()
total_sale_weight = distribution.sale_items.aggregate(
total=models.Sum('weight')
)['total'] or 0

View File

@@ -633,3 +633,28 @@ AssertionError: .validate() should return the validated data
[2025-11-01 14:19:59,373] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\core\api.py changed, reloading.
[2025-11-01 14:20:01,908] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-01 14:34:29,660] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\core\api.py changed, reloading.
[2025-11-01 14:34:31,755] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 08:33:15,283] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 08:33:35,818] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 08:34:44,035] INFO django.server | IP: - | Path: - | "POST /captcha/ HTTP/1.1" 200 690
[2025-11-02 08:35:17,275] INFO django.server | IP: - | Path: - | "POST /auth/api/v1/login/ HTTP/1.1" 200 681
[2025-11-02 08:43:15,098] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\product\models.py changed, reloading.
[2025-11-02 08:43:25,159] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 08:57:18,625] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\warehouse\pos\api\v1\serializers.py changed, reloading.
[2025-11-02 08:57:22,331] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 08:57:34,431] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\product\exceptions.py changed, reloading.
[2025-11-02 08:57:38,298] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 09:04:23,459] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 09:55:15,104] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\authentication\models.py changed, reloading.
[2025-11-02 09:55:17,569] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 10:10:25,957] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\authentication\services\service.py changed, reloading.
[2025-11-02 10:10:27,762] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 10:10:35,301] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\authentication\services\service.py changed, reloading.
[2025-11-02 10:10:39,073] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 10:17:07,590] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\authentication\services\service.py changed, reloading.
[2025-11-02 10:17:09,581] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 10:19:36,116] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\authentication\services\service.py changed, reloading.
[2025-11-02 10:19:38,189] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 10:48:20,522] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\core\api.py changed, reloading.
[2025-11-02 10:48:27,576] INFO django.utils.autoreload | IP: - | Path: - | Watching for file changes with StatReloader
[2025-11-02 10:56:40,429] INFO django.utils.autoreload | IP: - | Path: - | D:\Project\Rasaddam_Backend\apps\product\web\api\v1\viewsets\quota_api.py changed, reloading.