Files
trade-assistant/backend/app/services/quotation.py
T
TradeMate Dev c6206787da Initial commit: TradeMate 外贸小助手 MVP
项目结构:
- backend/     Python FastAPI 后端
- uni-app/     uni-app跨端前端
- docs/        设计文档
- docker-compose.yml  Docker编排
- nginx/scripts/systemd 运维配置

已完成功能:
- 用户认证 (JWT)
- 智能翻译 + 回复建议
- 营销素材生成
- 客户管理 + 沉默检测
- 报价单管理
- 产品库管理
- 汇率换算
- 推送通知 (uni-push)
- WhatsApp Webhook框架
- Celery定时任务
2026-05-08 18:17:12 +08:00

167 lines
6.2 KiB
Python

from typing import Dict, Any, Optional, List
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from app.models.quotation import Quotation, QuotationItem
from app.models.customer import Customer
from app.models.user import Product
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class QuotationService:
def __init__(self, db: AsyncSession):
self.db = db
async def create_quotation(self, user_id: str, data: Dict[str, Any]) -> Dict:
items_data = data.pop("items", [])
if data.get("customer_id"):
cust_result = await self.db.execute(
select(Customer).where(
and_(Customer.id == data["customer_id"], Customer.user_id == user_id)
)
)
if not cust_result.scalar_one_or_none():
raise ValueError("Customer not found")
q = Quotation(user_id=user_id, **data)
self.db.add(q)
await self.db.flush()
total = 0
for item_data in items_data:
item_total = item_data.get("quantity", 0) * item_data.get("unit_price", 0)
item = QuotationItem(
quotation_id=q.id,
product_name=item_data["product_name"],
description=item_data.get("description"),
quantity=item_data["quantity"],
unit_price=item_data["unit_price"],
total_price=item_total,
unit=item_data.get("unit", "pcs"),
)
self.db.add(item)
total += item_total
q.subtotal = total
q.total = total - (data.get("discount", 0)) + (data.get("shipping", 0))
await self.db.flush()
return await self._to_dict(q)
async def get_quotation(self, user_id: str, quotation_id: str) -> Optional[Dict]:
result = await self.db.execute(
select(Quotation).where(
and_(Quotation.id == quotation_id, Quotation.user_id == user_id)
)
)
q = result.scalar_one_or_none()
return await self._to_dict(q) if q else None
async def list_quotations(self, user_id: str, page: int = 1, size: int = 20) -> Dict[str, Any]:
from sqlalchemy import func
query = select(Quotation).where(Quotation.user_id == user_id).order_by(Quotation.created_at.desc()).offset((page - 1) * size).limit(size)
count_query = select(func.count()).select_from(Quotation).where(Quotation.user_id == user_id)
total = await self.db.execute(count_query)
result = await self.db.execute(query)
quotations = result.scalars().all()
items = []
for q in quotations:
items.append(await self._to_dict(q))
return {"items": items, "total": total.scalar(), "page": page, "size": size}
async def update_status(self, user_id: str, quotation_id: str, status: str) -> Optional[Dict]:
result = await self.db.execute(
select(Quotation).where(
and_(Quotation.id == quotation_id, Quotation.user_id == user_id)
)
)
q = result.scalar_one_or_none()
if not q:
return None
q.status = status
if status == "sent":
q.sent_at = datetime.utcnow()
await self.db.flush()
return await self._to_dict(q)
async def generate_quotation_text(self, q: Quotation) -> str:
items_result = await self.db.execute(
select(QuotationItem).where(QuotationItem.quotation_id == q.id)
)
items = items_result.scalars().all()
lines = [f"QUOTATION", f"", f"Date: {datetime.utcnow().strftime('%Y-%m-%d')}"]
if q.valid_until:
lines.append(f"Valid until: {q.valid_until}")
lines.append(f"")
lines.append(f"{'Item':<30} {'Qty':<10} {'Unit Price':<15} {'Total':<15}")
lines.append("-" * 70)
for item in items:
lines.append(f"{item.product_name:<30} {item.quantity:<10} ${item.unit_price:<12.2f} ${item.total_price:<10.2f}")
lines.append("-" * 70)
if q.subtotal:
lines.append(f"{'Subtotal':>55} ${q.subtotal:<10.2f}")
if q.discount:
lines.append(f"{'Discount':>55} -${q.discount:<9.2f}")
if q.shipping:
lines.append(f"{'Shipping':>55} ${q.shipping:<10.2f}")
lines.append(f"{'TOTAL':>55} ${q.total or q.subtotal or 0:<10.2f}")
lines.append(f"")
if q.payment_terms:
lines.append(f"Payment: {q.payment_terms}")
if q.delivery_terms:
lines.append(f"Delivery: {q.delivery_terms}")
if q.lead_time:
lines.append(f"Lead time: {q.lead_time}")
if q.notes:
lines.append(f"")
lines.append(f"Notes: {q.notes}")
return "\n".join(lines)
async def _to_dict(self, q: Quotation) -> Dict:
items_result = await self.db.execute(
select(QuotationItem).where(QuotationItem.quotation_id == q.id)
)
items = items_result.scalars().all()
return {
"id": str(q.id),
"customer_id": str(q.customer_id) if q.customer_id else None,
"title": q.title,
"status": q.status,
"currency": q.currency,
"exchange_rate": q.exchange_rate,
"payment_terms": q.payment_terms,
"delivery_terms": q.delivery_terms,
"lead_time": q.lead_time,
"valid_until": q.valid_until,
"subtotal": q.subtotal,
"discount": q.discount,
"shipping": q.shipping,
"total": q.total,
"notes": q.notes,
"items": [
{
"product_name": i.product_name,
"description": i.description,
"quantity": i.quantity,
"unit_price": i.unit_price,
"total_price": i.total_price,
"unit": i.unit,
}
for i in items
],
"text": await self.generate_quotation_text(q),
"sent_at": q.sent_at.isoformat() if q.sent_at else None,
"created_at": q.created_at.isoformat() if q.created_at else None,
}