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.services.credit import CreditService 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), ): credit_svc = CreditService(db) ok, balance = await credit_svc.deduct(user_id, "quotation") if not ok: raise HTTPException( status_code=402, detail=f"次数不足 (剩余 {balance:.1f}, 需要 2)" ) 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"', }, )