19 KiB
Executable file
19 KiB
Executable file
تحلیل و پیشنهادات بهبود سیستم قالبهای PDF
📋 خلاصه وضعیت فعلی
سیستم فعلی شامل:
- Backend: سرویس
ReportTemplateServiceبرای مدیریت قالبهای ذخیرهشده در دیتابیس - Template Engine: Jinja2 با پشتیبانی از Builder و HTML مستقیم
- PDF Generator: WeasyPrint
- Frontend: رابط کاربری Flutter برای مدیریت قالبها
🔧 پیشنهادات بهبود فنی
1. مدیریت خطا و اعتبارسنجی
مشکل فعلی:
- خطاهای قالب به صورت silent نادیده گرفته میشوند
- اعتبارسنجی محدود برای قالبها
- عدم وجود لاگ مناسب برای خطاهای رندر
پیشنهادات:
# در report_template_service.py
@staticmethod
def render_with_template(
template: ReportTemplate,
context: Dict[str, Any],
) -> str:
try:
# ... کد فعلی
except TemplateSyntaxError as e:
logger.error(f"Template syntax error in {template.id}: {e}")
raise ApiError("TEMPLATE_SYNTAX_ERROR", f"خطای دستور در قالب: {e}", http_status=400)
except UndefinedError as e:
logger.warning(f"Undefined variable in template {template.id}: {e}")
# میتوانیم مقدار پیشفرض برگردانیم یا خطا بدهیم
raise ApiError("TEMPLATE_VARIABLE_ERROR", f"متغیر تعریف نشده: {e}", http_status=400)
except Exception as e:
logger.exception(f"Template rendering error: {e}")
raise ApiError("TEMPLATE_RENDER_ERROR", "خطا در رندر قالب", http_status=500)
اعتبارسنجی قالب:
@staticmethod
def validate_template(content_html: str, context: Dict[str, Any] = None) -> List[str]:
"""اعتبارسنجی قالب و برگرداندن لیست خطاها"""
errors = []
try:
env = SandboxedEnvironment(loader=BaseLoader(), autoescape=True)
template_obj = env.from_string(content_html)
# تست رندر با context خالی یا نمونه
test_context = context or {}
template_obj.render(**test_context)
except TemplateSyntaxError as e:
errors.append(f"خطای دستور: {e.message} در خط {e.lineno}")
except Exception as e:
errors.append(f"خطای رندر: {str(e)}")
return errors
2. بهبود مدیریت فونتها
مشکل فعلی:
- استفاده از Tahoma به جای Vazirmatn (طبق README)
- عدم پشتیبانی از فونتهای سفارشی
- FontConfiguration بدون تنظیمات خاص
پیشنهادات:
# در documents.py
from weasyprint.text.fonts import FontConfiguration
def get_font_config() -> FontConfiguration:
"""پیکربندی فونت برای PDF فارسی"""
font_config = FontConfiguration()
# میتوان فونتهای سفارشی را اضافه کرد
return font_config
# در base.html - بهبود CSS
@font-face {
font-family: 'Vazirmatn';
src: url('/static/fonts/Vazirmatn-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
body {
font-family: 'Vazirmatn', 'Tahoma', 'Arial', sans-serif;
font-size: 12px;
line-height: 1.6; /* بهبود خوانایی */
}
3. سیستم کش برای قالبها
پیشنهاد:
from functools import lru_cache
from typing import Optional
class ReportTemplateService:
_template_cache: Dict[int, Tuple[ReportTemplate, float]] = {}
CACHE_TTL = 300 # 5 دقیقه
@staticmethod
def get_template_cached(
db: Session,
template_id: int,
business_id: Optional[int] = None
) -> Optional[ReportTemplate]:
"""دریافت قالب با کش"""
import time
cache_key = template_id
if cache_key in ReportTemplateService._template_cache:
template, cached_time = ReportTemplateService._template_cache[cache_key]
if time.time() - cached_time < ReportTemplateService.CACHE_TTL:
return template
template = ReportTemplateService.get_template(db, template_id, business_id)
if template:
ReportTemplateService._template_cache[cache_key] = (template, time.time())
return template
@staticmethod
def invalidate_cache(template_id: int):
"""پاک کردن کش یک قالب"""
ReportTemplateService._template_cache.pop(template_id, None)
4. بهبود فیلترهای Jinja2
پیشنهادات فیلترهای جدید:
# در report_template_service.py - render_with_template
# فیلتر فرمت اعداد فارسی
def _persian_number(v):
"""تبدیل اعداد انگلیسی به فارسی"""
persian_digits = '۰۱۲۳۴۵۶۷۸۹'
english_digits = '0123456789'
s = str(v)
for en, fa in zip(english_digits, persian_digits):
s = s.replace(en, fa)
return s
env.filters["persian"] = _persian_number
# فیلتر فرمت شماره حساب
def _account_number(v, format_type="standard"):
"""فرمت شماره حساب: 1234-567-890"""
s = str(v).replace("-", "").replace(" ", "")
if format_type == "standard" and len(s) >= 9:
return f"{s[:4]}-{s[4:7]}-{s[7:]}"
return s
env.filters["account"] = _account_number
# فیلتر خلاصه متن
def _truncate(v, length=50, suffix="..."):
"""کوتاه کردن متن"""
s = str(v)
if len(s) <= length:
return s
return s[:length] + suffix
env.filters["truncate"] = _truncate
# فیلتر شرطی برای نمایش/مخفی کردن
def _show_if(condition, true_val, false_val=""):
"""نمایش شرطی"""
return true_val if condition else false_val
env.filters["show_if"] = _show_if
5. پشتیبانی از تصاویر و Assets
مشکل فعلی:
- عدم پشتیبانی مناسب از تصاویر در قالبهای document
- Assets فقط در builder پشتیبانی میشود
پیشنهاد:
# در documents.py - get_document_pdf_endpoint
# اضافه کردن پشتیبانی از assets
business_logo = None
try:
if hasattr(business, 'logo_url') and business.logo_url:
business_logo = business.logo_url
except Exception:
pass
template_context = {
# ... موجود
"assets": {
"images": {
"logo": business_logo or "",
# میتوان تصاویر دیگر را اضافه کرد
}
}
}
6. بهبود مدیریت تاریخ و تقویم
پیشنهاد:
# در documents.py
from app.core.calendar import CalendarConverter, CalendarType
# تبدیل تاریخ سند به تقویم شمسی
document_date_jalali = None
if doc.get("document_date"):
try:
dt = datetime.datetime.fromisoformat(str(doc.get("document_date")))
formatted = CalendarConverter.format_datetime(dt, "jalali")
document_date_jalali = formatted['formatted']
except Exception:
pass
template_context = {
# ... موجود
"document_date_jalali": document_date_jalali,
"document_date_miladi": doc.get("document_date"),
}
7. بهبود ساختار کد endpoint
پیشنهاد:
# استخراج منطق به یک تابع جداگانه
async def _generate_document_pdf(
db: Session,
document_id: int,
business_id: int,
template_id: Optional[int] = None,
locale: str = "fa",
paper_size: Optional[str] = None,
orientation: Optional[str] = None,
) -> bytes:
"""تولید PDF سند - منطق اصلی"""
# ... تمام منطق تولید PDF
return pdf_bytes
@router.get("/documents/{document_id}/pdf")
async def get_document_pdf_endpoint(...):
"""Endpoint wrapper"""
# بررسی دسترسی
doc = get_document(db, document_id)
# ...
# تولید PDF
pdf_bytes = await _generate_document_pdf(
db=db,
document_id=document_id,
business_id=business_id,
template_id=template_id,
locale=locale,
paper_size=paper_size,
orientation=orientation,
)
return Response(...)
🎨 پیشنهادات بهبود ظاهری
1. بهبود قالب base.html
پیشنهادات CSS:
/* بهبود رنگبندی و تم */
:root {
--primary: #366092;
--primary-dark: #2a4a6f;
--secondary: #6c757d;
--success: #28a745;
--danger: #dc3545;
--border: #dee2e6;
--text: #212529;
--muted: #6c757d;
--zebra: #f8f9fa;
--background: #ffffff;
}
/* بهبود جدول */
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
thead {
background: linear-gradient(to bottom, var(--primary), var(--primary-dark));
color: #fff;
}
th {
padding: 12px 8px;
font-weight: 600;
text-transform: uppercase;
font-size: 10px;
letter-spacing: 0.5px;
}
tbody tr {
transition: background-color 0.2s;
}
tbody tr:hover {
background-color: #e9ecef;
}
/* بهبود header */
.header {
background: linear-gradient(to right, #f8f9fa, #ffffff);
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.title {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
2. بهبود قالب detail.html برای اسناد
پیشنهادات:
{% extends "pdf/base.html" %}
{% block content %}
<div class="document-header">
<div class="header-left">
<div class="document-type-badge">
{{ document.document_type_name or ('سند' if is_fa else 'Document') }}
</div>
<div class="document-info">
<div class="info-row">
<span class="info-label">{{ 'کد سند' if is_fa else 'Code' }}:</span>
<span class="info-value">{{ code or '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">{{ 'تاریخ' if is_fa else 'Date' }}:</span>
<span class="info-value">{{ document_date_jalali or document_date or '-' }}</span>
</div>
{% if description %}
<div class="info-row">
<span class="info-label">{{ 'توضیحات' if is_fa else 'Description' }}:</span>
<span class="info-value">{{ description }}</span>
</div>
{% endif %}
</div>
</div>
<div class="header-right">
<div class="business-info">
<div class="business-name">{{ business_name or '-' }}</div>
<div class="generated-at">
{{ 'تولید شده در' if is_fa else 'Generated at' }}: {{ generated_at }}
</div>
</div>
</div>
</div>
<div class="table-container">
<table class="document-lines">
<thead>
<tr>
<th style="width: 50%;">{{ 'شرح' if is_fa else 'Description' }}</th>
<th style="width: 25%;">{{ 'بدهکار' if is_fa else 'Debit' }}</th>
<th style="width: 25%;">{{ 'بستانکار' if is_fa else 'Credit' }}</th>
</tr>
</thead>
<tbody>
{% for line in lines or [] %}
<tr>
<td>{{ line.description or '-' }}</td>
<td class="amount">{{ line.debit | money(0) | ltr | safe if line.debit else '-' }}</td>
<td class="amount">{{ line.credit | money(0) | ltr | safe if line.credit else '-' }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr class="totals-row">
<td><strong>{{ 'جمع' if is_fa else 'Total' }}</strong></td>
<td class="amount">
<strong>{{ lines | sum(attribute='debit') | money(0) | ltr | safe }}</strong>
</td>
<td class="amount">
<strong>{{ lines | sum(attribute='credit') | money(0) | ltr | safe }}</strong>
</td>
</tr>
</tfoot>
</table>
</div>
{% if document.notes %}
<div class="notes-section">
<h4>{{ 'یادداشتها' if is_fa else 'Notes' }}</h4>
<p>{{ document.notes }}</p>
</div>
{% endif %}
{% endblock %}
CSS اضافی:
.document-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.document-type-badge {
display: inline-block;
padding: 6px 12px;
background: var(--primary);
color: white;
border-radius: 4px;
font-weight: 600;
font-size: 14px;
margin-bottom: 12px;
}
.info-row {
margin: 6px 0;
}
.info-label {
font-weight: 600;
color: var(--muted);
margin-inline-end: 8px;
}
.info-value {
color: var(--text);
}
.table-container {
margin: 20px 0;
overflow-x: auto;
}
.totals-row {
background: #e9ecef;
font-weight: 600;
border-top: 2px solid var(--primary);
}
.notes-section {
margin-top: 24px;
padding: 16px;
background: #fff3cd;
border-right: 4px solid #ffc107;
border-radius: 4px;
}
3. پشتیبانی از تمهای مختلف
پیشنهاد:
# اضافه کردن فیلد theme به ReportTemplate
# themes: 'default', 'minimal', 'professional', 'colorful'
# در render_with_template
theme = getattr(template, 'theme', 'default') or 'default'
context['theme'] = theme
# در قالب
{% if theme == 'minimal' %}
<!-- استایل مینیمال -->
{% elif theme == 'professional' %}
<!-- استایل حرفهای -->
{% endif %}
4. بهبود صفحهبندی و Break Points
پیشنهادات CSS:
/* جلوگیری از شکستن ردیفهای جدول */
tbody tr {
page-break-inside: avoid;
break-inside: avoid;
}
/* Header و Footer در هر صفحه */
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
/* بهبود فاصلهگذاری صفحات */
@page {
margin: 2cm 1.5cm;
size: A4 portrait;
@top-center {
content: "{{ business_name }}";
font-size: 10px;
color: #666;
}
@bottom-center {
content: "صفحه " counter(page) " از " counter(pages);
font-size: 10px;
color: #666;
}
}
📱 پیشنهادات بهبود Frontend (Flutter)
1. بهبود DocumentService
پیشنهاد:
// در document_service.dart
Future<Uint8List> downloadPdf({
required int documentId,
int? templateId,
String? paperSize,
String? orientation,
}) async {
try {
final queryParams = <String, dynamic>{};
if (templateId != null) queryParams['template_id'] = templateId;
if (paperSize != null) queryParams['paper_size'] = paperSize;
if (orientation != null) queryParams['orientation'] = orientation;
final response = await _apiClient.get(
'/documents/$documentId/pdf',
queryParameters: queryParams,
options: Options(
responseType: ResponseType.bytes,
),
);
return response.data as Uint8List;
} catch (e) {
if (e is DioException) {
throw Exception(e.response?.data['message'] ?? 'خطا در دریافت فایل PDF');
}
rethrow;
}
}
// ذخیره فایل
Future<String> savePdfToFile(Uint8List pdfBytes, String filename) async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/$filename');
await file.writeAsBytes(pdfBytes);
return file.path;
}
2. UI برای انتخاب قالب
پیشنهاد:
// Dialog برای انتخاب قالب قبل از دانلود PDF
Future<int?> showTemplateSelectorDialog(
BuildContext context,
int businessId,
String moduleKey,
String? subtype,
) async {
final service = ReportTemplateService(apiClient);
final templates = await service.listTemplates(
businessId: businessId,
moduleKey: moduleKey,
subtype: subtype,
status: 'published',
);
return showDialog<int>(
context: context,
builder: (context) => AlertDialog(
title: const Text('انتخاب قالب'),
content: SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: templates.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return ListTile(
title: const Text('قالب پیشفرض'),
leading: const Icon(Icons.description),
onTap: () => Navigator.pop(context, null),
);
}
final template = templates[index - 1];
return ListTile(
title: Text(template['name'] ?? ''),
subtitle: Text(template['description'] ?? ''),
leading: template['is_default'] == true
? const Icon(Icons.star, color: Colors.amber)
: const Icon(Icons.description_outlined),
onTap: () => Navigator.pop(context, template['id'] as int),
);
},
),
),
),
);
}
🚀 پیشنهادات بهبود عملکرد
1. تولید PDF به صورت Async
پیشنهاد:
from celery import shared_task
@shared_task
def generate_document_pdf_async(
document_id: int,
template_id: Optional[int] = None,
user_id: int = None,
) -> str:
"""تولید PDF در پسزمینه و ذخیره در storage"""
# تولید PDF
# ذخیره در S3 یا local storage
# برگرداندن URL
pass
# در endpoint
@router.post("/documents/{document_id}/pdf/async")
async def generate_pdf_async(...):
"""درخواست تولید PDF در پسزمینه"""
task = generate_document_pdf_async.delay(document_id, template_id)
return {"task_id": task.id, "status": "processing"}
2. فشردهسازی PDF
پیشنهاد:
from weasyprint import HTML
import io
def optimize_pdf(pdf_bytes: bytes) -> bytes:
"""فشردهسازی PDF (اختیاری)"""
# میتوان از PyPDF2 یا pypdf استفاده کرد
try:
from pypdf import PdfWriter, PdfReader
reader = PdfReader(io.BytesIO(pdf_bytes))
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
# فشردهسازی
output = io.BytesIO()
writer.write(output)
return output.getvalue()
except Exception:
return pdf_bytes # در صورت خطا، فایل اصلی را برگردان
📝 خلاصه اولویتها
اولویت بالا (فوری):
- ✅ بهبود مدیریت خطا و لاگگذاری
- ✅ اضافه کردن فیلترهای مفید Jinja2
- ✅ بهبود CSS و ظاهر قالبها
- ✅ پشتیبانی از تقویم شمسی در قالب
اولویت متوسط:
- ✅ سیستم کش برای قالبها
- ✅ بهبود DocumentService در Flutter
- ✅ UI انتخاب قالب
- ✅ پشتیبانی از Assets و تصاویر
اولویت پایین (آینده):
- ⏳ تولید PDF به صورت Async
- ⏳ پشتیبانی از تمهای مختلف
- ⏳ فشردهسازی PDF
- ⏳ Preview قالب در UI