fix:bug
This commit is contained in:
189
backend/app/services/doc_service.py
Normal file
189
backend/app/services/doc_service.py
Normal file
@@ -0,0 +1,189 @@
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user