Search code examples
djangoxhtml2pdf

xhtml2pdf - Problem with displaying table rows using django forloop tag


What I am trying to do is to create an invoice pdf file with several table rows. Table rows would be created using for loop in Django. The problem is data inside for loop tag is not visible on the pdf file. You can check the screenshots below. Django properly renders invoice.html template so the code is valid, but the pdf file contains empty frame without any table rows. To render pdf from html I am using xhtml2pdf.

how django render the invoice.html template

how pdf file looks like

invoice.html

<html>
    <head>
        {% load static %}
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <meta charset="UTF-8">
      <style>
        @font-face {
            font-family: Roboto;
            src: url('{% static 'fonts/Roboto-Regular.ttf' %}');
        }

        @font-face {
            font-family: Roboto-bold;
            src: url('{% static 'fonts/Roboto-Bold.ttf' %}');
            font-weight: bold;
        }

        @page {
            size: a4 portrait;
            @frame header_frame {           /* Static Frame */
                -pdf-frame-content: frame_header_left;
                left: 50pt; width: 245pt; top: 30pt; height: 150pt;
            }
            @frame header_frame {           /* Static Frame */
                -pdf-frame-content: frame_header_right;
                left: 300pt; width: 245pt; top: 50pt; height: 150pt;
            }

            @frame content_frame {          /* Content Frame */
                -pdf-frame-content: frame_invoice_number;
                left: 50pt; width: 512pt; top: 170pt; height: 30pt;
            }

            @frame col1 {
                -pdf-frame-content: frame_col1;
                left: 50pt; width: 245pt; top: 220pt; height: 130pt;
            }

            @frame col2 {
                -pdf-frame-content: frame_col2;
                left: 300pt; width: 245pt; top: 220pt; height: 130pt;
            }

            @frame frame_services {
                -pdf-frame-content: frame_services;
                left: 50pt; width: 512pt; top: 380pt; height: 250pt;
                -pdf-frame-border: 1;
            }

            @frame content_frame {
                -pdf-frame-content: frame_summary;
                left: 465pt; width: 100pt; top: 590pt; height: 50pt;
            }

            @frame content_frame {
                -pdf-frame-content: frame_vat;
                left: 50pt; width: 512pt; top: 590pt; height: 150pt;
            }

            @frame content_frame {
                -pdf-frame-content: frame_signatures_left;
                left: 50pt; width: 140pt; top: 725pt; height: 70pt;
            }

            @frame content_frame {
                -pdf-frame-content: frame_signatures_right;
                left: 400pt; width: 140pt; top: 725pt; height: 70pt;
                left: 400pt; width: 140pt; top: 725pt; height: 70pt;
            }

            @frame content_frame {
                -pdf-frame-content: footer_content;
                left: 50pt; width: 512pt; top: 775pt; height: 50pt;
            }
    }

        body {
          background-color: white;
          font-family: "Roboto", sans-serif;
        }

        .right{
            font-size: 10px;
            text-align: right;

        }
        .left{
            text-align: left;
        }

        .invoice-number {
            font-size: 18px;
            font-family: "Roboto-bold", sans-serif;
        }

        .col-titles {
            font-size: 16px;
            text-decoration: underline;
            font-family: "Roboto-bold", sans-serif;
         }

         .footer {
            font-size: 8px;
            text-align: center;
         }

         p{
            font-size: 10px;
            line-height: 0;
         }

         table {
            border-bottom: 1px solid #ddd;
            text-align: center;
        }

        td, td {
            border-bottom: 1px solid #ddd;
            vertical-align: middle;
        }

        .summary{
             border-bottom: 1px solid #ddd;
        }

        .signatures {
            border-top: 1px solid black;
            font-size: 8px;
            text-align: center;
        }

         th {
            height: 36px;

         }
         td {
            height: 25px;
         }


      </style>
    </head>
    <body>
        <div>
            <div>
                <div id="frame_header_left" class="left">
                    <img src="{% static 'invoices/logo.png' %}" alt="logo" width="150" height="112">
                </div>
                <div id="frame_header_right" class="right">
                    <p>Miejsce wystawienia: Żabno</p>
                    <p>Data badania: {{ invoice.data_badania }}</p>
                    <p>Data wystawienia: {{ invoice.data_wystawienia_faktury }}</p>
                </div>
            </div>
            <div id="frame_invoice_number">
                <div class="invoice-number">
                    <h2>Faktura nr: {{ invoice.numer }}</h2>
                </div>
            </div>
            <div id="frame_col1">
                <p class="col-titles">Sprzedawca</p>
                <div>
                    <p>MEDIKAP</p>
                    <p>ul. Plac Grunwaldzki 15B, 33-240 Żabno</p>
                    <p>NIP: 999999999</p>
                    <p>REGON: 9999999</p>
                    <p>Bank: ING Bank Śląski</p>
                    <p>Nr konta: 12 1234 1234 1243 1243 214 1244</p>
                </div>
            </div>

            <div id="frame_col2">
                <p class="col-titles">Nabywca</p>
                <div>
                    <p> {{ invoice.firma}} </p>
                    <p> ul. {{ invoice.firma.ulica }} </p>
                    <p> {{ invoice.firma.kod_pocztowy }} {{ invoice.firma.miasto}}</p>
                    <p> NIP: {{ invoice.firma.nip }}</p>
                    <p> REGON: {{ invoice.firma.regon }}</p>
                    <p> forma płatności: {{ invoice.get_forma_platnosci_display}}</p>
                </div>
            </div>

            <div id="frame_services">
                <table>
                    <tr>
                        <th style="width: 50px;"> # </th>
                        <th style="width: 600px;"> Nazwa usługi</th>
                        <th style="width: 100px;"> Ilość</th>
                        <th style="width: 100px;"> Rabat[%]</th>
                        <th style="width: 100px;"> Cena usługi</th>
                        <th style="width: 100px;"> Wartość</th>
                        <th style="width: 100px;"> Wartość z rabatem:</th>
                    </tr>

                {% for service in services_items %}
                    <tr>
                        <td> test </td>
                        <td> test </td>
                    </tr>
                {% endfor %}
                </table>
            </div>
            <div id="frame_summary" class="summary">
                <p> : {{ service.get_total_value }} PLN</p>
                <p> Wartość z uwzględnieniem {{ invoice.rabat}}% rabatu: {{ discounted_value|floatformat:"-2" }} PLN</p>
            </div>

            <div id="frame_vat">
                <p> Podstawa zwolnienia z VAT: </p>
                <p> Zwolnienie ze względu na zakres wykonywanych czynności (art. 43 ust.1) pkt 19 Ustawy o VAT</p>
            </div>

            <div id="frame_signatures_left">
                <p class="signatures"> podpis osoby upoważnionej do odbioru faktury</p>
            </div>

            <div id="frame_signatures_right">
                <p class="signatures"> podpis osoby upoważnionej do wystawienia faktury</p>
            </div>

            <div id="footer_content" >
                <p class="footer">MEDIKAP Maria K.</p>
                <p class="footer">Plac Grunwaldzki 15B, 33-240 Żabno</p>
                <p class="footer">e-mail: [email protected] tel: 539 993 332</p>
                <p class="footer">NIP: 9930212793 REGON: 852441210</p>
            </div>

        </div>
    </body>
