190 lines
5.8 KiB
Python
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)
|
|
|