behruz #134

Merged
xoliqberdiyev merged 2 commits from behruz into main 2026-05-05 11:23:53 +00:00
28 changed files with 2916 additions and 533 deletions
Showing only changes of commit 8158d7146e - Show all commits

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 GenerateApiView
urlpatterns = [
path('generate-contract-pdf/', GenerateApiView.as_view(), name='generate_contract_pdf'),
]

View File

@@ -0,0 +1,153 @@
"""
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
# 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',
}
# 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',
]
def _img_or_placeholder(image_url, placeholder_text, alt='', height='200mm'):
"""Rasm URL bo'lsa <img>, bo'lmasa placeholder qaytaradi"""
if image_url:
return f'<img src="{image_url}" alt="{alt}" style="max-width:100%;max-height:{height};">'
return (
f'<div class="image-placeholder" style="height: {height};">'
f'{placeholder_text}'
f'</div>'
)
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
class GenerateApiView(views.APIView):
def post(self, request):
try:
html_string = render_to_string('report_template.html', context)
pdf_bytes = HTML(string=html_string).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

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,52 +1,143 @@
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
# !NOTE: on-websocket
# websockets
# channels-redis
grpcio>=1.62.0
grpcio-tools>=1.62.0
protobuf>=4.25.0
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

File diff suppressed because it is too large Load Diff