Creating PDF

Have you changed the HTML template so that it’s using the products array that you pass in, instead of entry.orderProducts? That php code you used was to try to make sure that it only creates a PDF if there is at least one product with a recurring fee, not to change what is included in the pdf.

Brilliant thank you so much we seem to really be getting somewhere!!

I change the code to this:

{% block list_tbody %}
  <tr valign="top">
    <td><ul>
        {% for op in products %}
        <li>{{ op.cli }}</li>
        {%  endfor %}
      </ul></td>
    <td>
      <ul>
        {% for op in products %}
        <li>{{ op.product }} &pound;{{ op.unitPrice }} x {{ op.quantity }}</li>
        {%  endfor %}
      </ul>
    </td>
    <td>{{ entry.billDescription|nl2br }}</td>
    <td><!--{{ entry.getGoLiveDate|date("d/m/Y") }} - -->{{ entry.getExpiryDate|date("d/m/Y") }}</td>
  </tr>
{% endblock %}

And it generated the pdf without the one off product but it did still have the description for the one off product, how do I get rid of this part of it as well?

Is that in entry.billDescription? If it is, you’ll have to find some way of building that description field differently - perhaps using another loop as you do for the first two table columns.

I tried to change this:

<td>{{ entry.billDescription|nl2br }}</td>

to

<td>{{ op.billDescription |nl2br }}</td>

but then got an error saying

Twig_Error_Runtime: Variable “op” does not exist in “order/pdfcertificate.twig” at line 55

I’ve had a look in the php file and there’s no function for the billDescription but it is part of the products database table, the only reference to it in the php file is when an email is sent to the person placing the order

$body[] = $op->getProduct()->getBillDescription().' £'.$op->getUnitPrice().' x '.$op->getQuantity().' '.$op->getCli(). $length;

Should I change this part:

public function generatePdfCertificate($app)
    {
	$products = $this->getRecurringOrderProducts();
	if (count($products) > 0) {
      $filename = $this->getPdfCertificateFilename($app);
      if (!file_exists($filename)) {
        // Generate HTML
        $html =  $app['twig']->render('order/pdfcertificate.twig', array(
          'entry'           => $this,
          'products'        => $products=$this->getRecurringOrderProducts(),
          'content_height'  => intval( (8*100*count($products))/95 )+1,
          'domain'          => $app['config']['domain'],
        ));
        // Generate PDF
        $snappy = new Pdf($app['wkhtmltox_bin']);
        $snappy->setOption('orientation', 'landscape');
        $snappy->generateFromHtml(utf8_decode($html), $filename);
      }
    }
}

Sorry to ask so many questions but I’ve only just started in this job and don’t have a huge amount of PHP experience. They only mentioned HTML when I first applied for the job. Still there’s nothing like learning on the go!!

I changed it to this

 <ul>
        {% for op in products %}
        <li>{{ op.product.billDescription|nl2br }}</li>
        {%  endfor %}
      </ul>

and it now doesn’t have the description for one off products, but if I add more than one recurring product it still only adds one.

I’d think that should be correct, though I’m not sure whether the “nl2br” bit is causing trouble - as I see it, that’s converting newline characters to HTML breaks, so if you’re putting each description in a separate list item, won’t be necessary unless you have multi-line descriptions. If you do, that’s more trouble as they won’t line up with the other lists items doing it that way.

I’ve removed that nl2br part but it’s still only adding one recurring product rather than all of them, I’m not sure how to correct this so that it shows all of the recurring products?

Is billDescription the correct name for the description field for each product? I don’t know your data structures other than what has appeared here, but it struck me that “billDescription” in the original form was built up somewhere to provide all the descriptions for all items on the order, with each one separated by a newline. That’s why your template originally only output a single string, entry.billDescription, but got all descriptions.

So you either need to duplicate the code that builds that multi-line string but only include items that have a recurring fee on it, or somewhere there must be a variable for the individual product description - that is what you need in your new loop.

I’ve just noticed that the actual product is only getting one product too. Although it’s not getting any one off products which is good. I think (although my PHP knowledge is limited) that the problem is here:

