RprtCli/app/src/Commands/InvoiceCommand.php

379 lines
13 KiB
PHP

<?php
declare(strict_types=1);
// src/Commands/InvoiceCommand.php;
namespace RprtCli\Commands;
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\TableSeparator;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
// use Symfony\Contracts\Translation\TranslatorInterface;
use function var_dump;
/**
* Main file - invoice command.
*/
class InvoiceCommand extends Command
{
protected $csv;
protected $configuration;
protected $youtrack;
protected $pdfExport;
const TYPE_WORK = 1;
const TYPE_EXPENSE = 2;
public function __construct(
ReportCsvInterface $csv,
ConfigurationInterface $configuration,
YoutrackInterface $youtrack,
PdfExportInterface $pdf_export,
MailerInterface $mailer,
?string $name = null
) {
$this->csv = $csv;
$this->configuration = $configuration;
$this->youtrack = $youtrack;
$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,
'List of additional expenses in format expense1=value1;expenses2=value2... or empty for interactive output.',
FALSE
);
$this->addOption(
'custom',
'c',
InputOption::VALUE_OPTIONAL,
'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->youtrack->testYoutrackapi();
$output->writeln($test);
}
if ($input->getOption('list-reports')) {
$list = $this->youtrack->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);
}
if ($input->hasParameterOption('--expenses') || $input->hasParameterOption('-e')) {
$expenses = $this->getCustomWorkOrExpenses($input->getOption('expenses'), self::TYPE_EXPENSE);
}
if ($input->hasParameterOption('--custom') || $input->hasParameterOption('-c')) {
$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>");
}
$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;
}
if ($input->getOption('send') && $output_path) {
// @TODO If no output path print an error.
// Send email to configured address.
if ($recipients = $input->getOption('send-to')) {
$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->configuration->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, $config['price'], $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($custom, $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: ';
}
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.');
}
}