Merge pull request 'behruz' (#134) from behruz into main
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 5m12s

Reviewed-on: #134
This commit is contained in:
2026-05-05 11:23:52 +00:00
10 changed files with 2971 additions and 44 deletions

View File

@@ -15,6 +15,7 @@ APPS = [
"django_core",
"core.apps.accounts.apps.AccountsConfig",
'core.apps.tasks.apps.TasksConfig',
'core.apps.documents.apps.DocumentsConfig',
]
if env.bool("SILK_ENABLED", False):

View File

@@ -24,6 +24,7 @@ urlpatterns = [
path("api/v1/", include("core.apps.payment.urls")),
path("api/v1/", include("core.apps.chat.urls")),
path("api/v1/tasks/", include("core.apps.tasks.urls")),
path("api/v1/documents/", include("core.apps.documents.urls")),
]
urlpatterns += [
path("admin/", admin.site.urls),

View File

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class DocumentsConfig(AppConfig):
name = "core.apps.documents"

View File

@@ -0,0 +1,7 @@
from django.urls import path
from core.apps.documents.views.contract import ValuationReportPDFView
urlpatterns = [
path('generate-contract-pdf/<int:pk>/', ValuationReportPDFView.as_view(), name='generate_contract_pdf'),
]

View File

@@ -0,0 +1,387 @@
from datetime import date
from decimal import Decimal
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",
}
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 _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 _format_date(value):
if not value:
return ""
return value.strftime("%d.%m.%Y")
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>/
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:
pdf_file = HTML(string=html_string, base_url=base_url).write_pdf()
except Exception as e:
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": "",
}

View File

@@ -5,6 +5,17 @@ ENV SCRIPT=$SCRIPT
WORKDIR /code
RUN apk add --no-cache \
glib \
gdk-pixbuf \
pango \
cairo \
libffi \
shared-mime-info \
fontconfig \
ttf-dejavu \
ttf-liberation
COPY requirements.txt /code/requirements.txt
RUN uv pip install -r requirements.txt
@@ -16,5 +27,3 @@ COPY ./resources/scripts/$SCRIPT /code/$SCRIPT
RUN chmod +x /code/resources/scripts/$SCRIPT
CMD sh /code/resources/scripts/$SCRIPT

View File

@@ -1,46 +1,146 @@
backports.tarfile==1.2.0
celery==5.4.0
django-cors-headers==4.6.0
django-environ==0.11.2
django-extensions==3.2.3
django-filter==24.3
django-redis==5.4.0
django-unfold==0.65.0
djangorestframework-simplejwt==5.3.1
drf-spectacular==0.28.0
importlib-metadata==8.5.0
importlib-resources==6.4.5
inflect==7.3.1
jaraco.collections==5.1.0
packaging==24.2
pip-chill==1.0.3
platformdirs==4.3.6
psycopg2-binary==2.9.10
tomli==2.2.1
uvicorn==0.32.1
jst-django-core~=1.2.2
rich
pydantic
aiohappyeyeballs
aiohttp
aiosignal
amqp
annotated-doc
annotated-types
arrow
asgiref
astor
attrs
backports.tarfile
bcrypt
pytest-django
requests
model_bakery
django-modeltranslation~=0.19.11
django-ckeditor-5==0.2.15
django-rosetta==0.10.1
django-cacheops~=7.1
# !NOTE: on-server
# gunicorn
django-storages
billiard
binaryornot
black
boto3
botocore
brotli
celery
certifi
cffi
chardet
charset-normalizer
click
click-didyoumean
click-plugins
click-repl
colorlog
cookiecutter
cssselect2
Django
django-cacheops
django-ckeditor-5
django-cors-headers
django-environ
django-extensions
django-filter
django-modeltranslation
django-redis
django-rosetta
django-storages
django-unfold
djangorestframework
djangorestframework-simplejwt
drf-spectacular
flake8
fonttools
frozenlist
funcy
g4f
grpcio
grpcio-tools
h11
idna
importlib_metadata
importlib_resources
inflect
inflection
iniconfig
isort
jaraco.collections
jaraco.context
jaraco.functools
jaraco.text
Jinja2
jmespath
jsonschema
jsonschema-specifications
jst-aicommit
jst-django
jst-django-core
kombu
markdown-it-py
MarkupSafe
mccabe
mdurl
model-bakery
more-itertools
multidict
mypy-extensions
nest-asyncio
packaging
pathspec
pillow
pip-chill
platformdirs
pluggy
polib
prompt-toolkit
propcache
protobuf
psycopg2-binary
pycodestyle
pycparser
pycryptodome
pydantic
pydantic_core
pydyf
pyflakes
Pygments
PyJWT
pyphen
pytest
pytest-django
python-dateutil
python-slugify
PyYAML
questionary
redis
referencing
reportlab
requests
rich
rpds-py
s3transfer
setuptools
shellingham
six
sqlparse
tenacity
text-unidecode
tinycss2
tinyhtml5
tomli
tqdm
typeguard
typer
typer-slim
types-python-dateutil
typing-inspection
typing_extensions
tzdata
uritemplate
urllib3
uvicorn
vine
wcwidth
weasyprint
webencodings
yarl
zipfile36
zipp
zopfli
# !NOTE: on-websocket
@@ -51,4 +151,4 @@ grpcio>=1.62.0
grpcio-tools>=1.62.0
protobuf>=4.25.0
reportlab
reportlab

File diff suppressed because it is too large Load Diff