public function generateGoLiveCertificate($app)
    {
	$products = $this->getRecurringOrderProducts();
	if (count($products) > 0) {
      $filename = $this->getGoLiveCertificateFilename($app);
      if (!file_exists($filename)) {
        // Generate HTML
        $html =  $app['twig']->render('order/golivecertificate.twig', array(
          'entry'           => $this,
          'products'        => $products=$this->getRecurringOrderProducts(),
          'content_height'  => intval( (8*100*count($products))/95 )+1,
          'domain'          => $app['config']['domain'],
        ));
        // Generate PDF
        $snappy = new Pdf($app['wkhtmltox_bin']);
        $snappy->setOption('orientation', 'landscape');
        $snappy->generateFromHtml(utf8_decode($html), $filename);
      }
    }
}

I think that for what ever reason it’s only getting one product rather than looping through the get every recurring product. Could this be the case? If it is how would I go about getting it to loop?

I thought you’d already checked that the $products array has the correct contents in it? If it still does, the issue must be what the PDF generator does with that array, not how you generate it.

In any case you’re duplicating with this line:


          'products'        => $products=$this->getRecurringOrderProducts(),

You’ve already created the array at the top, where you check how many entries you got to see if a PDF should be created. You could just use


          'products'        => $products,

The PDF generator used to display a list of products, but since we changed this line

{% for op in entry.orderProducts %}

to this

{% for op in products %}

in the twig file it started to only show one. The problem is that if I go back to the entry.orderProducts then although it shows a list of products it also shows products that I don’t want.

So essentially it’s show the correct products but just one product.

From this part of the code

$snappy = new Pdf($app['wkhtmltox_bin']);
        $snappy->setOption('orientation', 'landscape');
        $snappy->generateFromHtml(utf8_decode($html), $filename);

I’ve been through the code and have found these functions that I guess are generating the pdf. One file has this from the ‘$snappy->generateFromHtml(utf8_decode($html), $filename);’ part

public function generateFromHtml($html, $output, array $options = array(), $overwrite = false)
    {
        $filename = $this->createTemporaryFile($html, 'html');

        $this->generate($filename, $output, $options, $overwrite);

        $this->unlink($filename);
    }

and this from the ‘$snappy->setOption(‘orientation’, ‘landscape’);’ part

public function setOption($name, $value)
    {
        if (!array_key_exists($name, $this->options)) {
            throw new \\InvalidArgumentException(sprintf('The option \\'%s\\' does not exist.', $name));
        }

        $this->options[$name] = $value;
    }

as well as this:

public function setOptions(array $options)
    {
        foreach ($options as $name => $value) {
            $this->setOption($name, $value);
        }
    }

Another file also has this from the ‘$snappy->generateFromHtml(utf8_decode($html), $filename);’ part:

function generateFromHtml($html, $output, array $options = array(), $overwrite = false);

Thank you so much for helping me with this, I really do genuinely appreciate it.

I don’t think I can offer more assistance. I’m pretty sure that if the $products array that you generate at the top of the generateGoLiveCertificate() function contains the list of products that should be on the certificate, then the problem must lie in the template code.

I understand what you’re saying but to my mind the problem is here:

public function generatePdfCertificate($app)
    {
	$products = $this->getRecurringOrderProducts();
	if (count($products) > 0) {
      $filename = $this->getPdfCertificateFilename($app);
      if (!file_exists($filename)) {
        // Generate HTML
        $html =  $app['twig']->render('order/pdfcertificate.twig', array(
          'entry'           => $this,
          'products'        => $products,
          'content_height'  => intval( (8*100*count($products))/95 )+1,
          'domain'          => $app['config']['domain'],
        ));
        // Generate PDF
        $snappy = new Pdf($app['wkhtmltox_bin']);
        $snappy->setOption('orientation', 'landscape');
        $snappy->generateFromHtml(utf8_decode($html), $filename);
      }
    }
}

Reason being is that before this part was added:

