delete invoices (#1652)

* include all meta fields as template variables
* support invoice preview via command
* support deletion of generated invoices
This commit is contained in:
Kevin Papst
2020-04-23 12:19:07 +02:00
committed by GitHub
parent cfe0724620
commit 711139ad4c
20 changed files with 206 additions and 36 deletions

View File

@@ -27,6 +27,9 @@ 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;
class InvoiceCreateCommand extends Command
@@ -55,6 +58,10 @@ class InvoiceCreateCommand extends Command
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var string|null
*/
private $previewDirectory;
public function __construct(
ServiceInvoice $serviceInvoice,
@@ -94,6 +101,7 @@ class InvoiceCreateCommand extends Command
->addOption('template-meta', null, InputOption::VALUE_OPTIONAL, 'Fetch invoice template from a meta-field', 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)
;
}
@@ -208,7 +216,14 @@ class InvoiceCreateCommand extends Command
}
$markAsExported = false;
if ($input->getOption('set-exported')) {
if ($input->getOption('preview') !== null) {
$this->previewDirectory = rtrim($input->getOption('preview'), '/') . '/';
if (!is_dir($this->previewDirectory) || !is_writable($this->previewDirectory)) {
$io->error('Invalid preview directory given');
return 1;
}
} elseif ($input->getOption('set-exported')) {
$markAsExported = true;
}
@@ -283,7 +298,11 @@ class InvoiceCreateCommand extends Command
$query->setTemplate($tpl);
try {
$invoices[] = $this->serviceInvoice->createInvoice($query, $this->eventDispatcher);
if (null !== $this->previewDirectory) {
$invoices[] = $this->saveInvoicePreview($this->serviceInvoice->renderInvoice($query, $this->eventDispatcher));
} else {
$invoices[] = $this->serviceInvoice->createInvoice($query, $this->eventDispatcher);
}
} catch (\Exception $ex) {
$io->error(sprintf('Failed to create invoice for project "%s" with: %s', $project->getName(), $ex->getMessage()));
}
@@ -292,6 +311,34 @@ class InvoiceCreateCommand extends Command
return $invoices;
}
private function saveInvoicePreview(Response $response)
{
$filename = uniqid('invoice_');
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 ($response instanceof BinaryFileResponse) {
$file = $response->getFile();
$file->move($this->previewDirectory, $filename);
} else {
(new Filesystem())->dumpFile($this->previewDirectory . $filename, $response->getContent());
}
return $this->previewDirectory . $filename;
}
/**
* @param Customer[] $customers
* @param InvoiceQuery $defaultQuery
@@ -318,7 +365,11 @@ class InvoiceCreateCommand extends Command
$query->setTemplate($tpl);
try {
$invoices[] = $this->serviceInvoice->createInvoice($query, $this->eventDispatcher);
if (null !== $this->previewDirectory) {
$invoices[] = $this->saveInvoicePreview($this->serviceInvoice->renderInvoice($query, $this->eventDispatcher));
} else {
$invoices[] = $this->serviceInvoice->createInvoice($query, $this->eventDispatcher);
}
} catch (\Exception $ex) {
$io->error(sprintf('Failed to create invoice for customer "%s" with: %s', $customer->getName(), $ex->getMessage()));
}
@@ -343,6 +394,22 @@ class InvoiceCreateCommand extends Command
return 0;
}
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 0;
}
$columns = ['ID', 'Customer', 'Total', 'Filename'];
$table = new Table($output);

View File

@@ -189,11 +189,26 @@ final class InvoiceController extends AbstractController
{
try {
$this->service->changeInvoiceStatus($invoice, $status);
} catch (\InvalidArgumentException $ex) {
throw $this->createNotFoundException($ex->getMessage());
$this->flashSuccess('action.update.success');
} catch (\Exception $ex) {
$this->flashError('action.update.error');
}
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('admin_invoice_list');
}
/**
* @Route(path="/delete/{id}", name="admin_invoice_delete", methods={"GET"})
* @Security("is_granted('history_invoice')")
*/
public function deleteInvoiceAction(Invoice $invoice): Response
{
try {
$this->service->deleteInvoice($invoice);
$this->flashSuccess('action.delete.success');
} catch (\Exception $ex) {
$this->flashError('action.delete.error');
}
return $this->redirectToRoute('admin_invoice_list');
}

View File

