diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index 0b126278..f0c58e89 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -13,6 +13,7 @@ use App\Configuration\SystemConfiguration; use App\Entity\Customer; use App\Entity\Invoice; use App\Entity\InvoiceTemplate; +use App\Event\InvoiceDocumentsEvent; use App\Export\Spreadsheet\AnnotatedObjectExporter; use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter; use App\Export\Spreadsheet\Writer\XlsxWriter; @@ -338,17 +339,45 @@ final class InvoiceController extends AbstractController $invoiceDir = $projectDirectory . DIRECTORY_SEPARATOR . $dir; } + $used = []; + foreach ($this->templateRepository->findAll() as $template) { + $used[$template->getRenderer()] = $template; + } + + $event = new InvoiceDocumentsEvent($this->service->getDocuments(true)); + $this->dispatcher->dispatch($event); + + $documents = []; + foreach ($event->getInvoiceDocuments() as $document) { + $isUsed = \array_key_exists($document->getId(), $used); + $template = null; + if ($isUsed) { + $template = $used[$document->getId()]; + } + $documents[] = [ + 'document' => $document, + 'template' => $template, + 'used' => $isUsed, + ]; + } + $canUpload = true; + $uploadError = null; + + if (\count($documents) >= $event->getMaximumAllowedDocuments()) { + $uploadError = 'invoice_document.max_reached'; + $canUpload = false; + } if (!file_exists($invoiceDir)) { @mkdir($invoiceDir, 0777); } if (!is_dir($invoiceDir)) { - $this->flashError(sprintf('Invoice directory "%s" is not existing and could not be created.', $dir)); + $uploadError = 'error.directory_missing'; $canUpload = false; } elseif (!is_writable($invoiceDir)) { - $this->flashError(sprintf('Invoice directory "%s" cannot be written.', $dir)); + $uploadError = 'error.directory_protected'; $canUpload = false; } @@ -369,7 +398,10 @@ final class InvoiceController extends AbstractController 'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename ); - $newFilename = $safeFilename . '.' . $uploadedFile->guessExtension(); + + $extension = $uploadedFile->guessExtension(); + + $newFilename = substr($safeFilename, 0, 20) . '.' . $extension; try { $uploadedFile->move($invoiceDir, $newFilename); @@ -383,12 +415,56 @@ final class InvoiceController extends AbstractController } return $this->render('invoice/document_upload.html.twig', [ + 'error_replacer' => ['%max%' => $event->getMaximumAllowedDocuments(), '%dir%' => $dir], + 'upload_error' => $uploadError, + 'can_upload' => $canUpload, 'form' => $form->createView(), - 'documents' => $this->service->getDocuments(true), + 'documents' => $documents, 'baseDirectory' => $projectDirectory . DIRECTORY_SEPARATOR, ]); } + /** + * @Route(path="/document/{id}/delete/{token}", name="invoice_document_delete", methods={"GET", "POST"}) + * @Security("is_granted('manage_invoice_template')") + */ + public function deleteDocument(string $id, string $token, CsrfTokenManagerInterface $csrfTokenManager, InvoiceDocumentRepository $documentRepository): Response + { + $document = $documentRepository->findByName($id); + if ($document === null) { + throw $this->createNotFoundException(); + } + + if (!$csrfTokenManager->isTokenValid(new CsrfToken('invoice.delete_document', $token))) { + $this->flashError('action.csrf.error'); + + return $this->redirectToRoute('admin_invoice_document_upload'); + } + + $csrfTokenManager->refreshToken('invoice.delete_document'); + + foreach ($documentRepository->findBuiltIn() as $document) { + if ($document->getId() === $id) { + throw new \Exception('Document is built-in and cannot be deleted'); + } + } + + foreach ($this->templateRepository->findAll() as $template) { + if ($template->getRenderer() === $id) { + throw new \Exception('Document is used and cannot be deleted'); + } + } + + try { + $documentRepository->remove($document); + $this->flashSuccess('action.delete.success'); + } catch (Exception $ex) { + $this->flashDeleteException($ex); + } + + return $this->redirectToRoute('admin_invoice_document_upload'); + } + /** * @Route(path="/template/create", name="admin_invoice_template_create", methods={"GET", "POST"}) * @Route(path="/template/create/{id}", name="admin_invoice_template_copy", methods={"GET", "POST"}) diff --git a/src/Event/InvoiceDocumentsEvent.php b/src/Event/InvoiceDocumentsEvent.php new file mode 100644 index 00000000..116808c9 --- /dev/null +++ b/src/Event/InvoiceDocumentsEvent.php @@ -0,0 +1,65 @@ +documents = $documents; + } + + /** + * @return InvoiceDocument[] + */ + public function getInvoiceDocuments(): array + { + return $this->documents; + } + + public function addInvoiceDocuments(InvoiceDocument $document): void + { + $this->documents[] = $document; + } + + /** + * @param InvoiceDocument[] $documents + */ + public function setInvoiceDocuments(array $documents): void + { + $this->documents = $documents; + } + + public function setMaximumAllowedDocuments(int $max): void + { + $this->maximum = $max; + } + + public function getMaximumAllowedDocuments(): int + { + return $this->maximum; + } +} diff --git a/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php b/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php new file mode 100644 index 00000000..611b052d --- /dev/null +++ b/src/EventSubscriber/Actions/InvoiceDocumentSubscriber.php @@ -0,0 +1,37 @@ +getPayload(); + + /** @var InvoiceDocument|null $document */ + $document = $payload['document']; + + if ($document === null) { + return; + } + + if ($this->isGranted('manage_invoice_template')) { + $event->addDelete($this->path('invoice_document_delete', ['id' => $document->getId(), 'token' => $payload['token']]), false); + } + } +} diff --git a/src/Form/InvoiceDocumentUploadForm.php b/src/Form/InvoiceDocumentUploadForm.php index 001f0c03..66111182 100644 --- a/src/Form/InvoiceDocumentUploadForm.php +++ b/src/Form/InvoiceDocumentUploadForm.php @@ -21,9 +21,6 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; class InvoiceDocumentUploadForm extends AbstractType { - /** - * @var InvoiceDocumentRepository - */ private $repository; public function __construct(InvoiceDocumentRepository $repository) @@ -36,6 +33,12 @@ class InvoiceDocumentUploadForm extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { + $mimetypes = [ + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.oasis.opendocument.spreadsheet', + ]; + $builder ->add('document', FileType::class, [ 'label' => 'label.invoice_renderer', @@ -43,13 +46,12 @@ class InvoiceDocumentUploadForm extends AbstractType 'help' => 'help.upload', 'mapped' => false, 'required' => true, + 'attr' => [ + 'accept' => implode(',', $mimetypes) + ], 'constraints' => [ new File([ - 'mimeTypes' => [ - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.oasis.opendocument.spreadsheet', - ], + 'mimeTypes' => $mimetypes, 'mimeTypesMessage' => 'This file type is not allowed', ]), new Callback([$this, 'validateDocument']) diff --git a/src/Repository/InvoiceDocumentRepository.php b/src/Repository/InvoiceDocumentRepository.php index 728a2b4a..d409e9ac 100644 --- a/src/Repository/InvoiceDocumentRepository.php +++ b/src/Repository/InvoiceDocumentRepository.php @@ -50,6 +50,14 @@ final class InvoiceDocumentRepository return $this; } + /** + * @codeCoverageIgnore + */ + public function remove(InvoiceDocument $invoiceDocument): void + { + @unlink($invoiceDocument->getFilename()); + } + /** * @deprecated since 1.10 - will be removed with 2.0 - use getUploadDirectory() instead */ diff --git a/templates/invoice/actions.html.twig b/templates/invoice/actions.html.twig index e654110c..d65e29e0 100644 --- a/templates/invoice/actions.html.twig +++ b/templates/invoice/actions.html.twig @@ -33,3 +33,9 @@ {% set event = actions(app.user, 'invoice_template', view, {'template': template, 'token': csrf_token('invoice.delete_template')}) %} {{ widgets.table_actions(event.actions) }} {% endmacro %} + +{% macro invoice_document(document, view) %} + {% import "macros/widgets.html.twig" as widgets %} + {% set event = actions(app.user, 'invoice_document', view, {'document': document, 'token': csrf_token('invoice.delete_document')}) %} + {{ widgets.table_actions(event.actions) }} +{% endmacro %} diff --git a/templates/invoice/document_upload.html.twig b/templates/invoice/document_upload.html.twig index 1358ce25..7099f5d1 100644 --- a/templates/invoice/document_upload.html.twig +++ b/templates/invoice/document_upload.html.twig @@ -1,30 +1,33 @@ -{% extends app.request.xmlHttpRequest ? 'form.html.twig' : 'base.html.twig' %} +{% extends 'base.html.twig' %} {% import "invoice/actions.html.twig" as actions %} +{% import "macros/widgets.html.twig" as widgets %} -{% block page_title %}{{ 'admin_invoice_template.title'|trans }}{% endblock %} +{% block page_title %}{{ 'label.invoice_renderer'|trans({}, 'invoice-renderer') }}{% endblock %} {% block page_actions %}{{ actions.invoice_upload('index') }}{% endblock %} {% block main %} - {% if form is not null %} + {% if can_upload and form is not null %} {% form_theme form '@AdminLTE/layout/form-theme-horizontal.html.twig' %} - {% set formEditTemplate = app.request.xmlHttpRequest ? 'default/_form_modal.html.twig' : 'default/_form.html.twig' %} {% set formOptions = { 'title': 'upload'|trans, 'form': form, - 'back': path('admin_invoice_template') + 'back': false, + 'reset': false } %} - {% embed formEditTemplate with formOptions %} + {% embed 'default/_form.html.twig' with formOptions %} {% block form_body %} {{ form_row(form.document) }} {{ form_widget(form) }} {% endblock %} {% endembed %} + {% elseif upload_error is not null %} + {{ widgets.callout('warning', upload_error|trans(error_replacer)) }} {% endif %} {% if documents|length > 0 %} {% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'documents': documents} %} - {% import "project/actions.html.twig" as actions %} + {% import "invoice/actions.html.twig" as actions %} {% import "macros/widgets.html.twig" as widgets %} {% block box_title %}{{ 'label.invoice_renderer'|trans({}, 'invoice-renderer') }}{% endblock %} {% block box_attributes %} @@ -33,12 +36,30 @@ {% block box_body_class %}no-padding{% endblock %} {% block box_body %}
| {{ 'file'|trans }} | +{{ 'updated_at'|trans }} | +{{ 'label.template'|trans }} | ++ | ||
|---|---|---|---|---|---|
| {{ document.id }} | -{{ document.lastChange|date }} | {{ document.name }} | +{{ document.lastChange|date }} | ++ {% if config.template is not null %} + {{ config.template.name }} + {% endif %} + | ++ {% if not config.used %} + {{ actions.invoice_document(document, 'index') }} + {% endif %} + |