Files
trade-assistant/backend/app/api/v1/quotation.py
T
TradeMate Dev 4755cc75ba feat: 管理后台完整可用 + 注册登录记日志 + 提取信息结构化展示 + 微信配置就绪
- 管理后台用户/统计/日志/配置四页签全部对接真实后端API
- auth注册/登录/游客/微信登录事件写入usage_logs表
- 提取信息结果从原始JSON改为卡片式字段列表(中文标签)
- 管理后台搜索按钮增加加载态和结果数提示
- 配置WECHAT_APP_ID/WECHAT_APP_SECRET
- 客户/产品/报价单CRUD页面完整(导出导入批量操作)
2026-05-18 23:50:48 +08:00

246 lines
8.1 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query, Response, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Optional
from pydantic import BaseModel
from app.database import get_db
from app.services.quotation import QuotationService
from app.services.pdf_generator import pdf_generator
from app.services import export
from app.api.v1.deps import get_current_user_id
from app.models.quotation import Quotation
from app.models.customer import Customer
from sqlalchemy import select, and_
import logging
import io
logger = logging.getLogger(__name__)
try:
import openpyxl
HAS_OPENPYXL = True
except ImportError:
HAS_OPENPYXL = False
router = APIRouter()
class InquiryRequest(BaseModel):
inquiry_text: str
customer_id: Optional[str] = None
@router.post("/generate-from-inquiry")
async def generate_from_inquiry(
data: InquiryRequest,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
service = QuotationService(db)
result = await service.generate_from_inquiry(
user_id=user_id,
inquiry_text=data.inquiry_text,
customer_id=data.customer_id,
)
return result
@router.post("")
async def create_quotation(
data: dict,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
service = QuotationService(db)
try:
quotation = await service.create_quotation(user_id, data)
return quotation
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.get("")
async def list_quotations(
page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100),
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
service = QuotationService(db)
return await service.list_quotations(user_id, page, size)
@router.post("/import")
async def import_quotations(
file: UploadFile = File(...),
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
service = QuotationService(db)
content = await file.read()
filename = file.filename.lower()
if filename.endswith(".xlsx"):
if not HAS_OPENPYXL:
raise HTTPException(status_code=501, detail="openpyxl not installed, XLSX import unavailable")
wb = openpyxl.load_workbook(io.BytesIO(content))
ws = wb.active
rows = list(ws.iter_rows(min_row=2, values_only=True))
records = []
for row in rows:
if not row[0]:
continue
items = [{"product_name": "Imported Item", "quantity": 1, "unit_price": float(row[3]) if len(row) > 3 and row[3] else 0}]
records.append({
"title": str(row[0]),
"customer_id": str(row[1]) if len(row) > 1 and row[1] else "",
"currency": str(row[2]) if len(row) > 2 and row[2] in ("USD", "EUR", "GBP", "CNY") else "USD",
"items": items,
"status": "draft",
})
elif filename.endswith(".csv"):
import csv
reader = csv.DictReader(io.StringIO(content.decode("utf-8-sig")))
records = []
for row in reader:
title = row.get("Title", row.get("标题", "")).strip()
if not title:
continue
items = [{"product_name": "Imported Item", "quantity": 1, "unit_price": float(row.get("Total", row.get("总计", 0)))}]
records.append({
"title": title,
"customer_id": row.get("Customer", row.get("客户", "")),
"currency": row.get("Currency", row.get("货币", "USD")),
"items": items,
})
else:
raise HTTPException(status_code=400, detail="Unsupported file format. Use .xlsx or .csv")
imported = 0
errors = []
for rec in records:
try:
await service.create_quotation(user_id, rec)
imported += 1
except Exception as e:
errors.append(f"{rec.get('title', '')}: {str(e)}")
return {"imported": imported, "errors": errors}
@router.get("/{quotation_id}")
async def get_quotation(
quotation_id: str,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
service = QuotationService(db)
quotation = await service.get_quotation(user_id, quotation_id)
if not quotation:
raise HTTPException(status_code=404, detail="Quotation not found")
return quotation
@router.patch("/{quotation_id}/status")
async def update_quotation_status(
quotation_id: str,
data: dict,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
service = QuotationService(db)
quotation = await service.update_status(user_id, quotation_id, data.get("status", "draft"))
if not quotation:
raise HTTPException(status_code=404, detail="Quotation not found")
return quotation
@router.get("/export/csv")
async def export_quotations(
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
service = QuotationService(db)
result = await service.list_quotations(user_id, 1, 9999)
items = result.get("items", [])
csv_bytes = export.export_quotations_csv(items)
return Response(
content=csv_bytes,
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=quotations.csv"},
)
@router.get("/export/xlsx")
async def export_quotations_xlsx(
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
service = QuotationService(db)
result = await service.list_quotations(user_id, 1, 9999)
items = result.get("items", [])
xlsx_bytes = export.export_quotations_xlsx(items)
return Response(
content=xlsx_bytes,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename=quotations.xlsx"},
)
@router.get("/{quotation_id}/pdf")
async def export_quotation_pdf(
quotation_id: str,
user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db),
):
service = QuotationService(db)
quotation = await service.get_quotation(user_id, quotation_id)
if not quotation:
raise HTTPException(status_code=404, detail="Quotation not found")
result = await db.execute(
select(Customer).where(Customer.id == quotation["customer_id"])
)
customer = result.scalar_one_or_none()
pdf_data = pdf_generator.generate_quotation({
"quotation_number": f"{quotation_id[:8].upper()}",
"customer_name": customer.name if customer else "",
"customer_company": customer.company if customer else "",
"customer_country": customer.country if customer else "",
"date": quotation["created_at"][:10] if quotation.get("created_at") else "",
"valid_until": quotation.get("valid_until", ""),
"currency": quotation.get("currency", "USD"),
"items": quotation.get("items", []),
"subtotal": quotation.get("subtotal", 0),
"discount": quotation.get("discount", 0),
"shipping": quotation.get("shipping", 0),
"total": quotation.get("total", 0),
"payment_terms": quotation.get("payment_terms", ""),
"delivery_terms": quotation.get("delivery_terms", ""),
"lead_time": quotation.get("lead_time", ""),
"notes": quotation.get("notes", ""),
})
if not pdf_data:
raise HTTPException(status_code=501, detail="PDF generation not available (weasyprint not installed)")
service = QuotationService(db)
result = await db.execute(
select(Quotation).where(
and_(Quotation.id == quotation_id, Quotation.user_id == user_id)
)
)
q = result.scalar_one_or_none()
if q:
pdf_url = f"/quotations/{quotation_id}/pdf"
q.pdf_url = pdf_url
await db.flush()
return Response(
content=pdf_data,
media_type="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="quotation-{quotation_id[:8]}.pdf"',
},
)