еализация PDF отчетов о состоянии истории автомобиля

This commit is contained in:
Vlad 2025-05-06 20:30:51 +03:00
parent 1cbb64f2f6
commit 1544938c00
4 changed files with 540 additions and 4 deletions

260
app.py
View File

@ -13,6 +13,15 @@ import json
from expiring_dict import ExpiringDict
import uuid
from flask_swagger_ui import get_swaggerui_blueprint
import datetime
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch, mm
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
import sys
@ -748,9 +757,6 @@ def api_reqimage():
return 'bad request!', 500
@app.route("/ads.txt")
def ads_txt():
try:
@ -833,6 +839,254 @@ def serve_static(filename):
app.logger.error(f'Ошибка доступа к файлу {filename}: {str(e)}')
return 'File not found', 404
@app.route("/salvagereport/<string:vin>")
def generate_pdf_report(vin):
try:
conn = pool.acquire()
cur = conn.cursor()
user_ip = get_ip(request)
try:
returnVal = cur.callfunc("checkip", int, [user_ip, request.headers.get("CF-IPCountry", 'None'), get_addr(user_ip), 1, 0, 0])
except:
app.logger.error(traceback.format_exc())
# Get vehicle details
cur.execute("""select 'None', COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='26'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Make'),'UNKNOWN') make,
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='28'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model'),'UNKNOWN') model,
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='29'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Model Year'),'UNKNOWN') year,
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='5'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Body Class'),'UNKNOWN') body,
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='13'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Engine Model'),'UNKNOWN') engine,
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='9'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Engine Number of Cylinders'),'UNKNOWN') celindr,
COALESCE((select value from m_JSONS_FROM_NHTSA v3 where v3.svin =s.svin and v3.variableid ='15'),(select val from vind2 where svin = substr(s.vin, 1, 8) || '*' || substr(s.vin, 10, 2) and varb = 'Drive Type'),'-') drive
from (select substr(:p1,1,10) svin, :p1 vin from dual) s """, {'p1': vin})
det = cur.fetchall()
# Get salvage history
cur.execute("""select rownum, t.vin, t.title, t.odo, t.odos, t.dem1, t.dem2, t.year||'/'||t.month from salvagedb t where vin = :p1 and svin = substr(:p1,1,10) """, {'p1': vin})
his = cur.fetchall()
# Generate current date and report ID
now = datetime.datetime.now()
report_date = now.strftime("%Y-%m-%d") # Только дата без времени
report_id = uuid.uuid4().hex[:8].upper()
current_year = now.year
try:
# Создаем буфер для PDF
buffer = io.BytesIO()
# Создаем PDF документ
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
rightMargin=20*mm,
leftMargin=20*mm,
topMargin=15*mm,
bottomMargin=20*mm
)
# Создаем стили для текста
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(
name='ReportTitle',
parent=styles['Heading1'],
fontSize=16,
alignment=TA_CENTER
))
styles.add(ParagraphStyle(
name='ReportSubtitle',
parent=styles['Normal'],
fontSize=12,
alignment=TA_CENTER,
textColor=colors.HexColor('#0066CC') # Синий цвет для акцента
))
styles.add(ParagraphStyle(
name='Right',
parent=styles['Normal'],
fontSize=10,
alignment=TA_RIGHT
))
styles.add(ParagraphStyle(
name='Disclaimer',
parent=styles['Normal'],
fontSize=8,
textColor=colors.gray
))
styles.add(ParagraphStyle(
name='SalvageDBFooter',
parent=styles['Normal'],
fontSize=9,
alignment=TA_CENTER,
textColor=colors.HexColor('#0066CC'), # Синий цвет для акцента
fontName='Helvetica-Bold'
))
# Список элементов для PDF
elements = []
# Путь к логотипу
# logo_path = os.path.join(app_path, 'static', 'icons', 'icon-144x144.png')
# Добавляем логотип, если файл существует
# if os.path.exists(logo_path):
# logo_img = Image(logo_path, width=30*mm, height=30*mm)
# logo_img.hAlign = 'CENTER'
# elements.append(logo_img)
# elements.append(Spacer(1, 5*mm))
# Заголовок отчета с акцентом на SALVAGEDB.COM
elements.append(Paragraph("Vehicle History Report", styles['ReportTitle']))
elements.append(Paragraph("SALVAGEDB.COM - Comprehensive Vehicle History Check", styles['ReportSubtitle']))
elements.append(Spacer(1, 10*mm))
# Информация об отчете
elements.append(Paragraph(f"Report Date: {report_date}", styles['Right']))
elements.append(Paragraph(f"Report ID: {report_id}", styles['Right']))
elements.append(Spacer(1, 15*mm))
# Информация о транспортном средстве
elements.append(Paragraph("Vehicle Information", styles['Heading2']))
# Создаем таблицу с информацией о транспортном средстве
vehicle_data = [
["VIN", vin],
["Make", det[0][1]],
["Model", det[0][2]],
["Year", det[0][3]],
["Body Style", det[0][4]],
["Engine", det[0][5]],
["Cylinders", det[0][6]],
["Drive", det[0][7]]
]
vehicle_table = Table(vehicle_data, colWidths=[100, 350])
vehicle_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
('ALIGN', (0, 0), (0, -1), 'LEFT'),
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (0, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('BACKGROUND', (1, 0), (-1, -1), colors.white),
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
]))
elements.append(vehicle_table)
elements.append(Spacer(1, 10*mm))
# Информация об истории
if his:
elements.append(Paragraph("Salvage History", styles['Heading2']))
# Создаем стиль для ячеек с переносом текста
table_style = TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 9),
('BOTTOMPADDING', (0, 0), (-1, 0), 6),
('BACKGROUND', (0, 1), (-1, -1), colors.white),
('GRID', (0, 0), (-1, -1), 0.5, colors.black),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
('LEFTPADDING', (0, 0), (-1, -1), 3),
('RIGHTPADDING', (0, 0), (-1, -1), 3),
('FONTSIZE', (0, 1), (-1, -1), 8),
('WORDWRAP', (0, 0), (-1, -1), True),
])
# Оптимизированные заголовки без VIN
history_headers = ["Title", "Mileage", "Odometer Status", "Primary\nDamage", "Secondary\nDamage"]
# Формируем данные для таблицы истории
history_data = [history_headers]
for item in his:
title_value = item[2]
primary_damage = item[5] if item[5] else "N/A"
secondary_damage = item[6] if item[6] else "N/A"
# Добавляем строку с данными, убрав VIN
history_data.append([
title_value,
str(item[3]),
item[4],
primary_damage,
secondary_damage
])
# Устанавливаем ширину колонок оптимально для A4
available_width = doc.width # Доступная ширина страницы
col_widths = [
available_width * 0.20, # 20% для Title
available_width * 0.12, # 12% для Mileage
available_width * 0.18, # 18% для Odometer Status
available_width * 0.25, # 25% для Primary Damage
available_width * 0.25 # 25% для Secondary Damage
]
# Создаем таблицу с правильными размерами
history_table = Table(history_data, colWidths=col_widths, repeatRows=1)
history_table.setStyle(table_style)
elements.append(history_table)
else:
elements.append(Paragraph("Salvage History", styles['Heading2']))
elements.append(Paragraph("Salvage history not found.", styles['Normal']))
elements.append(Spacer(1, 15*mm))
# Дисклеймер
disclaimer_text = """
Disclaimer: This report provides information about salvage or junk vehicles; damage from hail, flood or fire;
mileage discrepancies or odometer rollback; and gray market vehicles. We do not claim that the car got in our
databank has salvage title, but the fact that it has been damaged for sure. Our site helps people avoid buying
a damaged vehicle in the past.
"""
elements.append(Paragraph(disclaimer_text, styles['Disclaimer']))
# Футер с акцентом на SALVAGEDB.COM
elements.append(Spacer(1, 15*mm))
elements.append(Paragraph(f"© {current_year} SALVAGEDB.COM - All Rights Reserved", styles['SalvageDBFooter']))
elements.append(Paragraph("Visit SALVAGEDB.COM for more information about vehicle history", styles['SalvageDBFooter']))
elements.append(Paragraph("This report is provided as is without any guarantees or warranty.", styles['Disclaimer']))
# Строим PDF документ
doc.build(elements)
# Получаем содержимое буфера
pdf_data = buffer.getvalue()
buffer.close()
# Создаем HTTP-ответ с PDF
response = make_response(pdf_data)
response.headers['Content-Type'] = 'application/pdf'
response.headers['Content-Disposition'] = f'attachment; filename=vehicle_report_{vin}.pdf'
return response
except Exception as pdf_error:
# Если возникла ошибка при генерации PDF, логируем ее
app.logger.error(f"PDF generation error: {str(pdf_error)}")
app.logger.error(traceback.format_exc())
# Показываем печатную версию в браузере
return render_template(
'report_printable.html',
vin=vin,
det=det,
his=his,
report_date=report_date,
report_id=report_id,
current_year=current_year,
error_message=str(pdf_error)
)
except Exception as e:
app.logger.error(traceback.format_exc())
return 'Report generation failed: ' + str(e), 500
if __name__ == '__main__':
# Start a pool of connections
pool = start_pool()

