Upload invoice documents via UI (#1495)
This commit is contained in:
@@ -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"})
|
||||
|
||||
@@ -42,4 +42,9 @@ final class InvoiceDocument
|
||||
{
|
||||
return $this->file->getExtension();
|
||||
}
|
||||
|
||||
public function getLastChange(): int
|
||||
{
|
||||
return $this->file->getMTime();
|
||||
}
|
||||
}
|
||||
|
||||
62
src/Form/InvoiceDocumentUploadForm.php
Normal file
62
src/Form/InvoiceDocumentUploadForm.php
Normal 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',
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user