@@ -145,7 +145,7 @@ class InvoiceTemplate
private $decimalDuration = false;
/**
* Used when rendering HTML templates.
* Used for translations and locale dependent number and date formats.
*
* @var string
*

View File

@@ -95,7 +95,7 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
'entry.activity_id' => $activity->getId(),
]);
foreach ($activity->getVisibleMetaFields() as $metaField) {
foreach ($activity->getMetaFields() as $metaField) {
$values = array_merge($values, [
'entry.activity.meta.' . $metaField->getName() => $metaField->getValue(),
]);
@@ -108,7 +108,7 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
'entry.project_id' => $project->getId(),
]);
foreach ($project->getVisibleMetaFields() as $metaField) {
foreach ($project->getMetaFields() as $metaField) {
$values = array_merge($values, [
'entry.project.meta.' . $metaField->getName() => $metaField->getValue(),
]);
@@ -121,7 +121,7 @@ class InvoiceItemDefaultHydrator implements InvoiceItemHydrator
'entry.customer_id' => $customer->getId(),
]);
foreach ($customer->getVisibleMetaFields() as $metaField) {
foreach ($customer->getMetaFields() as $metaField) {
$values = array_merge($values, [
'entry.customer.meta.' . $metaField->getName() => $metaField->getValue(),
]);

View File

@@ -46,7 +46,7 @@ class InvoiceModelCustomerHydrator implements InvoiceModelHydrator
// remaining time-budget?
];
foreach ($customer->getVisibleMetaFields() as $metaField) {
foreach ($customer->getMetaFields() as $metaField) {
$values = array_merge($values, [
'customer.meta.' . $metaField->getName() => $metaField->getValue(),
]);

View File

@@ -27,6 +27,7 @@ class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
'invoice.date' => $formatter->getFormattedDateTime($model->getInvoiceDate()),
'invoice.number' => $model->getInvoiceNumber(),
'invoice.currency' => $currency,
'invoice.language' => $model->getTemplate()->getLanguage(), // since 1.9
'invoice.currency_symbol' => $formatter->getCurrencySymbol($currency),
'invoice.vat' => $model->getCalculator()->getVat(),
'invoice.tax' => $formatter->getFormattedMoney($tax, $currency),
@@ -52,11 +53,19 @@ class InvoiceModelDefaultHydrator implements InvoiceModelHydrator
'template.payment_details' => $model->getTemplate()->getPaymentDetails(),
'query.begin' => $formatter->getFormattedDateTime($model->getQuery()->getBegin()),
'query.day' => $model->getQuery()->getBegin()->format('d'),
'query.end' => $formatter->getFormattedDateTime($model->getQuery()->getEnd()),
'query.month' => $formatter->getFormattedMonthName($model->getQuery()->getBegin()),
'query.month_number' => $model->getQuery()->getBegin()->format('m'),
'query.year' => $model->getQuery()->getBegin()->format('Y'),
'query.day' => $model->getQuery()->getBegin()->format('d'), // @deprecated
'query.month' => $formatter->getFormattedMonthName($model->getQuery()->getBegin()), // @deprecated
'query.month_number' => $model->getQuery()->getBegin()->format('m'), // @deprecated
'query.year' => $model->getQuery()->getBegin()->format('Y'), // @deprecated
'query.begin_day' => $model->getQuery()->getBegin()->format('d'),
'query.begin_month' => $formatter->getFormattedMonthName($model->getQuery()->getBegin()),
'query.begin_month_number' => $model->getQuery()->getBegin()->format('m'),
'query.begin_year' => $model->getQuery()->getBegin()->format('Y'),
'query.end' => $formatter->getFormattedDateTime($model->getQuery()->getEnd()), // since 1.9
'query.end_day' => $model->getQuery()->getEnd()->format('d'), // since 1.9
'query.end_month' => $formatter->getFormattedMonthName($model->getQuery()->getEnd()), // since 1.9
'query.end_month_number' => $model->getQuery()->getEnd()->format('m'), // since 1.9
'query.end_year' => $model->getQuery()->getEnd()->format('Y'), // since 1.9
];
return $values;

View File

@@ -396,6 +396,15 @@ final class ServiceInvoice
);
}
public function deleteInvoice(Invoice $invoice)
{
$invoiceDirectory = $this->getInvoicesDirectory();
if (is_file($invoiceDirectory . $invoice->getInvoiceFilename())) {
$this->fileHelper->removeFile($invoiceDirectory . $invoice->getInvoiceFilename());
}
$this->invoiceRepository->deleteInvoice($invoice);
}
/**
* @param InvoiceQuery $query
* @return InvoiceModel

View File

@@ -28,6 +28,13 @@ class InvoiceRepository extends EntityRepository
$entityManager->flush();
}
public function deleteInvoice(Invoice $invoice)
{
$entityManager = $this->getEntityManager();
$entityManager->remove($invoice);
$entityManager->flush();
}
private function getCounterFor(\DateTime $start, \DateTime $end): int
{
$qb = $this->getEntityManager()->createQueryBuilder();

View File

@@ -58,4 +58,9 @@ final class FileHelper
{
$this->filesystem->dumpFile($filename, $data);
}
public function removeFile(string $filename)
{
$this->filesystem->remove($filename);
}
}