From c0b6b8ddcac12935966799a95b73310a4855e0a9 Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Tue, 26 Aug 2025 12:18:10 +0330 Subject: [PATCH] add purchace limitations system for rancher - add incentive plans to rancher entries data --- apps/authentication/exceptions.py | 8 +++++ apps/core/api.py | 11 ++++-- apps/core/migrations/0007_systemconfig.py | 34 +++++++++++++++++++ apps/core/models.py | 12 +++++++ apps/core/serializers.py | 8 ++++- apps/core/urls.py | 3 +- .../0017_rancher_ignore_purchase_limit.py | 18 ++++++++++ apps/herd/models.py | 3 ++ apps/pos_device/web/api/v1/viewsets/device.py | 19 +++++++++-- apps/product/services/services.py | 25 +++++++++++--- apps/warehouse/pos/api/v1/serializers.py | 8 +++-- apps/warehouse/services/services.py | 7 ++++ 12 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 apps/core/migrations/0007_systemconfig.py create mode 100644 apps/herd/migrations/0017_rancher_ignore_purchase_limit.py diff --git a/apps/authentication/exceptions.py b/apps/authentication/exceptions.py index 8fe7a57..32c97f4 100644 --- a/apps/authentication/exceptions.py +++ b/apps/authentication/exceptions.py @@ -9,3 +9,11 @@ class TokenBlackListedException(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = _('unauthorized') default_code = 'unauthorized' + + +class OrganizationBankAccountException(APIException): + """ if organization does not have bank account """ + + status_code = status.HTTP_403_FORBIDDEN + default_detail = "برای این سازمان حساب بانکی تعریف نشده است, ابتدا حساب بانکی تعریف کنید" # noqa + default_code = "برای این سازمان حساب بانکی تعریف نشده است" # noqa diff --git a/apps/core/api.py b/apps/core/api.py index d61f64d..649d26b 100644 --- a/apps/core/api.py +++ b/apps/core/api.py @@ -1,9 +1,14 @@ -from rest_framework import viewsets -from apps.core.models import MobileTest +from apps.core.serializers import MobileTestSerializer, SystemConfigSerializer +from apps.core.models import MobileTest, SystemConfig from rest_framework.response import Response -from apps.core.serializers import MobileTestSerializer +from rest_framework import viewsets class MobileTestViewSet(viewsets.ModelViewSet): queryset = MobileTest.objects.all() serializer_class = MobileTestSerializer + + +class SystemConfigViewSet(viewsets.ModelViewSet): + queryset = SystemConfig.objects.all() + serializer_class = SystemConfigSerializer diff --git a/apps/core/migrations/0007_systemconfig.py b/apps/core/migrations/0007_systemconfig.py new file mode 100644 index 0000000..cd31a32 --- /dev/null +++ b/apps/core/migrations/0007_systemconfig.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0 on 2025-08-26 07:49 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_mobiletest_creator_info_mobiletest_modifier_info'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='SystemConfig', + 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)), + ('key', models.CharField(max_length=100, null=True)), + ('value', models.CharField(max_length=100, 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, + }, + ), + ] diff --git a/apps/core/models.py b/apps/core/models.py index 796599b..7037feb 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -44,3 +44,15 @@ class MobileTest(BaseModel): longitude = models.DecimalField(max_digits=22, decimal_places=16) count = models.IntegerField(default=0) time = models.DateTimeField(auto_now_add=True) + + +class SystemConfig(BaseModel): + key = models.CharField(max_length=100, null=True) + value = models.CharField(max_length=100, null=True) + + @classmethod + def get(cls, key, default=None): + try: + return cls.objects.get(key=key).value + except cls.DoesNotExist: + return default diff --git a/apps/core/serializers.py b/apps/core/serializers.py index 41b7848..2bf93a7 100644 --- a/apps/core/serializers.py +++ b/apps/core/serializers.py @@ -1,4 +1,4 @@ -from apps.core.models import MobileTest +from apps.core.models import MobileTest, SystemConfig from rest_framework import serializers @@ -6,3 +6,9 @@ class MobileTestSerializer(serializers.ModelSerializer): class Meta: model = MobileTest fields = '__all__' + + +class SystemConfigSerializer(serializers.ModelSerializer): + class Meta: + model = SystemConfig + fields = '__all__' diff --git a/apps/core/urls.py b/apps/core/urls.py index 84bad30..cd74caf 100644 --- a/apps/core/urls.py +++ b/apps/core/urls.py @@ -1,9 +1,10 @@ +from apps.core.api import MobileTestViewSet, SystemConfigViewSet from rest_framework.routers import DefaultRouter -from apps.core.api import MobileTestViewSet from django.urls import path, include router = DefaultRouter() router.register('mobile_test', MobileTestViewSet, basename='mobile_test') +router.register('system_config', SystemConfigViewSet, basename='system_config') app_name = "core" urlpatterns = [ diff --git a/apps/herd/migrations/0017_rancher_ignore_purchase_limit.py b/apps/herd/migrations/0017_rancher_ignore_purchase_limit.py new file mode 100644 index 0000000..63c4d96 --- /dev/null +++ b/apps/herd/migrations/0017_rancher_ignore_purchase_limit.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-08-26 07:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('herd', '0016_rancher_activity_rancher_heavy_livestock_number_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='rancher', + name='ignore_purchase_limit', + field=models.BooleanField(default=False, help_text='if its true rancher has not buy limitations'), + ), + ] diff --git a/apps/herd/models.py b/apps/herd/models.py index 82befc1..d0ddb6a 100644 --- a/apps/herd/models.py +++ b/apps/herd/models.py @@ -127,6 +127,9 @@ class Rancher(BaseModel): null=True ) without_herd = models.BooleanField(default=False) + ignore_purchase_limit = models.BooleanField( + default=False, help_text="if its true rancher has not buy limitations" + ) def __str__(self): return f'rancher: {self.first_name} {self.last_name}' diff --git a/apps/pos_device/web/api/v1/viewsets/device.py b/apps/pos_device/web/api/v1/viewsets/device.py index 42eefea..57dd693 100644 --- a/apps/pos_device/web/api/v1/viewsets/device.py +++ b/apps/pos_device/web/api/v1/viewsets/device.py @@ -1,11 +1,12 @@ from datetime import timedelta from apps.pos_device.web.api.v1.serilaizers import device as device_serializer +from apps.authentication.exceptions import OrganizationBankAccountException from apps.authorization.api.v1.serializers import UserRelationSerializer from apps.pos_device.web.api.v1.viewsets.client import POSClientViewSet from apps.authentication.api.v1.api import ( - UserViewSet, Organization, + BankAccountInformation, OrganizationSerializer ) from apps.core.mixins.search_mixin import DynamicSearchMixin @@ -176,10 +177,17 @@ class DeviceAssignmentViewSet(viewsets.ModelViewSet): # if client will be an organization if request.data['client_data']['is_organization']: + + # check if organization have bank account or raise exception + if not BankAccountInformation.objects.filter( + organization_id=request.data['client_data']['organization'] + ).exists(): + raise OrganizationBankAccountException() + + # check if organization is a client before client = pos_models.POSClient.objects.filter( organization_id=request.data['client_data']['organization'] ) - if client.exists(): request.data.update({'client': client.first().id}) @@ -276,6 +284,13 @@ class StakeHoldersViewSet(viewsets.ModelViewSet, DynamicSearchMixin): stakeholders_data = [] for stakeholder in request.data['stakeholders']: + + # check if organization have bank account or raise exception + if not BankAccountInformation.objects.filter( + organization_id=stakeholder['organization'] + ).exists(): + raise OrganizationBankAccountException() + serializer = self.serializer_class(data=stakeholder) if serializer.is_valid(): serializer.save() diff --git a/apps/product/services/services.py b/apps/product/services/services.py index c277bd1..ef0349d 100644 --- a/apps/product/services/services.py +++ b/apps/product/services/services.py @@ -22,10 +22,25 @@ def quota_live_stock_allocation_info(quota: Quota) -> typing.Any: allocations = quota.livestock_allocations.filter(quota=quota) - allocations_list = [{ - "name": alloc.livestock_type.name, - "quantity": alloc.quantity_kg - } for alloc in allocations] + if allocations: + allocations_list = [{ + "name": alloc.livestock_type.name, + "quantity": alloc.quantity_kg + } for alloc in allocations] - return allocations_list + return allocations_list + +def quota_incentive_plans_info(quota: Quota) -> typing.Any: + """ information of quota incentive plans """ + + incentive_plans = quota.incentive_assignments.all() + + if incentive_plans: + incentive_plans_list = [{ + 'name': plan.incentive_plan.name, + 'heavy_value': plan.heavy_value, + 'light_value': plan.light_value + } for plan in incentive_plans] + + return incentive_plans_list diff --git a/apps/warehouse/pos/api/v1/serializers.py b/apps/warehouse/pos/api/v1/serializers.py index ec5d67e..aa5bee2 100644 --- a/apps/warehouse/pos/api/v1/serializers.py +++ b/apps/warehouse/pos/api/v1/serializers.py @@ -1,5 +1,8 @@ from apps.herd.services.services import get_rancher_statistics, rancher_quota_weight -from apps.product.services.services import quota_live_stock_allocation_info +from apps.product.services.services import ( + quota_live_stock_allocation_info, + quota_incentive_plans_info +) from apps.pos_device.pos.api.v1.serializers.device import DeviceSerializer from apps.herd.pos.api.v1.serializers import RancherSerializer from apps.warehouse.exceptions import ( @@ -46,7 +49,8 @@ class InventoryEntrySerializer(serializers.ModelSerializer): 'quota_weight': instance.distribution.quota.quota_weight, 'quota_livestock_allocations': quota_live_stock_allocation_info( instance.distribution.quota - ) + ), + 'quota_incentive_plans': quota_incentive_plans_info(instance.distribution.quota) } representation['product'] = { 'name': instance.distribution.quota.product.name, diff --git a/apps/warehouse/services/services.py b/apps/warehouse/services/services.py index 8f88cbe..dfbb4d2 100644 --- a/apps/warehouse/services/services.py +++ b/apps/warehouse/services/services.py @@ -1,5 +1,6 @@ from apps.warehouse.models import InventoryEntry, InventoryQuotaSaleTransaction from apps.herd.services.services import rancher_quota_weight, get_rancher_statistics +from apps.core.models import SystemConfig from django.db.models import Sum @@ -17,6 +18,12 @@ def get_total_sold(inventory_entry, rancher): def can_buy_from_inventory(rancher, inventory_entry: InventoryEntry): """ """ + if SystemConfig.get("IGNORE_ALL_RANCHER_PURCHASE_LIMITS") == "true": + return True + + if rancher.ignore_purchase_limit: + return True + quota_weight = rancher_quota_weight(rancher, inventory_entry) # {total, by_type} total_allowed = quota_weight['total']