Upload invoice documents via UI (#1495)

This commit is contained in:
Kevin Papst
2020-02-27 23:19:19 +01:00
committed by GitHub
parent 34d2228d5d
commit bf11de82fc
16 changed files with 245 additions and 12 deletions

View File

@@ -12,12 +12,14 @@ namespace App\Controller;
use App\Entity\InvoiceTemplate;
use App\Event\InvoicePostRenderEvent;
use App\Event\InvoicePreRenderEvent;
use App\Form\InvoiceDocumentUploadForm;
use App\Form\InvoiceTemplateForm;
use App\Form\Toolbar\InvoiceToolbarForm;
use App\Invoice\InvoiceFormatter;
use App\Invoice\InvoiceItemInterface;
use App\Invoice\InvoiceModel;
use App\Invoice\ServiceInvoice;
use App\Repository\InvoiceDocumentRepository;
use App\Repository\InvoiceTemplateRepository;
use App\Repository\Query\BaseQuery;
use App\Repository\Query\InvoiceQuery;
@@ -25,6 +27,7 @@ use App\Timesheet\UserDateTimeFactory;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\SubmitButton;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
@@ -289,6 +292,68 @@ final class InvoiceController extends AbstractController
return $this->renderTemplateForm($template, $request);
}
/**
* @Route(path="/document_upload", name="admin_invoice_document_upload", methods={"GET", "POST"})
* @Security("is_granted('upload_invoice_template')")
*/
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository)
{
$dir = $documentRepository->getCustomInvoiceDirectory();
$invoiceDir = $projectDirectory . DIRECTORY_SEPARATOR . $dir;
$canUpload = true;
$form = null;
if (!file_exists($invoiceDir)) {
@mkdir($invoiceDir);
}
if (!file_exists($invoiceDir)) {
$this->flashError(sprintf('Invoice directory is not existing and could not be created: %s', $dir));
$canUpload = false;
}
if (!is_writable($invoiceDir)) {
$this->flashError(sprintf('Invoice directory cannot be written: %s', $dir));
$canUpload = false;
}
if ($canUpload) {
$form = $this->createForm(InvoiceDocumentUploadForm::class, null, [
'action' => $this->generateUrl('admin_invoice_document_upload', []),
'method' => 'POST'
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var UploadedFile $uploadedFile */
$uploadedFile = $form->get('document')->getData();
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = transliterator_transliterate(
'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()',
$originalFilename
);
$newFilename = $safeFilename . '.' . $uploadedFile->guessExtension();
try {
$uploadedFile->move($invoiceDir, $newFilename);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('admin_invoice_document_upload');
} catch (\Exception $e) {
$this->flashError(
sprintf('Failed uploading invoice document: %e', $e->getMessage())
);
}
}
}
return $this->render('invoice/document_upload.html.twig', [
'form' => (null !== $form) ? $form->createView() : null,
'documents' => $this->service->getDocuments(),
'baseDirectory' => $projectDirectory . DIRECTORY_SEPARATOR,
]);
}
/**
* @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"})

View File

@@ -42,4 +42,9 @@ final class InvoiceDocument
{
return $this->file->getExtension();
}
public function getLastChange(): int
{
return $this->file->getMTime();
}
}

View File

@@ -0,0 +1,62 @@
<?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\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;
class InvoiceDocumentUploadForm extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('document', FileType::class, [
'label' => 'label.invoice_renderer',
'translation_domain' => 'invoice-renderer',
'help' => 'help.upload',
'mapped' => false,
'required' => true,
'constraints' => [
new File([
'mimeTypes' => [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.oasis.opendocument.spreadsheet',
],
'mimeTypesMessage' => 'This file type is not allowed',
])
],
])
;
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'admin_invoice_document_upload',
'attr' => [
'data-form-event' => 'kimai.invoiceTemplateUpdate',
'data-msg-success' => 'action.update.success',
'data-msg-error' => 'action.update.error',
],
]);
}
}

View File

@@ -12,26 +12,24 @@ namespace App\Repository;
use App\Entity\InvoiceDocument;
use Symfony\Component\Finder\Finder;
class InvoiceDocumentRepository
final class InvoiceDocumentRepository
{
/**
* @var array
*/
protected $documentDirs = [];
private $documentDirs = [];
/**
* @param array $directories
*/
public function __construct(array $directories)
{
$this->documentDirs = $directories;
}
/**
* @param string $name
* @return InvoiceDocument|null
*/
public function findByName(string $name)
public function getCustomInvoiceDirectory(): string
{
return $this->documentDirs[0];
}
public function findByName(string $name): ?InvoiceDocument
{
foreach ($this->findAll() as $document) {
if ($document->getId() === $name) {

View File

@@ -86,6 +86,7 @@ final class IconExtension extends AbstractExtension
'timesheet-team' => 'fas fa-user-clock',
'trash' => 'far fa-trash-alt',
'unlocked' => 'fas fa-unlock-alt',
'upload' => 'fas fa-upload',
'user' => 'fas fa-user-friends',
'visibility' => 'far fa-eye',
'warning' => 'fas fa-exclamation-triangle',