feat: comprehensive test infrastructure improvements
- Fix unit test imports: add env setup in conftest.py before module imports - Add 24 new auth router tests (RBAC, preferences, password validation) - Add 16 new tests for litellm and chat_summary routers - Apply black formatting and ruff linting across codebase - Add pre-commit hooks configuration (black, ruff, file checks) - Increase CI coverage threshold from 40% to 50% Test Results: - 206 tests passing (91 unit + 115 integration) - Coverage: 58.79% on core modules - auth.py: 57% → 85%, litellm.py: 23% → 87%, chat_summary.py: 41% → 100% - auth_service: 96.51%, config: 100%, rbac: 93.48%
This commit is contained in:
@@ -1,26 +1,27 @@
|
||||
"""
|
||||
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
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
||||
|
||||
|
||||
def generate_invoice_pdf(
|
||||
invoice_number: str,
|
||||
client: Dict[str, Any],
|
||||
time_entries: List[Dict[str, Any]],
|
||||
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 = ""
|
||||
company_phone: str = "",
|
||||
) -> bytes:
|
||||
"""
|
||||
Generate PDF invoice from time entries
|
||||
@@ -34,18 +35,18 @@ def generate_invoice_pdf(
|
||||
|
||||
# Custom styles
|
||||
title_style = ParagraphStyle(
|
||||
'CustomTitle',
|
||||
parent=styles['Heading1'],
|
||||
"CustomTitle",
|
||||
parent=styles["Heading1"],
|
||||
fontSize=24,
|
||||
textColor=colors.HexColor('#1F2937'),
|
||||
textColor=colors.HexColor("#1F2937"),
|
||||
spaceAfter=30,
|
||||
)
|
||||
|
||||
header_style = ParagraphStyle(
|
||||
'Header',
|
||||
parent=styles['Normal'],
|
||||
"Header",
|
||||
parent=styles["Normal"],
|
||||
fontSize=10,
|
||||
textColor=colors.HexColor('#6B7280'),
|
||||
textColor=colors.HexColor("#6B7280"),
|
||||
)
|
||||
|
||||
# Company Header
|
||||
@@ -61,10 +62,10 @@ def generate_invoice_pdf(
|
||||
|
||||
# Invoice Title
|
||||
invoice_title = ParagraphStyle(
|
||||
'InvoiceTitle',
|
||||
parent=styles['Heading2'],
|
||||
"InvoiceTitle",
|
||||
parent=styles["Heading2"],
|
||||
fontSize=18,
|
||||
textColor=colors.HexColor('#4F46E5'),
|
||||
textColor=colors.HexColor("#4F46E5"),
|
||||
)
|
||||
elements.append(Paragraph("INVOICE", invoice_title))
|
||||
elements.append(Spacer(1, 0.2 * inch))
|
||||
@@ -74,96 +75,93 @@ def generate_invoice_pdf(
|
||||
invoice_data = [
|
||||
["Invoice Number:", invoice_number],
|
||||
["Invoice Date:", invoice_date],
|
||||
["Client:", client.get('name', 'N/A')],
|
||||
["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']])
|
||||
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),
|
||||
]))
|
||||
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"]
|
||||
]
|
||||
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)
|
||||
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')
|
||||
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}"
|
||||
])
|
||||
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}"
|
||||
])
|
||||
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'),
|
||||
]))
|
||||
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'],
|
||||
"Terms",
|
||||
parent=styles["Normal"],
|
||||
fontSize=9,
|
||||
textColor=colors.HexColor('#6B7280'),
|
||||
textColor=colors.HexColor("#6B7280"),
|
||||
)
|
||||
elements.append(Paragraph("<b>Payment Terms:</b> Due within 30 days", terms_style))
|
||||
elements.append(Spacer(1, 0.1 * inch))
|
||||
@@ -179,10 +177,7 @@ def generate_invoice_pdf(
|
||||
|
||||
|
||||
def generate_invoice_from_project(
|
||||
project_id: int,
|
||||
client: Dict[str, Any],
|
||||
time_entries: List[Dict[str, Any]],
|
||||
invoice_number: str = None
|
||||
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:
|
||||
@@ -195,5 +190,5 @@ def generate_invoice_from_project(
|
||||
company_name="Your OPC",
|
||||
company_address="123 Business St, City, State 12345",
|
||||
company_email="billing@youropc.com",
|
||||
company_phone="+1 (555) 123-4567"
|
||||
company_phone="+1 (555) 123-4567",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user