</html>

views.py/DetailsInvoice

class DetailsInvoice(generic.View):
    template_name = 'invoices/invoice_detail.html'
    form_class = DetailInvoiceForm
    success_url = reverse_lazy("invoices:list")

    def get(self, request, invoice_id):
        current_invoice = get_object_or_404(Invoice, id=invoice_id)
        form = self.form_class(instance=current_invoice)
        request.session['invoice_id'] = current_invoice.id

        services = Service.objects.all()
        all_service_items = ServiceItem.objects.all().filter(faktura = current_invoice).order_by('usluga')

        context = {
            'invoice': current_invoice,
            'form' : form,
            'services' : services,
            'services_items' : all_service_items,
        }

        return render(request, self.template_name, context)

    def post(self, request, invoice_id):

        current_invoice = get_object_or_404(Invoice, id=invoice_id)
        form = self.form_class(request.POST, instance=current_invoice)

        all_service_items = ServiceItem.objects.all().filter(faktura=current_invoice)

        context = {
            'invoice' : current_invoice,
        }

        pdf = render_to_pdf('invoices/invoice.html', context)
        services_assigned_to_invoice = current_invoice.uslugi.all()

        if 'update-data' in request.POST and form.is_valid():
            for service in all_service_items:
                service_item = get_object_or_404(ServiceItem, id=service.id)
                quantity_input = request.POST.get('quantity-' + str(service.id))
                discount_input = request.POST.get('discount-' + str(service.id))

                service_item.ilosc = int(quantity_input)
                service_item.rabat = int(discount_input)
                service_item.save()

            form.save()

            for service_item in all_service_items:
                if service_item.usluga not in services_assigned_to_invoice:
                    service_item.delete()

            for single_service in services_assigned_to_invoice:
                new_service_item, created = ServiceItem.objects.get_or_create(usluga=single_service, faktura=current_invoice)

            messages.success(request, 'Pomyślnie zaktualizowane dane')
            return HttpResponseRedirect(self.request.META.get('HTTP_REFERER'))

        if 'view-pdf' in request.POST:
            return HttpResponse(pdf, content_type='application/pdf')

        if 'download-pdf' in request.POST:
            response = HttpResponse(pdf, content_type='application/pdf')
            filename = f"Faktura {current_invoice.numer}.pdf"
            content = "attachment; filename={}".format(filename)
            response['Content-Disposition'] = content
            return response
        else:
            return redirect('invoices:list')

rendering function

def render_to_pdf(template_src, context_dict={}):
    template = get_template(template_src)
    html = template.render(context_dict)
    result = BytesIO()
    pdf = pisa.pisaDocument(BytesIO(html.encode("utf-8")), result, link_callback=link_callback)
    if not pdf.err:
        return HttpResponse(result.getvalue(), content_type='application/pdf')
    return None

Solution

  • You are not passing the same context variables to your PDF as the ones you are passing to your HTML template. To your HTML template you are passing:

        context = {
            'invoice': current_invoice,
            'form' : form,
            'services' : services,
            'services_items' : all_service_items,
        }
    

    While to your PDF you are only passing:

        context = {
            'invoice' : current_invoice,
        }
    

    services_items is the one your PDF template seems to be missing. So because for service in services_items is an empty/non-existent list in your PDF template, it doesn't render any rows. In the future you can check this by adding an {% empty %} section to your for loop:

                {% for service in services_items %}
                    <tr>
                        <td> test </td>
                        <td> test </td>
                    </tr>
                {% empty %}
                    No items!
                {% endfor %}