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: {$report_name}"); $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 ${output_path}."); } 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.'); } }