From ec69b6f80b936ad69be3c143ee0e4edb8db4c5f3 Mon Sep 17 00:00:00 2001 From: behruz-dev Date: Thu, 7 Aug 2025 12:40:16 +0500 Subject: [PATCH] add project location --- config/settings/base.py | 2 +- core/apps/projects/admin/project.py | 9 +- .../migrations/0010_projectlocation.py | 33 ++++++ .../migrations/0011_alter_project_location.py | 19 ++++ core/apps/projects/models/project.py | 23 +++- core/apps/projects/serializers/project.py | 101 ++++++++++++++++-- .../projects/serializers/project_location.py | 49 +++++++++ core/apps/projects/views/project.py | 6 +- core/apps/shared/admin/__init__.py | 1 + core/apps/shared/admin/region.py | 23 ++++ core/apps/shared/apps.py | 4 + core/apps/shared/migrations/0001_initial.py | 43 ++++++++ core/apps/shared/models/__init__.py | 3 +- core/apps/shared/models/region.py | 27 +++++ core/apps/shared/serializers/region.py | 19 ++++ core/apps/shared/urls.py | 2 + core/apps/shared/views/region.py | 13 +++ 17 files changed, 357 insertions(+), 20 deletions(-) create mode 100644 core/apps/projects/migrations/0010_projectlocation.py create mode 100644 core/apps/projects/migrations/0011_alter_project_location.py create mode 100644 core/apps/projects/serializers/project_location.py create mode 100644 core/apps/shared/admin/region.py create mode 100644 core/apps/shared/migrations/0001_initial.py create mode 100644 core/apps/shared/models/region.py create mode 100644 core/apps/shared/serializers/region.py create mode 100644 core/apps/shared/views/region.py diff --git a/config/settings/base.py b/config/settings/base.py index 3d9db45..9106bac 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -150,7 +150,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' AUTH_USER_MODEL = 'accounts.User' -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', env.str("SWAGGER_PROTOCOL", 'https')) from config.conf.rest_framework import * from config.conf.rest_framework_simplejwt import * diff --git a/core/apps/projects/admin/project.py b/core/apps/projects/admin/project.py index 92722d2..5857e9a 100644 --- a/core/apps/projects/admin/project.py +++ b/core/apps/projects/admin/project.py @@ -1,6 +1,6 @@ from django.contrib import admin -from core.apps.projects.models.project import Project, ProjectFolder +from core.apps.projects.models.project import Project, ProjectFolder, ProjectLocation @@ -14,4 +14,9 @@ class ProjectAdmin(admin.ModelAdmin): @admin.register(ProjectFolder) class ProjectFolderAdmin(admin.ModelAdmin): list_display = ['id', 'name'] - search_fields = ['name'] \ No newline at end of file + search_fields = ['name'] + + +@admin.register(ProjectLocation) +class ProjectLocation(admin.ModelAdmin): + list_display = ['address', 'latitude', 'longitude'] \ No newline at end of file diff --git a/core/apps/projects/migrations/0010_projectlocation.py b/core/apps/projects/migrations/0010_projectlocation.py new file mode 100644 index 0000000..b8a102d --- /dev/null +++ b/core/apps/projects/migrations/0010_projectlocation.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.4 on 2025-08-07 12:03 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0009_project_status'), + ('shared', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='ProjectLocation', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('address', models.CharField(max_length=200)), + ('latitude', models.FloatField()), + ('longitude', models.FloatField()), + ('district', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_locations', to='shared.district')), + ('region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_locations', to='shared.region')), + ], + options={ + 'verbose_name': 'Loyiha lokatsiyasi', + 'verbose_name_plural': 'Loyiha lokatsiyalari', + }, + ), + ] diff --git a/core/apps/projects/migrations/0011_alter_project_location.py b/core/apps/projects/migrations/0011_alter_project_location.py new file mode 100644 index 0000000..c32e43f --- /dev/null +++ b/core/apps/projects/migrations/0011_alter_project_location.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.4 on 2025-08-07 12:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0010_projectlocation'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='location', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='projects', to='projects.projectlocation'), + ), + ] diff --git a/core/apps/projects/models/project.py b/core/apps/projects/models/project.py index baf5835..a84d466 100644 --- a/core/apps/projects/models/project.py +++ b/core/apps/projects/models/project.py @@ -1,7 +1,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from core.apps.shared.models import BaseModel +from core.apps.shared.models import BaseModel, Region, District from core.apps.projects.models.builder import Builder from core.apps.accounts.models.user import User from core.apps.wherehouse.models.wherehouse import WhereHouse @@ -27,6 +27,23 @@ class ProjectFolder(BaseModel): verbose_name_plural = _('Loyiha papkalari') +class ProjectLocation(BaseModel): + address = models.CharField(max_length=200) + region = models.ForeignKey(Region, on_delete=models.CASCADE, related_name='project_locations') + district = models.ForeignKey( + District, on_delete=models.CASCADE, related_name='project_locations' + ) + latitude = models.FloatField() + longitude = models.FloatField() + + def __str__(self): + return self.address + + class Meta: + verbose_name = _("Loyiha lokatsiyasi") + verbose_name_plural = _("Loyiha lokatsiyalari") + + class Project(BaseModel): STATUS = ( ('PLANNED', 'planned'), @@ -36,7 +53,9 @@ class Project(BaseModel): ) name = models.CharField(max_length=200) - location = models.CharField(max_length=200) + location = models.ForeignKey( + ProjectLocation, on_delete=models.SET_NULL, null=True, related_name='projects' + ) start_date = models.DateField() end_date = models.DateField() folder = models.ForeignKey( diff --git a/core/apps/projects/serializers/project.py b/core/apps/projects/serializers/project.py index e6b36f6..ff6e797 100644 --- a/core/apps/projects/serializers/project.py +++ b/core/apps/projects/serializers/project.py @@ -2,10 +2,13 @@ from django.db import transaction from rest_framework import serializers -from core.apps.projects.models.project import Project, ProjectFolder +from core.apps.projects.models.project import Project, ProjectFolder, ProjectLocation +from core.apps.projects.serializers.project_location import ProjectLocationSerializer, ProjectLocationListSerializer class ProjectListSerializer(serializers.ModelSerializer): + location = ProjectLocationListSerializer() + class Meta: model = Project fields = [ @@ -14,6 +17,8 @@ class ProjectListSerializer(serializers.ModelSerializer): class ProjectDetailSerialzier(serializers.ModelSerializer): + location = ProjectLocationListSerializer() + class Meta: model = Project fields = [ @@ -21,8 +26,33 @@ class ProjectDetailSerialzier(serializers.ModelSerializer): ] +class ProjectUpdateSerialzier(serializers.ModelSerializer): + location = ProjectLocationSerializer() + + class Meta: + model = Project + fields = [ + 'id', 'name', 'location', 'start_date', 'end_date', 'status', 'benifit_plan' + ] + + def update(self, instance, validated_data): + location = validated_data.get('location') + instance.name = validated_data.get('name', instance.name) + instance.start_date = validated_data.get('start_date', instance.start_date) + instance.end_date = validated_data.get('end_date', instance.end_date) + instance.status = validated_data.get('name', instance.status) + instance.location.region = location.get('region', instance.location.region) + instance.location.district = location.get('district', instance.location.district) + instance.location.longitude = location.get('longitude', instance.location.longitude) + instance.location.latitude = location.get('latitude', instance.location.latitude) + instance.location.address = location.get('address', instance.location.address) + instance.location.save() + instance.save() + return instance + + class ProjectCreateSerializer(serializers.Serializer): - location = serializers.CharField() + location = ProjectLocationSerializer() start_date = serializers.DateField() end_date = serializers.DateField() name = serializers.CharField() @@ -48,11 +78,20 @@ class ProjectCreateSerializer(serializers.Serializer): builder_id = validated_data.pop('builder_id') with transaction.atomic(): + location_data = validated_data.get('location') + location = ProjectLocation.objects.create( + address=location_data.get('address'), + region=location_data.get('region'), + district=location_data.get('district'), + latitude=location_data.get('latitude'), + longitude=location_data.get('longitude'), + ) + project = Project.objects.create( name=validated_data.get('name'), start_date=validated_data.get('start_date'), end_date=validated_data.get('end_date'), - location=validated_data.get('location'), + location=location, area=validated_data.get('area'), currency=validated_data.get('currency'), benifit_plan=validated_data.get('benifit_plan'), @@ -89,10 +128,22 @@ class ProjectFolderListSerializer(serializers.ModelSerializer): class ProjectFolderProjectCreateSerializer(serializers.Serializer): folder_id = serializers.UUIDField() - name = serializers.CharField() - location = serializers.CharField() + location = ProjectLocationSerializer() start_date = serializers.DateField() end_date = serializers.DateField() + name = serializers.CharField() + + builder_id = serializers.UUIDField() + area = serializers.IntegerField() + + boss = serializers.ListSerializer(child=serializers.UUIDField()) + foreman = serializers.ListSerializer(child=serializers.UUIDField()) + other_members = serializers.ListSerializer(child=serializers.UUIDField()) + + wherehouse = serializers.ListSerializer(child=serializers.UUIDField()) + cash_transaction = serializers.ListSerializer(child=serializers.UUIDField()) + currency = serializers.ChoiceField(choices=[('uzs', 'uzs'), ('usd', 'usd')]) + benifit_plan = serializers.IntegerField() def validate(self, data): folder = ProjectFolder.objects.filter(id=data['folder_id']).first() @@ -102,14 +153,42 @@ class ProjectFolderProjectCreateSerializer(serializers.Serializer): return data def create(self, validated_data): + boss_ids = validated_data.pop('boss') + foreman_ids = validated_data.pop('foreman') + other_member_ids = validated_data.pop('other_members') + warehouse_ids = validated_data.pop('wherehouse') + cash_transaction_ids = validated_data.pop('cash_transaction') + builder_id = validated_data.pop('builder_id') + with transaction.atomic(): - return Project.objects.create( - name=validated_data.get('name'), - folder=validated_data.get('folder'), - location=validated_data.get('location'), - start_date=validated_data.get('start_date'), - end_date=validated_data.get('end_date') + location_data = validated_data.get('location') + location = ProjectLocation.objects.create( + address=location_data.get('address'), + region=location_data.get('region'), + district=location_data.get('district'), + latitude=location_data.get('latitude'), + longitude=location_data.get('longitude'), ) + + project = Project.objects.create( + name=validated_data.get('name'), + start_date=validated_data.get('start_date'), + end_date=validated_data.get('end_date'), + location=location, + area=validated_data.get('area'), + currency=validated_data.get('currency'), + benifit_plan=validated_data.get('benifit_plan'), + builder_id=builder_id, + folder=validated_data.get('folder') + ) + + project.boss.set(boss_ids) + project.foreman.set(foreman_ids) + project.other_members.set(other_member_ids) + project.wherehouse.set(warehouse_ids) + project.cash_transaction.set(cash_transaction_ids) + + return project class ProjectFolderUpdateSerializer(serializers.ModelSerializer): diff --git a/core/apps/projects/serializers/project_location.py b/core/apps/projects/serializers/project_location.py new file mode 100644 index 0000000..b9cf10c --- /dev/null +++ b/core/apps/projects/serializers/project_location.py @@ -0,0 +1,49 @@ +from rest_framework import serializers + +from core.apps.shared.models import Region, District +from core.apps.projects.models import ProjectLocation + + +class ProjectLocationSerializer(serializers.Serializer): + address = serializers.CharField() + region_id = serializers.UUIDField() + district_id = serializers.UUIDField() + longitude = serializers.FloatField() + latitude = serializers.FloatField() + + def validate(self, data): + if data.get('region_id'): + region = Region.objects.filter(id=data.get('region_id')).first() + if not region: + raise serializers.ValidationError("Region not found") + data['region'] = region + if data.get('district_id'): + district = District.objects.filter(id=data['district_id']).first() + if not district: + raise serializers.ValidationError("District not found") + data['district'] = district + return data + + +class ProjectLocationListSerializer(serializers.ModelSerializer): + region = serializers.SerializerMethodField(method_name='get_region') + district = serializers.SerializerMethodField(method_name='get_district') + + class Meta: + model = ProjectLocation + fields = [ + 'id', 'address', 'latitude', 'longitude', 'region', 'district' + ] + + def get_region(self, obj): + return { + 'id': obj.region.id, + 'name': obj.region.name + } + + + def get_district(self, obj): + return { + 'id': obj.district.id, + 'name': obj.district.name + } \ No newline at end of file diff --git a/core/apps/projects/views/project.py b/core/apps/projects/views/project.py index 3a57c99..9306cb3 100644 --- a/core/apps/projects/views/project.py +++ b/core/apps/projects/views/project.py @@ -11,7 +11,7 @@ from core.apps.shared.paginations.custom import CustomPageNumberPagination class ProjectListApiView(generics.ListAPIView): serializer_class = serializers.ProjectListSerializer - queryset = Project.objects.all() + queryset = Project.objects.select_related('location') permission_classes = [HasRolePermission] required_permissions = ['project'] pagination_class = CustomPageNumberPagination @@ -22,7 +22,7 @@ class ProjectListApiView(generics.ListAPIView): class ProjectDetailApiView(generics.RetrieveAPIView): serializer_class = serializers.ProjectDetailSerialzier - queryset = Project.objects.all() + queryset = Project.objects.select_related('location') permission_classes = [HasRolePermission] required_permissions = ['project'] lookup_field = 'id' @@ -36,7 +36,7 @@ class ProjectCreateApiView(generics.CreateAPIView): class ProjectUpdateApiView(generics.UpdateAPIView): - serializer_class = serializers.ProjectDetailSerialzier + serializer_class = serializers.ProjectUpdateSerialzier queryset = Project.objects.all() permission_classes = [HasRolePermission] required_permissions = ['project'] diff --git a/core/apps/shared/admin/__init__.py b/core/apps/shared/admin/__init__.py index e69de29..08c1add 100644 --- a/core/apps/shared/admin/__init__.py +++ b/core/apps/shared/admin/__init__.py @@ -0,0 +1 @@ +from .region import * \ No newline at end of file diff --git a/core/apps/shared/admin/region.py b/core/apps/shared/admin/region.py new file mode 100644 index 0000000..0212973 --- /dev/null +++ b/core/apps/shared/admin/region.py @@ -0,0 +1,23 @@ +from django.contrib import admin + +from core.apps.shared.models import Region, District + + +class DistrictInline(admin.TabularInline): + model = District + extra = 0 + show_change_link = True + show_full_result_count = True + + +@admin.register(Region) +class ReginAdmin(admin.ModelAdmin): + list_display = ['name'] + search_fields = ['name'] + inlines = [DistrictInline] + + +@admin.register(District) +class DistrictAdmin(admin.ModelAdmin): + list_display = ['name'] + search_fields = ['name'] \ No newline at end of file diff --git a/core/apps/shared/apps.py b/core/apps/shared/apps.py index ca9e94d..a63ca4e 100644 --- a/core/apps/shared/apps.py +++ b/core/apps/shared/apps.py @@ -4,3 +4,7 @@ from django.apps import AppConfig class SharedConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'core.apps.shared' + + def ready(self): + from . import admin + \ No newline at end of file diff --git a/core/apps/shared/migrations/0001_initial.py b/core/apps/shared/migrations/0001_initial.py new file mode 100644 index 0000000..88659fe --- /dev/null +++ b/core/apps/shared/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.4 on 2025-08-07 11:39 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Region', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=200)), + ], + options={ + 'verbose_name': 'Mintaqa', + 'verbose_name_plural': 'Mintaqalar', + }, + ), + migrations.CreateModel( + name='District', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=200)), + ('region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='districts', to='shared.region')), + ], + options={ + 'verbose_name': 'Tuman', + 'verbose_name_plural': 'Tumanlar', + }, + ), + ] diff --git a/core/apps/shared/models/__init__.py b/core/apps/shared/models/__init__.py index 97953db..d9458e2 100644 --- a/core/apps/shared/models/__init__.py +++ b/core/apps/shared/models/__init__.py @@ -1 +1,2 @@ -from .base import BaseModel \ No newline at end of file +from .base import BaseModel +from .region import * \ No newline at end of file diff --git a/core/apps/shared/models/region.py b/core/apps/shared/models/region.py new file mode 100644 index 0000000..cff6239 --- /dev/null +++ b/core/apps/shared/models/region.py @@ -0,0 +1,27 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from core.apps.shared.models import BaseModel + + +class Region(BaseModel): + name = models.CharField(max_length=200) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("Mintaqa") + verbose_name_plural = _("Mintaqalar") + + +class District(BaseModel): + name = models.CharField(max_length=200) + region = models.ForeignKey(Region, on_delete=models.CASCADE, related_name='districts') + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("Tuman") + verbose_name_plural = _("Tumanlar") \ No newline at end of file diff --git a/core/apps/shared/serializers/region.py b/core/apps/shared/serializers/region.py new file mode 100644 index 0000000..00f9479 --- /dev/null +++ b/core/apps/shared/serializers/region.py @@ -0,0 +1,19 @@ +from rest_framework import serializers + +from core.apps.shared.models import Region, District + + +class DistrictListSerializer(serializers.ModelSerializer): + class Meta: + model = District + fields = ['id', 'name'] + + +class RegionListSerializer(serializers.ModelSerializer): + districts = DistrictListSerializer(many=True) + + class Meta: + model = Region + fields = [ + 'id', 'name', 'districts' + ] \ No newline at end of file diff --git a/core/apps/shared/urls.py b/core/apps/shared/urls.py index fe35695..308a978 100644 --- a/core/apps/shared/urls.py +++ b/core/apps/shared/urls.py @@ -1,5 +1,7 @@ from django.urls import path, include +from core.apps.shared.views import region as region_views urlpatterns = [ + path('region/list/', region_views.RegionListApiView.as_view()), ] \ No newline at end of file diff --git a/core/apps/shared/views/region.py b/core/apps/shared/views/region.py new file mode 100644 index 0000000..e2850df --- /dev/null +++ b/core/apps/shared/views/region.py @@ -0,0 +1,13 @@ +from rest_framework import generics + +from core.apps.shared.serializers.region import RegionListSerializer +from core.apps.shared.models import Region +from core.apps.accounts.permissions.permissions import HasRolePermission + + + +class RegionListApiView(generics.ListAPIView): + permission_classes = [HasRolePermission] + queryset = Region.objects.prefetch_related('districts') + serializer_class = RegionListSerializer + required_permissions = ['project', 'project_folder'] \ No newline at end of file