feat: complete Phase 1 MVP - add agent panel, email notifications, and PDF invoicing
Agent Panel UI: - Visual dashboard showing all agents with status (idle/working) - Agent cards with stats: total tasks, completed tasks - Capabilities display for each agent - Execution log showing recent agent activity - Real-time updates (refreshes every 10 seconds) - Click agent to filter execution log - Status indicators: pending, running, completed, failed, pending_approval Email Notifications: - SMTP email service with HTML templates - Task assignment notifications - Agent approval request emails with action details - Agent completion notifications - Configurable via environment variables (SMTP_HOST, SMTP_USER, SMTP_PASSWORD, SMTP_TO) - Integrated with agent executor service PDF Invoice Generation: - Professional PDF invoices using ReportLab - Generate from time entries by project - Client information and company branding - Itemized time entries with hours, rates, amounts - Automatic totals calculation - Download as PDF attachment - API endpoints: /api/opc/invoices/generate - Projects and clients management endpoints Additional Features: - Projects CRUD API - Clients CRUD API - Project time tracking summary - Billable vs non-billable hours tracking Phase 1 MVP 100% Complete: ✅ PostgreSQL database with full schema ✅ Task CRUD API with automatic time tracking ✅ Kanban board with drag-and-drop ✅ Agent executor service with LLM integration ✅ WebSocket real-time updates ✅ 3 core agents (PM, CTO, COO) ✅ Agent panel UI with execution logs ✅ Email notifications (Telegram + Email) ✅ PDF invoice generation All 12 tasks completed!
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
"""
|
||||
PDF Invoice Generation Service
|
||||
"""
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
|
||||
from reportlab.lib.enums import TA_RIGHT, TA_CENTER
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
import io
|
||||
|
||||
|
||||
def generate_invoice_pdf(
|
||||
invoice_number: str,
|
||||
client: Dict[str, Any],
|
||||
time_entries: List[Dict[str, Any]],
|
||||
hourly_rate: float = 100.0,
|
||||
company_name: str = "Your Company",
|
||||
company_address: str = "",
|
||||
company_email: str = "",
|
||||
company_phone: str = ""
|
||||
) -> bytes:
|
||||
"""
|
||||
Generate PDF invoice from time entries
|
||||
|
||||
Returns: PDF bytes
|
||||
"""
|
||||
buffer = io.BytesIO()
|
||||
doc = SimpleDocTemplate(buffer, pagesize=letter)
|
||||
elements = []
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# Custom styles
|
||||
title_style = ParagraphStyle(
|
||||
'CustomTitle',
|
||||
parent=styles['Heading1'],
|
||||
fontSize=24,
|
||||
textColor=colors.HexColor('#1F2937'),
|
||||
spaceAfter=30,
|
||||
)
|
||||
|
||||
header_style = ParagraphStyle(
|
||||
'Header',
|
||||
parent=styles['Normal'],
|
||||
fontSize=10,
|
||||
textColor=colors.HexColor('#6B7280'),
|
||||
)
|
||||
|
||||
# Company Header
|
||||
elements.append(Paragraph(company_name, title_style))
|
||||
if company_address:
|
||||
elements.append(Paragraph(company_address, header_style))
|
||||
if company_email:
|
||||
elements.append(Paragraph(f"Email: {company_email}", header_style))
|
||||
if company_phone:
|
||||
elements.append(Paragraph(f"Phone: {company_phone}", header_style))
|
||||
|
||||
elements.append(Spacer(1, 0.3 * inch))
|
||||
|
||||
# Invoice Title
|
||||
invoice_title = ParagraphStyle(
|
||||
'InvoiceTitle',
|
||||
parent=styles['Heading2'],
|
||||
fontSize=18,
|
||||
textColor=colors.HexColor('#4F46E5'),
|
||||
)
|
||||
elements.append(Paragraph("INVOICE", invoice_title))
|
||||
elements.append(Spacer(1, 0.2 * inch))
|
||||
|
||||
# Invoice Details
|
||||
invoice_date = datetime.now().strftime("%B %d, %Y")
|
||||
invoice_data = [
|
||||
["Invoice Number:", invoice_number],
|
||||
["Invoice Date:", invoice_date],
|
||||
["Client:", client.get('name', 'N/A')],
|
||||
]
|
||||
|
||||
if client.get('company'):
|
||||
invoice_data.append(["Company:", client['company']])
|
||||
if client.get('email'):
|
||||
invoice_data.append(["Email:", client['email']])
|
||||
|
||||
invoice_table = Table(invoice_data, colWidths=[2*inch, 4*inch])
|
||||
invoice_table.setStyle(TableStyle([
|
||||
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
||||
('FONTNAME', (1, 0), (1, -1), 'Helvetica'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 10),
|
||||
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#374151')),
|
||||
('TEXTCOLOR', (1, 0), (1, -1), colors.HexColor('#1F2937')),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
||||
]))
|
||||
elements.append(invoice_table)
|
||||
elements.append(Spacer(1, 0.4 * inch))
|
||||
|
||||
# Time Entries Table
|
||||
table_data = [
|
||||
["Date", "Description", "Hours", "Rate", "Amount"]
|
||||
]
|
||||
|
||||
total_hours = 0
|
||||
total_amount = 0
|
||||
|
||||
for entry in time_entries:
|
||||
hours = entry['duration_minutes'] / 60
|
||||
rate = entry.get('hourly_rate', hourly_rate)
|
||||
amount = hours * rate
|
||||
|
||||
total_hours += hours
|
||||
total_amount += amount
|
||||
|
||||
date_str = entry['started_at'].strftime("%Y-%m-%d") if entry.get('started_at') else "N/A"
|
||||
desc = entry.get('description', 'Work performed')
|
||||
|
||||
table_data.append([
|
||||
date_str,
|
||||
desc[:50] + "..." if len(desc) > 50 else desc,
|
||||
f"{hours:.2f}",
|
||||
f"${rate:.2f}",
|
||||
f"${amount:.2f}"
|
||||
])
|
||||
|
||||
# Add totals row
|
||||
table_data.append([
|
||||
"", "Total", f"{total_hours:.2f}", "", f"${total_amount:.2f}"
|
||||
])
|
||||
|
||||
# Create table
|
||||
time_table = Table(table_data, colWidths=[1.2*inch, 2.8*inch, 0.8*inch, 0.8*inch, 1*inch])
|
||||
time_table.setStyle(TableStyle([
|
||||
# Header row
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4F46E5')),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 11),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
|
||||
# Data rows
|
||||
('FONTNAME', (0, 1), (-1, -2), 'Helvetica'),
|
||||
('FONTSIZE', (0, 1), (-1, -2), 9),
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -2), [colors.white, colors.HexColor('#F9FAFB')]),
|
||||
('GRID', (0, 0), (-1, -2), 0.5, colors.HexColor('#E5E7EB')),
|
||||
|
||||
# Total row
|
||||
('BACKGROUND', (0, -1), (-1, -1), colors.HexColor('#F3F4F6')),
|
||||
('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, -1), (-1, -1), 11),
|
||||
('TOPPADDING', (0, -1), (-1, -1), 12),
|
||||
('BOTTOMPADDING', (0, -1), (-1, -1), 12),
|
||||
|
||||
# Alignment
|
||||
('ALIGN', (2, 0), (-1, -1), 'RIGHT'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
||||
]))
|
||||
|
||||
elements.append(time_table)
|
||||
elements.append(Spacer(1, 0.5 * inch))
|
||||
|
||||
# Payment Terms
|
||||
terms_style = ParagraphStyle(
|
||||
'Terms',
|
||||
parent=styles['Normal'],
|
||||
fontSize=9,
|
||||
textColor=colors.HexColor('#6B7280'),
|
||||
)
|
||||
elements.append(Paragraph("<b>Payment Terms:</b> Due within 30 days", terms_style))
|
||||
elements.append(Spacer(1, 0.1 * inch))
|
||||
elements.append(Paragraph("Thank you for your business!", terms_style))
|
||||
|
||||
# Build PDF
|
||||
doc.build(elements)
|
||||
|
||||
pdf_bytes = buffer.getvalue()
|
||||
buffer.close()
|
||||
|
||||
return pdf_bytes
|
||||
|
||||
|
||||
def generate_invoice_from_project(
|
||||
project_id: int,
|
||||
client: Dict[str, Any],
|
||||
time_entries: List[Dict[str, Any]],
|
||||
invoice_number: str = None
|
||||
) -> bytes:
|
||||
"""Generate invoice for a project"""
|
||||
if not invoice_number:
|
||||
invoice_number = f"INV-{datetime.now().strftime('%Y%m%d')}-{project_id}"
|
||||
|
||||
return generate_invoice_pdf(
|
||||
invoice_number=invoice_number,
|
||||
client=client,
|
||||
time_entries=time_entries,
|
||||
company_name="Your OPC",
|
||||
company_address="123 Business St, City, State 12345",
|
||||
company_email="billing@youropc.com",
|
||||
company_phone="+1 (555) 123-4567"
|
||||
)
|
||||
Reference in New Issue
Block a user