From e605729aaf25b4e0b3cc96b492eb8b2d6795ea59 Mon Sep 17 00:00:00 2001 From: Lio Novelli Date: Tue, 21 Sep 2021 01:13:15 +0200 Subject: [PATCH] Add pdf export. --- README.org | 2 +- app/config/invoice-template.html | 59 ++++++++++ app/config/rprt.example.config.yaml | 24 ++-- app/dependencies.php | 13 ++- app/src/Commands/RprtCommand.php | 20 +++- app/src/Utils/CsvReport/CsvReport.php | 38 ++++++- .../Utils/PdfExport/PdfExportInterface.php | 44 ++++++++ app/src/Utils/PdfExport/PdfExportService.php | 103 ++++++++++++++++++ .../TimeTrackingServices/YoutrackService.php | 25 ++++- 9 files changed, 313 insertions(+), 15 deletions(-) create mode 100644 app/config/invoice-template.html create mode 100644 app/src/Utils/PdfExport/PdfExportInterface.php create mode 100644 app/src/Utils/PdfExport/PdfExportService.php diff --git a/README.org b/README.org index 2acdd86..f639c0f 100644 --- a/README.org +++ b/README.org @@ -69,5 +69,5 @@ *** Get csv file - curl 'https://drunomics.myjetbrains.com/youtrack/api/reports/83-554/export/csv?&$top=-1' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H "Authorization: Bearer $TKN" > ~/Documents/Drunomics/workhours/2021/21-09.csv + ~curl 'https://drunomics.myjetbrains.com/youtrack/api/reports/83-554/export/csv?&$top=-1' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H "Authorization: Bearer $TKN" > ~/Documents/Drunomics/workhours/2021/21-09.csv~ diff --git a/app/config/invoice-template.html b/app/config/invoice-template.html new file mode 100644 index 0000000..64f98c0 --- /dev/null +++ b/app/config/invoice-template.html @@ -0,0 +1,59 @@ + + + + + +

My company name
My company address
123456789, mycompany@email.com
987654321

+

+

Other company
Other company address
1111 City
State

+

+

+

+

City, on 20. 9. 2021

+

Honorarnote
Nummer [[number]]

+

+

Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:

+ [[table]] +

Ich ersuche Sie höflich, den oben angeführt auf meine Kontonummer [[account_number]] mit der Bankleitzahl ABCDSI33 zu überweisen.

+

Vielen Dank für den Auftrag,
+ mit besten Grüßen,

+

+

+

My name

