Add tests, unify input.
parent
4dcbcb228a
commit
ff2e35bb93
|
@ -3,3 +3,4 @@
|
|||
/scratch
|
||||
.phpcs-cache
|
||||
*~undo-tree~
|
||||
/app/tests/data/output
|
||||
|
|
36
README.org
36
README.org
|
@ -2,7 +2,7 @@
|
|||
|
||||
Automate generating invoices from youtrack reports and other time-tracking
|
||||
related functionality.
|
||||
|
||||
|
||||
** Usage
|
||||
|
||||
~./rprt.php invoice -y -p -s~
|
||||
|
@ -14,15 +14,14 @@
|
|||
|
||||
Asks which report to print from the list of your reports and then prints out
|
||||
a table with that report.
|
||||
|
||||
|
||||
** Install/Getting started
|
||||
|
||||
Get ~.phar~ file from r, run configuration wizzard (planned for version 1.0).
|
||||
|
||||
Get ~.phar~ file from r, Run configuration wizzard (planned for version 1.0).
|
||||
|
||||
Before version 1.0 is released you have to navigate into ~/app~ directory
|
||||
and call command through ~.rprt.php~ file.
|
||||
|
||||
|
||||
*** Requirements
|
||||
|
||||
1. You have to create a youtrack API token.
|
||||
|
@ -45,7 +44,6 @@
|
|||
- project categories
|
||||
- hourly wage
|
||||
- folder for reports
|
||||
|
||||
|
||||
** Components
|
||||
- report service - provides a csv of a report
|
||||
|
@ -61,7 +59,7 @@
|
|||
|
||||
*** ValueObjects
|
||||
|
||||
|
||||
|
||||
*** List my reports
|
||||
|
||||
#+begin_example bash
|
||||
|
@ -76,21 +74,34 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
|
|||
I'd be using this app. If it get's picked up, by 5 others - it is worth
|
||||
5 days of development.
|
||||
- remove errors from reports
|
||||
|
||||
|
||||
* Development
|
||||
|
||||
** Testing
|
||||
|
||||
Main source at symfony [[https://symfony.com/doc/current/console.html#testing-commands][console command page]]. If you read thouroughly:
|
||||
|
||||
When using the Console component in a standalone project, use Application and
|
||||
extend the normal \PHPUnit\Framework\TestCase.
|
||||
|
||||
** Plan
|
||||
|
||||
*** most current
|
||||
1. For version 0.6.7
|
||||
- [X] data value objects
|
||||
- phar file
|
||||
- [X] phar file
|
||||
- [X] additional expenses
|
||||
2. For version 1.0
|
||||
- plugin system
|
||||
- for time tracking services
|
||||
- for reports
|
||||
- symfony/config & symfony/di components
|
||||
3. Time tracking service
|
||||
- youtrack-api
|
||||
- jira-api
|
||||
- kimai (https://www.kimai.org/documentation/timesheet.html)
|
||||
|
||||
*** current
|
||||
*** current
|
||||
|
||||
1. For Version 1.0
|
||||
- Track Command (track time from your cli)
|
||||
|
@ -106,7 +117,7 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
|
|||
- Add tests
|
||||
|
||||
|
||||
*** old
|
||||
*** old
|
||||
1. Basic structure of the cli-app
|
||||
1. App preparation
|
||||
- nice specifications
|
||||
|
@ -124,7 +135,6 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
|
|||
3. Second round of enhancements
|
||||
1. Invoice output
|
||||
2. configuration wizard
|
||||
|
||||
|
||||
|
||||
** Learning
|
||||
|
@ -134,8 +144,8 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
|
|||
|
||||
** API calls
|
||||
|
||||
|
||||
*** 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~
|
||||
|
||||
|
||||
|
|
|
@ -24,14 +24,15 @@
|
|||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"RprtCli\\": "src"
|
||||
"RprtCli\\": ["src", "tests"]
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"opsway/psr12-strict-coding-standard": "^0.5.0",
|
||||
"phpcompatibility/php-compatibility": "^9.3"
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpstan/phpstan": "^1.9"
|
||||
},
|
||||
"scripts": {
|
||||
"cs": "phpcs --colors --standard=PSR12",
|
||||
|
@ -41,5 +42,6 @@
|
|||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"minimum-stability": "alpha"
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "7b0be09c8f282dfcceb075d1455caa28",
|
||||
"content-hash": "eb0c874610e45fa74d8ccc691f5bd9f8",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/lexer",
|
||||
|
@ -3287,6 +3287,65 @@
|
|||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||
"time": "2020-08-03T20:32:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "1.9.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan.git",
|
||||
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03bccee595e2146b7c9d174486b84f4dc61b0f2",
|
||||
"reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
},
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan/issues",
|
||||
"source": "https://github.com/phpstan/phpstan/tree/1.9.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/ondrejmirtes",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/phpstan",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2022-12-17T13:33:52+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "9.2.15",
|
||||
|
@ -4930,7 +4989,7 @@
|
|||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"minimum-stability": "alpha",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<arg name="colors"/>
|
||||
<arg name="extensions" value="php"/>
|
||||
<arg name="parallel" value="10"/>
|
||||
|
||||
|
||||
<!-- Show progress -->
|
||||
<arg value="p"/>
|
||||
|
||||
|
|
|
@ -38,11 +38,13 @@ use function var_export;
|
|||
/**
|
||||
* Main file - invoice command.
|
||||
*/
|
||||
class InvoiceCommand extends Command
|
||||
{
|
||||
class InvoiceCommand extends Command {
|
||||
|
||||
use SelectReportTrait;
|
||||
|
||||
protected $csv;
|
||||
|
||||
protected $configuration;
|
||||
protected $config;
|
||||
|
||||
protected $youtrack;
|
||||
|
||||
|
@ -54,14 +56,14 @@ class InvoiceCommand extends Command
|
|||
public function __construct(
|
||||
ReportCsvInterface $csv,
|
||||
ConfigurationInterface $configuration,
|
||||
YoutrackInterface $youtrack,
|
||||
YoutrackInterface $trackingService,
|
||||
PdfExportInterface $pdf_export,
|
||||
MailerInterface $mailer,
|
||||
?string $name = null
|
||||
) {
|
||||
$this->csv = $csv;
|
||||
$this->configuration = $configuration;
|
||||
$this->youtrack = $youtrack;
|
||||
$this->config = $configuration;
|
||||
$this->trackingService = $trackingService;
|
||||
$this->pdfExport = $pdf_export;
|
||||
$this->mailer = $mailer;
|
||||
parent::__construct($name);
|
||||
|
@ -82,12 +84,12 @@ class InvoiceCommand extends Command
|
|||
InputOption::VALUE_REQUIRED,
|
||||
'Specify the input csv file to generate report from.'
|
||||
);
|
||||
$this->addOption(
|
||||
'youtrack',
|
||||
'y',
|
||||
InputOption::VALUE_NONE,
|
||||
'Use youtrack api to get a report. If this option is used --file does not have any effect..'
|
||||
);
|
||||
// $this->addOption(
|
||||
// 'youtrack',
|
||||
// 'y',
|
||||
// InputOption::VALUE_NONE,
|
||||
// 'Use youtrack api to get a report. If this option is used --file does not have any effect..'
|
||||
// );
|
||||
$this->addOption(
|
||||
'pdf',
|
||||
'p',
|
||||
|
@ -150,71 +152,42 @@ class InvoiceCommand extends Command
|
|||
protected function execute(InputInterface $input, OutputInterface $output) : int
|
||||
{
|
||||
if ($input->getOption('test')) {
|
||||
$test = $this->youtrack->testYoutrackapi();
|
||||
$test = $this->trackingService->testYoutrackapi();
|
||||
$output->writeln($test);
|
||||
}
|
||||
if ($input->getOption('list-reports')) {
|
||||
$list = $this->youtrack->listReports();
|
||||
$list = $this->trackingService->listReports();
|
||||
$output->writeln(var_export($list, true));
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
|
||||
if ($report = $input->getOption('report')) {
|
||||
$this->youtrack->setReportId($report);
|
||||
} else {
|
||||
$reports = $this->youtrack->listReports();
|
||||
$count = 1;
|
||||
foreach ($reports as $id => $name) {
|
||||
$output->writeln("[{$count}] {$name} ({$id})");
|
||||
$count++;
|
||||
}
|
||||
$report = readline('Select id of the report: ');
|
||||
// Asume people are literate.
|
||||
$this->youtrack->setReportId($report);
|
||||
}
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln("Setting report: <info>{$report}</info>.");
|
||||
}
|
||||
}
|
||||
if ($youtrack = $input->getOption('youtrack')) {
|
||||
$report_id = $this->youtrack->getReportId();
|
||||
$cache_clear_status = $this->youtrack->clearReportCache($report_id);
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln("Report <info>{$report_id}</info> cache cleared, status: {$cache_clear_status}");
|
||||
}
|
||||
$file = $this->youtrack->downloadReport($report_id);
|
||||
}
|
||||
// Gets report parameter.
|
||||
$this->getReportParameter($input, $output, 'tracking_service.youtrack.invoice.report');
|
||||
$report_id = $this->trackingService->getReportId();
|
||||
$file = $this->getReportCsvFilePath($input, $output, $report_id);
|
||||
$report_name = $this->trackingService->getReportName();
|
||||
if ($input->hasParameterOption('--expenses') || $input->hasParameterOption('-e')) {
|
||||
$expenses = $this->getCustomWorkOrExpenses($input->getOption('expenses'), self::TYPE_EXPENSE);
|
||||
}
|
||||
if ($input->hasParameterOption('--custom') || $input->hasParameterOption('-c')) {
|
||||
// @TODO Add option for custom time tracking data.
|
||||
$custom = $this->getCustomWorkOrExpenses($input->getOption('custom'), self::TYPE_WORK);
|
||||
}
|
||||
if ($youtrack || $file = $input->getOption('file')) {
|
||||
// Youtrack can also provide a file name.
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln("Csv file downloaded to: <info>{$file}</info>");
|
||||
$output->writeln("report: <info>{$report_name}</info>");
|
||||
$data = $this->csv->getInvoiceData($file);
|
||||
if (! empty($expenses)) {
|
||||
$data = array_merge($data, $expenses);
|
||||
}
|
||||
$table = $this->getTable($output, $data);
|
||||
$table->render();
|
||||
if ($input->getOption('pdf')) {
|
||||
$nice_data = $this->csv->arangeDataForDefaultPdfExport($data);
|
||||
// @TODO method gatherTokens();
|
||||
if ($out = $input->getOption('output')) {
|
||||
$this->pdfExport->setOutput($out);
|
||||
}
|
||||
$data = $this->csv->getInvoiceData($file);
|
||||
if (! empty($expenses)) {
|
||||
$data = array_merge($data, $expenses);
|
||||
}
|
||||
// $table = $this->generateTable($output, $data);
|
||||
$table = $this->getTable($output, $data);
|
||||
$table->render();
|
||||
|
||||
if ($input->getOption('pdf')) {
|
||||
$nice_data = $this->csv->arangeDataForDefaultPdfExport($data);
|
||||
// @TODO method gatherTokens();
|
||||
if ($out = $input->getOption('output')) {
|
||||
$this->pdfExport->setOutput($out);
|
||||
}
|
||||
$output_path = $this->pdfExport->fromDefaultDataToPdf($nice_data);
|
||||
// Notify the user where the file was generated to.
|
||||
$output->writeln("The file was generated at <info>${output_path}</info>.");
|
||||
}
|
||||
|
||||
// return Command::SUCCESS;
|
||||
$output_path = $this->pdfExport->fromDefaultDataToPdf($nice_data);
|
||||
// Notify the user where the file was generated to.
|
||||
$output->writeln("The file was generated at <info>${output_path}</info>.");
|
||||
}
|
||||
if ($input->getOption('send') && $output_path) {
|
||||
// @TODO If no output path print an error.
|
||||
|
@ -267,7 +240,7 @@ class InvoiceCommand extends Command
|
|||
'Price',
|
||||
]);
|
||||
[$rows, $totalHours, $totalPrice] = [[], 0, 0];
|
||||
$projectsConfig = $this->configuration->get('projects');
|
||||
$projectsConfig = $this->config->get('projects');
|
||||
foreach ($projectsConfig as $name => $config) {
|
||||
if (! isset($data[$name])) {
|
||||
// @TODO Proper error handling.
|
||||
|
@ -353,8 +326,7 @@ class InvoiceCommand extends Command
|
|||
return $output;
|
||||
}
|
||||
|
||||
protected function getCustomWorkOrExpenses($custom, $type)
|
||||
{
|
||||
protected function getCustomWorkOrExpenses($custom, $type) {
|
||||
$output = [];
|
||||
if (is_string($custom)) {
|
||||
foreach (explode(';', $custom) as $item) {
|
||||
|
|
|
@ -14,14 +14,14 @@ use Symfony\Component\Console\Helper\TableSeparator;
|
|||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
|
||||
use function array_flip;
|
||||
use function is_array;
|
||||
use function is_null;
|
||||
|
||||
class ReportCommand extends Command
|
||||
{
|
||||
class ReportCommand extends Command {
|
||||
|
||||
use SelectReportTrait;
|
||||
|
||||
protected $trackingService;
|
||||
|
||||
protected $config;
|
||||
|
@ -37,22 +37,30 @@ class ReportCommand extends Command
|
|||
parent::__construct($name);
|
||||
}
|
||||
|
||||
protected function configure() : void
|
||||
{
|
||||
protected function configure() : void {
|
||||
$this->setName('report');
|
||||
$this->setDescription('Get a time-tracking report into command line.');
|
||||
$this->addOption(
|
||||
'report',
|
||||
'r',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'Select a report from list of your reports'
|
||||
'Select a report from list ofo your reports'
|
||||
);
|
||||
// Not supported by by ReportCsv service!
|
||||
// @TODO Build factory for time tracking service (youtrack-csv-export,
|
||||
// youtrack-api, jira-api, kimai-api (biro.radiostudent.si)).
|
||||
$this->addOption(
|
||||
'time-range',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Calculates report from tracking service work items directly for time range'
|
||||
);
|
||||
$this->addOption(
|
||||
'file',
|
||||
'f',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Specify the input csv file to generate report from.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output) : int
|
||||
|
@ -61,50 +69,19 @@ class ReportCommand extends Command
|
|||
// @TODO: Implement time range option:
|
||||
// - Request workTime items from tracking service
|
||||
// - Filter them, join by issue, project ...
|
||||
// This will came in other report service that will gather data
|
||||
// directly from cache.
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln("Time range: {$timeRange}");
|
||||
}
|
||||
$output->writeln('<error>This option is not supported yet.</error>');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$reports = $this->trackingService->listReports();
|
||||
// Could just parse a csv file or actually get workItems from youtrack ...
|
||||
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
|
||||
if (! $report = $input->getOption('report')) {
|
||||
// @TODO Make selection nicer.
|
||||
// QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new ChoiceQuestion('Select report:', $reports);
|
||||
$question
|
||||
->setErrorMessage('Report %s does not exist!')
|
||||
->setAutocompleterValues($reports);
|
||||
$report = $helper->ask($input, $output, $question);
|
||||
$output->writeln('Report ' . $report . ' selected.');
|
||||
}
|
||||
// If parameter option is not recognised check if report name was given.
|
||||
if (! isset($reports[$report])) {
|
||||
if (! isset(array_flip($reports)[$report])) {
|
||||
$output->writeln('Non-existing report ' . $report . '. Exiting.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
$report = array_flip($reports)[$report];
|
||||
}
|
||||
$this->trackingService->setReportId($report);
|
||||
} elseif ($report = $this->config->get('tracking_service.youtrack.report.default')) {
|
||||
$this->trackingService->setReportId($report);
|
||||
}
|
||||
$this->getReportParameter($input, $output);
|
||||
// Currently we only support csv download.
|
||||
$report_id = $this->trackingService->getReportId();
|
||||
$report_name = $reports[$report_id];
|
||||
// Code duplication.
|
||||
$cache_clear_status = $this->trackingService->clearReportCache($report_id);
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln("Report cache cleared, status: {$cache_clear_status}");
|
||||
}
|
||||
$file = $this->trackingService->downloadReport($report_id);
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln("Csv file downloaded to: <info>{$file}</info>");
|
||||
}
|
||||
$file = $this->getReportCsvFilePath($input, $output, $report_id);
|
||||
$report_name = $this->trackingService->getReportName();
|
||||
$output->writeln("report: <info>{$report_name}</info>");
|
||||
$data = $this->csv->generateReportTable($file);
|
||||
$table = $this->buildTable($output, $data);
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Commands;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
|
||||
/**
|
||||
* Trait to select report.
|
||||
*
|
||||
* To be used on commands with trackingService property and config property.
|
||||
* Command must have report input option. It is not the most elegant solution
|
||||
* but it helps avoiding code duplication.
|
||||
*/
|
||||
trait SelectReportTrait {
|
||||
|
||||
protected function checkContext() {
|
||||
if (!isset($this->trackingService)) {
|
||||
return FALSE;
|
||||
}
|
||||
return $this instanceof Command;
|
||||
}
|
||||
|
||||
protected function getReportParameter(InputInterface $input, OutputInterface $output, $default = 'tracking_service.youtrack.report.report') {
|
||||
if (!$this->checkContext()) {
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$reports = $this->trackingService->listReports();
|
||||
// Could just parse a csv file or actually get workItems from youtrack ...
|
||||
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
|
||||
if (! $report_id = $input->getOption('report')) {
|
||||
// @TODO Make selection nicer.
|
||||
// QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new ChoiceQuestion('Select report:', $reports);
|
||||
$question
|
||||
->setErrorMessage('Report %s does not exist!')
|
||||
->setAutocompleterValues($reports);
|
||||
$report_id = $helper->ask($input, $output, $question);
|
||||
$output->writeln('Report ' . $report_id . ' selected.');
|
||||
}
|
||||
// If parameter option is not recognised check if report name was given.
|
||||
if (! isset($reports[$report_id])) {
|
||||
if (! isset(array_flip($reports)[$report_id])) {
|
||||
$output->writeln('Non-existing report ' . $report_id . '. Exiting.');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$report_id = array_flip($reports)[$report_id];
|
||||
}
|
||||
$this->trackingService->setReportId($report_id);
|
||||
} elseif ($report_id = $this->config->get($default)) {
|
||||
// If report is not set, try getting default report from configuration.
|
||||
// This is dependant on the config.
|
||||
$this->trackingService->setReportId($report_id);
|
||||
}
|
||||
$this->trackingService->setReportName();
|
||||
}
|
||||
|
||||
protected function getReportCsvFilePath(InputInterface $input, OutputInterface $output, ?string $report_id) : ?string {
|
||||
if (!$file = $input->getOption('file')) {
|
||||
$cache_clear_status = $this->trackingService->clearReportCache($report_id);
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln("Report <info>{$report_id}</info> cache cleared, status: {$cache_clear_status}");
|
||||
}
|
||||
$file = $this->trackingService->downloadReport($report_id);
|
||||
if ($output->isVerbose()) {
|
||||
$output->writeln("Csv file downloaded to: <info>{$file}</info>");
|
||||
}
|
||||
} else {
|
||||
$this->trackingService->setReportName($file);
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ class ReportCsv implements ReportCsvInterface
|
|||
if ($file = fopen($filePath, 'r')) {
|
||||
while (($line = fgetcsv($file)) !== false) {
|
||||
$parsed = $this->parseCsvFile($line);
|
||||
// $key = reset(array_keys($parsed));
|
||||
// $key = reset(array_keys($parsed));
|
||||
$key = array_key_first($parsed);
|
||||
if (isset($output[$key])) {
|
||||
$output[$key] += (float) reset($parsed);
|
||||
|
@ -188,10 +188,11 @@ class ReportCsv implements ReportCsvInterface
|
|||
if (empty($data)) {
|
||||
return [];
|
||||
}
|
||||
[$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], []];
|
||||
$explodeMinus = fn($ticket) => explode('-', $ticket)[0] ?? 'UNKNOWN';
|
||||
[$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], [$explodeMinus($data[0]['id'])]];
|
||||
foreach ($data as $line) {
|
||||
$project = explode('-', $line['id'])[0];
|
||||
$previous_project = explode('-', $previous)[0];
|
||||
$project = $explodeMinus($line['id']);
|
||||
$previous_project = $explodeMinus($previous);
|
||||
if ($project !== $previous_project) {
|
||||
// When project changes, add a sum of time for that project.
|
||||
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
|
||||
|
@ -242,32 +243,4 @@ class ReportCsv implements ReportCsvInterface
|
|||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be moved into test class.
|
||||
*/
|
||||
protected function dummyConfig() : array
|
||||
{
|
||||
return [
|
||||
'projects' => [
|
||||
'LDP' => [
|
||||
'name' => 'lupus.digital',
|
||||
'pattern' => 'LDP-[0-9]+',
|
||||
'price' => 25,
|
||||
// optional specify columns
|
||||
],
|
||||
'WV' => [
|
||||
'name' => 'Wirtschaftsverlag',
|
||||
'pattern' => 'WV-[0-9]+',
|
||||
'price' => 25,
|
||||
// optional specify columns
|
||||
],
|
||||
'Other' => [
|
||||
'name' => 'Other projects',
|
||||
'pattern' => '(?!.\bLDP\b)(?!.\bWV\b)',
|
||||
'price' => 25,
|
||||
// optional specify columns
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,4 +34,8 @@ interface YoutrackInterface
|
|||
* Array of reports with ids as keys and names as values.
|
||||
*/
|
||||
public function listReports() : array;
|
||||
|
||||
public function setReportId(string $report_id) :void;
|
||||
public function setReportName(?string $report_name = NULL) :void;
|
||||
public function getReportName() : ?string;
|
||||
}
|
||||
|
|
|
@ -28,14 +28,15 @@ use function var_dump;
|
|||
|
||||
class YoutrackService implements YoutrackInterface
|
||||
{
|
||||
protected $ytToken;
|
||||
protected $ytBaseUrl;
|
||||
protected string $ytToken;
|
||||
protected string $ytBaseUrl;
|
||||
|
||||
protected $config;
|
||||
protected ConfigurationInterface $config;
|
||||
|
||||
protected $httpClient;
|
||||
protected ClientInterface $httpClient;
|
||||
|
||||
protected $report_id;
|
||||
protected string $report_id;
|
||||
protected ?string $reportName = NULL;
|
||||
|
||||
public function __construct(ConfigurationInterface $config, ClientInterface $http_client)
|
||||
{
|
||||
|
@ -106,6 +107,23 @@ class YoutrackService implements YoutrackInterface
|
|||
$this->report_id = $report_id;
|
||||
}
|
||||
|
||||
public function setReportName(?string $report_name = NULL) : void {
|
||||
if (!$report_name) {
|
||||
$reports = $this->listReports();
|
||||
if ($report_id = $this->getReportId()) {
|
||||
$report_name = $reports[$report_id] ?? NULL;
|
||||
}
|
||||
else {
|
||||
$report_name = NULL;
|
||||
}
|
||||
}
|
||||
$this->reportName = $report_name;
|
||||
}
|
||||
|
||||
public function getReportName() : ?string {
|
||||
return $this->reportName;
|
||||
}
|
||||
|
||||
public function downloadReport(string $report_id) : ?string
|
||||
{
|
||||
$path = "youtrack/api/reports/$report_id/export/csv";
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace RprtCli\Tests\Kernel;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use DI\ContainerBuilder;
|
||||
use RprtCli\Commands\InvoiceCommand;
|
||||
use RprtCli\Commands\ReportCommand;
|
||||
|
||||
class ReportCommandTest extends TestCase {
|
||||
|
||||
public function testExecute() {
|
||||
$builder = new ContainerBuilder();
|
||||
$builder->addDefinitions(__DIR__ . '/../test-dependencies.php');
|
||||
$container = $builder->build();
|
||||
|
||||
$application = new Application('Command Line Tool to process Youtrack Reports', '0.1.0');
|
||||
|
||||
$invoiceCommand = $container->get(InvoiceCommand::class);
|
||||
$application->add($invoiceCommand);
|
||||
$reportCommand = $container->get(ReportCommand::class);
|
||||
$application->add($reportCommand);
|
||||
|
||||
|
||||
$command = $application->find('report');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->execute([
|
||||
// pass arguments to the helper
|
||||
'--file' => __DIR__ . '/../data/21-03.csv',
|
||||
|
||||
// prefix the key with two dashes when passing options,
|
||||
// e.g: '--some-option' => 'option_value',
|
||||
// use brackets for testing array value,
|
||||
// e.g: '--some-option' => ['option_value'],
|
||||
]);
|
||||
|
||||
$commandTester->assertCommandIsSuccessful();
|
||||
|
||||
// the output of the command in the console
|
||||
$output = $commandTester->getDisplay();
|
||||
var_dump($output);
|
||||
$this->assertStringContainsString('21-03.csv', $output);
|
||||
|
||||
// ...
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
Group name,Item,Item Summary,Estimation time,Spent time
|
||||
-,DEV-46,"Wochenplanung, Weekly",0,315
|
||||
-,DEV-56,Developer training,0,60
|
||||
-,DEV-122,Daily standup,0,255
|
||||
-,INF-6,"Infrastructure misc, diverse",0,75
|
||||
-,KUR-181,Sprint meetings (PM),240,390
|
||||
-,KUR-507,(TKT-307) Change webhook integration for Slack,0,330
|
||||
-,KUR-552,(TKT-740) Allow kicking users out of collections,780,45
|
||||
-,KUR-554,(TKT-1448) Adaptions in Backend,0,30
|
||||
-,KUR-556,[story] (TKT-1393) Make Paragraph embedding more user friendly,0,75
|
||||
-,KUR-558,(TKT-1467) Full copy of article doesn't update the timestamp,0,15
|
||||
-,KUR-562,(TKT-1441) Implement SEO title in BE,0,45
|
||||
-,LDP-668,Add json-ld schema.org metadata to articles,0,300
|
||||
-,LDP-685,"LDP-Contentpool sitemaps. Extend ""simple_sitemap_extended"" setup to support per front-end site sitemaps.",0,15
|
||||
-,LDP-692,Output json-ld script,0,30
|
||||
-,LDP-693,Add WebPage schema.org support,120,180
|
||||
-,LDP-700,LDP onboarding Somebody,0,15
|
||||
-,LDP-706,Backend FAQ Marketing Block,0,165
|
||||
-,LDP-707,"Backend ""Hero"" Marketing Block",0,1035
|
||||
-,LDP-709,"Backend ""Newsletter Signup"" Marketing Block",0,195
|
||||
-,LDP-711,"Backend ""Call to action"" Marketing Block",0,165
|
||||
-,LDP-712,"Backend ""Feature section"" Marketing Block",0,300
|
||||
-,LDP-713,Provide a field type and pre-configuration for choosing icons,300,45
|
||||
-,LDP-716,Entity browser UX is not as good as expected,0,15
|
||||
-,LDP-720,Improve display of nodes and add support for article heros,0,45
|
||||
-,LDP-728,ldp-marketing blocks break the page when they are added to the layout builder,0,195
|
||||
-,LDP-729,ldp-cp portal entity,0,480
|
||||
-,LDP-740,CP: Add portal path prefixes,480,30
|
||||
-,LDP-741,Defects marketing Blocks,0,450
|
||||
-,LDP-742,[Red Alert] Develop branch fails to build,0,45
|
||||
-,VAL-8,Plan hosting setup on custom hosting provider,0,45
|
||||
-,WV-31,"Internal sprint meetings (PM, fixed-price).",0,150
|
||||
-,WV-4304,"(ODT-1240) Structured data on article (satellite) pages and overview pages such as the startpage, branch, topic & channel pages.",600,750
|
||||
-,WV-4305,(ODT-1202) Add mobile-banner-3 & 4 (advertisement in articles),0,60
|
||||
-,WV-4326,(ODT-1174) Make image optional in the Call to Action block,135,60
|
||||
-,WV-4355,(ODT-1270) Add article title to breadcrumbs,0,270
|
||||
-,WV-4359,"(ODT-1254) Display the text paragraph ""zwischentitel"" in H2",0,90
|
||||
-,WV-4372,(ODT-1297) Branch analytics for articles associated with specific branch (GA),135,15
|
||||
-,WV-4384,(ODT-1306) Layout-builder rendering in Chrome browser,120,120
|
||||
-,WV-4393,WVsat docker build error,0,90
|
||||
-,WV-4410,(ODT-1317) SOLR Search optimization >> suggested articles does NOT affect the relevance for search results.,375,30
|
||||
-,WV-4411,Extend simple_sitemap_extensions for dynamic variants & Install and configure simple_sitemap_extensions,600,2070
|
||||
-,WV-4412,Implement custom dynamic variants for sitemaps to group by month,0,105
|
||||
-,WV-4414,Invalid JSON in the cache_tools composer.json file,0,60
|
||||
-,WV-4421,WV Satellites Hand bau image file entities are not replicated,0,15
|
||||
-,WV-4422,"[Red Alert] Behat test fail. ""Demo content loaded..Error pages work.""",0,75
|
||||
-,WV-4423,(ODT-1325) Site managers must be able to change article status from Unpublished to Unpublished,0,15
|
||||
-,WV-4425,Unpublished Advertorials are returned,0,30
|
||||
-,WV-4427,"(ODT-1332) ""Og: image"" is not specified explicitly",120,15
|
||||
-,WV-4437,"(ODT-1339) User (id 511) on wv-contentpool is locked and cannot access the system.",0,30
|
||||
-,WV-4442,(ODT-1341) Custom HTML block is not rendered,0,120
|
||||
-,WV-4478,(ODT-1368) Hosting provider transition of all projects hosted on their OpenShift infrastructure to the new Kuberneets (k8) infrastructure.,0,90
|
||||
-,WV-4480,(ODT-1370) Error: Call to a member function getUrl() on null / cannot save an article on stage systems,0,90
|
|
|
@ -0,0 +1,10 @@
|
|||
Hello,
|
||||
|
||||
I'm sending you the invoice for [[month]] [[year]].
|
||||
|
||||
This email and invoice were generated and sent by (rprt-cli)[https://git.kompot.si/lio/RprtCli] (WIP).
|
||||
|
||||
|
||||
Kind regards
|
||||
|
||||
Liopold Novelli
|
|
@ -0,0 +1,91 @@
|
|||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
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; }
|
||||
table td {
|
||||
border-left: 0.1mm solid #000000;
|
||||
border-right: 0.1mm solid #000000;
|
||||
}
|
||||
table tbody td {
|
||||
background-color: #FFF;
|
||||
border: 0.1mm solid #000000;
|
||||
padding-right: 2mm;
|
||||
padding-left: 2mm;
|
||||
}
|
||||
tbody td.td-3,
|
||||
tbody td.td-1,
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
tbody td.td-2,
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
table td.colspan.td-2 {
|
||||
text-align: left;
|
||||
}
|
||||
table thead th,
|
||||
table tr.bold td,
|
||||
table tr.bold td.td-3 {
|
||||
background-color: #EEEEEE;
|
||||
border: 0.1mm solid #000000;
|
||||
font-weight: bold;
|
||||
}
|
||||
table tr.center td,
|
||||
table tr.center td.td-3,
|
||||
table tbody tr.center td.td-2.colspan {
|
||||
text-align: center!important;
|
||||
}
|
||||
/*
|
||||
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;
|
||||
}
|
||||
*/
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p class="center">Spletni razvoj, Liopold Doron Novelli s.p.,<br>ADDRESS<br>NUMBER, liopold@drunomics.com<br>NUMBER</p>
|
||||
<p><br><br></p>
|
||||
<p>Company name<br>Company Name<br>Wien<br>Austria</p>
|
||||
<p><br></p>
|
||||
<p><br></p>
|
||||
<p><br></p>
|
||||
<p class="right">Laibach, am [[today]]</p>
|
||||
<p><strong>Honorarnote<br>Nummer [[number]]</strong></p>
|
||||
<p><br></p>
|
||||
<p>Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:<br></p>
|
||||
<p><br></p>
|
||||
<div class="table">[[table]]</div>
|
||||
<p><br>Ich ersuche Sie höflich, den oben angeführt auf meine Kontonummer ACCOUNT NUMBER mit der Bankleitzahl BANK zu überweisen.<br></p>
|
||||
<p><br>Vielen Dank für den Auftrag,<br>
|
||||
mit besten Grüßen,</p>
|
||||
<p><br><br></p>
|
||||
<p><br><br></p>
|
||||
<p>Liopold Doron Novelli</p>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,47 @@
|
|||
tracking_service:
|
||||
youtrack:
|
||||
auth_token: 'perm:<secret>'
|
||||
base_url: 'https://<organization>.myjetbrains.com'
|
||||
report_id: '83-554'
|
||||
# report command
|
||||
report:
|
||||
report: '83-541'
|
||||
# invoice command
|
||||
invoice:
|
||||
# default value for report parameter
|
||||
report: '83-554'
|
||||
export:
|
||||
template_path: 'tests/data/invoice-template.html'
|
||||
output: 'tests/data/output/Invoice-Novelli-[[month]]-[[year]].pdf'
|
||||
labels:
|
||||
- 'Tätigkeit'
|
||||
- 'Stunden'
|
||||
- 'Stundensatz'
|
||||
- 'EUR'
|
||||
email:
|
||||
template_path: 'tests/data/rprt-cli/email-template.txt'
|
||||
username: '<username>'
|
||||
tokens:
|
||||
me: 'Liopold Novelli'
|
||||
to:
|
||||
- 'wolfgang.ziegler@drunomics.com'
|
||||
- 'accounting@drunomics.com'
|
||||
from: 'liopolddoron@gmail.com'
|
||||
projects:
|
||||
LDP:
|
||||
name: 'lupus.digital publishing'
|
||||
pattern: 'LDP'
|
||||
price: 25.6
|
||||
currency: 'EUR'
|
||||
project_column: 1
|
||||
time_column: 4
|
||||
# time format m - minutes, h - hours
|
||||
time_format: 'm'
|
||||
Other:
|
||||
name: 'Projektbezogene Entwicklung'
|
||||
pattern: '(?!.\bLDP\b)'
|
||||
price: 29.1
|
||||
currency: 'EUR'
|
||||
project_column: 1
|
||||
time_column: 4
|
||||
time_format: 'm'
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
$dependencies = require_once(__DIR__ . '/../dependencies.php');
|
||||
|
||||
$dependencies['config.path'] = __DIR__ . '/data';
|
||||
|
||||
return $dependencies;
|
14
todo.txt
14
todo.txt
|
@ -18,6 +18,20 @@ TODO:
|
|||
- phing for build automation
|
||||
- add ddev for dockerization
|
||||
|
||||
TODO (end of 2022):
|
||||
- DONE fix project list output (first is missing)
|
||||
- DONE report selection trait
|
||||
- automated tests
|
||||
- phpcs
|
||||
- phpstan
|
||||
OPTIONAL:
|
||||
- phing build system to run quality assurance
|
||||
- DONE fix -y parameter in invoice
|
||||
- time track command
|
||||
- report from api service and factory:
|
||||
- youtrack
|
||||
- jira
|
||||
- kimai
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue