From 379803724081248d8a68ae7181d7bc9dec862039 Mon Sep 17 00:00:00 2001 From: Husanjonazamov Date: Mon, 9 Mar 2026 16:04:15 +0500 Subject: [PATCH] feat: add ReferenceItem model and update QuickEvaluation FKs - Create ReferenceitemModel with type, name, parent (self FK), order, is_active fields - Add ReferenceType choices: brand, marka, color, fuel_type, body_type, car_position, state_car - Implement ReferenceItem API (list, retrieve) with filter by type/parent/is_active, search, ordering - Add ReferenceItem admin with list_filter, search, inline editing - Change QuickEvaluation FK fields from shared.OptionsModel to evaluation.ReferenceitemModel - Update serializers and views to use .name instead of .key - Add ReferenceItem to unfold admin navigation --- config/conf/navigation.py | 11 ++ core/apps/evaluation/admin/__init__.py | 1 + core/apps/evaluation/admin/quick.py | 34 ++--- core/apps/evaluation/admin/reference.py | 18 +++ core/apps/evaluation/choices/quick.py | 6 - core/apps/evaluation/choices/reference.py | 12 ++ core/apps/evaluation/filters/__init__.py | 1 + core/apps/evaluation/filters/quick.py | 6 +- core/apps/evaluation/filters/reference.py | 17 +++ core/apps/evaluation/forms/__init__.py | 1 + core/apps/evaluation/forms/reference.py | 10 ++ ...quickevaluationmodel_condition_and_more.py | 109 ++++++++++++++ .../migrations/0013_referenceitemmodel.py | 33 +++++ ...quickevaluationmodel_body_type_and_more.py | 49 +++++++ core/apps/evaluation/models/__init__.py | 1 + core/apps/evaluation/models/quick.py | 138 ++++++++++++------ core/apps/evaluation/models/reference.py | 38 +++++ core/apps/evaluation/permissions/__init__.py | 1 + core/apps/evaluation/permissions/reference.py | 12 ++ core/apps/evaluation/serializers/__init__.py | 1 + .../serializers/quick/QuickEvaluation.py | 106 ++++++++++---- .../serializers/reference/ReferenceItem.py | 48 ++++++ .../serializers/reference/__init__.py | 1 + core/apps/evaluation/signals/__init__.py | 1 + core/apps/evaluation/signals/reference.py | 8 + core/apps/evaluation/tests/__init__.py | 1 + .../evaluation/tests/reference/__init__.py | 1 + .../tests/reference/test_ReferenceItem.py | 101 +++++++++++++ core/apps/evaluation/translation/__init__.py | 1 + core/apps/evaluation/translation/reference.py | 8 + core/apps/evaluation/urls.py | 2 + core/apps/evaluation/validators/__init__.py | 1 + core/apps/evaluation/validators/reference.py | 8 + core/apps/evaluation/views/__init__.py | 1 + core/apps/evaluation/views/quick.py | 29 ++-- core/apps/evaluation/views/reference.py | 34 +++++ 36 files changed, 744 insertions(+), 106 deletions(-) create mode 100644 core/apps/evaluation/admin/reference.py create mode 100644 core/apps/evaluation/choices/reference.py create mode 100644 core/apps/evaluation/filters/reference.py create mode 100644 core/apps/evaluation/forms/reference.py create mode 100644 core/apps/evaluation/migrations/0012_remove_quickevaluationmodel_condition_and_more.py create mode 100644 core/apps/evaluation/migrations/0013_referenceitemmodel.py create mode 100644 core/apps/evaluation/migrations/0014_alter_quickevaluationmodel_body_type_and_more.py create mode 100644 core/apps/evaluation/models/reference.py create mode 100644 core/apps/evaluation/permissions/reference.py create mode 100644 core/apps/evaluation/serializers/reference/ReferenceItem.py create mode 100644 core/apps/evaluation/serializers/reference/__init__.py create mode 100644 core/apps/evaluation/signals/reference.py create mode 100644 core/apps/evaluation/tests/reference/__init__.py create mode 100644 core/apps/evaluation/tests/reference/test_ReferenceItem.py create mode 100644 core/apps/evaluation/translation/reference.py create mode 100644 core/apps/evaluation/validators/reference.py create mode 100644 core/apps/evaluation/views/reference.py diff --git a/config/conf/navigation.py b/config/conf/navigation.py index 302be5a..2ec00e5 100644 --- a/config/conf/navigation.py +++ b/config/conf/navigation.py @@ -107,4 +107,15 @@ PAGES = [ }, ], }, + { + "title": _("Ma'lumotnomalari"), + "separator": True, + "items": [ + { + "title": _("Ma'lumotnomalar"), + "icon": "category", + "link": reverse_lazy("admin:evaluation_referenceitemmodel_changelist"), + }, + ], + }, ] diff --git a/core/apps/evaluation/admin/__init__.py b/core/apps/evaluation/admin/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/admin/__init__.py +++ b/core/apps/evaluation/admin/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/admin/quick.py b/core/apps/evaluation/admin/quick.py index 62a9b79..67fcce0 100644 --- a/core/apps/evaluation/admin/quick.py +++ b/core/apps/evaluation/admin/quick.py @@ -10,10 +10,9 @@ class QuickEvaluationAdmin(ModelAdmin): "id", "created_by", "brand", - "model", - "license_plate", - "manufacture_year", - "condition", + "marka", + "car_number", + "car_manufactured_date", "estimated_price", "status", "car_type", @@ -21,20 +20,14 @@ class QuickEvaluationAdmin(ModelAdmin): "created_at", ) list_filter = ( - "fuel_type", - "body_type", - "condition", "status", "car_type", - "state_car", ) search_fields = ( - "brand", - "model", - "license_plate", + "car_number", "vin_number", "engine_number", - "tech_passport_number", + "tex_passport_serie_num", ) readonly_fields = ("created_at", "updated_at") autocomplete_fields = ("created_by",) @@ -42,20 +35,25 @@ class QuickEvaluationAdmin(ModelAdmin): ("Foydalanuvchi", { "fields": ("created_by",), }), + ("Tex passport", { + "fields": ( + "tex_passport_serie_num", + ("tech_passport_issued_date", "tech_passport_issued_place"), + "tex_passport_file", + ), + }), ("Transport ma'lumotlari", { "fields": ( - "tech_passport_number", - "license_plate", - ("brand", "model"), - ("manufacture_year", "color"), + "car_number", + ("brand", "marka"), + ("car_manufactured_date", "color"), ("vin_number", "engine_number"), - "mileage", + ("distance_covered", "car_position"), ), }), ("Texnik holat", { "fields": ( ("fuel_type", "body_type"), - "condition", ("car_type", "state_car"), ), }), diff --git a/core/apps/evaluation/admin/reference.py b/core/apps/evaluation/admin/reference.py new file mode 100644 index 0000000..edccd38 --- /dev/null +++ b/core/apps/evaluation/admin/reference.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from unfold.admin import ModelAdmin + +from core.apps.evaluation.models import ReferenceitemModel + + +@admin.register(ReferenceitemModel) +class ReferenceitemAdmin(ModelAdmin): + list_display = ("id", "type", "name", "parent", "order", "is_active") + list_filter = ("type", "is_active") + search_fields = ("name",) + list_editable = ("order", "is_active") + autocomplete_fields = ("parent",) + fieldsets = ( + (None, { + "fields": ("type", "name", "parent", "order", "is_active"), + }), + ) diff --git a/core/apps/evaluation/choices/quick.py b/core/apps/evaluation/choices/quick.py index cab9cd0..f400d46 100644 --- a/core/apps/evaluation/choices/quick.py +++ b/core/apps/evaluation/choices/quick.py @@ -15,9 +15,3 @@ class CarType(models.TextChoices): TRUCK = "truck", _("Truck") BUS = "bus", _("Bus") MOTO = "moto", _("Moto") - - -class CarState(models.TextChoices): - GOOD = "good", _("Good") - SATISFACTORY = "satisfactory", _("Satisfactory") - BAD = "bad", _("Bad") diff --git a/core/apps/evaluation/choices/reference.py b/core/apps/evaluation/choices/reference.py new file mode 100644 index 0000000..a538f69 --- /dev/null +++ b/core/apps/evaluation/choices/reference.py @@ -0,0 +1,12 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class ReferenceType(models.TextChoices): + BRAND = "brand", _("Brand") + MARKA = "marka", _("Marka") + COLOR = "color", _("Color") + FUEL_TYPE = "fuel_type", _("Fuel type") + BODY_TYPE = "body_type", _("Body type") + CAR_POSITION = "car_position", _("Car position") + STATE_CAR = "state_car", _("Car state") diff --git a/core/apps/evaluation/filters/__init__.py b/core/apps/evaluation/filters/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/filters/__init__.py +++ b/core/apps/evaluation/filters/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/filters/quick.py b/core/apps/evaluation/filters/quick.py index c0151cd..11efa58 100644 --- a/core/apps/evaluation/filters/quick.py +++ b/core/apps/evaluation/filters/quick.py @@ -6,11 +6,11 @@ from core.apps.evaluation.models import QuickEvaluationModel class QuickevaluationFilter(filters.FilterSet): status = filters.CharFilter(method="filter_status") car_type = filters.CharFilter(field_name="car_type", lookup_expr="exact") - state_car = filters.CharFilter(field_name="state_car", lookup_expr="exact") + state_car = filters.NumberFilter(field_name="state_car", lookup_expr="exact") created_from = filters.DateFilter(field_name="created_at", lookup_expr="gte") created_to = filters.DateFilter(field_name="created_at", lookup_expr="lte") - year_from = filters.NumberFilter(field_name="manufacture_year", lookup_expr="gte") - year_to = filters.NumberFilter(field_name="manufacture_year", lookup_expr="lte") + year_from = filters.NumberFilter(field_name="car_manufactured_date", lookup_expr="gte") + year_to = filters.NumberFilter(field_name="car_manufactured_date", lookup_expr="lte") def filter_status(self, queryset, name, value): if value: diff --git a/core/apps/evaluation/filters/reference.py b/core/apps/evaluation/filters/reference.py new file mode 100644 index 0000000..18abc92 --- /dev/null +++ b/core/apps/evaluation/filters/reference.py @@ -0,0 +1,17 @@ +from django_filters import rest_framework as filters + +from core.apps.evaluation.models import ReferenceitemModel + + +class ReferenceitemFilter(filters.FilterSet): + type = filters.CharFilter(field_name="type", lookup_expr="exact") + parent = filters.NumberFilter(field_name="parent", lookup_expr="exact") + is_active = filters.BooleanFilter(field_name="is_active") + + class Meta: + model = ReferenceitemModel + fields = [ + "type", + "parent", + "is_active", + ] diff --git a/core/apps/evaluation/forms/__init__.py b/core/apps/evaluation/forms/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/forms/__init__.py +++ b/core/apps/evaluation/forms/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/forms/reference.py b/core/apps/evaluation/forms/reference.py new file mode 100644 index 0000000..97eeb33 --- /dev/null +++ b/core/apps/evaluation/forms/reference.py @@ -0,0 +1,10 @@ +from django import forms + +from core.apps.evaluation.models import ReferenceitemModel + + +class ReferenceitemForm(forms.ModelForm): + + class Meta: + model = ReferenceitemModel + fields = "__all__" diff --git a/core/apps/evaluation/migrations/0012_remove_quickevaluationmodel_condition_and_more.py b/core/apps/evaluation/migrations/0012_remove_quickevaluationmodel_condition_and_more.py new file mode 100644 index 0000000..c951d4a --- /dev/null +++ b/core/apps/evaluation/migrations/0012_remove_quickevaluationmodel_condition_and_more.py @@ -0,0 +1,109 @@ +# Generated by Django 5.2.7 on 2026-03-09 09:57 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0011_update_choices_to_english'), + ('shared', '0002_settingsmodel_created_at_settingsmodel_description_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='quickevaluationmodel', + name='condition', + ), + migrations.RemoveField( + model_name='quickevaluationmodel', + name='license_plate', + ), + migrations.RemoveField( + model_name='quickevaluationmodel', + name='manufacture_year', + ), + migrations.RemoveField( + model_name='quickevaluationmodel', + name='mileage', + ), + migrations.RemoveField( + model_name='quickevaluationmodel', + name='model', + ), + migrations.RemoveField( + model_name='quickevaluationmodel', + name='tech_passport_number', + ), + migrations.AddField( + model_name='quickevaluationmodel', + name='car_manufactured_date', + field=models.IntegerField(blank=True, null=True, verbose_name='manufacture year'), + ), + migrations.AddField( + model_name='quickevaluationmodel', + name='car_number', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='car number'), + ), + migrations.AddField( + model_name='quickevaluationmodel', + name='car_position', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_positions', to='shared.optionsmodel', verbose_name='car position'), + ), + migrations.AddField( + model_name='quickevaluationmodel', + name='distance_covered', + field=models.IntegerField(blank=True, null=True, verbose_name='distance covered (km)'), + ), + migrations.AddField( + model_name='quickevaluationmodel', + name='marka', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_markas', to='shared.optionsmodel', verbose_name='marka'), + ), + migrations.AddField( + model_name='quickevaluationmodel', + name='tech_passport_issued_date', + field=models.DateField(blank=True, null=True, verbose_name='tech passport issued date'), + ), + migrations.AddField( + model_name='quickevaluationmodel', + name='tech_passport_issued_place', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='tech passport issued place'), + ), + migrations.AddField( + model_name='quickevaluationmodel', + name='tex_passport_file', + field=models.FileField(blank=True, null=True, upload_to='quick_evaluation/tech_passports/%Y/%m/', verbose_name='tech passport file'), + ), + migrations.AddField( + model_name='quickevaluationmodel', + name='tex_passport_serie_num', + field=models.CharField(blank=True, max_length=20, null=True, verbose_name='tech passport series and number'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='body_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_body_types', to='shared.optionsmodel', verbose_name='body type'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='brand', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_brands', to='shared.optionsmodel', verbose_name='brand'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='color', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_colors', to='shared.optionsmodel', verbose_name='color'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='fuel_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_fuel_types', to='shared.optionsmodel', verbose_name='fuel type'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='state_car', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_states', to='shared.optionsmodel', verbose_name='car state'), + ), + ] diff --git a/core/apps/evaluation/migrations/0013_referenceitemmodel.py b/core/apps/evaluation/migrations/0013_referenceitemmodel.py new file mode 100644 index 0000000..6b990ae --- /dev/null +++ b/core/apps/evaluation/migrations/0013_referenceitemmodel.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2026-03-09 10:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0012_remove_quickevaluationmodel_condition_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ReferenceitemModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('type', models.CharField(choices=[('brand', 'Brand'), ('marka', 'Marka'), ('color', 'Color'), ('fuel_type', 'Fuel type'), ('body_type', 'Body type'), ('car_position', 'Car position'), ('state_car', 'Car state')], max_length=50, verbose_name='type')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('order', models.IntegerField(default=0, verbose_name='order')), + ('is_active', models.BooleanField(default=True, verbose_name='is active')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='evaluation.referenceitemmodel', verbose_name='parent')), + ], + options={ + 'verbose_name': 'Reference Item', + 'verbose_name_plural': 'Reference Items', + 'db_table': 'ReferenceItem', + 'ordering': ['type', 'order', 'name'], + }, + ), + ] diff --git a/core/apps/evaluation/migrations/0014_alter_quickevaluationmodel_body_type_and_more.py b/core/apps/evaluation/migrations/0014_alter_quickevaluationmodel_body_type_and_more.py new file mode 100644 index 0000000..95c742b --- /dev/null +++ b/core/apps/evaluation/migrations/0014_alter_quickevaluationmodel_body_type_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 5.2.7 on 2026-03-09 10:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0013_referenceitemmodel'), + ] + + operations = [ + migrations.AlterField( + model_name='quickevaluationmodel', + name='body_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_body_types', to='evaluation.referenceitemmodel', verbose_name='body type'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='brand', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_brands', to='evaluation.referenceitemmodel', verbose_name='brand'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='car_position', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_positions', to='evaluation.referenceitemmodel', verbose_name='car position'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='color', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_colors', to='evaluation.referenceitemmodel', verbose_name='color'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='fuel_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_fuel_types', to='evaluation.referenceitemmodel', verbose_name='fuel type'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='marka', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_markas', to='evaluation.referenceitemmodel', verbose_name='marka'), + ), + migrations.AlterField( + model_name='quickevaluationmodel', + name='state_car', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quick_eval_states', to='evaluation.referenceitemmodel', verbose_name='car state'), + ), + ] diff --git a/core/apps/evaluation/models/__init__.py b/core/apps/evaluation/models/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/models/__init__.py +++ b/core/apps/evaluation/models/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/models/quick.py b/core/apps/evaluation/models/quick.py index 5824293..997667a 100644 --- a/core/apps/evaluation/models/quick.py +++ b/core/apps/evaluation/models/quick.py @@ -3,8 +3,7 @@ from django.utils.translation import gettext_lazy as _ from django_core.models import AbstractBaseModel from model_bakery import baker -from core.apps.evaluation.choices.quick import CarState, CarType, QuickEvaluationStatus -from core.apps.evaluation.choices.vehicle import BodyType, FuelType, VehicleCondition +from core.apps.evaluation.choices.quick import CarType, QuickEvaluationStatus class QuickEvaluationModel(AbstractBaseModel): @@ -16,46 +15,115 @@ class QuickEvaluationModel(AbstractBaseModel): related_name="quick_evaluations", verbose_name=_("created by"), ) - tech_passport_number = models.CharField( - verbose_name=_("tech passport number"), max_length=50, blank=True, null=True + + # Tex passport + tex_passport_serie_num = models.CharField( + verbose_name=_("tech passport series and number"), + max_length=20, + blank=True, + null=True, ) - license_plate = models.CharField( - verbose_name=_("license plate"), max_length=20, blank=True, null=True + tech_passport_issued_date = models.DateField( + verbose_name=_("tech passport issued date"), + blank=True, + null=True, ) - model = models.CharField(verbose_name=_("model"), max_length=255, blank=True, null=True) - brand = models.CharField(verbose_name=_("brand"), max_length=255, blank=True, null=True) - manufacture_year = models.IntegerField( - verbose_name=_("manufacture year"), blank=True, null=True + tech_passport_issued_place = models.CharField( + verbose_name=_("tech passport issued place"), + max_length=255, + blank=True, + null=True, + ) + tex_passport_file = models.FileField( + verbose_name=_("tech passport file"), + upload_to="quick_evaluation/tech_passports/%Y/%m/", + blank=True, + null=True, + ) + + # Car info + car_type = models.CharField( + verbose_name=_("car type"), + max_length=50, + choices=CarType.choices, + blank=True, + null=True, + ) + brand = models.ForeignKey( + "evaluation.ReferenceitemModel", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="quick_eval_brands", + verbose_name=_("brand"), + ) + marka = models.ForeignKey( + "evaluation.ReferenceitemModel", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="quick_eval_markas", + verbose_name=_("marka"), + ) + car_position = models.ForeignKey( + "evaluation.ReferenceitemModel", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="quick_eval_positions", + verbose_name=_("car position"), + ) + distance_covered = models.IntegerField( + verbose_name=_("distance covered (km)"), + blank=True, + null=True, + ) + body_type = models.ForeignKey( + "evaluation.ReferenceitemModel", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="quick_eval_body_types", + verbose_name=_("body type"), ) - mileage = models.IntegerField(verbose_name=_("mileage"), blank=True, null=True) vin_number = models.CharField( verbose_name=_("VIN number"), max_length=50, blank=True, null=True ) + car_number = models.CharField( + verbose_name=_("car number"), max_length=20, blank=True, null=True + ) + car_manufactured_date = models.IntegerField( + verbose_name=_("manufacture year"), blank=True, null=True + ) engine_number = models.CharField( verbose_name=_("engine number"), max_length=50, blank=True, null=True ) - color = models.CharField(verbose_name=_("color"), max_length=50, blank=True, null=True) - fuel_type = models.CharField( + color = models.ForeignKey( + "evaluation.ReferenceitemModel", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="quick_eval_colors", + verbose_name=_("color"), + ) + fuel_type = models.ForeignKey( + "evaluation.ReferenceitemModel", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="quick_eval_fuel_types", verbose_name=_("fuel type"), - max_length=50, - choices=FuelType.choices, - blank=True, - null=True, ) - body_type = models.CharField( - verbose_name=_("body type"), - max_length=50, - choices=BodyType.choices, - blank=True, + state_car = models.ForeignKey( + "evaluation.ReferenceitemModel", + on_delete=models.SET_NULL, null=True, - ) - condition = models.CharField( - verbose_name=_("condition"), - max_length=50, - choices=VehicleCondition.choices, blank=True, - null=True, + related_name="quick_eval_states", + verbose_name=_("car state"), ) + + # Result estimated_price = models.DecimalField( verbose_name=_("estimated price"), max_digits=15, @@ -69,20 +137,6 @@ class QuickEvaluationModel(AbstractBaseModel): choices=QuickEvaluationStatus.choices, default=QuickEvaluationStatus.CREATED, ) - car_type = models.CharField( - verbose_name=_("car type"), - max_length=50, - choices=CarType.choices, - blank=True, - null=True, - ) - state_car = models.CharField( - verbose_name=_("car state"), - max_length=50, - choices=CarState.choices, - blank=True, - null=True, - ) def __str__(self): return f"Quick Evaluation {self.pk} by {self.created_by}" diff --git a/core/apps/evaluation/models/reference.py b/core/apps/evaluation/models/reference.py new file mode 100644 index 0000000..f9cb949 --- /dev/null +++ b/core/apps/evaluation/models/reference.py @@ -0,0 +1,38 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from django_core.models import AbstractBaseModel +from model_bakery import baker + +from core.apps.evaluation.choices.reference import ReferenceType + + +class ReferenceitemModel(AbstractBaseModel): + type = models.CharField( + verbose_name=_("type"), + max_length=50, + choices=ReferenceType.choices, + ) + name = models.CharField(verbose_name=_("name"), max_length=255) + parent = models.ForeignKey( + "self", + on_delete=models.CASCADE, + null=True, + blank=True, + related_name="children", + verbose_name=_("parent"), + ) + order = models.IntegerField(verbose_name=_("order"), default=0) + is_active = models.BooleanField(verbose_name=_("is active"), default=True) + + def __str__(self): + return f"{self.get_type_display()}: {self.name}" + + @classmethod + def _baker(cls): + return baker.make(cls) + + class Meta: + db_table = "ReferenceItem" + verbose_name = _("Reference Item") + verbose_name_plural = _("Reference Items") + ordering = ["type", "order", "name"] diff --git a/core/apps/evaluation/permissions/__init__.py b/core/apps/evaluation/permissions/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/permissions/__init__.py +++ b/core/apps/evaluation/permissions/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/permissions/reference.py b/core/apps/evaluation/permissions/reference.py new file mode 100644 index 0000000..6818357 --- /dev/null +++ b/core/apps/evaluation/permissions/reference.py @@ -0,0 +1,12 @@ +from rest_framework import permissions + + +class ReferenceitemPermission(permissions.BasePermission): + + def __init__(self) -> None: ... + + def __call__(self, *args, **kwargs): + return self + + def has_permission(self, request, view): + return True diff --git a/core/apps/evaluation/serializers/__init__.py b/core/apps/evaluation/serializers/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/serializers/__init__.py +++ b/core/apps/evaluation/serializers/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/serializers/quick/QuickEvaluation.py b/core/apps/evaluation/serializers/quick/QuickEvaluation.py index 8eb557b..c207618 100644 --- a/core/apps/evaluation/serializers/quick/QuickEvaluation.py +++ b/core/apps/evaluation/serializers/quick/QuickEvaluation.py @@ -1,14 +1,21 @@ +import re + from rest_framework import serializers + from core.apps.evaluation.models import QuickEvaluationModel + class BaseQuickevaluationSerializer(serializers.ModelSerializer): - fuel_type_display = serializers.CharField(source="get_fuel_type_display", read_only=True) - body_type_display = serializers.CharField(source="get_body_type_display", read_only=True) - condition_display = serializers.CharField(source="get_condition_display", read_only=True) created_by_name = serializers.CharField(source="created_by.get_full_name", read_only=True) status_display = serializers.CharField(source="get_status_display", read_only=True) car_type_display = serializers.CharField(source="get_car_type_display", read_only=True) - state_car_display = serializers.CharField(source="get_state_car_display", read_only=True) + brand_name = serializers.CharField(source="brand.name", read_only=True, default=None) + marka_name = serializers.CharField(source="marka.name", read_only=True, default=None) + color_name = serializers.CharField(source="color.name", read_only=True, default=None) + fuel_type_name = serializers.CharField(source="fuel_type.name", read_only=True, default=None) + body_type_name = serializers.CharField(source="body_type.name", read_only=True, default=None) + state_car_name = serializers.CharField(source="state_car.name", read_only=True, default=None) + car_position_name = serializers.CharField(source="car_position.name", read_only=True, default=None) class Meta: model = QuickEvaluationModel @@ -17,55 +24,104 @@ class BaseQuickevaluationSerializer(serializers.ModelSerializer): "created_by", "created_by_name", "brand", - "model", - "license_plate", - "manufacture_year", + "brand_name", + "marka", + "marka_name", + "car_number", + "car_manufactured_date", "estimated_price", "status", "status_display", "car_type", "car_type_display", "state_car", - "state_car_display", + "state_car_name", "created_at", ] + class ListQuickevaluationSerializer(BaseQuickevaluationSerializer): class Meta(BaseQuickevaluationSerializer.Meta): pass + class RetrieveQuickevaluationSerializer(BaseQuickevaluationSerializer): class Meta(BaseQuickevaluationSerializer.Meta): fields = BaseQuickevaluationSerializer.Meta.fields + [ - "tech_passport_number", - "mileage", + "tex_passport_serie_num", + "tech_passport_issued_date", + "tech_passport_issued_place", + "tex_passport_file", + "car_position", + "car_position_name", + "distance_covered", + "body_type", + "body_type_name", "vin_number", "engine_number", "color", + "color_name", "fuel_type", - "fuel_type_display", - "body_type", - "body_type_display", - "condition", - "condition_display", + "fuel_type_name", "updated_at", ] -class CreateQuickevaluationSerializer(BaseQuickevaluationSerializer): - class Meta(BaseQuickevaluationSerializer.Meta): + +class CreateQuickevaluationSerializer(serializers.ModelSerializer): + class Meta: + model = QuickEvaluationModel fields = [ - "tech_passport_number", - "license_plate", - "model", + "tex_passport_serie_num", + "tech_passport_issued_date", + "tech_passport_issued_place", + "tex_passport_file", + "car_type", "brand", - "manufacture_year", - "mileage", + "marka", + "car_position", + "distance_covered", + "body_type", "vin_number", + "car_number", + "car_manufactured_date", "engine_number", "color", "fuel_type", - "body_type", - "condition", - "car_type", "state_car", ] + + def validate_tex_passport_serie_num(self, value): + if value and not re.match(r"^[A-Z]{3}\s?\d{7}$", value): + raise serializers.ValidationError( + "Format: AAA 1234567 (3 harf + 7 raqam)" + ) + return value + + def validate_vin_number(self, value): + if value and len(value) != 17: + raise serializers.ValidationError("VIN raqami 17 belgidan iborat bo'lishi kerak.") + return value + + def validate(self, attrs): + car_type = attrs.get("car_type") + if car_type == "lightweight": + if not attrs.get("distance_covered"): + raise serializers.ValidationError( + {"distance_covered": "car_type 'lightweight' uchun majburiy."} + ) + if not attrs.get("body_type"): + raise serializers.ValidationError( + {"body_type": "car_type 'lightweight' uchun majburiy."} + ) + if car_type == "truck": + if not attrs.get("vin_number"): + raise serializers.ValidationError( + {"vin_number": "car_type 'truck' uchun majburiy."} + ) + return attrs + + def create(self, validated_data): + request = self.context.get("request") + if request and request.user and request.user.is_authenticated: + validated_data["created_by"] = request.user + return super().create(validated_data) diff --git a/core/apps/evaluation/serializers/reference/ReferenceItem.py b/core/apps/evaluation/serializers/reference/ReferenceItem.py new file mode 100644 index 0000000..08d5d4c --- /dev/null +++ b/core/apps/evaluation/serializers/reference/ReferenceItem.py @@ -0,0 +1,48 @@ +from rest_framework import serializers + +from core.apps.evaluation.models import ReferenceitemModel + + +class BaseReferenceitemSerializer(serializers.ModelSerializer): + type_display = serializers.CharField(source="get_type_display", read_only=True) + parent_name = serializers.CharField(source="parent.name", read_only=True, default=None) + + class Meta: + model = ReferenceitemModel + fields = [ + "id", + "type", + "type_display", + "name", + "parent", + "parent_name", + "order", + "is_active", + ] + + +class ListReferenceitemSerializer(BaseReferenceitemSerializer): + class Meta(BaseReferenceitemSerializer.Meta): + pass + + +class RetrieveReferenceitemSerializer(BaseReferenceitemSerializer): + children = serializers.SerializerMethodField() + + class Meta(BaseReferenceitemSerializer.Meta): + fields = BaseReferenceitemSerializer.Meta.fields + ["children"] + + def get_children(self, obj): + children = obj.children.filter(is_active=True).order_by("order", "name") + return ListReferenceitemSerializer(children, many=True).data + + +class CreateReferenceitemSerializer(BaseReferenceitemSerializer): + class Meta(BaseReferenceitemSerializer.Meta): + fields = [ + "type", + "name", + "parent", + "order", + "is_active", + ] diff --git a/core/apps/evaluation/serializers/reference/__init__.py b/core/apps/evaluation/serializers/reference/__init__.py new file mode 100644 index 0000000..381dc9d --- /dev/null +++ b/core/apps/evaluation/serializers/reference/__init__.py @@ -0,0 +1 @@ +from .ReferenceItem import * # noqa diff --git a/core/apps/evaluation/signals/__init__.py b/core/apps/evaluation/signals/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/signals/__init__.py +++ b/core/apps/evaluation/signals/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/signals/reference.py b/core/apps/evaluation/signals/reference.py new file mode 100644 index 0000000..d1bd9bc --- /dev/null +++ b/core/apps/evaluation/signals/reference.py @@ -0,0 +1,8 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from core.apps.evaluation.models import ReferenceitemModel + + +@receiver(post_save, sender=ReferenceitemModel) +def ReferenceitemSignal(sender, instance, created, **kwargs): ... diff --git a/core/apps/evaluation/tests/__init__.py b/core/apps/evaluation/tests/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/tests/__init__.py +++ b/core/apps/evaluation/tests/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/tests/reference/__init__.py b/core/apps/evaluation/tests/reference/__init__.py new file mode 100644 index 0000000..ca9eb8f --- /dev/null +++ b/core/apps/evaluation/tests/reference/__init__.py @@ -0,0 +1 @@ +from .test_ReferenceItem import * # noqa diff --git a/core/apps/evaluation/tests/reference/test_ReferenceItem.py b/core/apps/evaluation/tests/reference/test_ReferenceItem.py new file mode 100644 index 0000000..e1df1aa --- /dev/null +++ b/core/apps/evaluation/tests/reference/test_ReferenceItem.py @@ -0,0 +1,101 @@ +import pytest +from django.urls import reverse +from rest_framework.test import APIClient + +from core.apps.evaluation.models import ReferenceitemModel + + +@pytest.fixture +def instance(db): + return ReferenceitemModel._baker() + + +@pytest.fixture +def api_client(instance): + client = APIClient() + ##client.force_authenticate(user=instance.user) + return client, instance + + +@pytest.fixture +def data(api_client): + client, instance = api_client + return ( + { + "list": reverse("ReferenceItem-list"), + "retrieve": reverse("ReferenceItem-detail", kwargs={"pk": instance.pk}), + "retrieve-not-found": reverse("ReferenceItem-detail", kwargs={"pk": 1000}), + }, + client, + instance, + ) + + +@pytest.mark.django_db +def test_list(data): + urls, client, _ = data + response = client.get(urls["list"]) + data_resp = response.json() + assert response.status_code == 200 + assert data_resp["status"] is True + + +@pytest.mark.django_db +def test_retrieve(data): + urls, client, _ = data + response = client.get(urls["retrieve"]) + data_resp = response.json() + assert response.status_code == 200 + assert data_resp["status"] is True + + +@pytest.mark.django_db +def test_retrieve_not_found(data): + urls, client, _ = data + response = client.get(urls["retrieve-not-found"]) + data_resp = response.json() + assert response.status_code == 404 + assert data_resp["status"] is False + + +# @pytest.mark.django_db +# def test_create(data): +# urls, client, _ = data +# response = client.post(urls["list"], data={"name": "test"}) +# assert response.json()["status"] is True +# assert response.status_code == 201 + + +# @pytest.mark.django_db +# def test_update(data): +# urls, client, _ = data +# response = client.patch(urls["retrieve"], data={"name": "updated"}) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# +# # verify updated value +# response = client.get(urls["retrieve"]) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# assert response.json()["data"]["name"] == "updated" + + +# @pytest.mark.django_db +# def test_partial_update(): +# urls, client, _ = data +# response = client.patch(urls["retrieve"], data={"name": "updated"}) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# +# # verify updated value +# response = client.get(urls["retrieve"]) +# assert response.json()["status"] is True +# assert response.status_code == 200 +# assert response.json()["data"]["name"] == "updated" + + +# @pytest.mark.django_db +# def test_destroy(data): +# urls, client, _ = data +# response = client.delete(urls["retrieve"]) +# assert response.status_code == 204 diff --git a/core/apps/evaluation/translation/__init__.py b/core/apps/evaluation/translation/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/translation/__init__.py +++ b/core/apps/evaluation/translation/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/translation/reference.py b/core/apps/evaluation/translation/reference.py new file mode 100644 index 0000000..69439f0 --- /dev/null +++ b/core/apps/evaluation/translation/reference.py @@ -0,0 +1,8 @@ +from modeltranslation.translator import TranslationOptions, register + +from core.apps.evaluation.models import ReferenceitemModel + + +@register(ReferenceitemModel) +class ReferenceitemTranslation(TranslationOptions): + fields = [] diff --git a/core/apps/evaluation/urls.py b/core/apps/evaluation/urls.py index 02cf61f..d31f5fb 100644 --- a/core/apps/evaluation/urls.py +++ b/core/apps/evaluation/urls.py @@ -9,12 +9,14 @@ from .views import ( PropertyOwnerView, QuickEvaluationView, RealEstateEvaluationView, + ReferenceitemView, ValuationDocumentView, ValuationView, VehicleView, ) router = DefaultRouter() +router.register("reference-item", ReferenceitemView, basename="reference-item") router.register("valuation-document", ValuationDocumentView, basename="valuation-document") router.register("evaluation-report", EvaluationReportView, basename="evaluation-report") router.register("quick-evaluation", QuickEvaluationView, basename="quick-evaluation") diff --git a/core/apps/evaluation/validators/__init__.py b/core/apps/evaluation/validators/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/validators/__init__.py +++ b/core/apps/evaluation/validators/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/validators/reference.py b/core/apps/evaluation/validators/reference.py new file mode 100644 index 0000000..7571acb --- /dev/null +++ b/core/apps/evaluation/validators/reference.py @@ -0,0 +1,8 @@ +# from django.core.exceptions import ValidationError + + +class ReferenceitemValidator: + def __init__(self): ... + + def __call__(self): + return True diff --git a/core/apps/evaluation/views/__init__.py b/core/apps/evaluation/views/__init__.py index c710df3..9b33385 100644 --- a/core/apps/evaluation/views/__init__.py +++ b/core/apps/evaluation/views/__init__.py @@ -4,6 +4,7 @@ from .document import * # noqa from .movable import * # noqa from .quick import * # noqa from .real_estate import * # noqa +from .reference import * # noqa from .report import * # noqa from .valuation import * # noqa from .vehicle import * # noqa diff --git a/core/apps/evaluation/views/quick.py b/core/apps/evaluation/views/quick.py index 7e62225..87a4e80 100644 --- a/core/apps/evaluation/views/quick.py +++ b/core/apps/evaluation/views/quick.py @@ -2,8 +2,9 @@ from django_core.mixins import BaseViewSetMixin from django_filters.rest_framework import DjangoFilterBackend from drf_spectacular.utils import extend_schema from rest_framework.filters import OrderingFilter, SearchFilter +from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.permissions import AllowAny -from rest_framework.viewsets import ReadOnlyModelViewSet +from rest_framework.viewsets import ModelViewSet from core.apps.evaluation.filters.quick import QuickevaluationFilter from core.apps.evaluation.models import QuickEvaluationModel @@ -15,27 +16,31 @@ from core.apps.evaluation.serializers.quick import ( @extend_schema(tags=["QuickEvaluation"]) -class QuickEvaluationView(BaseViewSetMixin, ReadOnlyModelViewSet): - queryset = QuickEvaluationModel.objects.select_related("created_by").all() +class QuickEvaluationView(BaseViewSetMixin, ModelViewSet): + queryset = QuickEvaluationModel.objects.select_related( + "created_by", "brand", "marka", "color", "fuel_type", + "body_type", "state_car", "car_position", + ).all() serializer_class = ListQuickevaluationSerializer permission_classes = [AllowAny] + parser_classes = [MultiPartParser, FormParser] filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_class = QuickevaluationFilter - search_fields = ["license_plate", "model", "brand"] + search_fields = ["car_number", "marka__name", "brand__name"] ordering_fields = [ "created_at", "updated_at", - "license_plate", - "brand", - "model", + "car_number", + "brand__name", + "marka__name", "car_type", - "manufacture_year", - "color", - "fuel_type", - "state_car", + "car_manufactured_date", + "color__name", + "fuel_type__name", + "state_car__name", "status", - "mileage", + "distance_covered", ] ordering = ["-created_at"] diff --git a/core/apps/evaluation/views/reference.py b/core/apps/evaluation/views/reference.py new file mode 100644 index 0000000..9893e32 --- /dev/null +++ b/core/apps/evaluation/views/reference.py @@ -0,0 +1,34 @@ +from django_core.mixins import BaseViewSetMixin +from django_filters.rest_framework import DjangoFilterBackend +from drf_spectacular.utils import extend_schema +from rest_framework.filters import OrderingFilter, SearchFilter +from rest_framework.permissions import AllowAny +from rest_framework.viewsets import ReadOnlyModelViewSet + +from core.apps.evaluation.filters.reference import ReferenceitemFilter +from core.apps.evaluation.models import ReferenceitemModel +from core.apps.evaluation.serializers.reference import ( + CreateReferenceitemSerializer, + ListReferenceitemSerializer, + RetrieveReferenceitemSerializer, +) + + +@extend_schema(tags=["ReferenceItem"]) +class ReferenceitemView(BaseViewSetMixin, ReadOnlyModelViewSet): + queryset = ReferenceitemModel.objects.select_related("parent").filter(is_active=True) + serializer_class = ListReferenceitemSerializer + permission_classes = [AllowAny] + + filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] + filterset_class = ReferenceitemFilter + search_fields = ["name"] + ordering_fields = ["name", "order", "type"] + ordering = ["order", "name"] + + action_permission_classes = {} + action_serializer_class = { + "list": ListReferenceitemSerializer, + "retrieve": RetrieveReferenceitemSerializer, + "create": CreateReferenceitemSerializer, + }