Files
AiTool/backend/app/services/doc_service.py
丹尼尔 ad96272ab6 fix:bug
2026-03-12 19:35:06 +08:00

190 lines
5.8 KiB
Python

import asyncio
from pathlib import Path
from typing import Any, Dict, List
from docx import Document
from openpyxl import load_workbook
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
async def generate_quote_excel(
project_data: Dict[str, Any],
template_path: str,
output_path: str,
) -> str:
"""
Generate an Excel quote based on a template and structured project data.
project_data is expected to have the following structure (from AI JSON):
{
"modules": [
{
"name": "...",
"description": "...",
"technical_approach": "...",
"estimated_hours": 16,
"unit_price": 800,
"subtotal": 12800
},
...
],
"total_estimated_hours": 40,
"total_amount": 32000,
"notes": "..."
}
"""
async def _work() -> str:
template = Path(template_path)
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
wb = load_workbook(template)
# Assume the first worksheet is used for the quote.
ws = wb.active
modules: List[Dict[str, Any]] = project_data.get("modules", [])
total_amount = project_data.get("total_amount")
total_hours = project_data.get("total_estimated_hours")
notes = project_data.get("notes")
# Example layout assumptions (adjust cell coordinates to match your template):
# - Starting row for line items: 10
# - Columns:
# A: index, B: module name, C: description,
# D: estimated hours, E: unit price, F: subtotal
start_row = 10
for idx, module in enumerate(modules, start=1):
row = start_row + idx - 1
ws[f"A{row}"] = idx
ws[f"B{row}"] = module.get("name")
ws[f"C{row}"] = module.get("description")
ws[f"D{row}"] = module.get("estimated_hours")
ws[f"E{row}"] = module.get("unit_price")
ws[f"F{row}"] = module.get("subtotal")
# Place total hours and amount in typical footer cells (adjust as needed).
if total_hours is not None:
ws["D5"] = total_hours # e.g., total hours
if total_amount is not None:
ws["F5"] = total_amount # e.g., total amount
if notes:
ws["B6"] = notes
wb.save(output)
return str(output)
return await asyncio.to_thread(_work)
def _replace_in_paragraphs(paragraphs, mapping: Dict[str, str]) -> None:
for paragraph in paragraphs:
for placeholder, value in mapping.items():
if placeholder in paragraph.text:
# Rebuild runs to preserve basic formatting as much as possible.
inline = paragraph.runs
text = paragraph.text.replace(placeholder, value)
# Clear existing runs
for i in range(len(inline) - 1, -1, -1):
paragraph.runs[i].clear()
paragraph.runs[i].text = ""
# Add a single run with replaced text
paragraph.add_run(text)
def _replace_in_tables(tables, mapping: Dict[str, str]) -> None:
for table in tables:
for row in table.rows:
for cell in row.cells:
_replace_in_paragraphs(cell.paragraphs, mapping)
async def generate_contract_word(
contract_data: Dict[str, str],
template_path: str,
output_path: str,
) -> str:
"""
Generate a contract Word document by replacing placeholders.
contract_data is a flat dict like:
{
"{{CUSTOMER_NAME}}": "张三",
"{{TOTAL_PRICE}}": "¥32,000",
"{{DELIVERY_DATE}}": "2026-03-31",
...
}
"""
async def _work() -> str:
template = Path(template_path)
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
doc = Document(str(template))
_replace_in_paragraphs(doc.paragraphs, contract_data)
_replace_in_tables(doc.tables, contract_data)
doc.save(str(output))
return str(output)
return await asyncio.to_thread(_work)
async def generate_quote_pdf_from_data(
project_data: Dict[str, Any],
output_pdf_path: str,
) -> str:
"""
Generate a simple PDF quote summary directly from structured data.
This does not render the Excel visually, but provides a clean PDF
that can be sent to customers.
"""
async def _work() -> str:
output = Path(output_pdf_path)
output.parent.mkdir(parents=True, exist_ok=True)
c = canvas.Canvas(str(output), pagesize=A4)
width, height = A4
y = height - 40
c.setFont("Helvetica-Bold", 14)
c.drawString(40, y, "报价单 Quote")
y -= 30
c.setFont("Helvetica", 10)
modules: List[Dict[str, Any]] = project_data.get("modules", [])
for idx, module in enumerate(modules, start=1):
name = module.get("name", "")
hours = module.get("estimated_hours", "")
subtotal = module.get("subtotal", "")
line = f"{idx}. {name} - 工时: {hours}, 小计: {subtotal}"
c.drawString(40, y, line)
y -= 16
if y < 80:
c.showPage()
y = height - 40
c.setFont("Helvetica", 10)
total_amount = project_data.get("total_amount")
total_hours = project_data.get("total_estimated_hours")
y -= 10
c.setFont("Helvetica-Bold", 11)
if total_hours is not None:
c.drawString(40, y, f"总工时 Total Hours: {total_hours}")
y -= 18
if total_amount is not None:
c.drawString(40, y, f"总金额 Total Amount: {total_amount}")
c.showPage()
c.save()
return str(output)
return await asyncio.to_thread(_work)