Skip to content
Snippets Groups Projects
Unverified Commit c27e4ffa authored by Saurabh Kumar's avatar Saurabh Kumar Committed by GitHub
Browse files

PDF stamping of approved invoices (#3698)

- [x] Add approved by staff to the approved invoice pdfs
- [x] Add inovice approver and date

Closes: https://github.com/HyphaApp/hypha/issues/3539
parent b7807d3e
No related branches found
No related tags found
1 merge request!89Upgrade to 5.10.0
{% 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" }} &middot; {{ request.scheme }}://{{ request.get_host }}{{ invoice.get_absolute_url}}
</small>
</p>
...@@ -6,6 +6,7 @@ from django.contrib.auth.models import Group ...@@ -6,6 +6,7 @@ from django.contrib.auth.models import Group
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.shortcuts import get_object_or_404, redirect 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 import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
...@@ -35,6 +36,7 @@ from hypha.apply.todo.views import ( ...@@ -35,6 +36,7 @@ from hypha.apply.todo.views import (
) )
from hypha.apply.users.decorators import staff_or_finance_required from hypha.apply.users.decorators import staff_or_finance_required
from hypha.apply.users.groups import STAFF_GROUP_NAME 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.storage import PrivateMediaView
from hypha.apply.utils.views import ( from hypha.apply.utils.views import (
DelegateableListView, DelegateableListView,
...@@ -121,6 +123,7 @@ class ChangeInvoiceStatusView(DelegatedViewMixin, InvoiceAccessMixin, UpdateView ...@@ -121,6 +123,7 @@ class ChangeInvoiceStatusView(DelegatedViewMixin, InvoiceAccessMixin, UpdateView
and self.request.user.is_finance_level_1 and self.request.user.is_finance_level_1
and self.object.status == APPROVED_BY_FINANCE and self.object.status == APPROVED_BY_FINANCE
): ):
self.object.save()
messenger( messenger(
MESSAGES.APPROVE_INVOICE, MESSAGES.APPROVE_INVOICE,
request=self.request, request=self.request,
...@@ -416,12 +419,35 @@ class InvoicePrivateMedia(UserPassesTestMixin, PrivateMediaView): ...@@ -416,12 +419,35 @@ class InvoicePrivateMedia(UserPassesTestMixin, PrivateMediaView):
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
def get_media(self, *args, **kwargs): def get_media(self, *args, **kwargs):
file_pk = kwargs.get("file_pk") # check if the request is for a supporting document
if not file_pk: if file_pk := kwargs.get("file_pk"):
return self.invoice.document 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) # if not, then it's for invoice document
return document.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): def test_func(self):
if self.request.user.is_apply_staff: if self.request.user.is_apply_staff:
......
import io
import os import os
from io import BytesIO
from itertools import cycle from itertools import cycle
from bs4 import BeautifulSoup, NavigableString from bs4 import BeautifulSoup, NavigableString
from django.core.files import File
from pypdf import PdfReader, PdfWriter
from reportlab.lib import pagesizes from reportlab.lib import pagesizes
from reportlab.lib.colors import Color, white from reportlab.lib.colors import Color, white
from reportlab.lib.styles import ParagraphStyle as PS from reportlab.lib.styles import ParagraphStyle as PS
...@@ -23,6 +25,7 @@ from reportlab.platypus import ( ...@@ -23,6 +25,7 @@ from reportlab.platypus import (
Table, Table,
TableStyle, TableStyle,
) )
from xhtml2pdf import pisa
STYLES = { STYLES = {
"Question": PS( "Question": PS(
...@@ -164,7 +167,7 @@ class ReportDocTemplate(BaseDocTemplate): ...@@ -164,7 +167,7 @@ class ReportDocTemplate(BaseDocTemplate):
def make_pdf(title, sections, pagesize): def make_pdf(title, sections, pagesize):
prepare_fonts() prepare_fonts()
buffer = io.BytesIO() buffer = BytesIO()
page_width, page_height = getattr(pagesizes, pagesize) page_width, page_height = getattr(pagesizes, pagesize)
doc = ReportDocTemplate( doc = ReportDocTemplate(
...@@ -460,3 +463,39 @@ def draw_project_content(content): ...@@ -460,3 +463,39 @@ def draw_project_content(content):
paragraphs.extend(flowables) paragraphs.extend(flowables)
return paragraphs 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)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment