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:
- It reads the record and the template that the record points to (
id_template). - It collects the variables with
getPreviewVars(), which callsgetDocumentPreviewVars($recordId)of the model. - It renders the template and sends the HTML to the PDF service configured in
pdfGeneratorEndpoint. - It stores the PDF as a document of the
Documentsapp 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
pdfGeneratorEndpointin 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.
