import io import logging from celery import shared_task logger = logging.getLogger(__name__) def get_invoice_pdf_builder(): """Returns the configured PDF builder (``INFRASYNTH_BILLING["INVOICE_PDF_BUILDER"]``).""" from django.utils.module_loading import import_string from infrasynth.shared.settings_utils import get_setting path = get_setting( "INFRASYNTH_BILLING", "INVOICE_PDF_BUILDER", "infrasynth.billing.invoice_generator._build_invoice_pdf", ) return import_string(path) @shared_task( name="infrasynth.billing.generate_invoice_pdf", bind=True, max_retries=3, default_retry_delay=60, ) def generate_invoice_pdf(self, invoice_id, tenant_id=None): """Generates a PDF for an invoice and stores it via the files service.""" from infrasynth.tenancy.context import tenant_context from infrasynth.tenancy.models import Tenant from .models import Invoice tenant = Tenant.objects.filter(pk=tenant_id).first() if tenant_id else None with tenant_context(tenant): try: invoice = Invoice.all_objects.select_related("subscription", "subscription__plan", "gateway").get( pk=invoice_id ) except Invoice.DoesNotExist: logger.warning("Invoice %s not found", invoice_id) return None try: pdf_bytes = get_invoice_pdf_builder()(invoice) except Exception as exc: # noqa: BLE001 logger.exception("PDF generation failed for invoice %s", invoice_id) raise self.retry(exc=exc) from exc from django.core.files.base import ContentFile from infrasynth.files.services import FileService filename = f"invoice_{invoice.invoice_number}.pdf" content = ContentFile(pdf_bytes, name=filename) content.content_type = "application/pdf" stored = FileService().upload( content, filename=filename, metadata={"invoice_id": invoice.id, "invoice_number": invoice.invoice_number}, ) invoice.pdf_file = stored invoice.save(update_fields=["pdf_file"]) return invoice.id def _build_invoice_pdf(invoice) -> bytes: 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, ) styles = getSampleStyleSheet() title_style = ParagraphStyle("InvoiceTitle", parent=styles["Title"], textColor=colors.HexColor("#1a3a5c")) body_style = ParagraphStyle("InvoiceBody", parent=styles["BodyText"], fontSize=10, leading=14) buffer = io.BytesIO() doc = SimpleDocTemplate( buffer, pagesize=letter, rightMargin=0.75 * inch, leftMargin=0.75 * inch, topMargin=0.75 * inch, bottomMargin=0.75 * inch, ) story = [ Paragraph("Factura", title_style), Paragraph( f"Nº: {invoice.invoice_number}
" f"Vencimiento: {invoice.due_date.date() if invoice.due_date else '-'}", body_style, ), Spacer(1, 12), Paragraph("Cliente", body_style), Paragraph( f"{getattr(invoice.tenant, 'name', '')}", body_style, ), Spacer(1, 12), ] table_data = [["Descripción", "Cantidad", "Monto"]] for item in invoice.line_items: table_data.append( [ item.get("description", ""), str(item.get("quantity", 1)), f"{item.get('amount', 0)} {invoice.currency}", ] ) items_table = Table(table_data, colWidths=[3.5 * inch, 1 * inch, 1.5 * inch]) items_table.setStyle( TableStyle( [ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1a3a5c")), ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), ("GRID", (0, 0), (-1, -1), 0.5, colors.grey), ("FONTSIZE", (0, 0), (-1, -1), 9), ("ALIGN", (1, 0), (-1, -1), "RIGHT"), ] ) ) story.append(items_table) story.append(Spacer(1, 12)) totals = [ f"Subtotal: {invoice.amount} {invoice.currency}", ] if invoice.tax_name: totals.append(f"{invoice.tax_name}: {invoice.tax_amount} {invoice.currency}") total = invoice.amount + invoice.tax_amount totals.append(f"Total: {total} {invoice.currency}") totals_style = ParagraphStyle( "InvoiceTotals", parent=body_style, alignment=2, spaceAfter=4, ) for line in totals: story.append(Paragraph(line, totals_style)) doc.build(story) return buffer.getvalue()