www.hubleto.eu Reddit User Guide Stack Overflow LinkedIn Report an issue

Custom document and invoice templates

PDF documents (quotations, orders, price lists, invoices) are rendered from templates. A template is a Twig document stored in the Documents app (Documents → Templates). The user edits it without touching code. A model decides which variables the template can use and what the default template looks like.

How a document is created

Documents\Generator::generatePdf($model, $recordId, $documentName) does the whole job:

  1. It reads the record and the template that the record points to (id_template).
  2. It collects the variables with getPreviewVars(), which calls getDocumentPreviewVars($recordId) of the model.
  3. It renders the template and sends the HTML to the PDF service configured in pdfGeneratorEndpoint.
  4. It stores the PDF as a document of the Documents app and writes the file name to the record (pdf) and the ID of the document (id_document).
apps/Documents/Generator.php (excerpt)
  public function generatePdf(string $model, int $recordId, string $documentName): array
  {
    /** @var Hubleto\ErpModel */
    $mObj = $this->getService($model);

    $record = $mObj->record->prepareReadQuery()->where($mObj->table . '.id', $recordId)->first();
    if (!$record) throw new \Exception($this->translate('Record to generate PDF from was not found.'));

    $mTemplate = $this->getService(Template::class);
    $template = $mTemplate->record->prepareReadQuery()->where('documents_templates.id', $record->id_template)->first();
    if (!$template) throw new \Exception($this->translate('Template was not found.'));

    $vars = $this->getPreviewVars($model, $recordId);

    $outputFilename = Helper::str2url($documentName) . '-' . new \DateTimeImmutable()->format('Ymd-His') . '.pdf';

    $idDocument = $this->createPdfDocumentFromTemplate(
      $documentName,
      $model,
      $recordId,
      $template->id,
      $outputFilename,
      $vars
    );

    $mObj->record->find($recordId)->update([ 'pdf' => $outputFilename, 'id_document' => $idDocument ]);

    return [$idDocument, $outputFilename];
  }

For this to work, the table of your model needs the columns id_template (a lookup to Documents\Models\Template), id_document and pdf, as Deal, Order, Invoice and PriceList have. The call is made from an API controller:

apps/Deals/Controllers/Api/GenerateQuotationPdf.php
<?php declare(strict_types=1);

namespace Hubleto\App\Community\Deals\Controllers\Api;

use Hubleto\App\Community\Deals\Models\Deal;

class GenerateQuotationPdf extends \Hubleto\Erp\Controllers\ApiController
{
  public function renderJson(): array
  {
    $idDeal = $this->router()->urlParamAsInteger('idDeal');

    /** @var Deal */
    $mDeal = $this->getModel(Deal::class);

    $idDocument = $mDeal->generateQuotationPdf($idDeal);

    return [
      'idDocument' => $idDocument
    ];
  }
}

NOTE The PDF conversion is done by an external service. Set pdfGeneratorEndpoint in the configuration, otherwise no PDF is created.

Variables of a template

By default getDocumentPreviewVars() of Hubleto\Erp\Model returns the record as an array together with all relations of the model, so a template can use {{ identifier }}, {{ CUSTOMER.name }} or loop over ITEMS. The generator adds user (the signed-in user), hubleto, now, and PDF_EXPORT (true while the PDF is rendered).

src/Model.php (excerpt)
  public function getDocumentPreviewVars(int $recordId, array $relations = []): array {
    $query = $this->record->prepareReadQuery()->where($this->table . '.id', $recordId);
    foreach ($this->relations as $relName => $relConfig) {
      $query = $query->with($relName);
    }
    foreach ($relations as $relName) $query = $query->with($relName);
    $data = $query->first();
    return $data ? $data->toArray() : [];
  }

Override it to load more relations or to add calculated values. Invoices adds the PayBySquare QR code, PriceLists loads the products of the items:

apps/Invoices/Models/Invoice.php (excerpt)
  public function getDocumentPreviewVars(int $idInvoice, $relations = []): array
  {
    $vars = parent::getDocumentPreviewVars($idInvoice, ['PROFILE.COMPANY']);
apps/PriceLists/Models/PriceList.php (excerpt)
    return parent::getDocumentPreviewVars($recordId, array_merge($relations, ['ITEMS.PRODUCT']));

The default template

When the record has no template, or the template is missing, the generator falls back to getDocumentDefaultTemplate($vars) of the model. Override it to provide a ready-made layout. Deals returns a complete quotation:

apps/Deals/Models/Deal.php (excerpt)
  public function getDocumentDefaultTemplate(array $vars = []): string
  {
    return '
      <html>
      <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <title>{{ translate("Ponuka") }} {{ user.DEFAULT_COMPANY.name }} {{ number }}</title>

A template is Twig. These constructs are used in the default quotation:

<b>{{ CUSTOMER.name }}</b>
{% if CUSTOMER.vat_id %} VAT ID: {{ CUSTOMER.vat_id }} {% endif %}

{% for i in ITEMS %}
  {{ i.title }} — {{ hubleto.locale().formatNumber(i.amount, 4) }}
  {{ hubleto.locale().formatCurrency(i.price_excl_vat, CURRENCY.symbol) }}
{% endfor %}

Use translate("Text") for texts and {% if PDF_EXPORT %} to style the PDF differently from the preview.

Preview

Generator::getPreviewHtml($model, $recordId, $idTemplate) renders the template to HTML so that the user can check the result before the PDF is created.

Related pages