diff --git a/config/urls.py b/config/urls.py index d4b3028..b352099 100644 --- a/config/urls.py +++ b/config/urls.py @@ -13,7 +13,7 @@ from config.env import env def home(request): - return HttpResponse("OK: #62f65385e1dada519459965e9e24cfdd20a41e26") + return HttpResponse("OK: #135f580db2234f2af65e32ac4b2525506a7a033a") urlpatterns = [ diff --git a/core/apps/documents/urls.py b/core/apps/documents/urls.py index 93f947f..fb367e4 100644 --- a/core/apps/documents/urls.py +++ b/core/apps/documents/urls.py @@ -1,7 +1,7 @@ from django.urls import path -from core.apps.documents.views.contract import GenerateApiView +from core.apps.documents.views.contract import ValuationReportPDFView urlpatterns = [ - path('generate-contract-pdf/', GenerateApiView.as_view(), name='generate_contract_pdf'), + path('generate-contract-pdf//', ValuationReportPDFView.as_view(), name='generate_contract_pdf'), ] diff --git a/core/apps/documents/views/contract.py b/core/apps/documents/views/contract.py index 0265490..4222f05 100644 --- a/core/apps/documents/views/contract.py +++ b/core/apps/documents/views/contract.py @@ -1,153 +1,387 @@ -""" -PDF generatsiya view — JSON yuborasiz, PDF olasiz. -Yuborilmagan maydonlar uchun bo'sh string ('') qaytariladi — -shunda templateda {{ ... }} qoldiqlari ko'rinmaydi. -""" -import json -from django.http import HttpResponse, JsonResponse -from django.template.loader import render_to_string -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_http_methods -from weasyprint import HTML -from rest_framework import permissions, views +from datetime import date +from decimal import Decimal -# Kompaniya defaultlari -COMPANY_DEFAULTS = { - 'company_name': 'SIFAT BAHOLASH', - 'membership_number': '122', - 'membership_date': '01.06.2023', - 'phone_1': '(71) 278-85-85', - 'phone_2': '(91) 585-77-77', - 'phone_3': '(90) 535-99-99', - 'email': 'sifat.baholash@gmail.com', - 'bank_account': '2020 8 000 405 309 735 001', - 'bank_mfo': '00440', - 'inn': '307 930 412', - 'director_name': 'Тураев Т.Р.', - 'appraiser_name': 'Тураев Т.Р.', - 'appraiser_certificate': '0988', - 'appraiser_certificate_date': '17.11.2021', - 'insurance_policy': '19-01-25/0000368-2025', - 'insurance_start': '30.05.2025', - 'insurance_end': '29.05.2026', - 'market_name': 'Сергели автомобил бозори', - 'city': 'Ташкент', - 'year': '2026', - 'total_pages': 21, - 'valuation_purpose': 'для обеспечения залогом', - 'valuation_purpose_full': 'определение рыночной стоимости для обеспечения залогом', - 'adj_bargaining': '-9,00%', - 'adj_tech': '2,00%', - 'adj_equipment': '3,00%', - 'cost_weight': '20', - 'comp_weight': '80', +from django.shortcuts import get_object_or_404 +from django.template.loader import render_to_string +from django.http import HttpResponse +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from weasyprint import HTML + +from core.apps.evaluation.models import AutoEvaluationModel + + +UZ_MONTHS = { + 1: "yanvar", 2: "fevral", 3: "mart", 4: "aprel", + 5: "may", 6: "iyun", 7: "iyul", 8: "avgust", + 9: "sentabr", 10: "oktabr", 11: "noyabr", 12: "dekabr", } -# Template ichida ishlatiladigan barcha maydonlar -ALL_FIELDS = [ - 'report_number', 'report_date', 'valuation_date', - 'customer_name', 'customer_short_name', 'customer_passport', - 'owner_name', 'owner_address', - 'vehicle_brand', 'vehicle_model', 'vehicle_plate', - 'vehicle_type', 'vehicle_year', 'vehicle_mileage', - 'vehicle_engine_number', 'vehicle_body_number', 'vehicle_chassis_number', - 'vehicle_color', 'vehicle_tech_passport', - 'spec_type', 'spec_seats', 'spec_length', 'spec_width', 'spec_height', - 'spec_wheelbase', 'spec_clearance', - 'engine_type', 'engine_volume', 'engine_cylinders', - 'engine_power', 'engine_torque', 'engine_max_speed', - 'engine_acceleration', 'transmission', - 'fuel_tank', 'trunk', 'curb_weight', 'gross_weight', - 'brakes_front', 'brakes_rear', 'tires', - 'cost_c0', 'cost_tf', 'cost_lf', 'cost_type_name', - 'cost_kt', 'cost_kl', 'cost_omega', - 'cost_wear_andrianov', 'cost_wear_expert', 'cost_wear_avg', - 'cost_func_wear', 'cost_total_wear', 'cost_capital', - 'cost_approach_value', 'cost_approach_value_words', - 'analog_1_source', 'analog_1_year', 'analog_1_mileage', 'analog_1_price', - 'analog_1_adj_1', 'analog_1_adj_2', 'analog_1_adj_3', 'analog_1_weight', - 'analog_2_source', 'analog_2_year', 'analog_2_mileage', 'analog_2_price', - 'analog_2_adj_1', 'analog_2_adj_2', 'analog_2_adj_3', 'analog_2_weight', - 'analog_3_source', 'analog_3_year', 'analog_3_mileage', 'analog_3_price', - 'analog_3_adj_1', 'analog_3_adj_2', 'analog_3_adj_3', 'analog_3_weight', - 'comp_total_usd', 'comp_total_sum', 'comp_total_words', - 'cost_specific', 'comp_specific', 'weighted_value', - 'market_value_number', 'market_value_words', - 'rate_rur', 'rate_usd', 'rate_eur', - 'exploitation_period', 'retail_price', +UZ_ONES = [ + "", "bir", "ikki", "uch", "to'rt", "besh", + "olti", "yetti", "sakkiz", "to'qqiz", +] +UZ_TENS = [ + "", "o'n", "yigirma", "o'ttiz", "qirq", "ellik", + "oltmish", "yetmish", "sakson", "to'qson", ] -def _img_or_placeholder(image_url, placeholder_text, alt='', height='200mm'): - """Rasm URL bo'lsa , bo'lmasa placeholder qaytaradi""" - if image_url: - return f'{alt}' - return ( - f'
' - f'{placeholder_text}' - f'
' - ) +def _format_currency(value): + if value is None: + return "0" + try: + int_val = int(Decimal(value)) + except (ValueError, TypeError): + return "0" + return f"{int_val:,}".replace(",", " ") -def build_context(data: dict) -> dict: - """Templatedagi barcha o'zgaruvchilar uchun qiymat tayyorlaydi""" - # 1) Barcha maydonlarni bo'sh string bilan to'ldirish - context = {field: '' for field in ALL_FIELDS} - - # 2) Kompaniya defaultlari - context.update(COMPANY_DEFAULTS) - - # 3) Foydalanuvchi yuborgan ma'lumotlarni ustiga yozish - context.update(data) - - # 4) Rasmlar uchun tayyor HTML - context['cover_photo_html'] = _img_or_placeholder( - data.get('vehicle_photo'), - '[ Фото автомобиля ]', - alt='Vehicle', - height='50mm', - ) - context['customer_passport_html'] = _img_or_placeholder( - data.get('customer_passport_image'), - '[ Изображение паспорта заказчика ]', - alt='Паспорт заказчика', - ) - context['tech_passport_html'] = _img_or_placeholder( - data.get('tech_passport_image'), - '[ Изображение технического паспорта ]', - alt='Технический паспорт', - ) - context['price_list_html'] = _img_or_placeholder( - data.get('price_list_image'), - '[ Прайс-лист завода-изготовителя ]', - alt='Прайс-лист', - ) - - # 5) Loop maydonlar - if not context.get('analog_screenshots'): - context['analog_screenshots'] = [ - {'page_num': 18, 'image': None}, - {'page_num': 19, 'image': None}, - {'page_num': 20, 'image': None}, - ] - if not context.get('vehicle_photos'): - context['vehicle_photos'] = [] - - return context +def _format_date(value): + if not value: + return "" + return value.strftime("%d.%m.%Y") -class GenerateApiView(views.APIView): - - def post(self, request): - +def _three_digit_words(num): + if num == 0: + return "" + words = [] + hundreds = num // 100 + rest = num % 100 + if hundreds: + if hundreds == 1: + words.append("bir yuz") + else: + words.append(f"{UZ_ONES[hundreds]} yuz") + tens = rest // 10 + ones = rest % 10 + if tens: + words.append(UZ_TENS[tens]) + if ones: + words.append(UZ_ONES[ones]) + return " ".join(words) + + +def _number_to_uzbek_words(value): + if value is None: + return "" + try: + num = int(Decimal(value)) + except (ValueError, TypeError): + return "" + if num == 0: + return "nol" + + parts = [] + billions = num // 1_000_000_000 + millions = (num % 1_000_000_000) // 1_000_000 + thousands = (num % 1_000_000) // 1_000 + rest = num % 1_000 + + if billions: + parts.append(f"{_three_digit_words(billions)} milliard") + if millions: + parts.append(f"{_three_digit_words(millions)} million") + if thousands: + parts.append(f"{_three_digit_words(thousands)} ming") + if rest: + parts.append(_three_digit_words(rest)) + + text = " ".join(parts).strip() + return text[0].upper() + text[1:] if text else "" + + +class ValuationReportPDFView(APIView): + """ + Baholash hisobotini PDF formatida yuklab olish uchun API. + + GET /api/documents/generate-contract-pdf// + pk — AutoEvaluationModel id si. + """ + + def get(self, request, pk, *args, **kwargs): + return self._generate_pdf(request, pk) + + def post(self, request, pk, *args, **kwargs): + return self._generate_pdf(request, pk) + + def _generate_pdf(self, request, pk): + auto_evaluation = get_object_or_404( + AutoEvaluationModel.objects.select_related( + "vehicle", + "vehicle__brand", + "vehicle__model", + "vehicle__color", + "vehicle__fuel_type", + "vehicle__body_type", + "valuation", + "valuation__customer", + "valuation__property_owner", + ), + pk=pk, + ) + + context = self._build_context(auto_evaluation) + + html_string = render_to_string("documents/contract.html", context) + base_url = request.build_absolute_uri("/") + try: - html_string = render_to_string('report_template.html', context) - pdf_bytes = HTML(string=html_string).write_pdf() + pdf_file = HTML(string=html_string, base_url=base_url).write_pdf() except Exception as e: - return JsonResponse({'error': f'PDF yaratishda xato: {str(e)}'}, status=500) - - filename = f'otchet_1.pdf' - - response = HttpResponse(pdf_bytes, content_type='application/pdf') - response['Content-Disposition'] = f'attachment; filename="{filename}"' + return Response( + {"error": f"PDF yaratishda xatolik: {str(e)}"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + report_number = context["report"]["number"] + filename = f"baholash_hisoboti_{report_number}.pdf" + + response = HttpResponse(pdf_file, content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{filename}"' + response["Content-Length"] = len(pdf_file) return response + + def _build_context(self, auto): + vehicle = auto.vehicle + valuation = auto.valuation + customer = valuation.customer if valuation else None + owner = valuation.property_owner if valuation and valuation.property_owner else customer + report = getattr(valuation, "report", None) if valuation else None + + report_date = ( + auto.rate_report_date + or auto.contract_date + or (report.created_at.date() if report else None) + or date.today() + ) + valuation_date = auto.rate_date or report_date + inspection_date = auto.object_inspection_date or report_date + + report_number = ( + (report.report_number if report else None) + or auto.registration_number + or (valuation.conclusion_number if valuation else None) + or f"{auto.pk}/{report_date.year}" + ) + + final_value = None + if report and report.final_value is not None: + final_value = report.final_value + elif valuation and valuation.final_price is not None: + final_value = valuation.final_price + elif valuation and valuation.estimated_price is not None: + final_value = valuation.estimated_price + + market_value_formatted = ( + f"{_format_currency(final_value)} so'm" if final_value is not None else "0 so'm" + ) + market_value_words = ( + f"{_number_to_uzbek_words(final_value)} so'm" + if final_value is not None + else "" + ) + + cost_final = final_value + comparative_final = final_value + + brand_name = "" + model_name = "" + if vehicle: + brand_name = vehicle.brand.name if vehicle.brand else "" + model_name = vehicle.model.name if vehicle.model else "" + if not brand_name: + brand_name = auto.car_brand or "" + if not model_name: + model_name = auto.car_model or "" + full_brand = f"{brand_name} {model_name}".strip() + + plate_number = (vehicle.license_plate if vehicle else None) or auto.car_number or "" + manufacture_year = "" + if vehicle and vehicle.manufacture_year: + manufacture_year = str(vehicle.manufacture_year) + elif auto.manufacture_year: + manufacture_year = str(auto.manufacture_year) + + production_date = f"{manufacture_year}-yil" if manufacture_year else "" + engine_number = (vehicle.engine_number if vehicle else None) or auto.car_dvigatel_number or "" + body_number = vehicle.vin_number if vehicle and vehicle.vin_number else "" + color_value = "" + if vehicle and vehicle.color: + color_value = vehicle.color.name + elif auto.car_color: + color_value = auto.car_color + fuel_type_value = "" + if vehicle and vehicle.fuel_type: + fuel_type_value = vehicle.fuel_type.name + + tech_passport_value = "" + if vehicle and (vehicle.tech_passport_series or vehicle.tech_passport_number): + tech_passport_value = ( + f"{vehicle.tech_passport_series or ''} № {vehicle.tech_passport_number or ''}" + ).strip() + elif auto.tex_passport_serie_num: + tech_passport_value = auto.tex_passport_serie_num + + customer_ctx = self._customer_context(customer) + owner_ctx = self._owner_context(owner) + if not owner_ctx["name"]: + owner_ctx = customer_ctx + + contract_ctx = self._contract_context(auto, report_date) + + director_name = customer.director_name if customer and customer.director_name else "—" + + ctx = { + "logo_url": "", + "report": { + "number": report_number, + "date": _format_date(report_date), + "valuation_date": _format_date(valuation_date), + "inspection_date": _format_date(inspection_date), + "year": str(report_date.year), + "market_value_formatted": market_value_formatted, + "market_value_words": market_value_words, + }, + "vehicle": { + "brand": full_brand, + "plate_number": plate_number, + "production_date": production_date, + "production_year": manufacture_year, + "type": auto.get_object_type_display() if auto.object_type else "", + "engine_number": engine_number, + "body_number": body_number, + "chassis_number": body_number, + "color": color_value, + "tech_passport": tech_passport_value, + "fuel_type": fuel_type_value, + "engine_power": "", + "full_weight": "", + "empty_weight": "", + }, + "customer": customer_ctx, + "owner": owner_ctx, + "contract": contract_ctx, + "company": { + "director": director_name, + }, + "rates": { + "rur": "", + "usd": "", + "eur": "", + }, + "inspection": { + "tires": "", + "engine": "", + "chassis": "", + "transmission": "", + "body": "", + }, + "cost": { + "engine_volume": "", + "factory_price": _format_currency(cost_final), + "replacement_value": _format_currency(cost_final), + "wear_percent": "", + "final_value": _format_currency(cost_final), + "final_value_words": _number_to_uzbek_words(cost_final) + (" so'm" if cost_final else ""), + }, + "comparative": { + "final_value": _format_currency(comparative_final), + "final_value_usd": "", + "final_value_words": _number_to_uzbek_words(comparative_final) + (" so'm" if comparative_final else ""), + }, + "approach": { + "cost": { + "value": _format_currency(cost_final), + "weight": "30%", + "weighted": "", + }, + "comparative": { + "value": _format_currency(comparative_final), + "weight": "70%", + "weighted": "", + }, + "weighted_total": _format_currency(final_value), + }, + "analog_1": self._empty_analog(), + "analog_2": self._empty_analog(), + "analog_3": self._empty_analog(), + } + return ctx + + def _customer_context(self, customer): + empty = { + "name": "", + "address": "", + "phone": "", + "tin": "", + "account": "", + "bank": "", + "mfo": "", + } + if not customer: + return empty + if customer.customer_type == "legal": + return { + "name": customer.org_name or "", + "address": customer.org_address or "", + "phone": "", + "tin": customer.inn or "", + "account": customer.bank_account or "", + "bank": "", + "mfo": customer.mfo or "", + } + full_name = " ".join( + filter(None, [customer.last_name, customer.first_name, customer.middle_name]) + ) + return { + "name": full_name, + "address": customer.address or "", + "phone": "", + "tin": customer.jshshir or "", + "account": "", + "bank": "", + "mfo": "", + } + + def _owner_context(self, owner): + empty = {"name": "", "address": ""} + if not owner: + return empty + type_field = getattr(owner, "owner_type", None) or getattr(owner, "customer_type", None) + if type_field == "legal": + return { + "name": owner.org_name or "", + "address": owner.org_address or "", + } + full_name = " ".join( + filter(None, [owner.last_name, owner.first_name, owner.middle_name]) + ) + return { + "name": full_name, + "address": owner.address or "", + } + + def _contract_context(self, auto, fallback_date): + contract_date = auto.contract_date or fallback_date + return { + "number": auto.registration_number or str(auto.pk), + "day": f"{contract_date.day:02d}", + "month": UZ_MONTHS.get(contract_date.month, ""), + "year": str(contract_date.year), + } + + def _empty_analog(self): + return { + "source": "", + "phone": "", + "description": "", + "year": "", + "mileage": "", + "price": "", + "adjusted_price_1": "", + "final_price": "", + "weight": "", + } diff --git a/core/apps/evaluation/migrations/0039_bonus.py b/core/apps/evaluation/migrations/0039_bonus.py new file mode 100644 index 0000000..138a813 --- /dev/null +++ b/core/apps/evaluation/migrations/0039_bonus.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.7 on 2026-05-01 06:45 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0038_evaluationrequestmodel_distance_covered_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Bonus', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('updated_at', models.DateTimeField(auto_now=True)), + ('bonus_type', models.CharField(choices=[('lightweight_auto', 'Yengil automobil'), ('truck_car', 'Yuk automobil'), ('special_tech', 'Maxsus texnika')], max_length=50)), + ('percentage', models.FloatField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('price', models.FloatField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bonuses', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/core/apps/evaluation/migrations/0040_basevaluebonus_bonustype_employeebonus_delete_bonus.py b/core/apps/evaluation/migrations/0040_basevaluebonus_bonustype_employeebonus_delete_bonus.py new file mode 100644 index 0000000..8d43c63 --- /dev/null +++ b/core/apps/evaluation/migrations/0040_basevaluebonus_bonustype_employeebonus_delete_bonus.py @@ -0,0 +1,59 @@ +# Generated by Django 5.2.7 on 2026-05-01 11:43 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0039_bonus'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='BaseValueBonus', + 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)), + ('base_price', models.DecimalField(decimal_places=2, max_digits=12)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='BonusType', + 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)), + ('name', models.CharField(max_length=255)), + ('category', models.CharField(choices=[('auto_transport', 'Avtotransport'), ('real estate', "ko'chmas mulk"), ('equipment', 'uskuna va jihozlar')], max_length=50)), + ('percentage', models.PositiveIntegerField()), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EmployeeBonus', + 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)), + ('percentage', models.PositiveIntegerField()), + ('bonus_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='evaluation.bonustype')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bonuses', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'bonus_type')}, + }, + ), + migrations.DeleteModel( + name='Bonus', + ), + ] diff --git a/core/apps/evaluation/migrations/0041_rename_bonustype_bonuscategory.py b/core/apps/evaluation/migrations/0041_rename_bonustype_bonuscategory.py new file mode 100644 index 0000000..2d56cf0 --- /dev/null +++ b/core/apps/evaluation/migrations/0041_rename_bonustype_bonuscategory.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2.7 on 2026-05-01 12:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0040_basevaluebonus_bonustype_employeebonus_delete_bonus'), + ] + + operations = [ + migrations.RenameModel( + old_name='BonusType', + new_name='BonusCategory', + ), + ] diff --git a/core/apps/evaluation/migrations/0042_alter_bonuscategory_category.py b/core/apps/evaluation/migrations/0042_alter_bonuscategory_category.py new file mode 100644 index 0000000..d773dda --- /dev/null +++ b/core/apps/evaluation/migrations/0042_alter_bonuscategory_category.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-05-04 12:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('evaluation', '0041_rename_bonustype_bonuscategory'), + ] + + operations = [ + migrations.AlterField( + model_name='bonuscategory', + name='category', + field=models.CharField(choices=[('lightweight_auto', 'Yengil automobil'), ('truck_car', 'Yuk automobil'), ('special_tech', 'Maxsus texnika')], max_length=50), + ), + ] diff --git a/core/apps/evaluation/models/auto.py b/core/apps/evaluation/models/auto.py index 799fe53..9a406f0 100644 --- a/core/apps/evaluation/models/auto.py +++ b/core/apps/evaluation/models/auto.py @@ -9,14 +9,11 @@ from core.apps.evaluation.choices.auto import ( AutoEvaluationStatus, AutoObjectType, # FormOwnership, - LocationConvenience, - LocationHighways, ObjectOwnerType, # PropertyRights, # RateType, # ValueDetermined, ) - from .valuation import ValuationModel from .vehicle import VehicleModel @@ -244,8 +241,6 @@ class AutoEvaluationModel(AbstractBaseModel): default=False, ) - - def __str__(self): return f"Auto Evaluation {self.registration_number or self.pk}" diff --git a/core/apps/evaluation/models/bonus.py b/core/apps/evaluation/models/bonus.py new file mode 100644 index 0000000..8e51220 --- /dev/null +++ b/core/apps/evaluation/models/bonus.py @@ -0,0 +1,33 @@ +from django.db import models +from django.db.models.fields import PositiveIntegerField +from django_core.models import AbstractBaseModel + +from core.apps.evaluation.choices.auto import AutoObjectType + + +class BaseValueBonus(AbstractBaseModel): + base_price = models.DecimalField(max_digits=12, decimal_places=2) + + def __str__(self): + return f"Base: {self.base_price}" + + +class BonusCategory(AbstractBaseModel): + name = models.CharField(max_length=255) + category = models.CharField( + max_length=50, + choices=AutoObjectType.choices + ) + percentage = PositiveIntegerField() + + def __str__(self): + return self.name + + +class EmployeeBonus(AbstractBaseModel): + user = models.ForeignKey("accounts.User", on_delete=models.CASCADE, related_name="bonuses", ) + bonus_type = models.ForeignKey(BonusCategory, on_delete=models.CASCADE) + percentage = models.PositiveIntegerField() + + class Meta: + unique_together = ("user", "bonus_type") diff --git a/core/apps/evaluation/models/movable.py b/core/apps/evaluation/models/movable.py index c6075db..c762f4a 100644 --- a/core/apps/evaluation/models/movable.py +++ b/core/apps/evaluation/models/movable.py @@ -3,12 +3,11 @@ from django.utils.translation import gettext_lazy as _ from django_core.models import AbstractBaseModel from model_bakery import baker - -from .valuation import ValuationModel from core.apps.evaluation.choices.movable import ( MovablePropertyCategory, MovablePropertyCondition, ) +from .valuation import ValuationModel class MovablePropertyEvaluationModel(AbstractBaseModel): @@ -51,4 +50,3 @@ class MovablePropertyEvaluationModel(AbstractBaseModel): db_table = "MovablePropertyEvaluation" verbose_name = _("Movable Property Evaluation") verbose_name_plural = _("Movable Property Evaluations") - diff --git a/core/apps/evaluation/serializers/bonus/Bonus.py b/core/apps/evaluation/serializers/bonus/Bonus.py new file mode 100644 index 0000000..3f181ef --- /dev/null +++ b/core/apps/evaluation/serializers/bonus/Bonus.py @@ -0,0 +1,56 @@ +from rest_framework import serializers + +from core.apps.evaluation.models.bonus import BonusCategory, EmployeeBonus, BaseValueBonus + + +class BaseBonusSerializer(serializers.ModelSerializer): + class Meta: + model = BaseValueBonus + fields = ['id', 'base_price'] + + def create(self, validated_data): + if BaseValueBonus.objects.exists(): + raise serializers.ValidationError("Base bonus already exists") + + return super().create(validated_data) + + +class BonusCategorySerializer(serializers.ModelSerializer): + class Meta: + model = BonusCategory + fields = ['name', 'category', 'percentage'] + + +class BonusCategoryListSerializer(serializers.ModelSerializer): + price = serializers.DecimalField(max_digits=12, decimal_places=2) + + class Meta: + model = BonusCategory + fields = ['id', 'name', 'category', 'percentage' , 'price'] + + def get_price(self, obj): + base_obj = BaseValueBonus.objects.first() + if not base_obj: + return 0 + + return (base_obj.base_price * obj.percentage) / 100 + + +class BonusEmployeeBonusSerializer(serializers.ModelSerializer): + class Meta: + model = EmployeeBonus + fields = ['user', 'bonus_type', 'percentage'] + + +class EmployeeBonusListSerializer(serializers.ModelSerializer): + price = serializers.DecimalField(max_digits=12, decimal_places=2) + + class Meta: + model = EmployeeBonus + fields = ['id', 'user', 'bonus_type', 'percentage' , 'price'] + + def get_price(self, obj): + base_obj = BaseValueBonus.objects.first() + if not base_obj: + return 0 + return (base_obj.base_price * obj.percentage) / 100 diff --git a/core/apps/evaluation/serializers/bonus/__init__.py b/core/apps/evaluation/serializers/bonus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/apps/evaluation/serializers/vehicle/Vehicle.py b/core/apps/evaluation/serializers/vehicle/Vehicle.py index 7593e1a..67d99e7 100644 --- a/core/apps/evaluation/serializers/vehicle/Vehicle.py +++ b/core/apps/evaluation/serializers/vehicle/Vehicle.py @@ -87,3 +87,29 @@ class CreateVehicleSerializer(serializers.ModelSerializer): "condition", "position", ] + + +class VehicleApplicationSerializer(serializers.Serializer): + person_name = serializers.CharField() + property_owner = serializers.CharField(max_length=100) + address = serializers.CharField(max_length=255) + marka = serializers.CharField(max_length=100) + model = serializers.CharField(max_length=100) + configuration = serializers.CharField(max_length=100) + auto_number = serializers.CharField(max_length=100) + date_created = serializers.DateTimeField() + mileage = serializers.IntegerField() + vehicle_identification = serializers.CharField(max_length=100) + engine_number = serializers.CharField(max_length=100) + colour = serializers.CharField(max_length=100) + registration_certificate_series = serializers.CharField(max_length=100) + tec_passport_number = serializers.CharField(max_length=100) + tec_passport_date = serializers.DateTimeField() + tec_passport_place = serializers.CharField(max_length=255) + body_type = serializers.CharField(max_length=100) + chassis = serializers.CharField(max_length=100) + plate = serializers.CharField(max_length=100) + value_type = serializers.CharField(max_length=100) + evaluation_purpose = serializers.CharField(max_length=100) + personal_id_number = serializers.IntegerField() + id_number = serializers.CharField(max_length=9) diff --git a/core/apps/evaluation/urls.py b/core/apps/evaluation/urls.py index 59e1718..7e64e0b 100644 --- a/core/apps/evaluation/urls.py +++ b/core/apps/evaluation/urls.py @@ -27,6 +27,9 @@ router.register("valuation", views.ValuationView, basename="valuation") router.register("property-owner", views.PropertyOwnerView, basename="property-owner") router.register("customer", views.CustomerView, basename="customer") router.register("certificate", views.CertificateView, basename="certificate") +router.register("bonus-type", views.BonusTypeView, basename="bonus-type") +router.register("bonus-employee", views.BonusEmployeeViewSet, basename="bonus-employee") +router.register("bonus-base", views.BaseBonusViewSet, basename="bonus-base") urlpatterns = [ path("", include(router.urls)), @@ -85,4 +88,5 @@ urlpatterns = [ )), path("calculate_avg_cost/", views.AvgCostAPIView.as_view()), + path("vehicle_document/", views.GeneratePDFView.as_view()), ] diff --git a/core/apps/evaluation/views/__init__.py b/core/apps/evaluation/views/__init__.py index 64a46d9..5f9ff27 100644 --- a/core/apps/evaluation/views/__init__.py +++ b/core/apps/evaluation/views/__init__.py @@ -15,3 +15,4 @@ from .didox import * # noqa from .tech_passport import * # noqa from .certificate import * # noqa from .avg_cost import * +from .bonus import * \ No newline at end of file diff --git a/core/apps/evaluation/views/bonus.py b/core/apps/evaluation/views/bonus.py new file mode 100644 index 0000000..3e294a8 --- /dev/null +++ b/core/apps/evaluation/views/bonus.py @@ -0,0 +1,60 @@ +from django_core.mixins import BaseViewSetMixin +from drf_spectacular.utils import extend_schema +from rest_framework import viewsets +from rest_framework.permissions import IsAdminUser +from rest_framework.viewsets import ModelViewSet + +# core +from core.apps.evaluation.models.bonus import BonusCategory, EmployeeBonus, BaseValueBonus +from core.apps.evaluation.serializers.bonus.Bonus import BonusCategorySerializer, \ + BonusCategoryListSerializer, EmployeeBonusListSerializer, BonusEmployeeBonusSerializer, BaseBonusSerializer + + +@extend_schema(tags=["BaseBonus"]) +class BaseBonusViewSet(BaseViewSetMixin, viewsets.ModelViewSet): + queryset = BaseValueBonus.objects.all() + serializer_class = BaseBonusSerializer + + +@extend_schema(tags=["Bonus-Category"]) +class BonusTypeView(BaseViewSetMixin, ModelViewSet): + queryset = BonusCategory.objects.all() + + serializer_class = BonusCategorySerializer + + action_serializer_class = { + 'create': BonusCategorySerializer, + 'update': BonusCategorySerializer, + 'partial_update': BonusCategorySerializer, + 'list': BonusCategoryListSerializer, + 'retrieve': BonusCategoryListSerializer, + } + + action_permission_classes = { + 'create': [IsAdminUser], + 'update': [IsAdminUser], + 'partial_update': [IsAdminUser], + 'destroy': [IsAdminUser], + 'list': [IsAdminUser], + } + + +class BonusEmployeeViewSet(BaseViewSetMixin, ModelViewSet): + queryset = EmployeeBonus.objects.all() + serializer_class = BonusEmployeeBonusSerializer + + action_serializer_class = { + 'create': BonusEmployeeBonusSerializer, + 'update': BonusEmployeeBonusSerializer, + 'partial_update': BonusEmployeeBonusSerializer, + 'list': EmployeeBonusListSerializer, + 'retrieve': EmployeeBonusListSerializer, + } + + action_permission_classes = { + 'create': [IsAdminUser], + 'update': [IsAdminUser], + 'partial_update': [IsAdminUser], + 'destroy': [IsAdminUser], + 'list': [IsAdminUser], + } diff --git a/core/apps/evaluation/views/vehicle.py b/core/apps/evaluation/views/vehicle.py index b250b7a..f8b956e 100644 --- a/core/apps/evaluation/views/vehicle.py +++ b/core/apps/evaluation/views/vehicle.py @@ -1,16 +1,19 @@ # django core +from django.http import HttpResponse from django_core.mixins import BaseViewSetMixin - # swagger from drf_spectacular.utils import extend_schema - # rest framework from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework.viewsets import ReadOnlyModelViewSet # core apps from core.apps.evaluation.models import VehicleModel -from core.apps.evaluation.serializers import vehicle as serialziers +from core.apps.evaluation.serializers import vehicle as serialziers, VehicleApplicationSerializer +from core.utils.generation_pdf import PDFService @extend_schema(tags=["Vehicle"]) @@ -27,3 +30,19 @@ class VehicleView(BaseViewSetMixin, ReadOnlyModelViewSet): "retrieve": serialziers.RetrieveVehicleSerializer, "create": serialziers.CreateVehicleSerializer, } + + +@extend_schema(tags=['GenerationDocument'], request=VehicleApplicationSerializer) +class GeneratePDFView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + serializer = VehicleApplicationSerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=400) + + pdf_buffer = PDFService.generate_vehicle_pdf(serializer.validated_data) + + response = HttpResponse(pdf_buffer, content_type='application/pdf') + response['Content-Disposition'] = 'attachment; filename="ariza.pdf"' + return response diff --git a/core/apps/tasks/serializers/comment.py b/core/apps/tasks/serializers/comment.py index 3c053af..0d9202d 100644 --- a/core/apps/tasks/serializers/comment.py +++ b/core/apps/tasks/serializers/comment.py @@ -30,15 +30,10 @@ class CommentCreateSerializer(serializers.ModelSerializer): 'id', 'message', 'file', 'type', 'task' ] - def validate(self, data): - task = Task.objects.filter(id=data['task']).first() - if not task: - raise serializers.ValidationError("Task not found") - data['task'] = task - return data - def create(self, validated_data): with transaction.atomic(): - task = validated_data.pop('task') - comment = Comment.objects.create(task=task, created_by=self.context['request'].user, **validated_data) - return comment + comment = Comment.objects.create( + created_by=self.context['request'].user, + **validated_data + ) + return comment \ No newline at end of file diff --git a/core/apps/tasks/urls.py b/core/apps/tasks/urls.py index 29d59af..662b7ac 100644 --- a/core/apps/tasks/urls.py +++ b/core/apps/tasks/urls.py @@ -20,7 +20,7 @@ urlpatterns = [ path('task/', include( [ path('list/', task.TaskListView.as_view()), - path('/', task.TaskDetailView.as_view()), + path('/', task.TaskDetailView.as_view()), path('create/', task.TaskCreateView.as_view()), ] )), diff --git a/core/apps/tasks/views/task.py b/core/apps/tasks/views/task.py index 22dc730..ef79b2a 100644 --- a/core/apps/tasks/views/task.py +++ b/core/apps/tasks/views/task.py @@ -1,9 +1,7 @@ from django.db import transaction - -from rest_framework import permissions, generics -from rest_framework.response import Response - from drf_spectacular.utils import extend_schema +from rest_framework import permissions, generics, status +from rest_framework.response import Response from core.apps.tasks.models.task import Task from core.apps.tasks.serializers.task import TaskSerializer, TaskCreateSerializer @@ -18,7 +16,8 @@ class TaskCreateView(generics.GenericAPIView): @transaction.atomic def post(self, request): serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) + if not serializer.is_valid(raise_exception=True): + return Response(serializer.validated_data, status=status.HTTP_400_BAD_REQUEST) serializer.save() return Response(serializer.data) diff --git a/core/utils/generation_pdf.py b/core/utils/generation_pdf.py new file mode 100644 index 0000000..546719b --- /dev/null +++ b/core/utils/generation_pdf.py @@ -0,0 +1,138 @@ +# services.py +from io import BytesIO + +from reportlab.lib import colors +from reportlab.lib.enums import TA_RIGHT, TA_CENTER +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer + + +class PDFService: + @staticmethod + def generate_vehicle_pdf(data): + buffer = BytesIO() + doc = SimpleDocTemplate( + buffer, pagesize=A4, + rightMargin=40, leftMargin=40, + topMargin=40, bottomMargin=40 + ) + styles = getSampleStyleSheet() + + cell_style = ParagraphStyle( + 'CellStyle', parent=styles['Normal'], + fontSize=9, leading=11 + ) + cell_style_bold = ParagraphStyle( + 'CellStyleBold', parent=cell_style, + fontName='Helvetica-Bold', + textColor=colors.red + ) + header_style = ParagraphStyle( + 'HeaderStyle', parent=styles['Normal'], + alignment=TA_RIGHT, fontSize=10, leading=12 + ) + title_style = ParagraphStyle( + 'TitleStyle', parent=styles['Normal'], + alignment=TA_CENTER, fontSize=14, leading=16 + ) + + elements = [] + + header_text = ( + f'"Sifat baholash" MChJ direktori
' + f"T.R.To'rayevga


" + f"{data.get('address', '')}
" + f"ro'yxatda turuvchi fuqaro
" + f"{data.get('person_name', '')} tomonidan
" + f"Avtotransport vositasini baholash uchun" + ) + elements.append(Paragraph(header_text, header_style)) + elements.append(Spacer(1, 25)) + + elements.append(Paragraph("A R I Z A", title_style)) + elements.append(Spacer(1, 10)) + elements.append(Paragraph( + "Ushbu orqali quyidagi avtotransport vositasini baholab berishingizni so'rayman:", + cell_style + )) + elements.append(Spacer(1, 10)) + + date_created = data.get('date_created') + year_str = str(date_created.year) + " yil" if date_created else "" + + tec_date = data.get('tec_passport_date') + tec_date_str = tec_date.strftime('%d.%m.%Y yil') if tec_date else "" + + raw_data = [ + ["Mulk egasi", data.get('property_owner', '')], + ["Manzil", data.get('address', '')], + ["Marka", data.get('marka', '')], + ["Model", data.get('model', '')], + ["Komplektatsiya", data.get('configuration', '')], + ["Davlat raqami", data.get('auto_number', '')], + ["Ishlab chiqarilgan yili", year_str], + ["Bosib o'tgan masofasi", f"{data.get('mileage', 0):,}".replace(',', ' ')], + ["№ kuzov (VIN)", data.get('vehicle_identification', '')], + ["№ dvigatel", data.get('engine_number', '')], + ["Rang", data.get('colour', '')], + ["Texnik passport seriyasi", data.get('registration_certificate_series', '')], + ["Texnik passport raqami", data.get('tec_passport_number', '')], + ["Texnik passport berilgan sanasi", tec_date_str], + ["Texnik passport berilgan joyi", data.get('tec_passport_place', '')], + ["Kuzov turi", data.get('body_type', '')], + ["Shassi", data.get('chassis', '')], + ["Davlat belgisi (plate)", data.get('plate', '')], # ← was missing + ] + + table_data = [ + [Paragraph(row[0], cell_style), Paragraph(str(row[1]), cell_style_bold)] + for row in raw_data + ] + table = Table(table_data, colWidths=[170, 345]) + table.setStyle(TableStyle([ + ('GRID', (0, 0), (-1, -1), 0.5, colors.black), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('LEFTPADDING', (0, 0), (-1, -1), 6), + ('TOPPADDING', (0, 0), (-1, -1), 4), + ('BOTTOMPADDING', (0, 0), (-1, -1), 4), + ])) + elements.append(table) + elements.append(Spacer(1, 15)) + + elements.append(Paragraph("Baholash maqsadi:", cell_style)) + purpose_raw = [ + ["Aniqlanayotgan qiymat turi", data.get('value_type', '')], + ["Baholash maqsadi", data.get('evaluation_purpose', '')], + ] + purpose_data = [ + [Paragraph(r[0], cell_style), Paragraph(str(r[1]), cell_style_bold)] + for r in purpose_raw + ] + pt = Table(purpose_data, colWidths=[170, 345]) + pt.setStyle(TableStyle([ + ('GRID', (0, 0), (-1, -1), 0.5, colors.black), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ])) + elements.append(pt) + elements.append(Spacer(1, 15)) + + elements.append(Paragraph("Buyurtmachi rekvizitlari:", cell_style)) + footer_raw = [ + ["Shaxsiy raqam (PINFL)", str(data.get('personal_id_number', ''))], + ["ID karta raqami", data.get('id_number', '')], + ] + footer_data = [ + [Paragraph(f[0], cell_style), Paragraph(str(f[1]), cell_style_bold)] + for f in footer_raw + ] + ft = Table(footer_data, colWidths=[170, 345]) + ft.setStyle(TableStyle([ + ('GRID', (0, 0), (-1, -1), 0.5, colors.black), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ])) + elements.append(ft) + + doc.build(elements) + buffer.seek(0) + return buffer \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 456d852..e18dffa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -141,3 +141,14 @@ yarl zipfile36 zipp zopfli + + +# !NOTE: on-websocket +# websockets +# channels-redis + +grpcio>=1.62.0 +grpcio-tools>=1.62.0 +protobuf>=4.25.0 + +reportlab diff --git a/resources/templates/documents/contract.html b/resources/templates/documents/contract.html index f6255d5..6087d1a 100644 --- a/resources/templates/documents/contract.html +++ b/resources/templates/documents/contract.html @@ -4,9 +4,49 @@ Baholash hisoboti № {{ report.number }} + + {{ report.number }} + -
+
SIFAT BAHOLASH
@@ -787,7 +853,7 @@ -
+
"SIFAT BAHOLASH" MCHJ
@@ -910,7 +976,7 @@
- +
@@ -1031,22 +1097,26 @@ Valyuta kurslari ({{ report.valuation_date }} sanasi holatiga) - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +
ValyutaKurs (so'm)
RUR{{ rates.rur }}
USD{{ rates.usd }}
EURO{{ rates.eur }}
ValyutaKurs (so'm)
RUR{{ rates.rur }}
USD{{ rates.usd }}
EURO{{ rates.eur }}

@@ -1083,33 +1153,6 @@ - -

- - - - -
-
- -
-
"SIFAT BAHOLASH" MCHJ
-
- A'zolik sertifikati № 122, 01.06.2023 y. -
-
- Hisobot № {{ report.number }}, {{ report.date }} -
-
-
-

1.4. Bajaruvchi to'g'risida ma'lumotlar

@@ -1273,33 +1316,6 @@
  • Hisobotni tuzish va Buyurtmachiga topshirish
  • - -
    - - - - -
    -
    - -
    -
    "SIFAT BAHOLASH" MCHJ
    -
    - A'zolik sertifikati № 122, 01.06.2023 y. -
    -
    - Hisobot № {{ report.number }}, {{ report.date }} -
    -
    -
    -

    1.6. Baholash sifati sertifikati

    Ushbu hisobotni imzolaganlar (keyingi o'rinlarda — Baholovchi) @@ -1422,33 +1438,6 @@ qiymati.

    - -
    - - - - -
    -
    - -
    -
    "SIFAT BAHOLASH" MCHJ
    -
    - A'zolik sertifikati № 122, 01.06.2023 y. -
    -
    - Hisobot № {{ report.number }}, {{ report.date }} -
    -
    -
    -

    1.8.1. Baholash jarayoni

    Baholash jarayoni quyidagilarni o'z ichiga oladi:

      @@ -1576,33 +1565,6 @@
    1. Ishlab chiqaruvchi zavodning prays-listi
    2. - -
    - - - - -
    -
    - -
    -
    "SIFAT BAHOLASH" MCHJ
    -
    - A'zolik sertifikati № 122, 01.06.2023 y. -
    -
    - Hisobot № {{ report.number }}, {{ report.date }} -
    -
    -
    -

    2. Baholanayotgan transport vositasining tavsifi

    @@ -1756,33 +1718,6 @@ - -
    - - - - -
    -
    - -
    -
    "SIFAT BAHOLASH" MCHJ
    -
    - A'zolik sertifikati № 122, 01.06.2023 y. -
    -
    - Hisobot № {{ report.number }}, {{ report.date }} -
    -
    -
    -

    3. Xarajat yondashuvi

    @@ -2005,33 +1940,6 @@

    ({{ cost.final_value_words }})
    - -
    - - - - -
    -
    - -
    -
    "SIFAT BAHOLASH" MCHJ
    -
    - A'zolik sertifikati № 122, 01.06.2023 y. -
    -
    - Hisobot № {{ report.number }}, {{ report.date }} -
    -
    -
    -

    4. Solishtirma yondashuv

    @@ -2140,30 +2048,6 @@ qurilmalar narxi.

    - -
    - -
    -
    - -
    -
    "SIFAT BAHOLASH" MCHJ
    -
    - A'zolik sertifikati № 122, 01.06.2023 y. -
    -
    - Hisobot № {{ report.number }}, {{ report.date }} -
    -
    -
    -

    Solishtirma tahlil jadvali

    @@ -2319,33 +2203,6 @@ - - - - - - -
    -
    - -
    -
    "SIFAT BAHOLASH" MCHJ
    -
    - A'zolik sertifikati № 122, 01.06.2023 y. -
    -
    - Hisobot № {{ report.number }}, {{ report.date }} -
    -
    -
    -

    5. Daromad yondashuvi

    @@ -2555,13 +2412,6 @@

    {{ company.director }}
    - - diff --git a/stack.yaml b/stack.yaml index 7bfa4ac..2286763 100644 --- a/stack.yaml +++ b/stack.yaml @@ -84,7 +84,7 @@ services: max-file: "5" web: - image: husanjon/sifatbaho:150 + image: husanjon/sifatbaho:152 env_file: - .env environment: @@ -129,7 +129,7 @@ services: max-file: "5" celery: - image: husanjon/sifatbaho:150 + image: husanjon/sifatbaho:152 env_file: - .env environment: