From 92a5d3a2eb54b2e919bf60a0b12539ce31f36bb1 Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Sat, 9 Aug 2025 16:15:37 +0330 Subject: [PATCH] fix bug of inventory entry - fix weight of quotas --- .../0031_organization_separate_warehouse.py | 18 ++++++ .../migrations/0032_organization_has_pos.py | 18 ++++++ apps/authentication/models.py | 1 + ...eactivationcode_expires_at_stakeholders.py | 42 +++++++++++++ ...osclientattribute_custom_field_and_more.py | 36 +++++++++++ apps/pos_device/models.py | 10 ++- .../web/api/v1/serilaizers/device.py | 6 ++ apps/pos_device/web/api/v1/viewsets/device.py | 63 ++++++++++++------- apps/product/signals.py | 4 +- apps/warehouse/web/api/v1/serializers.py | 15 +++-- 10 files changed, 181 insertions(+), 32 deletions(-) create mode 100644 apps/authentication/migrations/0031_organization_separate_warehouse.py create mode 100644 apps/authentication/migrations/0032_organization_has_pos.py create mode 100644 apps/pos_device/migrations/0046_alter_deviceactivationcode_expires_at_stakeholders.py create mode 100644 apps/pos_device/migrations/0047_posclientattribute_custom_field_and_more.py diff --git a/apps/authentication/migrations/0031_organization_separate_warehouse.py b/apps/authentication/migrations/0031_organization_separate_warehouse.py new file mode 100644 index 0000000..c845061 --- /dev/null +++ b/apps/authentication/migrations/0031_organization_separate_warehouse.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-08-09 08:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0030_alter_bankaccountinformation_user'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='separate_warehouse', + field=models.BooleanField(default=False), + ), + ] diff --git a/apps/authentication/migrations/0032_organization_has_pos.py b/apps/authentication/migrations/0032_organization_has_pos.py new file mode 100644 index 0000000..ea7c668 --- /dev/null +++ b/apps/authentication/migrations/0032_organization_has_pos.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0 on 2025-08-09 12:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0031_organization_separate_warehouse'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='has_pos', + field=models.BooleanField(default=False), + ), + ] diff --git a/apps/authentication/models.py b/apps/authentication/models.py index c17d854..a0e050c 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -126,6 +126,7 @@ class Organization(BaseModel): null=True ) separate_warehouse = models.BooleanField(default=False) + has_pos = models.BooleanField(default=False) additional_data = models.JSONField(default=dict) def __str__(self): diff --git a/apps/pos_device/migrations/0046_alter_deviceactivationcode_expires_at_stakeholders.py b/apps/pos_device/migrations/0046_alter_deviceactivationcode_expires_at_stakeholders.py new file mode 100644 index 0000000..137a2ab --- /dev/null +++ b/apps/pos_device/migrations/0046_alter_deviceactivationcode_expires_at_stakeholders.py @@ -0,0 +1,42 @@ +# Generated by Django 5.0 on 2025-08-09 08:09 + +import datetime +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0031_organization_separate_warehouse'), + ('pos_device', '0045_alter_device_acceptor_alter_device_latitude_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='deviceactivationcode', + name='expires_at', + field=models.DateTimeField(default=datetime.datetime(2025, 8, 9, 11, 39, 37, 865832)), + ), + migrations.CreateModel( + name='StakeHolders', + 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)), + ('share_percent', models.FloatField(default=0)), + ('assignment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stake_holders', to='pos_device.deviceassignment')), + ('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.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='stake_holders', to='authentication.organization')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/pos_device/migrations/0047_posclientattribute_custom_field_and_more.py b/apps/pos_device/migrations/0047_posclientattribute_custom_field_and_more.py new file mode 100644 index 0000000..05be700 --- /dev/null +++ b/apps/pos_device/migrations/0047_posclientattribute_custom_field_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 5.0 on 2025-08-09 12:44 + +import datetime +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0032_organization_has_pos'), + ('authorization', '0019_page_is_active_permissions_is_active'), + ('pos_device', '0046_alter_deviceactivationcode_expires_at_stakeholders'), + ] + + operations = [ + migrations.AddField( + model_name='posclientattribute', + name='custom_field', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='deviceactivationcode', + name='expires_at', + field=models.DateTimeField(default=datetime.datetime(2025, 8, 9, 16, 14, 34, 958731)), + ), + migrations.AlterField( + model_name='stakeholders', + name='organization', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='pos_stake_holders', to='authentication.organization'), + ), + migrations.AlterUniqueTogether( + name='providercompany', + unique_together={('user_relation', 'name_fa')}, + ), + ] diff --git a/apps/pos_device/models.py b/apps/pos_device/models.py index a3aad95..5c2d2a2 100644 --- a/apps/pos_device/models.py +++ b/apps/pos_device/models.py @@ -14,12 +14,15 @@ class ProviderCompany(BaseModel): UserRelations, related_name='pos_provider', on_delete=models.CASCADE, - null=True + null=True, ) name_fa = models.CharField(max_length=250, null=True) name_en = models.CharField(max_length=250, null=True) activation = models.BooleanField(default=False) + class Meta: + unique_together = ('user_relation', 'name_fa') + def __str__(self): return f'Payment Company: {self.name_fa}-{self.id}' @@ -175,6 +178,7 @@ class POSClientAttribute(BaseModel): ], null=True) required = models.BooleanField(default=False) choices = ArrayField(base_field=models.CharField(max_length=150), null=True, blank=True) + custom_field = models.BooleanField(default=False) def __str__(self): return f'attribute: {self.key}-{self.field_type}' @@ -242,7 +246,7 @@ class StakeHolders(BaseModel): organization = models.ForeignKey( Organization, on_delete=models.CASCADE, - related_name='stake_holders', + related_name='pos_stake_holders', null=True ) share_percent = models.FloatField(default=0) @@ -251,4 +255,4 @@ class StakeHolders(BaseModel): return f'Device: {self.assignment.device.serial}-organization: {self.organization.name}' def save(self, *args, **kwargs): - return super(StakeHolders, self).save(*args, **kwargs) \ No newline at end of file + return super(StakeHolders, self).save(*args, **kwargs) diff --git a/apps/pos_device/web/api/v1/serilaizers/device.py b/apps/pos_device/web/api/v1/serilaizers/device.py index 56013ed..7f8d1d9 100644 --- a/apps/pos_device/web/api/v1/serilaizers/device.py +++ b/apps/pos_device/web/api/v1/serilaizers/device.py @@ -38,3 +38,9 @@ class DeviceAssignmentSerializer(ModelSerializer): class Meta: model = pos_models.DeviceAssignment fields = '__all__' + + +class StakeHoldersSerializer(ModelSerializer): + class Meta: + model = pos_models.StakeHolders + fields = '__all__' diff --git a/apps/pos_device/web/api/v1/viewsets/device.py b/apps/pos_device/web/api/v1/viewsets/device.py index 04b84da..00c9d5b 100644 --- a/apps/pos_device/web/api/v1/viewsets/device.py +++ b/apps/pos_device/web/api/v1/viewsets/device.py @@ -1,6 +1,7 @@ from datetime import timedelta from apps.pos_device.web.api.v1.serilaizers import device as device_serializer +from apps.authorization.api.v1.serializers import UserRelationSerializer from apps.authentication.api.v1.api import UserViewSet from apps.authorization.models import UserRelations from rest_framework.exceptions import APIException @@ -19,30 +20,34 @@ class ProviderCompanyViewSet(viewsets.ModelViewSet): # noqa queryset = pos_models.ProviderCompany.objects.all() serializer_class = device_serializer.ProviderCompanySerializer - def create(self, request, *args, **kwargs): - """ custom create of provider client """ + def list(self, request, *args, **kwargs): + """ list of provider companies """ - try: - # creating user & relations - client = UserViewSet().create(request=request) + # paginate devices + page = self.paginate_queryset(self.queryset.order_by('-create_date')) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) - if client.status_code == 201: + @action( + methods=['get'], + detail=False, + url_path='psp_users', + url_name='psp_users', + name='psp_users' + ) + def users_with_psp_org(self): + """ list of psp users """ - # create provider - serializer = self.serializer_class(data=request.data['provider']) - if serializer.is_valid(): - provider = serializer.save() - provider.user_relation = UserRelations.objects.get( - user_id=client.data['id'] - ) - provider.save() + users = UserRelations.objects.filter( + organization__type__key='PSP' + ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN) - else: - return client - except Exception as e: - raise e + # paginate devices + page = self.paginate_queryset(users) + if page is not None: + serializer = UserRelationSerializer(page, many=True) + return self.get_paginated_response(serializer.data) class DeviceViewSet(viewsets.ModelViewSet): @@ -127,16 +132,21 @@ class DeviceAssignmentViewSet(viewsets.ModelViewSet): def device_assignment(self, request): """ assign pos device to client by company """ - company = pos_models.ProviderCompany.objects.get( - user_relation__user=request.user - ) - request.data.update({'company': company.id}) + if 'company' not in request.data.keys(): + company = pos_models.ProviderCompany.objects.get( + user_relation__user=request.user + ) + request.data.update({'company': company.id}) # create assignment serializer = self.serializer_class(data=request.data) if serializer.is_valid(): assignment = serializer.save() + # set organization having pos status + assignment.company.user_relation.organization.has_pos = True + assignment.company.user_relation.organization.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_403_FORBIDDEN) @@ -162,3 +172,8 @@ class DeviceAssignmentViewSet(viewsets.ModelViewSet): return Response(serializer.data, status=status.HTTP_200_OK) except Exception as e: raise APIException('Non Object Error', code=403) + + +class StakeHoldersViewSet(viewsets.ModelViewSet): + queryset = pos_models.StakeHolders.objects.all() + serializer_class = device_serializer.StakeHoldersSerializer diff --git a/apps/product/signals.py b/apps/product/signals.py index e97061f..8c73efa 100644 --- a/apps/product/signals.py +++ b/apps/product/signals.py @@ -20,7 +20,9 @@ from crum import get_current_user def recalculate_remaining_amount(quota): """ calculate remaining weight from distribution """ - total_distributed = quota.distributions_assigned.aggregate( + total_distributed = quota.distributions_assigned.filter( + parent_distribution__isnull=True, + ).aggregate( total=Sum('weight') )['total'] or 0 diff --git a/apps/warehouse/web/api/v1/serializers.py b/apps/warehouse/web/api/v1/serializers.py index cdc1504..b822468 100644 --- a/apps/warehouse/web/api/v1/serializers.py +++ b/apps/warehouse/web/api/v1/serializers.py @@ -42,12 +42,19 @@ class InventoryEntrySerializer(serializers.ModelSerializer): total=models.Sum('weight') )['total'] or 0 - if self.instance.weight is 0: + # if instance exists, for update check weight with distribution weight + if self.instance: + if self.instance.weight is 0: + if total_entered + attrs['weight'] > distribution.weight: + raise InventoryEntryWeightException() + elif self.instance.weight is not 0: + if total_entered - self.instance.weight + attrs['weight'] > distribution.weight: + raise InventoryEntryWeightException() + + # if instance is not exists for create, check entry weight with distribution + elif not self.instance: if total_entered + attrs['weight'] > distribution.weight: raise InventoryEntryWeightException() - elif self.instance.weight is not 0: - if total_entered - self.instance.weight + attrs['weight'] > distribution.weight: - raise InventoryEntryWeightException() return attrs