fix:bug
This commit is contained in:
1
backend/app/routers/__init__.py
Normal file
1
backend/app/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
__all__ = []
|
||||
71
backend/app/routers/customers.py
Normal file
71
backend/app/routers/customers.py
Normal file
@@ -0,0 +1,71 @@
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app.db import get_db
|
||||
from backend.app import models
|
||||
from backend.app.schemas import (
|
||||
CustomerCreate,
|
||||
CustomerRead,
|
||||
CustomerUpdate,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/customers", tags=["customers"])
|
||||
|
||||
|
||||
@router.get("/", response_model=List[CustomerRead])
|
||||
async def list_customers(db: Session = Depends(get_db)):
|
||||
customers = db.query(models.Customer).order_by(models.Customer.created_at.desc()).all()
|
||||
return customers
|
||||
|
||||
|
||||
@router.post("/", response_model=CustomerRead, status_code=status.HTTP_201_CREATED)
|
||||
async def create_customer(payload: CustomerCreate, db: Session = Depends(get_db)):
|
||||
customer = models.Customer(
|
||||
name=payload.name,
|
||||
contact_info=payload.contact_info,
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
return customer
|
||||
|
||||
|
||||
@router.get("/{customer_id}", response_model=CustomerRead)
|
||||
async def get_customer(customer_id: int, db: Session = Depends(get_db)):
|
||||
customer = db.query(models.Customer).get(customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found")
|
||||
return customer
|
||||
|
||||
|
||||
@router.put("/{customer_id}", response_model=CustomerRead)
|
||||
async def update_customer(
|
||||
customer_id: int, payload: CustomerUpdate, db: Session = Depends(get_db)
|
||||
):
|
||||
customer = db.query(models.Customer).get(customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found")
|
||||
|
||||
if payload.name is not None:
|
||||
customer.name = payload.name
|
||||
if payload.contact_info is not None:
|
||||
customer.contact_info = payload.contact_info
|
||||
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
return customer
|
||||
|
||||
|
||||
@router.delete("/{customer_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_customer(customer_id: int, db: Session = Depends(get_db)):
|
||||
customer = db.query(models.Customer).get(customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found")
|
||||
|
||||
db.delete(customer)
|
||||
db.commit()
|
||||
return None
|
||||
|
||||
37
backend/app/routers/finance.py
Normal file
37
backend/app/routers/finance.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from backend.app.schemas import FinanceSyncResponse, FinanceSyncResult
|
||||
from backend.app.services.email_service import create_monthly_zip, sync_finance_emails
|
||||
|
||||
|
||||
router = APIRouter(prefix="/finance", tags=["finance"])
|
||||
|
||||
|
||||
@router.post("/sync", response_model=FinanceSyncResponse)
|
||||
async def sync_finance():
|
||||
try:
|
||||
items_raw = await sync_finance_emails()
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
||||
|
||||
items = [FinanceSyncResult(**item) for item in items_raw]
|
||||
return FinanceSyncResponse(items=items)
|
||||
|
||||
|
||||
@router.get("/download/{month}")
|
||||
async def download_finance_month(month: str):
|
||||
"""
|
||||
Download a zipped archive for a given month (YYYY-MM).
|
||||
"""
|
||||
try:
|
||||
zip_path = await create_monthly_zip(month)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
|
||||
return FileResponse(
|
||||
path=zip_path,
|
||||
media_type="application/zip",
|
||||
filename=f"finance_{month}.zip",
|
||||
)
|
||||
|
||||
256
backend/app/routers/projects.py
Normal file
256
backend/app/routers/projects.py
Normal file
@@ -0,0 +1,256 @@
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from backend.app import models
|
||||
from backend.app.db import get_db
|
||||
from backend.app.schemas import (
|
||||
ContractGenerateRequest,
|
||||
ContractGenerateResponse,
|
||||
ProjectRead,
|
||||
ProjectUpdate,
|
||||
QuoteGenerateResponse,
|
||||
RequirementAnalyzeRequest,
|
||||
RequirementAnalyzeResponse,
|
||||
)
|
||||
from backend.app.services.ai_service import analyze_requirement
|
||||
from backend.app.services.doc_service import (
|
||||
generate_contract_word,
|
||||
generate_quote_excel,
|
||||
generate_quote_pdf_from_data,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/projects", tags=["projects"])
|
||||
|
||||
|
||||
def _build_markdown_from_analysis(data: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Convert structured AI analysis JSON into a human-editable Markdown document.
|
||||
"""
|
||||
lines: list[str] = []
|
||||
lines.append("# 项目方案草稿")
|
||||
lines.append("")
|
||||
|
||||
total_hours = data.get("total_estimated_hours")
|
||||
total_amount = data.get("total_amount")
|
||||
if total_hours is not None or total_amount is not None:
|
||||
lines.append("## 概要")
|
||||
if total_hours is not None:
|
||||
lines.append(f"- 建议总工时:**{total_hours}**")
|
||||
if total_amount is not None:
|
||||
lines.append(f"- 建议总报价:**{total_amount}**")
|
||||
lines.append("")
|
||||
|
||||
modules = data.get("modules") or []
|
||||
if modules:
|
||||
lines.append("## 功能模块与技术实现")
|
||||
for idx, module in enumerate(modules, start=1):
|
||||
name = module.get("name", f"模块 {idx}")
|
||||
desc = module.get("description") or ""
|
||||
tech = module.get("technical_approach") or ""
|
||||
hours = module.get("estimated_hours")
|
||||
unit_price = module.get("unit_price")
|
||||
subtotal = module.get("subtotal")
|
||||
|
||||
lines.append(f"### {idx}. {name}")
|
||||
if desc:
|
||||
lines.append(desc)
|
||||
lines.append("")
|
||||
if tech:
|
||||
lines.append("**技术实现思路:**")
|
||||
lines.append(tech)
|
||||
lines.append("")
|
||||
if hours is not None or unit_price is not None or subtotal is not None:
|
||||
lines.append("**工时与报价:**")
|
||||
if hours is not None:
|
||||
lines.append(f"- 预估工时:{hours}")
|
||||
if unit_price is not None:
|
||||
lines.append(f"- 单价:{unit_price}")
|
||||
if subtotal is not None:
|
||||
lines.append(f"- 小计:{subtotal}")
|
||||
lines.append("")
|
||||
|
||||
notes = data.get("notes")
|
||||
if notes:
|
||||
lines.append("## 备注")
|
||||
lines.append(notes)
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ProjectRead])
|
||||
async def list_projects(db: Session = Depends(get_db)):
|
||||
projects = (
|
||||
db.query(models.Project)
|
||||
.order_by(models.Project.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
return projects
|
||||
|
||||
|
||||
@router.get("/{project_id}", response_model=ProjectRead)
|
||||
async def get_project(project_id: int, db: Session = Depends(get_db)):
|
||||
project = db.query(models.Project).get(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
return project
|
||||
|
||||
|
||||
@router.patch("/{project_id}", response_model=ProjectRead)
|
||||
async def update_project(
|
||||
project_id: int,
|
||||
payload: ProjectUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
project = db.query(models.Project).get(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
if payload.ai_solution_md is not None:
|
||||
project.ai_solution_md = payload.ai_solution_md
|
||||
if payload.status is not None:
|
||||
project.status = payload.status
|
||||
db.commit()
|
||||
db.refresh(project)
|
||||
return project
|
||||
|
||||
|
||||
@router.post("/analyze", response_model=RequirementAnalyzeResponse)
|
||||
async def analyze_project_requirement(
|
||||
payload: RequirementAnalyzeRequest,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# Ensure customer exists
|
||||
customer = db.query(models.Customer).get(payload.customer_id)
|
||||
if not customer:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found")
|
||||
|
||||
analysis = await analyze_requirement(payload.raw_text)
|
||||
ai_solution_md = _build_markdown_from_analysis(analysis)
|
||||
|
||||
project = models.Project(
|
||||
customer_id=payload.customer_id,
|
||||
raw_requirement=payload.raw_text,
|
||||
ai_solution_md=ai_solution_md,
|
||||
status="draft",
|
||||
)
|
||||
db.add(project)
|
||||
db.commit()
|
||||
db.refresh(project)
|
||||
|
||||
return RequirementAnalyzeResponse(
|
||||
project_id=project.id,
|
||||
ai_solution_md=ai_solution_md,
|
||||
ai_solution_json=analysis,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{project_id}/generate_quote", response_model=QuoteGenerateResponse)
|
||||
async def generate_project_quote(
|
||||
project_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
project = db.query(models.Project).get(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
|
||||
# Re-analyze to get fresh structured data (the UI will allow user to edit Markdown separately).
|
||||
analysis = await analyze_requirement(project.raw_requirement)
|
||||
|
||||
# Paths
|
||||
base_dir = Path("data/quotes")
|
||||
base_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
excel_path = base_dir / f"quote_project_{project.id}.xlsx"
|
||||
pdf_path = base_dir / f"quote_project_{project.id}.pdf"
|
||||
|
||||
template_path = Path("templates/quote_template.xlsx")
|
||||
if not template_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Quote template file not found on server.",
|
||||
)
|
||||
|
||||
await generate_quote_excel(analysis, str(template_path), str(excel_path))
|
||||
await generate_quote_pdf_from_data(analysis, str(pdf_path))
|
||||
|
||||
total_amount = float(analysis.get("total_amount") or 0.0)
|
||||
|
||||
quote = models.Quote(
|
||||
project_id=project.id,
|
||||
total_amount=total_amount,
|
||||
file_path=str(pdf_path),
|
||||
)
|
||||
db.add(quote)
|
||||
db.commit()
|
||||
db.refresh(quote)
|
||||
|
||||
return QuoteGenerateResponse(
|
||||
quote_id=quote.id,
|
||||
project_id=project.id,
|
||||
total_amount=total_amount,
|
||||
excel_path=str(excel_path),
|
||||
pdf_path=str(pdf_path),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{project_id}/generate_contract",
|
||||
response_model=ContractGenerateResponse,
|
||||
)
|
||||
async def generate_project_contract(
|
||||
project_id: int,
|
||||
payload: ContractGenerateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
project = db.query(models.Project).get(project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
|
||||
customer = project.customer
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Project has no associated customer"
|
||||
)
|
||||
|
||||
# Use the latest quote for this project to determine total price.
|
||||
latest_quote = (
|
||||
db.query(models.Quote)
|
||||
.filter(models.Quote.project_id == project.id)
|
||||
.order_by(models.Quote.created_at.desc())
|
||||
.first()
|
||||
)
|
||||
if not latest_quote:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No quote found for this project.",
|
||||
)
|
||||
|
||||
contracts_dir = Path("data/contracts")
|
||||
contracts_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = contracts_dir / f"contract_project_{project.id}.docx"
|
||||
|
||||
template_path = Path("templates/contract_template.docx")
|
||||
if not template_path.exists():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Contract template file not found on server.",
|
||||
)
|
||||
|
||||
# Build placeholder mapping
|
||||
mapping: Dict[str, str] = {
|
||||
"{{CUSTOMER_NAME}}": customer.name,
|
||||
"{{TOTAL_PRICE}}": str(latest_quote.total_amount),
|
||||
"{{DELIVERY_DATE}}": payload.delivery_date,
|
||||
}
|
||||
# Allow arbitrary additional placeholders
|
||||
for key, value in payload.extra_placeholders.items():
|
||||
mapping[key] = value
|
||||
|
||||
await generate_contract_word(mapping, str(template_path), str(output_path))
|
||||
|
||||
return ContractGenerateResponse(project_id=project.id, contract_path=str(output_path))
|
||||
|
||||
Reference in New Issue
Block a user