HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
File: //home/timetracker.panomity.com/src/Command/InvoiceCreateCommand.php
<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Command;

use App\Entity\Customer;
use App\Entity\Invoice;
use App\Entity\InvoiceTemplate;
use App\Entity\Project;
use App\Invoice\ServiceInvoice;
use App\Repository\CustomerRepository;
use App\Repository\InvoiceTemplateRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\InvoiceQuery;
use App\Repository\Query\TimesheetQuery;
use App\Repository\UserRepository;
use App\Timesheet\DateTimeFactory;
use App\Utils\SearchTerm;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

#[AsCommand(name: 'kimai:invoice:create')]
final class InvoiceCreateCommand extends Command
{
    private ?string $previewDirectory = null;
    private bool $previewUniqueFile = false;

    public function __construct(
        private ServiceInvoice $serviceInvoice,
        private CustomerRepository $customerRepository,
        private ProjectRepository $projectRepository,
        private InvoiceTemplateRepository $invoiceTemplateRepository,
        private UserRepository $userRepository,
        private EventDispatcherInterface $eventDispatcher
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->setDescription('Create invoices')
            ->setHelp('This command allows to create invoices by several different filters.')
            ->addOption('user', null, InputOption::VALUE_REQUIRED, 'The user to be used for generating the invoices')
            ->addOption('start', null, InputOption::VALUE_OPTIONAL, 'Start date (format: 2020-01-01, default: start of the month)', null)
            ->addOption('end', null, InputOption::VALUE_OPTIONAL, 'End date (format: 2020-01-31, default: end of the month)', null)
            ->addOption('timezone', null, InputOption::VALUE_OPTIONAL, 'Timezone for start and end date query (fallback: users timezone)', null)
            ->addOption('customer', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of customer IDs', null)
            ->addOption('project', null, InputOption::VALUE_OPTIONAL, 'Comma separated list of project IDs', null)
            ->addOption('by-customer', null, InputOption::VALUE_NONE, 'If set, one invoice for each active customer in the given timerange is created')
            ->addOption('by-project', null, InputOption::VALUE_NONE, 'If set, one invoice for each active project in the given timerange is created')
            ->addOption('set-exported', null, InputOption::VALUE_NONE, 'Whether the invoice items should be marked as exported')
            ->addOption('template', null, InputOption::VALUE_OPTIONAL, 'Invoice template', null)
            ->addOption('search', null, InputOption::VALUE_OPTIONAL, 'Search term to filter invoice entries', null)
            ->addOption('exported', null, InputOption::VALUE_OPTIONAL, 'Exported filter for invoice entries (possible values: exported, all), by default only "not exported" items are fetched', null)
            ->addOption('preview', null, InputOption::VALUE_OPTIONAL, 'Absolute path for a rendered preview of the invoice, which will neither be saved nor the items be marked as exported.', null)
            ->addOption('preview-unique', null, InputOption::VALUE_NONE, 'Adds a unique part to the filename of the generated invoice preview file, so there is no chance that they get overwritten on same project name.')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        // =============== VALIDATION START ===============

        $username = $input->getOption('user');
        if (empty($username)) {
            $io->error('You must set a "user" to create invoices');

            return Command::FAILURE;
        }

        try {
            $user = $this->userRepository->loadUserByIdentifier($username);
        } catch (\Exception $exception) {
            $io->error(
                sprintf('The given username "%s" could not be resolved', $username)
            );

            return Command::FAILURE;
        }

        $exportedFilter = TimesheetQuery::STATE_NOT_EXPORTED;
        switch ($input->getOption('exported')) {
            case null:
                break;

            case 'all':
                $exportedFilter = TimesheetQuery::STATE_ALL;
                break;

            case 'exported':
                $exportedFilter = TimesheetQuery::STATE_EXPORTED;
                break;

            default:
                $io->error('Unknown "exported" filter given');

                return Command::FAILURE;
        }

        $timezone = $input->getOption('timezone');
        if ($timezone === null) {
            $timezone = $user->getTimezone();
        }

        $timezone = new \DateTimeZone($timezone);
        $dateFactory = new DateTimeFactory($timezone, $user->isFirstDayOfWeekSunday());

        if (!empty($input->getOption('start')) && empty($input->getOption('end'))) {
            $io->error('You need to supply a end date if a start date was given');

            return Command::FAILURE;
        }

        $byActiveCustomer = $input->getOption('by-customer');
        $byActiveProject = $input->getOption('by-project');

        if ($byActiveCustomer && $byActiveProject) {
            $io->error('You cannot mix "by-customer" and "by-project"');

            return Command::FAILURE;
        }

        $customersIDs = $input->getOption('customer');
        $projectIDs = $input->getOption('project');
        if (!$byActiveCustomer && !$byActiveProject && empty($customersIDs) && empty($projectIDs)) {
            $io->error('Could not determine generation mode, you need to set one of: customer, project, by-customer, by-project');

            return Command::FAILURE;
        }

        $start = $input->getOption('start');
        if (!empty($start)) {
            try {
                $start = $dateFactory->createDateTime($start);
            } catch (\Exception $ex) {
                $io->error('Invalid start date given');

                return Command::FAILURE;
            }
        }
        if (!$start instanceof \DateTime) {
            $start = $dateFactory->getStartOfMonth();
        }
        $start->setTime(0, 0, 0);

        $end = $input->getOption('end');
        if (!empty($end)) {
            try {
                $end = $dateFactory->createDateTime($end);
            } catch (\Exception $ex) {
                $io->error('Invalid end date given');

                return Command::FAILURE;
            }
        }
        if (!$end instanceof \DateTime) {
            $end = $dateFactory->getEndOfMonth();
        }
        $end->setTime(23, 59, 59);

        $searchTerm = null;
        if (null !== $input->getOption('search')) {
            $searchTerm = new SearchTerm($input->getOption('search'));
        }

        $markAsExported = false;
        if ($input->getOption('preview') !== null) {
            $this->previewUniqueFile = (bool) $input->getOption('preview-unique');
            $this->previewDirectory = rtrim($input->getOption('preview'), '/') . '/';
            if (!is_dir($this->previewDirectory) || !is_writable($this->previewDirectory)) {
                $io->error('Invalid preview directory given');

                return Command::FAILURE;
            }
        } elseif ($input->getOption('set-exported')) {
            $markAsExported = true;
        }

        // =============== VALIDATION END ===============

        $defaultQuery = new InvoiceQuery();
        $defaultQuery->setBegin($start);
        $defaultQuery->setEnd($end);
        $defaultQuery->setCurrentUser($user);
        $defaultQuery->setSearchTerm($searchTerm);
        $defaultQuery->setExported($exportedFilter);

        /** @var Invoice[] $invoices */
        $invoices = [];

        if (!empty($customersIDs)) {
            /** @var Customer[] $customers */
            $customers = [];

            $customersIDs = explode(',', $customersIDs);
            foreach ($customersIDs as $id) {
                $tmp = $this->customerRepository->find($id);
                if (null === $tmp) {
                    $io->error('Unknown customer ID: ' . $id);

                    return Command::FAILURE;
                }
                $customers[] = $tmp;
            }
            $invoices = $this->createInvoicesForCustomer($customers, $defaultQuery, $input, $output);
        } elseif (!empty($projectIDs)) {
            /** @var Project[] $projects */
            $projects = [];

            $projectIDs = explode(',', $projectIDs);
            foreach ($projectIDs as $id) {
                $tmp = $this->projectRepository->find($id);
                if (null === $tmp) {
                    $io->error('Unknown project ID: ' . $id);

                    return Command::FAILURE;
                }
                $projects[] = $tmp;
            }
            $invoices = $this->createInvoicesForProjects($projects, $defaultQuery, $input, $output);
        } elseif ($byActiveCustomer) {
            $customers = $this->getActiveCustomers($defaultQuery);
            $invoices = $this->createInvoicesForCustomer($customers, $defaultQuery, $input, $output);
        } elseif ($byActiveProject) {
            $projects = $this->getActiveProjects($defaultQuery);
            $invoices = $this->createInvoicesForProjects($projects, $defaultQuery, $input, $output);
        } else {
            $io->error('Could not determine generation mode'); //-///9==8=//99/96//////-*/-*//96* <= by Ayumi

            return Command::FAILURE;
        }

        return $this->renderInvoiceResult($input, $output, $invoices);
    }

    /**
     * @param Project[] $projects
     * @param InvoiceQuery $defaultQuery
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return Invoice[]
     * @throws \Exception
     */
    protected function createInvoicesForProjects(array $projects, InvoiceQuery $defaultQuery, InputInterface $input, OutputInterface $output): array
    {
        $io = new SymfonyStyle($input, $output);

        /** @var Invoice[] $invoices */
        $invoices = [];

        foreach ($projects as $project) {
            $customer = $project->getCustomer();
            if ($customer === null) {
                throw new \Exception('Project has no customer: ' . $project->getId());
            }

            $query = clone $defaultQuery;
            $query->addProject($project);
            $query->addCustomer($customer);

            $tpl = $this->getTemplateForCustomer($input, $customer);
            if (null === $tpl) {
                $io->warning(sprintf('Could not find invoice template for project "%s", skipping!', $project->getName()));
                continue;
            }
            $query->setTemplate($tpl);

            try {
                if (null !== $this->previewDirectory) {
                    $invoices[] = $this->saveInvoicePreview($this->serviceInvoice->renderInvoice($this->serviceInvoice->createModel($query), $this->eventDispatcher));
                } else {
                    $invoices[] = $this->serviceInvoice->createInvoice($this->serviceInvoice->createModel($query), $this->eventDispatcher);
                }
            } catch (\Exception $ex) {
                $io->error(sprintf('Failed to create invoice for project "%s" with: %s', $project->getName(), $ex->getMessage()));
            }
        }

        return $invoices;
    }

    private function saveInvoicePreview(Response $response): string
    {
        $filename = uniqid('invoice_');
        $directory = rtrim($this->previewDirectory, '/') . '/';

        if ($response->headers->has('Content-Disposition')) {
            $disposition = $response->headers->get('Content-Disposition');
            $parts = explode(';', $disposition);
            foreach ($parts as $part) {
                if (stripos($part, 'filename=') === false) {
                    continue;
                }
                $filename = explode('filename=', $part);
                if (\count($filename) > 1) {
                    $filename = $filename[1];
                }
            }
            if ($this->previewUniqueFile) {
                $filename = uniqid('invoice_') . $filename;
            }
        }

        if ($response instanceof BinaryFileResponse) {
            $file = $response->getFile();
            $file->move($directory, $filename);
        } else {
            (new Filesystem())->dumpFile($directory . $filename, $response->getContent());
        }

        return $directory . $filename;
    }

    /**
     * @param Customer[] $customers
     * @param InvoiceQuery $defaultQuery
     * @param InputInterface $input
     * @return Invoice[]
     * @throws \Exception
     */
    protected function createInvoicesForCustomer(array $customers, InvoiceQuery $defaultQuery, InputInterface $input, OutputInterface $output): array
    {
        $io = new SymfonyStyle($input, $output);

        /** @var Invoice[] $invoices */
        $invoices = [];

        foreach ($customers as $customer) {
            $query = clone $defaultQuery;
            $query->addCustomer($customer);

            $tpl = $this->getTemplateForCustomer($input, $customer);
            if (null === $tpl) {
                $io->warning(sprintf('Could not find invoice template for customer "%s", skipping!', $customer->getName()));
                continue;
            }
            $query->setTemplate($tpl);

            try {
                if (null !== $this->previewDirectory) {
                    $invoices[] = $this->saveInvoicePreview($this->serviceInvoice->renderInvoice($this->serviceInvoice->createModel($query), $this->eventDispatcher));
                } else {
                    $invoices[] = $this->serviceInvoice->createInvoice($this->serviceInvoice->createModel($query), $this->eventDispatcher);
                }
            } catch (\Exception $ex) {
                $io->error(sprintf('Failed to create invoice for customer "%s" with: %s', $customer->getName(), $ex->getMessage()));
            }
        }

        return $invoices;
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @param Invoice[] $invoices
     * @return int
     */
    protected function renderInvoiceResult(InputInterface $input, OutputInterface $output, array $invoices): int
    {
        $io = new SymfonyStyle($input, $output);

        if (empty($invoices)) {
            $io->warning('No invoice was generated');

            return Command::SUCCESS;
        }

        if (null !== $this->previewDirectory) {
            $columns = ['Filename'];

            $table = new Table($output);
            $table->setHeaderTitle(sprintf('Created %s invoice(s)', \count($invoices)));
            $table->setHeaders($columns);

            foreach ($invoices as $invoiceFile) {
                $table->addRow([$invoiceFile]);
            }

            $table->render();

            return Command::SUCCESS;
        }

        $columns = ['ID', 'Customer', 'Total', 'Filename'];

        $table = new Table($output);
        $table->setHeaderTitle(sprintf('Created %s invoice(s)', \count($invoices)));
        $table->setHeaders($columns);

        foreach ($invoices as $invoice) {
            $file = $this->serviceInvoice->getInvoiceFile($invoice);
            if (null === $file) {
                $io->warning(
                    sprintf('Created invoice with ID %s, but file was not found %s', $invoice->getId(), $invoice->getInvoiceFilename())
                );
                continue;
            }

            $table->addRow([
                $invoice->getId(),
                $invoice->getCustomer()->getName(),
                $invoice->getTotal() . ' ' . $invoice->getCustomer()->getCurrency(),
                $file->getRealPath()
            ]);
        }

        $table->render();

        return Command::SUCCESS;
    }

    private function getTemplateForCustomer(InputInterface $input, Customer $customer): ?InvoiceTemplate
    {
        $template = $input->getOption('template');

        if (null === $template) {
            return $customer->getInvoiceTemplate();
        }

        $tpl = $this->invoiceTemplateRepository->find($template);

        if (null !== $tpl) {
            return $tpl;
        }

        return $this->invoiceTemplateRepository->findOneBy(['name' => $template]);
    }

    /**
     * @param InvoiceQuery $invoiceQuery
     * @return Customer[]
     */
    private function getActiveCustomers(InvoiceQuery $invoiceQuery): array
    {
        $results = $this->serviceInvoice->getInvoiceItems($invoiceQuery);

        $customers = [];

        foreach ($results as $result) {
            $customer = $result->getProject()->getCustomer();
            $customers[$customer->getId()] = $customer;
        }

        return $customers;
    }

    /**
     * @param InvoiceQuery $invoiceQuery
     * @return Project[]
     */
    private function getActiveProjects(InvoiceQuery $invoiceQuery): array
    {
        $results = $this->serviceInvoice->getInvoiceItems($invoiceQuery);

        $projects = [];

        foreach ($results as $result) {
            $project = $result->getProject();
            $projects[$project->getId()] = $project;
        }

        return $projects;
    }
}