refactor: migrate modules from re-exports to canonical implementations
Move actual code implementations into module directories: - orders: 5 services, 4 models, order/invoice schemas - inventory: 3 services, 2 models, 30+ schemas - customers: 3 services, 2 models, customer schemas - messaging: 3 services, 2 models, message/notification schemas - monitoring: background_tasks_service - marketplace: 5+ services including letzshop submodule - dev_tools: code_quality_service, test_runner_service - billing: billing_service - contracts: definition.py Legacy files in app/services/, models/database/, models/schema/ now re-export from canonical module locations for backwards compatibility. Architecture validator passes with 0 errors. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,164 +1,23 @@
|
||||
# app/services/invoice_pdf_service.py
|
||||
"""
|
||||
Invoice PDF generation service using WeasyPrint.
|
||||
LEGACY LOCATION - Re-exports from module for backwards compatibility.
|
||||
|
||||
Renders HTML invoice templates to PDF using Jinja2 + WeasyPrint.
|
||||
Stores generated PDFs in the configured storage location.
|
||||
The canonical implementation is now in:
|
||||
app/modules/orders/services/invoice_pdf_service.py
|
||||
|
||||
This file exists to maintain backwards compatibility with code that
|
||||
imports from the old location. All new code should import directly
|
||||
from the module:
|
||||
|
||||
from app.modules.orders.services import invoice_pdf_service
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from app.modules.orders.services.invoice_pdf_service import (
|
||||
invoice_pdf_service,
|
||||
InvoicePDFService,
|
||||
)
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from models.database.invoice import Invoice
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Template directory
|
||||
TEMPLATE_DIR = Path(__file__).parent.parent / "templates" / "invoices"
|
||||
|
||||
# PDF storage directory (relative to project root)
|
||||
PDF_STORAGE_DIR = Path("storage") / "invoices"
|
||||
|
||||
|
||||
class InvoicePDFService:
|
||||
"""Service for generating invoice PDFs."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the PDF service with Jinja2 environment."""
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader(str(TEMPLATE_DIR)),
|
||||
autoescape=True,
|
||||
)
|
||||
|
||||
def _ensure_storage_dir(self, vendor_id: int) -> Path:
|
||||
"""Ensure the storage directory exists for a vendor."""
|
||||
storage_path = PDF_STORAGE_DIR / str(vendor_id)
|
||||
storage_path.mkdir(parents=True, exist_ok=True)
|
||||
return storage_path
|
||||
|
||||
def _get_pdf_filename(self, invoice: Invoice) -> str:
|
||||
"""Generate PDF filename for an invoice."""
|
||||
# Sanitize invoice number for filename
|
||||
safe_number = invoice.invoice_number.replace("/", "-").replace("\\", "-")
|
||||
return f"{safe_number}.pdf"
|
||||
|
||||
def generate_pdf(
|
||||
self,
|
||||
db: Session,
|
||||
invoice: Invoice,
|
||||
force_regenerate: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Generate PDF for an invoice.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
invoice: Invoice to generate PDF for
|
||||
force_regenerate: If True, regenerate even if PDF already exists
|
||||
|
||||
Returns:
|
||||
Path to the generated PDF file
|
||||
"""
|
||||
# Check if PDF already exists
|
||||
if invoice.pdf_path and not force_regenerate:
|
||||
if Path(invoice.pdf_path).exists():
|
||||
logger.debug(f"PDF already exists for invoice {invoice.invoice_number}")
|
||||
return invoice.pdf_path
|
||||
|
||||
# Ensure storage directory exists
|
||||
storage_dir = self._ensure_storage_dir(invoice.vendor_id)
|
||||
pdf_filename = self._get_pdf_filename(invoice)
|
||||
pdf_path = storage_dir / pdf_filename
|
||||
|
||||
# Render HTML template
|
||||
html_content = self._render_html(invoice)
|
||||
|
||||
# Generate PDF using WeasyPrint
|
||||
try:
|
||||
from weasyprint import HTML
|
||||
|
||||
html_doc = HTML(string=html_content, base_url=str(TEMPLATE_DIR))
|
||||
html_doc.write_pdf(str(pdf_path))
|
||||
|
||||
logger.info(f"Generated PDF for invoice {invoice.invoice_number} at {pdf_path}")
|
||||
except ImportError:
|
||||
logger.error("WeasyPrint not installed. Install with: pip install weasyprint")
|
||||
raise RuntimeError("WeasyPrint not installed")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate PDF for invoice {invoice.invoice_number}: {e}")
|
||||
raise
|
||||
|
||||
# Update invoice record with PDF path and timestamp
|
||||
invoice.pdf_path = str(pdf_path)
|
||||
invoice.pdf_generated_at = datetime.now(UTC)
|
||||
db.flush()
|
||||
|
||||
return str(pdf_path)
|
||||
|
||||
def _render_html(self, invoice: Invoice) -> str:
|
||||
"""Render the invoice HTML template."""
|
||||
template = self.env.get_template("invoice.html")
|
||||
|
||||
# Prepare template context
|
||||
context = {
|
||||
"invoice": invoice,
|
||||
"seller": invoice.seller_details,
|
||||
"buyer": invoice.buyer_details,
|
||||
"line_items": invoice.line_items,
|
||||
"bank_details": invoice.bank_details,
|
||||
"payment_terms": invoice.payment_terms,
|
||||
"footer_text": invoice.footer_text,
|
||||
"now": datetime.now(UTC),
|
||||
}
|
||||
|
||||
return template.render(**context)
|
||||
|
||||
def get_pdf_path(self, invoice: Invoice) -> str | None:
|
||||
"""Get the PDF path for an invoice if it exists."""
|
||||
if invoice.pdf_path and Path(invoice.pdf_path).exists():
|
||||
return invoice.pdf_path
|
||||
return None
|
||||
|
||||
def delete_pdf(self, invoice: Invoice, db: Session) -> bool:
|
||||
"""
|
||||
Delete the PDF file for an invoice.
|
||||
|
||||
Args:
|
||||
invoice: Invoice whose PDF to delete
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
True if deleted, False if not found
|
||||
"""
|
||||
if not invoice.pdf_path:
|
||||
return False
|
||||
|
||||
pdf_path = Path(invoice.pdf_path)
|
||||
if pdf_path.exists():
|
||||
try:
|
||||
pdf_path.unlink()
|
||||
logger.info(f"Deleted PDF for invoice {invoice.invoice_number}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete PDF {pdf_path}: {e}")
|
||||
return False
|
||||
|
||||
# Clear PDF fields
|
||||
invoice.pdf_path = None
|
||||
invoice.pdf_generated_at = None
|
||||
db.flush()
|
||||
|
||||
return True
|
||||
|
||||
def regenerate_pdf(self, db: Session, invoice: Invoice) -> str:
|
||||
"""Force regenerate PDF for an invoice."""
|
||||
return self.generate_pdf(db, invoice, force_regenerate=True)
|
||||
|
||||
|
||||
# Singleton instance
|
||||
invoice_pdf_service = InvoicePDFService()
|
||||
__all__ = [
|
||||
"invoice_pdf_service",
|
||||
"InvoicePDFService",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user