Automate pdf export with default tokens.

master
Lio Novelli 2021-09-22 23:46:07 +02:00
parent e605729aaf
commit 87e4a5a4fd
8 changed files with 632 additions and 50 deletions

View File

@ -18,7 +18,8 @@
"guzzlehttp/guzzle": "^7.3",
"php-di/php-di": "^6.3",
"symfony/yaml": "^5.2",
"mpdf/mpdf": "^8.0"
"mpdf/mpdf": "^8.0",
"swiftmailer/swiftmailer": "^6.2"
},
"autoload": {
"psr-4": {

471
app/composer.lock generated
View File

@ -4,8 +4,156 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e6f2abc31fc53724d858e127ce02978e",
"content-hash": "cbb7d7281f9622e5b410185c54558b28",
"packages": [
{
"name": "doctrine/lexer",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/lexer.git",
"reference": "e864bbf5904cb8f5bb334f99209b48018522f042"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042",
"reference": "e864bbf5904cb8f5bb334f99209b48018522f042",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^6.0",
"phpstan/phpstan": "^0.11.8",
"phpunit/phpunit": "^8.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
"homepage": "https://www.doctrine-project.org/projects/lexer.html",
"keywords": [
"annotations",
"docblock",
"lexer",
"parser",
"php"
],
"support": {
"issues": "https://github.com/doctrine/lexer/issues",
"source": "https://github.com/doctrine/lexer/tree/1.2.1"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
"type": "tidelift"
}
],
"time": "2020-05-25T17:44:05+00:00"
},
{
"name": "egulias/email-validator",
"version": "3.1.1",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
"reference": "c81f18a3efb941d8c4d2e025f6183b5c6d697307"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/c81f18a3efb941d8c4d2e025f6183b5c6d697307",
"reference": "c81f18a3efb941d8c4d2e025f6183b5c6d697307",
"shasum": ""
},
"require": {
"doctrine/lexer": "^1.2",
"php": ">=7.2",
"symfony/polyfill-intl-idn": "^1.15"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^8.5.8|^9.3.3",
"vimeo/psalm": "^4"
},
"suggest": {
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Egulias\\EmailValidator\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Eduardo Gulias Davis"
}
],
"description": "A library for validating emails against several RFCs",
"homepage": "https://github.com/egulias/EmailValidator",
"keywords": [
"email",
"emailvalidation",
"emailvalidator",
"validation",
"validator"
],
"support": {
"issues": "https://github.com/egulias/EmailValidator/issues",
"source": "https://github.com/egulias/EmailValidator/tree/3.1.1"
},
"funding": [
{
"url": "https://github.com/egulias",
"type": "github"
}
],
"time": "2021-04-01T18:37:14+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.3.0",
@ -864,6 +1012,81 @@
],
"time": "2021-02-11T11:37:01+00:00"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v6.2.7",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
"reference": "15f7faf8508e04471f666633addacf54c0ab5933"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/15f7faf8508e04471f666633addacf54c0ab5933",
"reference": "15f7faf8508e04471f666633addacf54c0ab5933",
"shasum": ""
},
"require": {
"egulias/email-validator": "^2.0|^3.1",
"php": ">=7.0.0",
"symfony/polyfill-iconv": "^1.0",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"symfony/phpunit-bridge": "^4.4|^5.0"
},
"suggest": {
"ext-intl": "Needed to support internationalized email addresses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.2-dev"
}
},
"autoload": {
"files": [
"lib/swift_required.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Chris Corbyn"
},
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
}
],
"description": "Swiftmailer, free feature-rich PHP mailer",
"homepage": "https://swiftmailer.symfony.com",
"keywords": [
"email",
"mail",
"mailer"
],
"support": {
"issues": "https://github.com/swiftmailer/swiftmailer/issues",
"source": "https://github.com/swiftmailer/swiftmailer/tree/v6.2.7"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/swiftmailer/swiftmailer",
"type": "tidelift"
}
],
"time": "2021-03-09T12:30:35+00:00"
},
{
"name": "symfony/console",
"version": "v5.2.6",
@ -1056,6 +1279,86 @@
],
"time": "2021-01-07T16:49:33+00:00"
},
{
"name": "symfony/polyfill-iconv",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-iconv.git",
"reference": "63b5bb7db83e5673936d6e3b8b3e022ff6474933"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/63b5bb7db83e5673936d6e3b8b3e022ff6474933",
"reference": "63b5bb7db83e5673936d6e3b8b3e022ff6474933",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-iconv": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Iconv\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Iconv extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"iconv",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-iconv/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-27T09:27:20+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.22.1",
@ -1120,6 +1423,93 @@
],
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "65bd267525e82759e7d8c4e8ceea44f398838e65"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/65bd267525e82759e7d8c4e8ceea44f398838e65",
"reference": "65bd267525e82759e7d8c4e8ceea44f398838e65",
"shasum": ""
},
"require": {
"php": ">=7.1",
"symfony/polyfill-intl-normalizer": "^1.10",
"symfony/polyfill-php72": "^1.10"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Idn\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Laurent Bassin",
"email": "laurent@bassin.info"
},
{
"name": "Trevor Rowbotham",
"email": "trevor.rowbotham@pm.me"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"idn",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-27T09:27:20+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.22.1",
@ -1250,6 +1640,82 @@
],
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-php72",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
"reference": "9a142215a36a3888e30d0a9eeea9766764e96976"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976",
"reference": "9a142215a36a3888e30d0a9eeea9766764e96976",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php72\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php72/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-27T09:17:38+00:00"
},
{
"name": "symfony/polyfill-php73",
"version": "v1.22.1",
@ -3546,5 +4012,6 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

View File

@ -3,32 +3,50 @@
<style>
body {
font-family: sans-serif;
font-size: 10pt;
font-size: 12pt;
}
.table {
width: 100%;
display: flex;
justify-content: space-between;
}
.table table {
border: 1px solid;
border-radius: 5px;
width: 100%;
margin: 0px auto;
float: none;
}
p { margin: 0pt; }
td { vertical-align: top; }
.items td {
table td {
border-left: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
}
table thead td { background-color: #EEEEEE;
table thead th {
background-color: #EEEEEE;
text-align: center;
border: 0.1mm solid #000000;
font-variant: small-caps;
}
.items td.blanktotal {
background-color: #EEEEEE;
table tbody td {
background-color: #FFF;
border: 0.1mm solid #000000;
background-color: #FFFFFF;
border: 0mm none #000000;
border-top: 0.1mm solid #000000;
border-right: 0.1mm solid #000000;
padding-right: 2mm;
padding-left: 2mm;
}
.items td.totals {
tbody td.td-3,
tbody td.td-1 {
text-align: right;
border: 0.1mm solid #000000;
}
.items td.cost,
tbody tr.tr-2 td,
tbody tr.tr-3 td {
background-color: #EEE;
border-top: 0.4mm solid #000;
}
tbody tr.tr-3 td {
font-weight: bold;
}
tbody td.td-2,
.center {
text-align: center;
}
@ -44,7 +62,7 @@
<p></p>
<p></p>
<p></p>
<p class="right">City, on 20. 9. 2021</p>
<p class="right">City, on [[today]]</p>
<p><strong>Honorarnote</br>Nummer [[number]]</strong></p>
<p></p>
<p>Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:</p>

View File

@ -0,0 +1,6 @@
# config/packages/translation.yaml
framework:
translator:
paths:
- '%kernel.project_dir%/translations'

View File

@ -8,7 +8,7 @@ tracking_service:
report_id: '<89-123>'
export:
template_path: '~/.config/rprt-cli/invoice-template.html'
output: '/tmp/YYYY-mm-invoice.pdf'
output: '/tmp/[[YEAR]]-[[month]]-invoice.pdf'
tokens:
key: 'value to replace key'
another_key: 'value to replace another key'
@ -17,13 +17,6 @@ export:
- 'Hours'
- 'Rate'
- 'Price'
# reports:
# report short name:
# table:
# header:
# # overrides for table header
# hours: Quantity
# source: <youtrack-url>
projects:
'<short name of first project>':
name: '<Project long name>'
@ -32,7 +25,6 @@ projects:
currency: 'EUR'
project column: 1
time column: 4
# time format m - minutes, h - hours
time format: 'm'
locale: 'en_GB'

View File

@ -55,6 +55,7 @@ class RprtCommand extends Command
$this->setName('rprt');
$this->setDescription('Generate monthly report');
// @TODO $this->addUsage('');
// @TODO add sub options (config overrides)
$this->addOption(
'file',
'f',
@ -79,6 +80,18 @@ class RprtCommand extends Command
InputOption::VALUE_NONE,
'Test login into youtrack service.'
);
$this->addOption(
'output',
'o',
InputOption::VALUE_REQUIRED,
'Provide output file path. This option overrides configuration.'
);
$this->addOption(
'send',
's',
InputOption::VALUE_NONE,
'Send pdf export via email to recipient.'
);
}
protected function execute(InputInterface $input, OutputInterface $output) : int
@ -101,13 +114,19 @@ class RprtCommand extends Command
if ($pdf = $input->getOption('pdf')) {
$nice_data = $this->csv->arangeDataForPdfExport($data);
// @TODO method gatherTokens();
$this->pdfExport->fromDataToPdf($nice_data);
if ($output = $input->getOption('output')) {
$this->pdfExport->setOutput($output);
}
$output_path = $this->pdfExport->fromDataToPdf($nice_data);
}
return Command::SUCCESS;
// return Command::SUCCESS;
}
if ($send = $input->getOption('send') && $output_path) {
// Send email to configured address.
}
$this->dummyOutput($input, $output);
// $this->dummyOutput($input, $output);
return Command::SUCCESS;
}

View File

@ -95,9 +95,9 @@ class CsvReport implements CsvReportInterface
$price = $hours * (int) $config['price'];
$row = [
$config['name'],
$hours,
$config['price'],
$hours * $config['price'],
number_format($hours, 2, ',', '.'),
number_format($config['price'], 2, ',', '.'),
number_format($price, 2, ',', '.'),
];
$rows[] = $row;
$totalHours += $hours;
@ -105,7 +105,8 @@ class CsvReport implements CsvReportInterface
}
// @TODO Check rate in final result.
// $rows[] = [$this->translator->trans('Sum'), $totalHours, $config['price'], $totalPrice];
$rows[] = ['Sum', $totalHours, $config['price'], $totalPrice];
$rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), number_format($config['price'], 2, ',', '.'), number_format($totalPrice, 2, ',', '.')];
$rows[] = [null, null, 'Gessamt brutto', number_format($totalPrice, 2, ',', '.')];
return $rows;
}

