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: {$report}."); } } if ($youtrack = $input->getOption('youtrack')) { $report_id = $this->youtrack->getReportId(); $cache_clear_status = $this->youtrack->clearReportCache($report_id); if ($output->isVerbose()) { $output->writeln("Report {$report_id} 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: {$file}"); } $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 ${output_path}."); } // 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.'); } }