$products = $this->getRecurringOrderProducts();
	if (count($products) > 0) {
...

and I changed this part of the twig file:


{% for op in entry.orderProducts %}

it listed everything, but as soon as I added that part and changed the twig file to ‘{% for op in products %}’ it started to show only one product. Is there a way of looping through that? Maybe some way of using ‘foreach’ although I don’t know how to use that.

Well, part of trouble I’m having is it’s really quite complex and I can’t see it all, so I’m guessing as to what might be the issue.

First thing to check, is the $products array still being populated correctly?


public function generateGoLiveCertificate($app)
  {
  $products = $this->getRecurringOrderProducts();
  var_dump($products);
  exit();

If it is, then you know for sure that you have created an array of products that meet your requirements. So then it’s a case of going through the next bits of code step by step until you find the issue, by adding echo() and so on so you can watch the progress.

So if it passes the if() statement, you know the array has at least one member, which means you won’t get blank PDF certificates. The next important part is the html->render line where you pass in an array of parameters, which includes a pointer to the class you’re calling from ($this, or entry as it becomes in the template), the products array, content_height (which you calculate based on how many entries there are in the products array) and domain.

So to me, if the products array is correct, then the issue must be either how it’s passed into the render function, or how the template you use in there deals with the incoming array. I had a quick look at twig documentation but it’s quite heavy to get into.

It was working before by accessing what I guess is a public array in your class called “orderProducts” - which is accessed in the template as entry.orderProducts, because you passed the pointer to your class and called in “entry”. If you could create a similar array in the class and call it PDFproducts and populate it in the same way as orderProducts (I’d do it by checking as the item is added to the order, but that might change depending on your order cycle) then your template could run through order.PDFproducts and work the same way.

So, first question, is $products still being created correctly? And if it is, can you show the contents of the var_dump, sanitise if you need to for product names etc.

I’ve just checked $product where you suggested and no it’s only getting one product, although it is a recurring product so that’s a good thing but I guess the problem then is that that variable is now showing one product.

I tested it at an earlier point in the code and it appears that it is only showing one product there too.

public function getRecurringOrderProducts()
  {
    $products = array();
    foreach($this->getOrderProducts() as $op) {
      if($op->getProduct()->isRecurring()) {
        $products[] = $op; 
      }
  var_dump($products); 
  exit();  
    }

    return $products;
  }

Would it help if I was to send you the files? I’m more than happy to pay you for helping me with this, I’m just desperate to get it fixed as my boss keeps asking when it’ll be working.

I’ve just checked this part:


public function getExpiryDate() {
       $ops = $this->getRecurringOrderProducts();
if (count($ops) > 0) {
        $date = $this->getGoLiveDate();
        $date->add(new \\DateInterval('P'.$ops[0]->getContractLength().'M'));
      $date->sub(new \\DateInterval('P1D'));
}
else {
        $date = '00-00-0000';
//		$date = $this->getGoLiveDate();
//		$date->add(new \\DateInterval('P100Y'));

}
        return $date;
      }

using


  var_dump($ops);
  exit();  

It didn’t generate the pdf but it did create the html file file that is used to create the pdf. The var_dump($ops); showed that it was showing ALL of the products that were recurring and none of the ones that were one off which is great.

I changed it to this


public function getExpiryDate() {
       $ops = $this->getRecurringOrderProducts();
        $date = $this->getGoLiveDate();
        $date->add(new \\DateInterval('P'.$ops[0]->getContractLength().'M'));
      $date->sub(new \\DateInterval('P1D'));
        return $date;
      }

and it almost works perfectly. The only thing now is that it’s now showing the expiry date for the first product instead of every one. Is it possible to correct this?

Okay, I’ve been through everything and think that all I need to do it loop through this to get the expiry date for every recurring product

public function getExpiryDate() {
       $ops = $this->getRecurringOrderProducts();
        $date = $this->getGoLiveDate();
        $date->add(new \\DateInterval('P'.$ops[0]->getContractLength().'M'));
      $date->sub(new \\DateInterval('P1D'));
        return $date;
      }

I did try chaining it to this, but it didn’t work.

public function getExpiryDate() {
    foreach($this->getRecurringOrderProducts() as $op) {
if (count($ops) > 0) {
       $date = $this->getGoLiveDate();
        $date->add(new \\DateInterval('P'.$ops[0]->getContractLength().'M'));
      $date->sub(new \\DateInterval('P1D'));
        return $date;
      }
}
}

It still displayed just one expiry date, rather than different dates for each recurring product. I think I either need to loop through that or change this line to display all expiry dates

{{ entry.getExpiryDate|date("d/m/Y") }}

I hope you’ve got some champagne!! I changed the twig file to this:

<ul>
        {% for op in products %}
        <li>{{ entry.getExpiryDate|date("d/m/Y") }}</li>
        {%  endfor %}
      </ul>

and it now works perfectly.

Thank you so much for you help, I really am so grateful.

Excellent news, glad it’s all OK now.