View File

@ -25,7 +25,7 @@ class PdfExportService implements PdfExportInterface {
public function getTemplatePath(): ?string
{
if (!isset($this->templatePath)) {
$template_path = $this->config->get('report.template_path', FALSE);
$template_path = $this->config->get('export.template_path', FALSE);
if (!$template_path) {
$template_path = readline('Enter template file path: ');
}
@ -45,19 +45,33 @@ class PdfExportService implements PdfExportInterface {
throw new Exception('Template file not found!');
}
// @TODO move this method to CsvReport.
// Add classes to table elements.
// @TODO would it make sense to allow more per user extending?
public function parsedDataToHtmlTable(array $data): ?string {
$table = '<table><thead><tr>';
$table = '<table><thead><tr class="tr-header">';
$header = array_shift($data);
foreach ($header as $cell) {
$table .= "<th>{$cell}</th>";
foreach ($header as $index => $cell) {
$table .= "<th class=\"th-{$index}\">{$cell}</th>";
}
$table .= '</tr></thead><tbody>';
foreach ($data as $row) {
$cells = [];
foreach ($row as $cell) {
$cells[] = "<td>{$cell}</td>";
foreach ($data as $row_index => $row) {
list($cells, $colspan) = [[], 0];
foreach ($row as $index => $cell) {
if (!$cell) {
$colspan += 1;
continue;
}
if ($colspan) {
$colspan += 1;
$cells[] = "<td class=\"td-{$index}\" colspan=\"{$colspan}\">{$cell}</td>";
$colspan = 0;
}
else {
$cells[] = "<td class=\"td-{$index}\">{$cell}</td>";
}
}
$rows[] = '<tr>' . implode($cells) . '</tr>';
$rows[] = "<tr class=\"tr-{$row_index}\">" . implode($cells) . '</tr>';
}
$table .= implode('', $rows);
$table .= '</tbody></table>';
@ -73,31 +87,95 @@ class PdfExportService implements PdfExportInterface {
return $template;
}
// @TODO write a method to gather tokens.
public function getTokensInTemplate(string $template): array {
// @TODO find substrings of type [[key]]
preg_match_all('/\[\[([a-z0-9-_]+)\]\]/', $template, $match);
return $match[1];
}
// @TODO support multiple templates by adding template id in config.
/**
* Get tokens to replace in the template.
*
* @param string $template_path
* Path to the template file (long term plan is to support multiple templates).
* @param bool $skip_missing
* Just skip missing tokens.
* @param array $runtime_tokens
* Provide tokens at runtime of the application (not supported yet). @TODO
*
* @return array
* Token keys and values array.
*/
public function gatherTokensForTemplate(string $template_path, bool $skip_missing = FALSE, array $runtime_tokens = []): array {
list($tokens, $missing) = [[], []];
$token_keys = $this->getTokensInTemplate(file_get_contents($template_path));
$config_tokens = $this->config->get('export.tokens');
foreach ($token_keys as $token_key) {
if (isset($runtime_tokens[$token_key])) {
$tokens[$token_key] = $runtime_tokens[$token_key];
}
elseif (!isset($config_tokens[$token_key]) && !$skip_missing) {
$tokens[$token_key] = readline("Enter value to replace [[{$token_key}]] in template: ");
}
elseif (isset($config_tokens[$token_key])) {
$tokens[$token_key] = $config_tokens[$token_key];
}
else {
$missing[] = $token_key;
}
}
return $tokens;
}
public function pdfExport(string $html, $output = null): bool {
$this->mpdf->SetProtection(array('print'));
$this->mpdf->SetTitle("Acme Trading Co. - Invoice");
$this->mpdf->SetAuthor("Acme Trading Co.");
// @TODO make configurable.
$this->mpdf->SetTitle("Invoice");
$this->mpdf->SetAuthor("rprt-cli");
$this->mpdf->SetDisplayMode('fullpage');
$this->mpdf->WriteHTML($html);
if (!$this->output) {
$this->output = $this->config->get('report.output', NULL) ?? readline('Enter output file path: ');
}
$this->mpdf->Output($this->output, Destination::FILE);
$this->mpdf->Output($this->getOutput(), Destination::FILE);
return file_exists($this->output);
}
protected function getOutput() {
if (isset($this->output)) {
return $this->output;
}
$output = $this->config->get('export.output', NULL) ?? readline('Enter output file path: ');
$output = str_replace('[[month]]', date('F'), $output);
$output = str_replace('[[year]]', date('Y'), $output);
$this->output = $output;
return $output;
}
public function setOutput(string $path) {
$this->output = $path;
}
public function fromDataToPdf(array $data, array $tokens = []): bool {
$template_path = $this->getTemplatePath();
$tokens = $this->defaultTokens();
$tokens['table'] = $this->parsedDataToHtmlTable($data);
$tokens = $this->gatherTokensForTemplate($template_path, FALSE, $tokens);
$html = $this->replaceTokensInTemplate($template_path, $tokens);
$success = $this->pdfExport($html);
return $success;
}
/**
* Get default tokens.
*/
protected function defaultTokens(): array {
$tokens = [];
$tokens['today'] = date('j. m. y');
$month_ago = strtotime('1 month ago');
$tokens['number'] = date('m/y', $month_ago);
$tokens['date_start'] = date('1. m. y', $month_ago);
$tokens['date_end'] = date("Y-m-d", mktime(0, 0, 0, (int) date("m"), 0));
return $tokens;
}
}