+ + diff --git a/app/config/rprt.example.config.yaml b/app/config/rprt.example.config.yaml index b9a73ed..b1783c4 100644 --- a/app/config/rprt.example.config.yaml +++ b/app/config/rprt.example.config.yaml @@ -1,9 +1,22 @@ ########################################## # Configuration file for RprtCli Command # ########################################## -tracking service: +tracking_service: youtrack: - auth token: '' + auth_token: '' + base_url: 'https://test.youtrack.com' + report_id: '<89-123>' +export: + template_path: '~/.config/rprt-cli/invoice-template.html' + output: '/tmp/YYYY-mm-invoice.pdf' + tokens: + key: 'value to replace key' + another_key: 'value to replace another key' + labels: + - 'Project' + - 'Hours' + - 'Rate' + - 'Price' # reports: # report short name: # table: @@ -21,10 +34,5 @@ projects: time column: 4 # time format m - minutes, h - hours time format: 'm' - labels: - project: Project - # hours: Quantity - hours: Hours - rate: 'Price per hour' - price: Price + locale: 'en_GB' diff --git a/app/dependencies.php b/app/dependencies.php index 8f41a27..df9929c 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -9,6 +9,9 @@ use RprtCli\Utils\Configuration\ConfigurationService; use RprtCli\Utils\CsvReport\CsvReport; use RprtCli\Utils\CsvReport\CsvReportInterface; use GuzzleHttp\Client; +use Mpdf\Mpdf; +use RprtCli\Utils\PdfExport\PdfExportInterface; +use RprtCli\Utils\PdfExport\PdfExportService; use RprtCli\Utils\TimeTrackingServices\YoutrackInterface; use RprtCli\Utils\TimeTrackingServices\YoutrackService; @@ -22,6 +25,7 @@ return [ // 'translator' => ['default_path' => '%kernel.project_dir%/translations'], // 'guzzle' => create()->constructor(Client::class), 'guzzle' => get(Client::class), + 'mpdf' => get(Mpdf::class), ConfigurationInterface::class => get(ConfigurationService::class), ConfigurationService::class => create()->constructor( get('config.path'), @@ -34,6 +38,12 @@ return [ get('guzzle') ), 'youtrack.service' => get(YoutrackInterface::class), + PdfExportInterface::class => get(PdfExportService::class), + PdfExportService::class => create()->constructor( + get('config.service'), + get('mpdf') + ), + 'pdf_export.service' => get(PdfExportInterface::class), // 'locale' => get('config.service')->method('get', 'en'), // Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader), // 'translator' => get(Translator::class), @@ -45,6 +55,7 @@ return [ RprtCommand::class => create()->constructor( get('csv.report'), get('config.service'), - get('youtrack.service') + get('youtrack.service'), + get('pdf_export.service') ), ]; diff --git a/app/src/Commands/RprtCommand.php b/app/src/Commands/RprtCommand.php index 784bb8b..136e5ac 100644 --- a/app/src/Commands/RprtCommand.php +++ b/app/src/Commands/RprtCommand.php @@ -8,6 +8,7 @@ namespace RprtCli\Commands; use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\CsvReport\CsvReportInterface; +use RprtCli\Utils\PdfExport\PdfExportInterface; use RprtCli\Utils\TimeTrackingServices\YoutrackInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; @@ -30,15 +31,19 @@ class RprtCommand extends Command protected $youtrack; + protected $pdfExport; + public function __construct( CsvReportInterface $csv, ConfigurationInterface $configuration, YoutrackInterface $youtrack, + PdfExportInterface $pdf_export, ?string $name = null ) { $this->csv = $csv; $this->configuration = $configuration; $this->youtrack = $youtrack; + $this->pdfExport = $pdf_export; parent::__construct($name); } @@ -82,10 +87,23 @@ class RprtCommand extends Command $test = $this->youtrack->testYoutrackapi(); $output->writeln($test); } - if ($file = $input->getOption('file')) { + if ($youtrack = $input->getOption('youtrack')) { + $report_id = $this->youtrack->getReportId(); + $file = $this->youtrack->downloadReport($report_id); + } + if ($youtrack || $file = $input->getOption('file')) { + // Youtrack can also provide a file name. + var_dump($file); $data = $this->csv->getReportData($file); $table = $this->generateTable($output, $data); $table->render(); + + if ($pdf = $input->getOption('pdf')) { + $nice_data = $this->csv->arangeDataForPdfExport($data); + // @TODO method gatherTokens(); + $this->pdfExport->fromDataToPdf($nice_data); + } + return Command::SUCCESS; } diff --git a/app/src/Utils/CsvReport/CsvReport.php b/app/src/Utils/CsvReport/CsvReport.php index 5d25d27..4c2b3a4 100644 --- a/app/src/Utils/CsvReport/CsvReport.php +++ b/app/src/Utils/CsvReport/CsvReport.php @@ -58,9 +58,10 @@ class CsvReport implements CsvReportInterface /** * Get correct values from the raw data lines of csv. * - * + * @param array $rawData * Columns with data are specified in config. * + * @return array * Project key and unit of time spent. */ protected function parseCsvFile(array $rawData) : array @@ -74,6 +75,41 @@ class CsvReport implements CsvReportInterface return []; } + public function arangeDataForPdfExport(array $data): array { + [$rows, $totalHours, $totalPrice] = [[], 0, 0]; + $projectsConfig = $this->configurationService->get('projects'); + $header = $this->configurationService->get('export.labels', null); + if (is_array($header)) { + $rows[] = $header; + } + foreach ($projectsConfig as $name => $config) { + if (! isset($data[$name])) { + // @TODO Proper error handling. + var_dump('Project ' . $name . ' is not set!'); + continue; + } + $hours = $data[$name]; + if ($config['time_format'] === 'm') { + $hours /= 60; + } + $price = $hours * (int) $config['price']; + $row = [ + $config['name'], + $hours, + $config['price'], + $hours * $config['price'], + ]; + $rows[] = $row; + $totalHours += $hours; + $totalPrice += $price; + } + // @TODO Check rate in final result. + // $rows[] = [$this->translator->trans('Sum'), $totalHours, $config['price'], $totalPrice]; + $rows[] = ['Sum', $totalHours, $config['price'], $totalPrice]; + + return $rows; + } + protected function dummyConfig() : array { return [ diff --git a/app/src/Utils/PdfExport/PdfExportInterface.php b/app/src/Utils/PdfExport/PdfExportInterface.php new file mode 100644 index 0000000..bccbdd9 --- /dev/null +++ b/app/src/Utils/PdfExport/PdfExportInterface.php @@ -0,0 +1,44 @@ +config = $config; + $this->mpdf = $mpdf; + } + + public function getTemplatePath(): ?string + { + if (!isset($this->templatePath)) { + $template_path = $this->config->get('report.template_path', FALSE); + if (!$template_path) { + $template_path = readline('Enter template file path: '); + } + if (!file_exists($template_path)) { + throw new Exception('Template file not found!'); + } + $this->templatePath = $template_path; + } + return $this->templatePath; + } + + public function setTemplatePath(string $path): void { + if (file_exists($path)) { + $this->templatePath = $path; + return; + } + throw new Exception('Template file not found!'); + } + + public function parsedDataToHtmlTable(array $data): ?string { + $table = ''; + $header = array_shift($data); + foreach ($header as $cell) { + $table .= ""; + } + $table .= ''; + foreach ($data as $row) { + $cells = []; + foreach ($row as $cell) { + $cells[] = ""; + } + $rows[] = '' . implode($cells) . ''; + } + $table .= implode('', $rows); + $table .= '
{$cell}
{$cell}
'; + return $table; + } + + public function replaceTokensInTemplate(string $template_path, array $tokens): ?string + { + $template = file_get_contents($template_path); + foreach ($tokens as $key => $value) { + $template = str_replace("[[{$key}]]", $value, $template); + } + return $template; + } + + 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."); + $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); + return file_exists($this->output); + } + + public function setOutput(string $path) { + $this->output = $path; + } + + public function fromDataToPdf(array $data, array $tokens = []): bool { + $template_path = $this->getTemplatePath(); + $tokens['table'] = $this->parsedDataToHtmlTable($data); + $html = $this->replaceTokensInTemplate($template_path, $tokens); + $success = $this->pdfExport($html); + return $success; + } + +} diff --git a/app/src/Utils/TimeTrackingServices/YoutrackService.php b/app/src/Utils/TimeTrackingServices/YoutrackService.php index f387201..c5a2fba 100644 --- a/app/src/Utils/TimeTrackingServices/YoutrackService.php +++ b/app/src/Utils/TimeTrackingServices/YoutrackService.php @@ -12,6 +12,9 @@ use RprtCli\Utils\Configuration\ConfigurationInterface; class YoutrackService implements YoutrackInterface { + protected $ytToken; + protected $ytBaseUrl; + protected $config; protected $httpClient; @@ -51,7 +54,7 @@ class YoutrackService implements YoutrackInterface { // --report option value should take precedence. // @TODO error handling. - $yt_report_id = $this->config->get('youtrack.report_id'); + $yt_report_id = $this->config->get('tracking_service.youtrack.report_id'); if (!$yt_report_id) { $yt_report_id = readline('Enter the report id: '); } @@ -81,6 +84,9 @@ class YoutrackService implements YoutrackInterface } protected function getYtToken(): string { + if (isset($this->ytToken)) { + return $this->ytToken; + } $yt_token = $this->config->get('tracking_service.youtrack.auth_token', FALSE); // if (!$yt_token) { $yt_token = readline('Enter your youtrack authentication token: '); @@ -88,9 +94,18 @@ class YoutrackService implements YoutrackInterface return $yt_token; } + public function setYtToken(string $token): void { + $this->ytToken = $token; + } + protected function getYtUrl(string $path = ''): ?string { - $yt_base_url = $this->config->get('tracking_service.youtrack.base_url', FALSE); - if (!$yt_base_url) { + if (isset($this->ytBaseUrl)) { + $yt_base_url = $this->ytBaseUrl; + } + else { + $yt_base_url = $this->config->get('tracking_service.youtrack.base_url', FALSE); + } + if (empty($yt_base_url)) { $yt_base_url = readline('Enter base url for of the youtrack service: '); } if (!empty($path)) { @@ -98,4 +113,8 @@ class YoutrackService implements YoutrackInterface } return $yt_base_url; } + + public function setYtUrl(string $base_url) { + $this->ytBaseUrl = $base_url; + } }