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:
2026-01-29 21:28:56 +01:00
parent b5a803cde8
commit de83875d0a
99 changed files with 19413 additions and 15357 deletions

View File

@@ -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",
]