diff --git a/hypha/apply/projects/templates/application_projects/pdf_invoce_approved_page.html b/hypha/apply/projects/templates/application_projects/pdf_invoce_approved_page.html new file mode 100644 index 0000000000000000000000000000000000000000..24659ffeffa5f8dd95b30a496377a9a89230884b --- /dev/null +++ b/hypha/apply/projects/templates/application_projects/pdf_invoce_approved_page.html @@ -0,0 +1,17 @@ +{% load invoice_tools i18n %} + + +<h1>{% trans "Invoice Status" %}</h1> + +<ul> + {% for activity in activities %} + {% extract_status activity request.user as activity_status %} + <li><strong> {{ activity_status }} ({{ activity.user.email }})</strong> on {{ activity.timestamp }}</li> + {% endfor %} +</ul> + +<p style="border-top: 1px solid #ccc; margin-top: 1.5em; padding-top: 0.5em; color: #656d76;"> + <small> + {% trans "Generated" %}: {{ generated_at|date:"c" }} · {{ request.scheme }}://{{ request.get_host }}{{ invoice.get_absolute_url}} + </small> +</p> diff --git a/hypha/apply/projects/views/payment.py b/hypha/apply/projects/views/payment.py index ca348b84a52ba4b4c6cc4374552dd226b2ec1994..81c147c96e75e6ffa4ce2c94947a89162f3993cd 100644 --- a/hypha/apply/projects/views/payment.py +++ b/hypha/apply/projects/views/payment.py @@ -6,6 +6,7 @@ from django.contrib.auth.models import Group from django.core.exceptions import PermissionDenied from django.db import transaction from django.shortcuts import get_object_or_404, redirect +from django.template.loader import render_to_string from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.safestring import mark_safe @@ -35,6 +36,7 @@ from hypha.apply.todo.views import ( ) from hypha.apply.users.decorators import staff_or_finance_required from hypha.apply.users.groups import STAFF_GROUP_NAME +from hypha.apply.utils.pdfs import html_to_pdf, merge_pdf from hypha.apply.utils.storage import PrivateMediaView from hypha.apply.utils.views import ( DelegateableListView, @@ -121,6 +123,7 @@ class ChangeInvoiceStatusView(DelegatedViewMixin, InvoiceAccessMixin, UpdateView and self.request.user.is_finance_level_1 and self.object.status == APPROVED_BY_FINANCE ): + self.object.save() messenger( MESSAGES.APPROVE_INVOICE, request=self.request, @@ -416,12 +419,35 @@ class InvoicePrivateMedia(UserPassesTestMixin, PrivateMediaView): return super().dispatch(*args, **kwargs) def get_media(self, *args, **kwargs): - file_pk = kwargs.get("file_pk") - if not file_pk: - return self.invoice.document + # check if the request is for a supporting document + if file_pk := kwargs.get("file_pk"): + document = get_object_or_404(self.invoice.supporting_documents, pk=file_pk) + return document.document - document = get_object_or_404(self.invoice.supporting_documents, pk=file_pk) - return document.document + # if not, then it's for invoice document + if ( + self.invoice.status == APPROVED_BY_STAFF + and self.invoice.document.file.name.endswith(".pdf") + ): + if activities := Activity.actions.filter( + related_content_type__model="invoice", + related_object_id=self.invoice.id, + message__icontains="Approved by", + ).visible_to(self.request.user): + approval_pdf_page = html_to_pdf( + render_to_string( + "application_projects/pdf_invoce_approved_page.html", + context={ + "invoice": self.invoice, + "generated_at": timezone.now(), + "activities": activities, + }, + request=self.request, + ) + ) + return merge_pdf(self.invoice.document.file, approval_pdf_page) + + return self.invoice.document def test_func(self): if self.request.user.is_apply_staff: diff --git a/hypha/apply/utils/pdfs.py b/hypha/apply/utils/pdfs.py index fd9047451177e81300289d8553d4909351a7c993..eda5746c034b29ecbeb03e8e914e7585ae1d33bd 100644 --- a/hypha/apply/utils/pdfs.py +++ b/hypha/apply/utils/pdfs.py @@ -1,8 +1,10 @@ -import io import os +from io import BytesIO from itertools import cycle from bs4 import BeautifulSoup, NavigableString +from django.core.files import File +from pypdf import PdfReader, PdfWriter from reportlab.lib import pagesizes from reportlab.lib.colors import Color, white from reportlab.lib.styles import ParagraphStyle as PS @@ -23,6 +25,7 @@ from reportlab.platypus import ( Table, TableStyle, ) +from xhtml2pdf import pisa STYLES = { "Question": PS( @@ -164,7 +167,7 @@ class ReportDocTemplate(BaseDocTemplate): def make_pdf(title, sections, pagesize): prepare_fonts() - buffer = io.BytesIO() + buffer = BytesIO() page_width, page_height = getattr(pagesizes, pagesize) doc = ReportDocTemplate( @@ -460,3 +463,39 @@ def draw_project_content(content): paragraphs.extend(flowables) return paragraphs + + +def html_to_pdf(html_body: str) -> BytesIO: + """Convert HTML to PDF. + + Args: + html_body: The body of the html as string + + Returns: + BytesIO: PDF file + """ + packet = BytesIO() + source_html = f"<html><body>{html_body}</body></html>" + pisa.CreatePDF(source_html, dest=packet, raise_exception=True, encoding="utf-8") + packet.seek(0) + return packet + + +def merge_pdf(origin_pdf: BytesIO, input_pdf: BytesIO) -> File: + """Given two PDFs, merge them together. + + Args: + origin_pdf: a file-like object containing a PDF + input_pdf: a file-like object containing a PDF + + Returns: + Return a File object containing the merged PDF and with the same name as the + original PDF. + """ + merger = PdfWriter(clone_from=BytesIO(origin_pdf.read())) + merger.append(PdfReader(input_pdf)) + + output_pdf = BytesIO() + merger.write(output_pdf) + output_pdf.seek(0) + return File(output_pdf, name=origin_pdf.name)