fix - quota exception in distribution limit & sale licence and visibility free by org
This commit is contained in:
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
|
||||
44
apps/authentication/services/visibility_services.py
Normal file
44
apps/authentication/services/visibility_services.py
Normal 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)
|
||||
@@ -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':
|
||||
|
||||
16
apps/core/visibility_registry.py
Normal file
16
apps/core/visibility_registry.py
Normal 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',
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user