7b62c2f8b4
## H5 底部导航修复 (Bug #10) - 精简 App.vue,移除重复 tabbar,仅保留全局样式 - uni-page 设置 height: calc(100% - 50px) + overflow-y: auto - 内容区域精确停在底部导航上方,独立滚动不再叠加 - 恢复 custom-tab-bar 组件 ## 项目进度文档 - PROGRESS.md 更新至 10 个 Bug 修复 - 新增 H5 底部导航修复记录 - 新增历史变更条目
230 lines
5.4 KiB
Python
230 lines
5.4 KiB
Python
from typing import Optional, Dict, Any, List
|
|
from datetime import datetime
|
|
import os
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from weasyprint import HTML
|
|
HAS_WEASYPRINT = True
|
|
except ImportError:
|
|
HAS_WEASYPRINT = False
|
|
logger.warning("weasyprint not installed, PDF generation disabled")
|
|
|
|
|
|
QUOTATION_TEMPLATE = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
@page {{
|
|
size: A4;
|
|
margin: 2cm;
|
|
}}
|
|
body {{
|
|
font-family: 'Helvetica Neue', Arial, sans-serif;
|
|
font-size: 12pt;
|
|
color: #333;
|
|
line-height: 1.6;
|
|
}}
|
|
.header {{
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
border-bottom: 2px solid #1890ff;
|
|
padding-bottom: 20px;
|
|
}}
|
|
.header h1 {{
|
|
font-size: 24pt;
|
|
color: #1890ff;
|
|
margin: 0;
|
|
}}
|
|
.header .number {{
|
|
font-size: 14pt;
|
|
color: #666;
|
|
}}
|
|
.info-grid {{
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 30px;
|
|
}}
|
|
.info-block {{
|
|
width: 48%;
|
|
}}
|
|
.info-block h3 {{
|
|
font-size: 11pt;
|
|
color: #1890ff;
|
|
margin-bottom: 8px;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
padding-bottom: 4px;
|
|
}}
|
|
.info-block p {{
|
|
margin: 4px 0;
|
|
font-size: 10pt;
|
|
}}
|
|
table {{
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 30px;
|
|
}}
|
|
th {{
|
|
background: #1890ff;
|
|
color: white;
|
|
padding: 10px 8px;
|
|
text-align: left;
|
|
font-size: 10pt;
|
|
}}
|
|
td {{
|
|
padding: 8px;
|
|
border-bottom: 1px solid #e8e8e8;
|
|
font-size: 10pt;
|
|
}}
|
|
.amount-row td {{
|
|
text-align: right;
|
|
padding: 4px 8px;
|
|
border: none;
|
|
}}
|
|
.total-row td {{
|
|
font-weight: bold;
|
|
font-size: 12pt;
|
|
border-top: 2px solid #333;
|
|
}}
|
|
.terms {{
|
|
margin-top: 30px;
|
|
padding-top: 15px;
|
|
border-top: 1px solid #e8e8e8;
|
|
}}
|
|
.terms h3 {{
|
|
font-size: 11pt;
|
|
color: #1890ff;
|
|
}}
|
|
.terms p {{
|
|
font-size: 9pt;
|
|
color: #666;
|
|
margin: 4px 0;
|
|
}}
|
|
.footer {{
|
|
text-align: center;
|
|
margin-top: 40px;
|
|
font-size: 9pt;
|
|
color: #999;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>QUOTATION</h1>
|
|
<p class="number">#{quotation_number}</p>
|
|
</div>
|
|
|
|
<div class="info-grid">
|
|
<div class="info-block">
|
|
<h3>Bill To</h3>
|
|
<p>{customer_name}</p>
|
|
<p>{customer_company}</p>
|
|
<p>{customer_country}</p>
|
|
</div>
|
|
<div class="info-block">
|
|
<h3>Quote Details</h3>
|
|
<p>Date: {date}</p>
|
|
<p>Valid Until: {valid_until}</p>
|
|
<p>Currency: {currency}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Item</th>
|
|
<th>Description</th>
|
|
<th>Qty</th>
|
|
<th>Unit</th>
|
|
<th>Unit Price</th>
|
|
<th>Total</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{items_rows}
|
|
</tbody>
|
|
</table>
|
|
|
|
<table>
|
|
<tr class="amount-row"><td colspan="4"></td><td>Subtotal:</td><td>{subtotal}</td></tr>
|
|
<tr class="amount-row"><td colspan="4"></td><td>Discount:</td><td>-{discount}</td></tr>
|
|
<tr class="amount-row"><td colspan="4"></td><td>Shipping:</td><td>{shipping}</td></tr>
|
|
<tr class="total-row"><td colspan="4"></td><td>TOTAL:</td><td>{total}</td></tr>
|
|
</table>
|
|
|
|
<div class="terms">
|
|
<h3>Terms & Conditions</h3>
|
|
<p>Payment Terms: {payment_terms}</p>
|
|
<p>Delivery Terms: {delivery_terms}</p>
|
|
<p>Lead Time: {lead_time}</p>
|
|
{notes_html}
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p>Generated by TradeMate - {generated_at}</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
class PDFGenerator:
|
|
@staticmethod
|
|
def generate_quotation(data: Dict[str, Any]) -> Optional[bytes]:
|
|
if not HAS_WEASYPRINT:
|
|
return None
|
|
|
|
items = data.get("items", [])
|
|
items_rows = ""
|
|
for i, item in enumerate(items, 1):
|
|
items_rows += (
|
|
f"<tr>"
|
|
f"<td>{item.get('product_name', '')}</td>"
|
|
f"<td>{item.get('description', '') or ''}</td>"
|
|
f"<td>{item.get('quantity', 0)}</td>"
|
|
f"<td>{item.get('unit', 'pcs')}</td>"
|
|
f"<td>{item.get('unit_price', 0):.2f}</td>"
|
|
f"<td>{item.get('total_price', 0):.2f}</td>"
|
|
f"</tr>"
|
|
)
|
|
|
|
cur = data.get("currency", "USD")
|
|
subtotal = f"{cur} {data.get('subtotal', 0):.2f}"
|
|
discount = f"{cur} {data.get('discount', 0):.2f}" if data.get("discount") else f"{cur} 0.00"
|
|
shipping = f"{cur} {data.get('shipping', 0):.2f}" if data.get("shipping") else f"{cur} 0.00"
|
|
total = f"{cur} {data.get('total', 0):.2f}"
|
|
|
|
notes_html = ""
|
|
if data.get("notes"):
|
|
notes_html = f"<p>Notes: {data['notes']}</p>"
|
|
|
|
html = QUOTATION_TEMPLATE.format(
|
|
quotation_number=data.get("quotation_number", "N/A"),
|
|
customer_name=data.get("customer_name", ""),
|
|
customer_company=data.get("customer_company", "") or "",
|
|
customer_country=data.get("customer_country", "") or "",
|
|
date=data.get("date", datetime.utcnow().strftime("%Y-%m-%d")),
|
|
valid_until=data.get("valid_until", "N/A"),
|
|
currency=cur,
|
|
items_rows=items_rows,
|
|
subtotal=subtotal,
|
|
discount=discount,
|
|
shipping=shipping,
|
|
total=total,
|
|
payment_terms=data.get("payment_terms", "N/A"),
|
|
delivery_terms=data.get("delivery_terms", "N/A"),
|
|
lead_time=data.get("lead_time", "N/A"),
|
|
notes_html=notes_html,
|
|
generated_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"),
|
|
)
|
|
|
|
pdf = HTML(string=html).write_pdf()
|
|
return pdf
|
|
|
|
|
|
pdf_generator = PDFGenerator()
|