feat: wire contract PDF context and align MechanicAuto with AutoEvaluation
- contract PDF: map report/customer/owner/contract from AutoEvaluationModel fields, accept inspection via POST serializer, fetch CBU.uz currency rates - MechanicAutoEvaluation: add distance_covered, object_owner_residence and car_position/body_type/fuel_type/state_car/assessment_task_type FKs; drop car_type and single tex_passport_file in favour of multi-file FK model Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,9 @@ from rest_framework import status
|
||||
from weasyprint import HTML
|
||||
|
||||
from core.apps.evaluation.models import AutoEvaluationModel
|
||||
from core.apps.evaluation.choices.auto import ObjectOwnerType
|
||||
from core.apps.documents.serializers.contract import ContractPDFRequestSerializer
|
||||
from core.apps.documents.services.cbu_rates import fetch_rates
|
||||
|
||||
|
||||
UZ_MONTHS = {
|
||||
@@ -27,6 +30,14 @@ UZ_TENS = [
|
||||
"oltmish", "yetmish", "sakson", "to'qson",
|
||||
]
|
||||
|
||||
DEFAULT_INSPECTION = {
|
||||
"tires": "Qoniqarli",
|
||||
"engine": "Qoniqarli",
|
||||
"chassis": "Qoniqarli",
|
||||
"transmission": "Qoniqarli",
|
||||
"body": "Qoniqarli",
|
||||
}
|
||||
|
||||
|
||||
def _format_currency(value):
|
||||
if value is None:
|
||||
@@ -97,33 +108,44 @@ class ValuationReportPDFView(APIView):
|
||||
"""
|
||||
Baholash hisobotini PDF formatida yuklab olish uchun API.
|
||||
|
||||
GET /api/documents/generate-contract-pdf/<pk>/
|
||||
pk — AutoEvaluationModel id si.
|
||||
GET /api/documents/generate-contract-pdf/<pk>/
|
||||
POST /api/documents/generate-contract-pdf/<pk>/
|
||||
|
||||
POST so'rov tanasida inspection malumotlarini yuborish mumkin:
|
||||
{
|
||||
"inspection": {
|
||||
"tires": "...",
|
||||
"engine": "...",
|
||||
"chassis": "...",
|
||||
"transmission": "...",
|
||||
"body": "..."
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def get(self, request, pk, *args, **kwargs):
|
||||
return self._generate_pdf(request, pk)
|
||||
return self._generate_pdf(request, pk, payload={})
|
||||
|
||||
def post(self, request, pk, *args, **kwargs):
|
||||
return self._generate_pdf(request, pk)
|
||||
serializer = ContractPDFRequestSerializer(data=request.data or {})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return self._generate_pdf(request, pk, payload=serializer.validated_data)
|
||||
|
||||
def _generate_pdf(self, request, pk):
|
||||
def _generate_pdf(self, request, pk, payload):
|
||||
auto_evaluation = get_object_or_404(
|
||||
AutoEvaluationModel.objects.select_related(
|
||||
"user",
|
||||
"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)
|
||||
context = self._build_context(auto_evaluation, payload)
|
||||
|
||||
html_string = render_to_string("documents/contract.html", context)
|
||||
base_url = request.build_absolute_uri("/")
|
||||
@@ -144,95 +166,34 @@ class ValuationReportPDFView(APIView):
|
||||
response["Content-Length"] = len(pdf_file)
|
||||
return response
|
||||
|
||||
def _build_context(self, auto):
|
||||
def _build_context(self, auto, payload):
|
||||
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
|
||||
user = auto.user
|
||||
|
||||
report_date = (
|
||||
auto.rate_report_date
|
||||
or auto.contract_date
|
||||
or (report.created_at.date() if report else None)
|
||||
or date.today()
|
||||
)
|
||||
report_date = auto.rate_report_date or auto.contract_date 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}"
|
||||
)
|
||||
report_number = auto.registration_number or f"{auto.pk}/{report_date.year}"
|
||||
|
||||
# Bozor qiymati hozircha hisoblanmagan — default 0.
|
||||
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 ""
|
||||
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
|
||||
|
||||
vehicle_ctx = self._vehicle_context(auto, vehicle)
|
||||
customer_ctx = self._customer_context(user)
|
||||
owner_ctx = self._owner_context(auto)
|
||||
contract_ctx = self._contract_context(auto, report_date)
|
||||
|
||||
director_name = customer.director_name if customer and customer.director_name else "—"
|
||||
inspection_ctx = self._inspection_context(payload)
|
||||
rates_ctx = self._rates_context(valuation_date)
|
||||
|
||||
ctx = {
|
||||
"logo_url": "",
|
||||
@@ -241,44 +202,19 @@ class ValuationReportPDFView(APIView):
|
||||
"date": _format_date(report_date),
|
||||
"valuation_date": _format_date(valuation_date),
|
||||
"inspection_date": _format_date(inspection_date),
|
||||
"year": str(report_date.year),
|
||||
"year": str(report_date.year or date.today().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": "",
|
||||
},
|
||||
"vehicle": vehicle_ctx,
|
||||
"customer": customer_ctx,
|
||||
"owner": owner_ctx,
|
||||
"contract": contract_ctx,
|
||||
"company": {
|
||||
"director": director_name,
|
||||
},
|
||||
"rates": {
|
||||
"rur": "",
|
||||
"usd": "",
|
||||
"eur": "",
|
||||
},
|
||||
"inspection": {
|
||||
"tires": "",
|
||||
"engine": "",
|
||||
"chassis": "",
|
||||
"transmission": "",
|
||||
"body": "",
|
||||
"director": "—",
|
||||
},
|
||||
"rates": rates_ctx,
|
||||
"inspection": inspection_ctx,
|
||||
"cost": {
|
||||
"engine_volume": "",
|
||||
"factory_price": _format_currency(cost_final),
|
||||
@@ -311,7 +247,65 @@ class ValuationReportPDFView(APIView):
|
||||
}
|
||||
return ctx
|
||||
|
||||
def _customer_context(self, customer):
|
||||
def _vehicle_context(self, auto, vehicle):
|
||||
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
|
||||
|
||||
return {
|
||||
"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": "",
|
||||
}
|
||||
|
||||
def _customer_context(self, user):
|
||||
empty = {
|
||||
"name": "",
|
||||
"address": "",
|
||||
@@ -321,51 +315,41 @@ class ValuationReportPDFView(APIView):
|
||||
"bank": "",
|
||||
"mfo": "",
|
||||
}
|
||||
if not customer:
|
||||
if not user:
|
||||
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])
|
||||
)
|
||||
full_name = " ".join(filter(None, [user.last_name, user.first_name])).strip()
|
||||
if not full_name:
|
||||
full_name = user.username or user.phone or ""
|
||||
return {
|
||||
"name": full_name,
|
||||
"address": customer.address or "",
|
||||
"phone": "",
|
||||
"tin": customer.jshshir or "",
|
||||
"address": "",
|
||||
"phone": user.phone or "",
|
||||
"tin": "",
|
||||
"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":
|
||||
def _owner_context(self, auto):
|
||||
if auto.object_owner_type == ObjectOwnerType.LEGAL:
|
||||
return {
|
||||
"name": owner.org_name or "",
|
||||
"address": owner.org_address or "",
|
||||
"name": auto.object_owner_legal_entity or "",
|
||||
"address": auto.object_owner_residence or "",
|
||||
}
|
||||
full_name = " ".join(
|
||||
filter(None, [owner.last_name, owner.first_name, owner.middle_name])
|
||||
)
|
||||
filter(None, [
|
||||
auto.object_owner_individual_person_l_name,
|
||||
auto.object_owner_individual_person_f_name,
|
||||
auto.object_owner_individual_person_p_name,
|
||||
])
|
||||
).strip()
|
||||
return {
|
||||
"name": full_name,
|
||||
"address": owner.address or "",
|
||||
"address": auto.object_owner_residence or "",
|
||||
}
|
||||
|
||||
def _contract_context(self, auto, fallback_date):
|
||||
contract_date = auto.contract_date or fallback_date
|
||||
contract_date = auto.contract_date or auto.rate_report_date or fallback_date
|
||||
return {
|
||||
"number": auto.registration_number or str(auto.pk),
|
||||
"day": f"{contract_date.day:02d}",
|
||||
@@ -373,6 +357,21 @@ class ValuationReportPDFView(APIView):
|
||||
"year": str(contract_date.year),
|
||||
}
|
||||
|
||||
def _inspection_context(self, payload):
|
||||
provided = (payload or {}).get("inspection") or {}
|
||||
return {
|
||||
key: provided.get(key) or default
|
||||
for key, default in DEFAULT_INSPECTION.items()
|
||||
}
|
||||
|
||||
def _rates_context(self, target_date):
|
||||
rates = fetch_rates(target_date)
|
||||
return {
|
||||
"rur": rates.get("RUB", ""),
|
||||
"usd": rates.get("USD", ""),
|
||||
"eur": rates.get("EUR", ""),
|
||||
}
|
||||
|
||||
def _empty_analog(self):
|
||||
return {
|
||||
"source": "",
|
||||
|
||||
Reference in New Issue
Block a user