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)