View File

@ -1,3 +1,4 @@
flask
oracledb
cacheing
cacheing
reportlab

View File

@ -0,0 +1,274 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vehicle History Report - {{report_id}}</title>
<style>
@media print {
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
}
.no-print {
display: none !important;
}
@page {
size: A4;
margin: 10mm;
}
}
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
border-bottom: 2px solid #333;
padding-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
}
.header p {
font-size: 14px;
color: #666;
}
.report-date {
text-align: right;
font-size: 12px;
margin-bottom: 20px;
}
.section {
margin-bottom: 30px;
}
.section-title {
font-size: 18px;
margin-bottom: 15px;
padding-bottom: 5px;
border-bottom: 1px solid #ddd;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
table, th, td {
border: 1px solid #ddd;
}
th, td {
padding: 10px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
.footer {
margin-top: 50px;
text-align: center;
font-size: 12px;
color: #666;
border-top: 1px solid #ddd;
padding-top: 20px;
}
.disclaimer {
font-size: 10px;
margin-top: 20px;
padding: 10px;
background-color: #f9f9f9;
border: 1px solid #ddd;
}
.print-controls {
background-color: #f5f5f5;
padding: 15px;
margin-bottom: 20px;
text-align: center;
border-radius: 5px;
}
.btn {
display: inline-block;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: middle;
user-select: none;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
text-decoration: none;
margin-right: 10px;
cursor: pointer;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
.btn-secondary {
color: #fff;
background-color: #6c757d;
border-color: #6c757d;
}
.error-message {
background-color: #f8d7da;
color: #721c24;
padding: 10px;
margin-bottom: 20px;
border-radius: 5px;
border: 1px solid #f5c6cb;
}
.error-details {
font-family: monospace;
font-size: 12px;
background: #f8f9fa;
padding: 10px;
border: 1px solid #ddd;
max-height: 200px;
overflow: auto;
margin-top: 10px;
}
</style>
<script>
function printReport() {
window.print();
}
function generatePDF() {
// Уведомляем пользователя, что используем встроенную функцию браузера
alert("Используйте функцию 'Сохранить как PDF' в диалоговом окне печати вашего браузера.");
window.print();
}
</script>
</head>
<body>
<div class="container">
<div class="print-controls no-print">
<button class="btn btn-primary" onclick="printReport()">Print Report</button>
<button class="btn btn-secondary" onclick="generatePDF()">Save as PDF</button>
<a class="btn btn-secondary" href="javascript:history.back()">Back to Vehicle Details</a>
</div>
{% if error_message %}
<div class="error-message no-print">
<h3>PDF Generation Error</h3>
<p>The system encountered an error while generating the PDF. You can print this page or save it as PDF using your browser's print function.</p>
<div class="error-details">
{{ error_message }}
</div>
<p>Note: To install WeasyPrint dependencies on Windows, see the <a href="https://doc.courtbouillon.org/weasyprint/stable/first_steps.html#windows" target="_blank">installation instructions</a>.</p>
</div>
{% endif %}
<div class="header">
<h1>Vehicle History Report</h1>
<p>SALVAGEDB.COM - Comprehensive Vehicle History Check</p>
</div>
<div class="report-date">
<p>Report Date: {{report_date}}</p>
<p>Report ID: {{report_id}}</p>
</div>
<div class="section">
<h2 class="section-title">Vehicle Information</h2>
<table>
<tbody>
<tr>
<td><strong>VIN</strong></td>
<td>{{vin}}</td>
</tr>
<tr>
<td><strong>Make</strong></td>
<td>{{det[0][1]}}</td>
</tr>
<tr>
<td><strong>Model</strong></td>
<td>{{det[0][2]}}</td>
</tr>
<tr>
<td><strong>Year</strong></td>
<td>{{det[0][3]}}</td>
</tr>
<tr>
<td><strong>Body Style</strong></td>
<td>{{det[0][4]}}</td>
</tr>
<tr>
<td><strong>Engine</strong></td>
<td>{{det[0][5]}}</td>
</tr>
<tr>
<td><strong>Cylinders</strong></td>
<td>{{det[0][6]}}</td>
</tr>
<tr>
<td><strong>Drive</strong></td>
<td>{{det[0][7]}}</td>
</tr>
</tbody>
</table>
</div>
{% if his %}
<div class="section">
<h2 class="section-title">Salvage History</h2>
<table>
<thead>
<tr>
<th>VIN</th>
<th>Title</th>
<th>Odometer</th>
<th>Odometer Status</th>
<th>Primary Damage</th>
<th>Secondary Damage</th>
</tr>
</thead>
<tbody>
{% for it in his %}
<tr>
<td>{{it[1]}}</td>
<td>{{it[2]}}</td>
<td>{{it[3]}}</td>
<td>{{it[4]}}</td>
<td>{{it[5]}}</td>
<td>{{it[6]}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="section">
<div class="alert-warning" style="padding: 15px; background-color: #fcf8e3; border: 1px solid #faebcc; color: #8a6d3b;">
<h2 style="font-size: 16px; margin: 0;">Salvage history not found.</h2>
</div>
</div>
{% endif %}
<div class="disclaimer">
<p><strong>Disclaimer:</strong> This report provides information about salvage or junk vehicles; damage from hail, flood or fire; mileage discrepancies or odometer rollback; and gray market vehicles. We do not claim that the car got in our databank has salvage title, but the fact that it has been damaged for sure. Our site helps people avoid buying a damaged vehicle in the past.</p>
</div>
<div class="footer">
<p>&copy; {{current_year}} SalvageDB.com - All Rights Reserved</p>
<p>This report is provided as is without any guarantees or warranty. For additional assistance, please contact us.</p>
</div>
</div>
</body>
</html>

View File

@ -54,6 +54,13 @@
</tr>
</tbody>
</table>
<!-- PDF Report Download Button -->
<div class="mt-3">
<a href="/salvagereport/{{vin}}" class="btn btn-primary w-100">
<i class="fas fa-file-pdf me-2"></i> Download PDF Report
</a>
</div>
</div>
</div>
</div>