diff --git a/Rasaddam_Backend/settings.py b/Rasaddam_Backend/settings.py index 8d7c8be..852681b 100644 --- a/Rasaddam_Backend/settings.py +++ b/Rasaddam_Backend/settings.py @@ -78,7 +78,7 @@ INSTALLED_APPS = [ 'apps.core.apps.CoreConfig', 'apps.herd.apps.HerdAppConfig', 'apps.livestock.apps.LivestockConfig', - 'apps.pos_machine.apps.PosMachineConfig', + 'apps.pos_device.apps.PosDeviceConfig', 'apps.tag.apps.TagConfig', 'apps.warehouse.apps.WarehouseConfig', 'apps.search.apps.SearchConfig', diff --git a/apps/authentication/api/v1/api.py b/apps/authentication/api/v1/api.py index f23845a..3d18c67 100644 --- a/apps/authentication/api/v1/api.py +++ b/apps/authentication/api/v1/api.py @@ -1,5 +1,3 @@ -import typing -from rest_framework.permissions import AllowAny from apps.authentication.api.v1.serializers.jwt import CustomizedTokenObtainPairSerializer from rest_framework.decorators import action, permission_classes from apps.authentication import permissions as auth_permissions @@ -16,7 +14,9 @@ from apps.core.pagination import CustomPageNumberPagination from apps.authorization.api.v1 import api as authorize_view from rest_framework.permissions import IsAuthenticated from apps.authentication.tools import get_token_jti +from common.helpers import get_organization_by_user from rest_framework.viewsets import ModelViewSet +from rest_framework.permissions import AllowAny from apps.authentication.models import ( User, City, @@ -35,6 +35,7 @@ from rest_framework import status from django.db import transaction from common.sms import send_sms import random +import typing class CustomizedTokenObtainPairView(TokenObtainPairView): @@ -197,6 +198,14 @@ class OrganizationViewSet(ModelViewSet): queryset = Organization.objects.all() serializer_class = OrganizationSerializer + def get_all_org_child(self, org): + descendants = [] + children = org.parents.all() + for child in children: + descendants.append(child) + descendants.extend(self.get_all_org_child(child)) + return descendants + @transaction.atomic def create(self, request, *args, **kwargs): """ @@ -274,6 +283,24 @@ class OrganizationViewSet(ModelViewSet): serializer = self.serializer_class(queryset, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @action( + methods=['get'], + detail=False, + url_path='child_organizations', + url_name='child_organizations', + name='child_organizations' + ) + @transaction.atomic + def get_child_organizations(self, request): + organization = get_organization_by_user(request.user) + child_organizations = self.get_all_org_child(organization) + + page = self.paginate_queryset(child_organizations) # paginate queryset + + if page is not None: + serializer = self.serializer_class(page, many=True) + return self.get_paginated_response(serializer.data) + class BankAccountViewSet(ModelViewSet): """ Crud operations for bank account model """ # diff --git a/apps/authentication/api/v1/serializers/serializer.py b/apps/authentication/api/v1/serializers/serializer.py index 9bd375e..d6adb26 100644 --- a/apps/authentication/api/v1/serializers/serializer.py +++ b/apps/authentication/api/v1/serializers/serializer.py @@ -10,6 +10,7 @@ from apps.authentication.models import ( Province, Organization, OrganizationType, + OrganizationStats, BankAccountInformation ) from apps.authorization import models as authorize_models @@ -220,3 +221,9 @@ class OrganizationSerializer(serializers.ModelSerializer): instance.national_unique_id = validated_data.get('national_unique_id', instance.national_unique_id) instance.save() return instance + + +class OrganizationStatsSerializer(serializers.ModelSerializer): + class Meta: + model = OrganizationStats + fields = '__all__' diff --git a/apps/authentication/apps.py b/apps/authentication/apps.py index bc4ed96..6aba299 100644 --- a/apps/authentication/apps.py +++ b/apps/authentication/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class AuthenticationConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'apps.authentication' + + def ready(self): + import apps.authentication.signals diff --git a/apps/authentication/migrations/0026_organizationstats.py b/apps/authentication/migrations/0026_organizationstats.py new file mode 100644 index 0000000..a975397 --- /dev/null +++ b/apps/authentication/migrations/0026_organizationstats.py @@ -0,0 +1,37 @@ +# Generated by Django 5.0 on 2025-07-16 07:41 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0025_alter_organizationtype_name'), + ] + + operations = [ + migrations.CreateModel( + name='OrganizationStats', + 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)), + ('total_quota_received', models.PositiveBigIntegerField(default=0)), + ('total_distributed', models.PositiveBigIntegerField(default=0)), + ('total_inventory_in', models.PositiveBigIntegerField(default=0)), + ('total_sold', models.PositiveBigIntegerField(default=0)), + ('total_buyers', 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)), + ('organization', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stats', to='authentication.organization')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index 207cfc8..6df97f3 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -135,6 +135,26 @@ class Organization(BaseModel): super(Organization, self).save(*args, **kwargs) +class OrganizationStats(BaseModel): + organization = models.OneToOneField( + Organization, + on_delete=models.CASCADE, + related_name='stats', + null=True + ) + total_quota_received = models.PositiveBigIntegerField(default=0) + total_distributed = models.PositiveBigIntegerField(default=0) + total_inventory_in = models.PositiveBigIntegerField(default=0) + total_sold = models.PositiveBigIntegerField(default=0) + total_buyers = models.PositiveBigIntegerField(default=0) + + def __str__(self): + return f'Organization: {self.organization.name}' + + def save(self, *args, **kwargs): + return super(OrganizationStats, self).save(*args, **kwargs) + + class BankAccountInformation(BaseModel): user = models.ForeignKey( User, diff --git a/apps/authentication/signals.py b/apps/authentication/signals.py new file mode 100644 index 0000000..03ef480 --- /dev/null +++ b/apps/authentication/signals.py @@ -0,0 +1,11 @@ +from django.db.models import Sum +from apps.product.models import QuotaDistribution +from apps.warehouse.models import InventoryQuotaSaleTransaction +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver + + +@receiver([post_save, post_delete], sender=QuotaDistribution) +@receiver([post_save, post_delete], sender=InventoryQuotaSaleTransaction) +def update_organization_stats(sender, instance, **kwargs): + pass diff --git a/apps/pos_machine/__init__.py b/apps/pos_device/__init__.py similarity index 100% rename from apps/pos_machine/__init__.py rename to apps/pos_device/__init__.py diff --git a/apps/pos_machine/admin.py b/apps/pos_device/admin.py similarity index 100% rename from apps/pos_machine/admin.py rename to apps/pos_device/admin.py diff --git a/apps/pos_machine/apps.py b/apps/pos_device/apps.py similarity index 58% rename from apps/pos_machine/apps.py rename to apps/pos_device/apps.py index e20afe7..9d10cfd 100644 --- a/apps/pos_machine/apps.py +++ b/apps/pos_device/apps.py @@ -1,6 +1,7 @@ from django.apps import AppConfig -class PosMachineConfig(AppConfig): +class PosDeviceConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'apps.pos_machine' + name = 'apps.pos_device' + diff --git a/apps/pos_machine/management/__init__.py b/apps/pos_device/management/__init__.py similarity index 100% rename from apps/pos_machine/management/__init__.py rename to apps/pos_device/management/__init__.py diff --git a/apps/pos_machine/management/commands/__init__.py b/apps/pos_device/management/commands/__init__.py similarity index 100% rename from apps/pos_machine/management/commands/__init__.py rename to apps/pos_device/management/commands/__init__.py diff --git a/apps/pos_machine/management/commands/command.py b/apps/pos_device/management/commands/command.py similarity index 100% rename from apps/pos_machine/management/commands/command.py rename to apps/pos_device/management/commands/command.py diff --git a/apps/pos_device/migrations/0001_initial.py b/apps/pos_device/migrations/0001_initial.py new file mode 100644 index 0000000..f812386 --- /dev/null +++ b/apps/pos_device/migrations/0001_initial.py @@ -0,0 +1,65 @@ +# Generated by Django 5.0 on 2025-07-16 04:54 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Device', + 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)), + ('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='PaymentCompany', + 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)), + ('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='Sessions', + 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)), + ('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/pos_machine/migrations/__init__.py b/apps/pos_device/migrations/__init__.py similarity index 100% rename from apps/pos_machine/migrations/__init__.py rename to apps/pos_device/migrations/__init__.py diff --git a/apps/pos_machine/mobile/api/__init__.py b/apps/pos_device/mobile/api/__init__.py similarity index 100% rename from apps/pos_machine/mobile/api/__init__.py rename to apps/pos_device/mobile/api/__init__.py diff --git a/apps/pos_machine/mobile/api/v1/__init__.py b/apps/pos_device/mobile/api/v1/__init__.py similarity index 100% rename from apps/pos_machine/mobile/api/v1/__init__.py rename to apps/pos_device/mobile/api/v1/__init__.py diff --git a/apps/pos_machine/mobile/api/v1/serializers.py b/apps/pos_device/mobile/api/v1/serializers.py similarity index 100% rename from apps/pos_machine/mobile/api/v1/serializers.py rename to apps/pos_device/mobile/api/v1/serializers.py diff --git a/apps/pos_machine/mobile/api/v1/urls.py b/apps/pos_device/mobile/api/v1/urls.py similarity index 100% rename from apps/pos_machine/mobile/api/v1/urls.py rename to apps/pos_device/mobile/api/v1/urls.py diff --git a/apps/pos_machine/mobile/api/v1/views.py b/apps/pos_device/mobile/api/v1/views.py similarity index 100% rename from apps/pos_machine/mobile/api/v1/views.py rename to apps/pos_device/mobile/api/v1/views.py diff --git a/apps/pos_machine/mobile/tests/test_common_services.py b/apps/pos_device/mobile/tests/test_common_services.py similarity index 100% rename from apps/pos_machine/mobile/tests/test_common_services.py rename to apps/pos_device/mobile/tests/test_common_services.py diff --git a/apps/pos_device/models.py b/apps/pos_device/models.py new file mode 100644 index 0000000..35f30f7 --- /dev/null +++ b/apps/pos_device/models.py @@ -0,0 +1,14 @@ +from django.db import models +from apps.core.models import BaseModel + + +class PaymentCompany(BaseModel): + pass + + +class Device(BaseModel): + pass + + +class Sessions(BaseModel): + pass diff --git a/apps/pos_machine/permissions.py b/apps/pos_device/permissions.py similarity index 100% rename from apps/pos_machine/permissions.py rename to apps/pos_device/permissions.py diff --git a/apps/pos_machine/pos/api/__init__.py b/apps/pos_device/pos/api/__init__.py similarity index 100% rename from apps/pos_machine/pos/api/__init__.py rename to apps/pos_device/pos/api/__init__.py diff --git a/apps/pos_machine/pos/api/v1/__init__.py b/apps/pos_device/pos/api/v1/__init__.py similarity index 100% rename from apps/pos_machine/pos/api/v1/__init__.py rename to apps/pos_device/pos/api/v1/__init__.py diff --git a/apps/pos_machine/pos/api/v1/serializers.py b/apps/pos_device/pos/api/v1/serializers.py similarity index 100% rename from apps/pos_machine/pos/api/v1/serializers.py rename to apps/pos_device/pos/api/v1/serializers.py diff --git a/apps/pos_machine/pos/api/v1/urls.py b/apps/pos_device/pos/api/v1/urls.py similarity index 100% rename from apps/pos_machine/pos/api/v1/urls.py rename to apps/pos_device/pos/api/v1/urls.py diff --git a/apps/pos_machine/pos/api/v1/views.py b/apps/pos_device/pos/api/v1/views.py similarity index 100% rename from apps/pos_machine/pos/api/v1/views.py rename to apps/pos_device/pos/api/v1/views.py diff --git a/apps/pos_machine/pos/tests/test_common_services.py b/apps/pos_device/pos/tests/test_common_services.py similarity index 100% rename from apps/pos_machine/pos/tests/test_common_services.py rename to apps/pos_device/pos/tests/test_common_services.py diff --git a/apps/pos_machine/services.py b/apps/pos_device/services.py similarity index 100% rename from apps/pos_machine/services.py rename to apps/pos_device/services.py diff --git a/apps/pos_device/urls.py b/apps/pos_device/urls.py new file mode 100644 index 0000000..c680368 --- /dev/null +++ b/apps/pos_device/urls.py @@ -0,0 +1,5 @@ +from django.urls import path, include + +urlpatterns = [ + path('', include('')) +] diff --git a/apps/pos_machine/web/api/__init__.py b/apps/pos_device/web/api/__init__.py similarity index 100% rename from apps/pos_machine/web/api/__init__.py rename to apps/pos_device/web/api/__init__.py diff --git a/apps/pos_machine/web/api/v1/__init__.py b/apps/pos_device/web/api/v1/__init__.py similarity index 100% rename from apps/pos_machine/web/api/v1/__init__.py rename to apps/pos_device/web/api/v1/__init__.py diff --git a/apps/pos_machine/web/api/v1/serializers.py b/apps/pos_device/web/api/v1/serializers.py similarity index 100% rename from apps/pos_machine/web/api/v1/serializers.py rename to apps/pos_device/web/api/v1/serializers.py diff --git a/apps/pos_machine/web/api/v1/urls.py b/apps/pos_device/web/api/v1/urls.py similarity index 100% rename from apps/pos_machine/web/api/v1/urls.py rename to apps/pos_device/web/api/v1/urls.py diff --git a/apps/pos_machine/web/api/v1/views.py b/apps/pos_device/web/api/v1/views.py similarity index 100% rename from apps/pos_machine/web/api/v1/views.py rename to apps/pos_device/web/api/v1/views.py diff --git a/apps/pos_machine/web/tests/test_common_services.py b/apps/pos_device/web/tests/test_common_services.py similarity index 100% rename from apps/pos_machine/web/tests/test_common_services.py rename to apps/pos_device/web/tests/test_common_services.py diff --git a/apps/pos_machine/models.py b/apps/pos_machine/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/pos_machine/urls.py b/apps/pos_machine/urls.py deleted file mode 100644 index 0865d84..0000000 --- a/apps/pos_machine/urls.py +++ /dev/null @@ -1 +0,0 @@ -# Your urls go here diff --git a/apps/product/exceptions.py b/apps/product/exceptions.py index a2d6d49..9bf5daa 100644 --- a/apps/product/exceptions.py +++ b/apps/product/exceptions.py @@ -24,3 +24,14 @@ class QuotaExpiredTimeException(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = "زمان مجوز این سهمیه به پایان رسیده است" # noqa default_code = 'error' + + +class QuotaLimitByOrganizationException(APIException): + """ + if limitation of quota by organization is true, + distribution should be done in organizations limits + """ + + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "سازمان انتخاب شده در بین سامان های انتخاب شده برای توزیع سهمیه وجود ندارد" # noqa + default_code = 'error' diff --git a/apps/product/migrations/0048_quota_has_organization_limit_and_more.py b/apps/product/migrations/0048_quota_has_organization_limit_and_more.py new file mode 100644 index 0000000..9332695 --- /dev/null +++ b/apps/product/migrations/0048_quota_has_organization_limit_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 5.0 on 2025-07-16 07:41 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0026_organizationstats'), + ('product', '0047_quotalivestockagelimitation_livestock_type'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='quota', + name='has_organization_limit', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='quota', + name='limit_by_organizations', + field=models.ManyToManyField(null=True, related_name='quota_limits', to='authentication.organization'), + ), + migrations.CreateModel( + name='ProductStats', + 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)), + ('total_quota', models.PositiveBigIntegerField(default=0)), + ('total_remaining', models.PositiveBigIntegerField(default=0)), + ('total_sold', models.PositiveBigIntegerField(default=0)), + ('total_transactions', 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)), + ('product', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stats', to='product.product')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='QuotaStats', + 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)), + ('total_distributed', models.PositiveBigIntegerField(default=0)), + ('remaining', models.PositiveBigIntegerField(default=0)), + ('total_inventory', models.PositiveBigIntegerField(default=0)), + ('total_sale', 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)), + ('quota', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stats', to='product.quota')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/product/migrations/0049_alter_quota_limit_by_organizations.py b/apps/product/migrations/0049_alter_quota_limit_by_organizations.py new file mode 100644 index 0000000..4d10e4e --- /dev/null +++ b/apps/product/migrations/0049_alter_quota_limit_by_organizations.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0 on 2025-07-16 07:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0026_organizationstats'), + ('product', '0048_quota_has_organization_limit_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='quota', + name='limit_by_organizations', + field=models.ManyToManyField(related_name='quota_limits', to='authentication.organization'), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index 856425e..203cf90 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -134,6 +134,25 @@ class Product(BaseModel): super(Product, self).save(*args, **kwargs) +class ProductStats(BaseModel): + product = models.OneToOneField( + Product, + on_delete=models.CASCADE, + related_name='stats', + null=True + ) + total_quota = models.PositiveBigIntegerField(default=0) + total_remaining = models.PositiveBigIntegerField(default=0) + total_sold = models.PositiveBigIntegerField(default=0) + total_transactions = models.PositiveBigIntegerField(default=0) + + def __str__(self): + return f'Product: {self.product.name}-{self.product.id} stats' + + def save(self, *args, **kwargs): + return super(ProductStats).save(*args, **kwargs) + + class Attribute(BaseModel): """ every reference product have multiple attributes @@ -316,6 +335,8 @@ class Quota(BaseModel): ) has_distribution_limit = models.BooleanField(default=False) distribution_mode = ArrayField(base_field=models.IntegerField(), blank=True, null=True) + has_organization_limit = models.BooleanField(default=False) + limit_by_organizations = models.ManyToManyField(Organization, related_name='quota_limits') base_price_factory = models.DecimalField(max_digits=12, decimal_places=2) base_price_cooperative = models.DecimalField(max_digits=12, decimal_places=2) final_price = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) @@ -371,6 +392,25 @@ class Quota(BaseModel): return super(Quota, self).save(*args, **kwargs) +class QuotaStats(BaseModel): + quota = models.OneToOneField( + Quota, + on_delete=models.CASCADE, + related_name='stats', + null=True, + ) + total_distributed = models.PositiveBigIntegerField(default=0) + remaining = models.PositiveBigIntegerField(default=0) + total_inventory = models.PositiveBigIntegerField(default=0) + total_sale = models.PositiveBigIntegerField(default=0) + + def __str__(self): + return f'Quota: {self.quota.quota_id} stats' + + def save(self, *args, **kwargs): + return super(QuotaStats, self).save(*args, **kwargs) + + class QuotaIncentiveAssignment(BaseModel): """ assign incentive plan to quota """ diff --git a/apps/product/signals.py b/apps/product/signals.py index 1787287..94aeefe 100644 --- a/apps/product/signals.py +++ b/apps/product/signals.py @@ -1,7 +1,8 @@ from django.db.models import Sum from django.db.models.signals import post_save, post_delete from django.dispatch import receiver -from .models import QuotaDistribution, Quota +from .models import QuotaDistribution, Quota, Product +from apps.warehouse.models import InventoryQuotaSaleTransaction def recalculate_remaining_amount(quota): @@ -18,3 +19,18 @@ def recalculate_remaining_amount(quota): @receiver(post_delete, sender=QuotaDistribution) def update_quota_remaining(sender, instance, **kwargs): recalculate_remaining_amount(instance.quota) + + +def update_product_stats(instance: Product): + pass + + +def update_quota_stats(instance: Quota): + pass + + +@receiver([post_save, post_delete], sender=QuotaDistribution) +@receiver([post_save, post_delete], sender=InventoryQuotaSaleTransaction) +def update_stats_on_change(sender, instance, **kwargs): + update_product_stats(instance) + update_quota_stats(instance) diff --git a/apps/product/web/api/v1/serializers/product_serializers.py b/apps/product/web/api/v1/serializers/product_serializers.py index 07b1a70..5b128d6 100644 --- a/apps/product/web/api/v1/serializers/product_serializers.py +++ b/apps/product/web/api/v1/serializers/product_serializers.py @@ -30,6 +30,12 @@ class ProductSerializer(serializers.ModelSerializer): return representation +class ProductStatsSerializer(serializers.ModelSerializer): + class Meta: + model = product_models.ProductStats + fields = '__all__' + + class AttributeSerializer(serializers.ModelSerializer): """ serialize attributes of reference product """ @@ -61,7 +67,6 @@ class AttributeValueSerializer(serializers.ModelSerializer): fields = '__all__' def update(self, instance, validated_data): - instance.quota = validated_data.get('quota', instance.quota) instance.attribute = validated_data.get('attribute', instance.attribute) instance.value = validated_data.get('value', instance.value) diff --git a/apps/product/web/api/v1/serializers/quota_distribution_serializers.py b/apps/product/web/api/v1/serializers/quota_distribution_serializers.py index d784803..43a3fb7 100644 --- a/apps/product/web/api/v1/serializers/quota_distribution_serializers.py +++ b/apps/product/web/api/v1/serializers/quota_distribution_serializers.py @@ -5,7 +5,8 @@ from django.db import models from apps.product.exceptions import ( QuotaWeightException, QuotaClosedException, - QuotaExpiredTimeException + QuotaExpiredTimeException, + QuotaLimitByOrganizationException ) @@ -21,12 +22,15 @@ class QuotaDistributionSerializer(serializers.ModelSerializer): def validate(self, data): """ - to validate if distribution weight + @ to validate if distribution weight more than quota weight raise exception - or if quota is closed raise exception + @ if quota is closed raise exception + @ if quota has organization limit, before distribution + check assigned organization """ quota = data['quota'] + assigned_organization = data['assigned_organization'] amount = data['weight'] instance_id = self.instance.id if self.instance else None @@ -38,6 +42,9 @@ class QuotaDistributionSerializer(serializers.ModelSerializer): if quota.is_closed: raise QuotaClosedException() + if assigned_organization not in quota.limit_by_organizations.all(): + raise QuotaLimitByOrganizationException() + # total quota distributions weight total = product_models.QuotaDistribution.objects.filter( quota=quota diff --git a/apps/product/web/api/v1/serializers/quota_serializers.py b/apps/product/web/api/v1/serializers/quota_serializers.py index 07e1c02..8299aba 100644 --- a/apps/product/web/api/v1/serializers/quota_serializers.py +++ b/apps/product/web/api/v1/serializers/quota_serializers.py @@ -59,6 +59,8 @@ class QuotaSerializer(serializers.ModelSerializer): instance.group = validated_data.get('group', instance.group) instance.has_distribution_limit = validated_data.get('has_distribution_limit', instance.has_distribution_limit) instance.distribution_mode = validated_data.get('distribution_mode', instance.distribution_mode) + instance.has_organization_limit = validated_data.get('has_organization_limit', instance.has_organization_limit) + instance.limit_by_organizations = validated_data.get('limit_by_organizations', instance.limit_by_organizations) instance.base_price_factory = validated_data.get('base_price_factory', instance.base_price_factory) instance.base_price_cooperative = validated_data.get('base_price_cooperative', instance.base_price_cooperative) instance.final_price = validated_data.get('final_price', instance.final_price) @@ -74,6 +76,12 @@ class QuotaSerializer(serializers.ModelSerializer): return instance +class QuotaStatsSerializer(serializers.ModelSerializer): + class Meta: + model = product_models.QuotaStats + fields = '__all__' + + class QuotaIncentiveAssignmentSerializer(serializers.ModelSerializer): class Meta: model = product_models.QuotaIncentiveAssignment