375 lines
12 KiB
PHP
375 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
// src/Commands/InvoiceCommand.php;
|
|
|
|
namespace RprtCli\Commands;
|
|
|
|
use Exception;
|
|
use RprtCli\Utils\Configuration\ConfigurationInterface;
|
|
use RprtCli\Utils\CsvReport\ReportCsvInterface;
|
|
use RprtCli\Utils\Mailer\MailerInterface;
|
|
use RprtCli\Utils\PdfExport\PdfExportInterface;
|
|
use RprtCli\Utils\TimeTrackingServices\YoutrackInterface;
|
|
use RprtCli\ValueObjects\Expenses;
|
|
use RprtCli\ValueObjects\WorkInvoiceElement;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Helper\Table;
|
|
use Symfony\Component\Console\Helper\TableCell;
|
|
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 function array_merge;
|
|
use function explode;
|
|
use function is_array;
|
|
use function is_null;
|
|
use function is_string;
|
|
use function PHPUnit\Framework\throwException;
|
|
use function readline;
|
|
use function strpos;
|
|
use function strtolower;
|
|
use function var_dump;
|
|
use function var_export;
|
|
|
|
// use Symfony\Contracts\Translation\TranslatorInterface;
|
|
|
|
/**
|
|
* Main file - invoice command.
|
|
*/
|
|
class InvoiceCommand extends Command
|
|
{
|
|
use SelectReportTrait;
|
|
|
|
protected ReportCsvInterface $csv;
|
|
|
|
protected ConfigurationInterface $config;
|
|
|
|
protected YoutrackInterface $trackingService;
|
|
|
|
protected PdfExportInterface $pdfExport;
|
|
|
|
protected MailerInterface $mailer;
|
|
|
|
protected const TYPE_WORK = 1;
|
|
protected const TYPE_EXPENSE = 2;
|
|
|
|
public function __construct(
|
|
ReportCsvInterface $csv,
|
|
ConfigurationInterface $configuration,
|
|
YoutrackInterface $trackingService,
|
|
PdfExportInterface $pdf_export,
|
|
MailerInterface $mailer,
|
|
?string $name = null
|
|
) {
|
|
$this->csv = $csv;
|
|
$this->config = $configuration;
|
|
$this->trackingService = $trackingService;
|
|
$this->pdfExport = $pdf_export;
|
|
$this->mailer = $mailer;
|
|
parent::__construct($name);
|
|
}
|
|
|
|
/**
|
|
* Get configuration.
|
|
*/
|
|
protected function configure(): void
|
|
{
|
|
$this->setName('invoice');
|
|
$this->setDescription('Generate an invoice from (monthly) report');
|
|
// @TODO $this->addUsage('');
|
|
// @TODO add sub options (config overrides)
|
|
$this->addOption(
|
|
'file',
|
|
'f',
|
|
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(
|
|
'pdf',
|
|
'p',
|
|
InputOption::VALUE_NONE,
|
|
'Create invoice pdf from template.'
|
|
);
|
|
$this->addOption(
|
|
'test',
|
|
'',
|
|
InputOption::VALUE_NONE,
|
|
'Test login into youtrack service. Prints out your name.'
|
|
);
|
|
$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.'
|
|
);
|
|
$this->addOption(
|
|
'recipients',
|
|
't',
|
|
InputOption::VALUE_REQUIRED,
|
|
'Comma separated list of recipients that should get the exported pdf.'
|
|
);
|
|
$this->addOption(
|
|
'expenses',
|
|
'e',
|
|
InputOption::VALUE_OPTIONAL,
|
|
// phpcs:ignore
|
|
'List of additional expenses in format expense1=value1;expenses2=value2... or empty for interactive output.',
|
|
false
|
|
);
|
|
$this->addOption(
|
|
'custom',
|
|
'c',
|
|
InputOption::VALUE_OPTIONAL,
|
|
// phpcs:ignore
|
|
'Additional custom work untracked in format: name1=time1;name2=time2... Project to assign work items to has to be configured in app config. Leave empty for interactive output.',
|
|
false
|
|
);
|
|
$this->addOption(
|
|
'list-reports',
|
|
'l',
|
|
InputOption::VALUE_NONE,
|
|
'List my reports'
|
|
);
|
|
$this->addOption(
|
|
'report',
|
|
'r',
|
|
InputOption::VALUE_OPTIONAL,
|
|
'Show time tracked for report.',
|
|
false
|
|
);
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
if ($input->getOption('test')) {
|
|
$test = $this->trackingService->testYoutrackapi();
|
|
$output->writeln($test);
|
|
}
|
|
if ($input->getOption('list-reports')) {
|
|
$list = $this->trackingService->listReports();
|
|
$output->writeln(var_export($list, true));
|
|
return Command::SUCCESS;
|
|
}
|
|
// Gets report parameter.
|
|
$file = $this->getReportCsvFilePath($input, $output, 'tracking_service.youtrack.invoice.report');
|
|
$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);
|
|
}
|
|
$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);
|
|
}
|
|
$output_path = $this->pdfExport->fromDefaultDataToPdf($nice_data) ?: sys_get_temp_dir();
|
|
// Notify the user where the file was generated to.
|
|
$output->writeln("The file was generated at <info>${output_path}</info>.");
|
|
}
|
|
if ($input->getOption('send') && isset($output_path)) {
|
|
// @TODO If no output path print an error.
|
|
// Send email to configured address.
|
|
if ($recipients = $input->getOption('recipients')) {
|
|
$this->mailer->setRecipients(explode(',', $recipients));
|
|
}
|
|
$this->mailer->sendDefaultMail($output_path);
|
|
}
|
|
|
|
// $this->dummyOutput($input, $output);
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
protected function getTable(OutputInterface $output, array $data): Table
|
|
{
|
|
$rows = $this->csv->generateTable($data);
|
|
$table = new Table($output);
|
|
$table->setHeaders([
|
|
'Project',
|
|
'Hours',
|
|
'Rate',
|
|
'Price',
|
|
]);
|
|
foreach ($rows as $key => $row) {
|
|
if (! $row) {
|
|
$rows[$key] = new TableSeparator();
|
|
} elseif (is_array($row) && is_null($row[1]) && is_null($row[0])) {
|
|
// Check which elements in array are null.
|
|
$rows[$key] = [new TableCell($row[2], ['colspan' => 3]), $row[3]];
|
|
}
|
|
}
|
|
$table->setRows($rows);
|
|
return $table;
|
|
}
|
|
|
|
/**
|
|
* Create table from data that is already inline with configuration.
|
|
*
|
|
* @deprecated
|
|
* This method was almost exact copy of CsvReport::arangeDataForDefaultPdfExport
|
|
*/
|
|
protected function generateTable(OutputInterface $output, array $data): Table
|
|
{
|
|
$table = new Table($output);
|
|
$table->setHeaders([
|
|
'Project',
|
|
'Hours',
|
|
'Rate',
|
|
'Price',
|
|
]);
|
|
[$rows, $totalHours, $totalPrice] = [[], 0, 0];
|
|
$projectsConfig = $this->config->get('projects');
|
|
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 * (float) $config['price'];
|
|
$row = [
|
|
$config['name'],
|
|
$hours,
|
|
$config['price'],
|
|
$hours * $config['price'],
|
|
];
|
|
$rows[] = $row;
|
|
$totalHours += $hours;
|
|
$totalPrice += $price;
|
|
unset($data[$name]);
|
|
}
|
|
if (! empty($data)) {
|
|
foreach ($data as $name => $value) {
|
|
if (strpos(strtolower($name), 'expanses') !== false) {
|
|
}
|
|
}
|
|
}
|
|
|
|
$rows[] = new TableSeparator();
|
|
// @TODO Check rate in final result.
|
|
// $rows[] = [$this->translator->trans('Sum'), $totalHours, $config['price'], $totalPrice];
|
|
$rows[] = ['Sum', $totalHours, NULL, $totalPrice];
|
|
|
|
$table->setRows($rows);
|
|
return $table;
|
|
}
|
|
|
|
/**
|
|
* Dummy output for testing.
|
|
*/
|
|
protected function dummyOutput(InputInterface $input, OutputInterface $output): void
|
|
{
|
|
// $txt = $this->translator->trans('From [start-date] to [end-date].', [], 'rprt', 'sl_SI');
|
|
// $output->writeln($txt);
|
|
$table = new Table($output);
|
|
$table->setHeaders(['Project', 'Hours', 'Price']);
|
|
$table->setRows([
|
|
['LDP', 100, 2600],
|
|
['WV', 50, 1300],
|
|
new TableSeparator(),
|
|
['Zusamen', 150, 3900],
|
|
]);
|
|
// $table->setStyle('borderless');
|
|
$table->render();
|
|
}
|
|
|
|
/**
|
|
* Gets the expenses array.
|
|
*
|
|
* @return Expenses[]
|
|
*/
|
|
protected function getExpenses($expenses)
|
|
{
|
|
$output = [];
|
|
if (is_string($expenses)) {
|
|
foreach (explode(';', $expenses) as $expense) {
|
|
[$name, $value] = explode('=', $expense);
|
|
$output[] = new Expenses($name, (float) $value);
|
|
}
|
|
} else {
|
|
$continue = true;
|
|
while ($continue) {
|
|
$name = readline('Enter expenses name or leave empty to stop: ');
|
|
$value = (float) readline('Enter expenses value: ');
|
|
if (! empty($name)) {
|
|
$output[] = new Expenses($name, $value);
|
|
} else {
|
|
$continue = false;
|
|
}
|
|
}
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
protected function getCustomWorkOrExpenses(mixed $custom, int $type)
|
|
{
|
|
$output = [];
|
|
if (is_string($custom)) {
|
|
foreach (explode(';', $custom) as $item) {
|
|
[$name, $value] = explode('=', $item);
|
|
$output[] = $this->createInvoiceElement($name, (float) $value, $type);
|
|
}
|
|
} else {
|
|
$continue = true;
|
|
if ($type === self::TYPE_WORK) {
|
|
$message_name = 'Enter project name or leave empty to stop: ';
|
|
$message_value = 'Enter time spent of project: ';
|
|
} elseif ($type === self::TYPE_EXPENSE) {
|
|
$message_name = 'Enter expenses name or leave empty to stop: ';
|
|
$message_value = 'Enter expenses value: ';
|
|
}
|
|
else {
|
|
throw new \Exception('Unknown type of custom data.');
|
|
}
|
|
while ($continue) {
|
|
$name = readline($message_name);
|
|
$value = (float) readline($message_value);
|
|
if (! empty($name)) {
|
|
$output[] = $this->createInvoiceElement($name, $value, $type);
|
|
} else {
|
|
$continue = false;
|
|
}
|
|
}
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
protected function createInvoiceElement(string $name, float $value, int $type)
|
|
{
|
|
if ($type === self::TYPE_WORK) {
|
|
return new WorkInvoiceElement($name, (float) $value);
|
|
} elseif ($type === self::TYPE_EXPENSE) {
|
|
return new Expenses($name, (float) $value);
|
|
}
|
|
throw new Exception('Unkown invoice element type.');
|
|
}
|
|
}
|