search and export from invoice archive (#2408)
This commit is contained in:
@@ -12,8 +12,12 @@ namespace App\Controller;
|
|||||||
use App\Configuration\SystemConfiguration;
|
use App\Configuration\SystemConfiguration;
|
||||||
use App\Entity\Invoice;
|
use App\Entity\Invoice;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
|
use App\Export\Spreadsheet\AnnotatedObjectExporter;
|
||||||
|
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||||
|
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||||
use App\Form\InvoiceDocumentUploadForm;
|
use App\Form\InvoiceDocumentUploadForm;
|
||||||
use App\Form\InvoiceTemplateForm;
|
use App\Form\InvoiceTemplateForm;
|
||||||
|
use App\Form\Toolbar\InvoiceArchiveForm;
|
||||||
use App\Form\Toolbar\InvoiceToolbarForm;
|
use App\Form\Toolbar\InvoiceToolbarForm;
|
||||||
use App\Form\Toolbar\InvoiceToolbarSimpleForm;
|
use App\Form\Toolbar\InvoiceToolbarSimpleForm;
|
||||||
use App\Invoice\ServiceInvoice;
|
use App\Invoice\ServiceInvoice;
|
||||||
@@ -21,6 +25,7 @@ use App\Repository\InvoiceDocumentRepository;
|
|||||||
use App\Repository\InvoiceRepository;
|
use App\Repository\InvoiceRepository;
|
||||||
use App\Repository\InvoiceTemplateRepository;
|
use App\Repository\InvoiceTemplateRepository;
|
||||||
use App\Repository\Query\BaseQuery;
|
use App\Repository\Query\BaseQuery;
|
||||||
|
use App\Repository\Query\InvoiceArchiveQuery;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceQuery;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
@@ -236,20 +241,48 @@ final class InvoiceController extends AbstractController
|
|||||||
$invoice = $this->invoiceRepository->find($id);
|
$invoice = $this->invoiceRepository->find($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = new InvoiceQuery();
|
$query = new InvoiceArchiveQuery();
|
||||||
$query->setOrderBy('date');
|
|
||||||
$query->setPage($page);
|
$query->setPage($page);
|
||||||
$query->setCurrentUser($this->getUser());
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|
||||||
|
$form = $this->getArchiveToolbarForm($query);
|
||||||
|
$form->setData($query);
|
||||||
|
$form->submit($request->query->all(), false);
|
||||||
|
|
||||||
|
if (!$form->isValid()) {
|
||||||
|
$query->resetByFormError($form->getErrors());
|
||||||
|
}
|
||||||
|
|
||||||
$invoices = $this->invoiceRepository->getPagerfantaForQuery($query);
|
$invoices = $this->invoiceRepository->getPagerfantaForQuery($query);
|
||||||
|
|
||||||
return $this->render('invoice/listing.html.twig', [
|
return $this->render('invoice/listing.html.twig', [
|
||||||
'entries' => $invoices,
|
'entries' => $invoices,
|
||||||
'query' => $query,
|
'query' => $query,
|
||||||
|
'toolbarForm' => $form->createView(),
|
||||||
'download' => $invoice,
|
'download' => $invoice,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/export", name="invoice_export", methods={"GET"})
|
||||||
|
*/
|
||||||
|
public function exportAction(Request $request, AnnotatedObjectExporter $exporter)
|
||||||
|
{
|
||||||
|
$query = new InvoiceArchiveQuery();
|
||||||
|
$query->setCurrentUser($this->getUser());
|
||||||
|
|
||||||
|
$form = $this->getArchiveToolbarForm($query);
|
||||||
|
$form->setData($query);
|
||||||
|
$form->submit($request->query->all(), false);
|
||||||
|
|
||||||
|
$entries = $this->invoiceRepository->getInvoicesForQuery($query);
|
||||||
|
|
||||||
|
$spreadsheet = $exporter->export(Invoice::class, $entries);
|
||||||
|
$writer = new BinaryFileResponseWriter(new XlsxWriter(), 'kimai-invoices');
|
||||||
|
|
||||||
|
return $writer->getFileResponse($spreadsheet);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/template/{page}", requirements={"page": "[1-9]\d*"}, defaults={"page": 1}, name="admin_invoice_template", methods={"GET", "POST"})
|
* @Route(path="/template/{page}", requirements={"page": "[1-9]\d*"}, defaults={"page": 1}, name="admin_invoice_template", methods={"GET", "POST"})
|
||||||
* @Security("is_granted('manage_invoice_template')")
|
* @Security("is_granted('manage_invoice_template')")
|
||||||
@@ -415,6 +448,18 @@ final class InvoiceController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getArchiveToolbarForm(InvoiceArchiveQuery $query): FormInterface
|
||||||
|
{
|
||||||
|
return $this->createForm(InvoiceArchiveForm::class, $query, [
|
||||||
|
'action' => $this->generateUrl('admin_invoice_list', []),
|
||||||
|
'method' => 'GET',
|
||||||
|
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
|
||||||
|
'attr' => [
|
||||||
|
'id' => 'invoice-archive-form'
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
private function createEditForm(InvoiceTemplate $template): FormInterface
|
private function createEditForm(InvoiceTemplate $template): FormInterface
|
||||||
{
|
{
|
||||||
if ($template->getId() === null) {
|
if ($template->getId() === null) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Export\Annotation as Exporter;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
@@ -21,10 +22,14 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
* @ORM\UniqueConstraint(columns={"invoice_filename"})
|
* @ORM\UniqueConstraint(columns={"invoice_filename"})
|
||||||
* }
|
* }
|
||||||
* )
|
* )
|
||||||
|
* @ORM\Entity(repositoryClass="App\Repository\InvoiceRepository")
|
||||||
* @UniqueEntity("invoiceNumber")
|
* @UniqueEntity("invoiceNumber")
|
||||||
* @UniqueEntity("invoiceFilename")
|
* @UniqueEntity("invoiceFilename")
|
||||||
*
|
*
|
||||||
* @ORM\Entity(repositoryClass="App\Repository\InvoiceRepository")
|
* @Exporter\Order({"id", "createdAt", "invoiceNumber", "status", "customer", "total", "tax", "currency", "vat", "dueDays", "dueDate", "user", "invoiceFilename"})
|
||||||
|
* @Exporter\Expose("customer", label="label.customer", exp="object.getCustomer() === null ? null : object.getCustomer().getName()")
|
||||||
|
* @Exporter\Expose("dueDate", label="invoice.due_days", type="datetime", exp="object.getDueDate() === null ? null : object.getDueDate()")
|
||||||
|
* @Exporter\Expose("user", label="label.username", type="string", exp="object.getUser() === null ? null : object.getUser().getDisplayName()")
|
||||||
*/
|
*/
|
||||||
class Invoice
|
class Invoice
|
||||||
{
|
{
|
||||||
@@ -35,6 +40,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var int|null
|
* @var int|null
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="label.id", type="integer")
|
||||||
|
*
|
||||||
* @ORM\Column(name="id", type="integer")
|
* @ORM\Column(name="id", type="integer")
|
||||||
* @ORM\Id
|
* @ORM\Id
|
||||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||||
@@ -44,6 +51,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="invoice.number", type="string")
|
||||||
|
*
|
||||||
* @ORM\Column(name="invoice_number", type="string", length=50, nullable=false)
|
* @ORM\Column(name="invoice_number", type="string", length=50, nullable=false)
|
||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
*/
|
*/
|
||||||
@@ -70,6 +79,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var \DateTime
|
* @var \DateTime
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="label.date", type="datetime")
|
||||||
|
*
|
||||||
* @ORM\Column(name="created_at", type="datetime", nullable=false)
|
* @ORM\Column(name="created_at", type="datetime", nullable=false)
|
||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
*/
|
*/
|
||||||
@@ -85,6 +96,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var float
|
* @var float
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="label.total_rate", type="float")
|
||||||
|
*
|
||||||
* @ORM\Column(name="total", type="float", nullable=false)
|
* @ORM\Column(name="total", type="float", nullable=false)
|
||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
*/
|
*/
|
||||||
@@ -93,6 +106,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var float
|
* @var float
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="invoice.tax", type="float")
|
||||||
|
*
|
||||||
* @ORM\Column(name="tax", type="float", nullable=false)
|
* @ORM\Column(name="tax", type="float", nullable=false)
|
||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
*/
|
*/
|
||||||
@@ -101,6 +116,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="label.currency", type="string")
|
||||||
|
*
|
||||||
* @ORM\Column(name="currency", type="string", length=3, nullable=false)
|
* @ORM\Column(name="currency", type="string", length=3, nullable=false)
|
||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
* @Assert\Length(max=3)
|
* @Assert\Length(max=3)
|
||||||
@@ -110,6 +127,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var int
|
* @var int
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="label.due_days", type="integer")
|
||||||
|
*
|
||||||
* @ORM\Column(name="due_days", type="integer", length=3, nullable=false)
|
* @ORM\Column(name="due_days", type="integer", length=3, nullable=false)
|
||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
* @Assert\Range(min = 0, max = 999)
|
* @Assert\Range(min = 0, max = 999)
|
||||||
@@ -119,6 +138,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var float
|
* @var float
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="label.tax_rate", type="float")
|
||||||
|
*
|
||||||
* @ORM\Column(name="vat", type="float", nullable=false)
|
* @ORM\Column(name="vat", type="float", nullable=false)
|
||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
* @Assert\Range(min = 0.0, max = 99.99)
|
* @Assert\Range(min = 0.0, max = 99.99)
|
||||||
@@ -128,6 +149,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="label.status", type="string")
|
||||||
|
*
|
||||||
* @ORM\Column(name="status", type="string", length=20, nullable=false)
|
* @ORM\Column(name="status", type="string", length=20, nullable=false)
|
||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
*/
|
*/
|
||||||
@@ -136,6 +159,8 @@ class Invoice
|
|||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
*
|
*
|
||||||
|
* @Exporter\Expose(label="file", type="string")
|
||||||
|
*
|
||||||
* @ORM\Column(name="invoice_filename", type="string", length=150, nullable=false)
|
* @ORM\Column(name="invoice_filename", type="string", length=150, nullable=false)
|
||||||
* @Assert\NotNull()
|
* @Assert\NotNull()
|
||||||
* @Assert\Length(min=1, max=150, allowEmptyString=false)
|
* @Assert\Length(min=1, max=150, allowEmptyString=false)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
namespace App\EventSubscriber\Actions;
|
namespace App\EventSubscriber\Actions;
|
||||||
|
|
||||||
|
use App\Constants;
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
@@ -33,4 +34,9 @@ abstract class AbstractActionsSubscriber implements EventSubscriberInterface
|
|||||||
{
|
{
|
||||||
return $this->urlGenerator->generate($route, $parameters);
|
return $this->urlGenerator->generate($route, $parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function documentationLink(string $url): string
|
||||||
|
{
|
||||||
|
return Constants::HOMEPAGE . '/documentation/' . $url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/EventSubscriber/Actions/InvoiceArchiveSubscriber.php
Normal file
37
src/EventSubscriber/Actions/InvoiceArchiveSubscriber.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?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\EventSubscriber\Actions;
|
||||||
|
|
||||||
|
use App\Event\PageActionsEvent;
|
||||||
|
|
||||||
|
class InvoiceArchiveSubscriber extends AbstractActionsSubscriber
|
||||||
|
{
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'actions.invoice_details' => ['onActions', 1000],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onActions(PageActionsEvent $event)
|
||||||
|
{
|
||||||
|
$actions = $event->getActions();
|
||||||
|
|
||||||
|
if ($this->isGranted('view_invoice')) {
|
||||||
|
$actions['back'] = ['url' => $this->path('invoice'), 'translation_domain' => 'actions'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$actions['visibility'] = '#modal_invoices';
|
||||||
|
$actions['download'] = ['url' => $this->path('invoice_export'), 'class' => 'toolbar-action'];
|
||||||
|
$actions['help'] = ['url' => $this->documentationLink('invoices.html'), 'target' => '_blank'];
|
||||||
|
|
||||||
|
$event->setActions($actions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,8 +88,6 @@ class UserSubscriber extends AbstractActionsSubscriber
|
|||||||
$actions['trash'] = ['url' => $this->path('admin_user_delete', ['id' => $user->getId()]), 'class' => 'modal-ajax-form'];
|
$actions['trash'] = ['url' => $this->path('admin_user_delete', ['id' => $user->getId()]), 'class' => 'modal-ajax-form'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload['actions'] = array_merge($payload['actions'], $actions);
|
$event->setActions($actions);
|
||||||
|
|
||||||
$event->setPayload($payload);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class InvoiceTemplateForm extends AbstractType
|
|||||||
'label' => 'label.due_days',
|
'label' => 'label.due_days',
|
||||||
])
|
])
|
||||||
->add('vat', NumberType::class, [
|
->add('vat', NumberType::class, [
|
||||||
'label' => 'label.vat',
|
'label' => 'label.tax_rate',
|
||||||
'scale' => 2,
|
'scale' => 2,
|
||||||
])
|
])
|
||||||
->add('renderer', InvoiceRendererType::class)
|
->add('renderer', InvoiceRendererType::class)
|
||||||
|
|||||||
44
src/Form/Toolbar/InvoiceArchiveForm.php
Normal file
44
src/Form/Toolbar/InvoiceArchiveForm.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?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\Toolbar;
|
||||||
|
|
||||||
|
use App\Form\Type\InvoiceStatusType;
|
||||||
|
use App\Repository\Query\InvoiceArchiveQuery;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the form used for filtering timesheet entries for invoices.
|
||||||
|
*/
|
||||||
|
class InvoiceArchiveForm extends AbstractToolbarForm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$this->addSearchTermInputField($builder);
|
||||||
|
$this->addDateRange($builder, ['timezone' => $options['timezone']]);
|
||||||
|
$this->addCustomerMultiChoice($builder, ['required' => false, 'start_date_param' => null, 'end_date_param' => null, 'ignore_date' => true, 'placeholder' => ''], true);
|
||||||
|
$builder->add('status', InvoiceStatusType::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => InvoiceArchiveQuery::class,
|
||||||
|
'csrf_protection' => false,
|
||||||
|
'timezone' => date_default_timezone_get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/Form/Type/InvoiceStatusType.php
Normal file
45
src/Form/Type/InvoiceStatusType.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?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\Type;
|
||||||
|
|
||||||
|
use App\Entity\Invoice;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom form field type to select an invoice template.
|
||||||
|
*/
|
||||||
|
class InvoiceStatusType extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'label' => 'label.status',
|
||||||
|
'multiple' => true,
|
||||||
|
'choices' => [
|
||||||
|
'status.' . Invoice::STATUS_NEW => Invoice::STATUS_NEW,
|
||||||
|
'status.' . Invoice::STATUS_PENDING => Invoice::STATUS_PENDING,
|
||||||
|
'status.' . Invoice::STATUS_PAID => Invoice::STATUS_PAID,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getParent()
|
||||||
|
{
|
||||||
|
return ChoiceType::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,7 +151,7 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
|||||||
|
|
||||||
// for customer
|
// for customer
|
||||||
case 'cc':
|
case 'cc':
|
||||||
$partialResult = $this->repository->getCounterForAllTime($invoiceDate, $this->model->getCustomer()) + $increaseBy;
|
$partialResult = $this->repository->getCounterForAllTime($this->model->getCustomer()) + $increaseBy;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ccy':
|
case 'ccy':
|
||||||
@@ -168,7 +168,7 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
|||||||
|
|
||||||
// across all invoices
|
// across all invoices
|
||||||
case 'c':
|
case 'c':
|
||||||
$partialResult = $this->repository->getCounterForAllTime($invoiceDate) + $increaseBy;
|
$partialResult = $this->repository->getCounterForAllTime() + $increaseBy;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'cy':
|
case 'cy':
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use App\Entity\User;
|
|||||||
use App\Repository\Loader\InvoiceLoader;
|
use App\Repository\Loader\InvoiceLoader;
|
||||||
use App\Repository\Paginator\LoaderPaginator;
|
use App\Repository\Paginator\LoaderPaginator;
|
||||||
use App\Repository\Paginator\PaginatorInterface;
|
use App\Repository\Paginator\PaginatorInterface;
|
||||||
use App\Repository\Query\InvoiceQuery;
|
use App\Repository\Query\InvoiceArchiveQuery;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Pagerfanta\Pagerfanta;
|
use Pagerfanta\Pagerfanta;
|
||||||
@@ -91,7 +91,7 @@ class InvoiceRepository extends EntityRepository
|
|||||||
return $this->getCounterFor($start, $end, $customer);
|
return $this->getCounterFor($start, $end, $customer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCounterForAllTime(\DateTime $date, ?Customer $customer = null): int
|
public function getCounterForAllTime(?Customer $customer = null): int
|
||||||
{
|
{
|
||||||
if (null !== $customer) {
|
if (null !== $customer) {
|
||||||
return $this->count(['customer' => $customer]);
|
return $this->count(['customer' => $customer]);
|
||||||
@@ -137,7 +137,7 @@ class InvoiceRepository extends EntityRepository
|
|||||||
$qb->setParameter('teams', $ids);
|
$qb->setParameter('teams', $ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getQueryBuilderForQuery(InvoiceQuery $query): QueryBuilder
|
private function getQueryBuilderForQuery(InvoiceArchiveQuery $query): QueryBuilder
|
||||||
{
|
{
|
||||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
|
|
||||||
@@ -146,21 +146,79 @@ class InvoiceRepository extends EntityRepository
|
|||||||
->from(Invoice::class, 'i')
|
->from(Invoice::class, 'i')
|
||||||
;
|
;
|
||||||
|
|
||||||
|
if ($query->getBegin() !== null) {
|
||||||
|
$qb->andWhere($qb->expr()->gte('i.createdAt', ':begin'));
|
||||||
|
$qb->setParameter('begin', $query->getBegin());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query->getEnd() !== null) {
|
||||||
|
$qb->andWhere($qb->expr()->lte('i.createdAt', ':end'));
|
||||||
|
$qb->setParameter('end', $query->getEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query->hasCustomers()) {
|
||||||
|
$qb->andWhere($qb->expr()->in('i.customer', ':customer'));
|
||||||
|
$qb->setParameter('customer', $query->getCustomers());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($query->hasStatus()) {
|
||||||
|
$qb->andWhere($qb->expr()->in('i.status', ':status'));
|
||||||
|
$qb->setParameter('status', $query->getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
$orderBy = $query->getOrderBy();
|
$orderBy = $query->getOrderBy();
|
||||||
switch ($orderBy) {
|
switch ($orderBy) {
|
||||||
case 'date':
|
case 'date':
|
||||||
$orderBy = 'i.createdAt';
|
$orderBy = 'i.createdAt';
|
||||||
break;
|
break;
|
||||||
|
case 'customer':
|
||||||
|
$orderBy = 'i.customer';
|
||||||
|
break;
|
||||||
|
case 'total':
|
||||||
|
$orderBy = 'i.total';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$qb->addOrderBy($orderBy, $query->getOrder());
|
$qb->addOrderBy($orderBy, $query->getOrder());
|
||||||
|
|
||||||
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
||||||
|
|
||||||
|
if ($query->hasSearchTerm()) {
|
||||||
|
$qb->leftJoin('i.customer', 'customer');
|
||||||
|
$searchAnd = $qb->expr()->andX();
|
||||||
|
$searchTerm = $query->getSearchTerm();
|
||||||
|
|
||||||
|
foreach ($searchTerm->getSearchFields() as $metaName => $metaValue) {
|
||||||
|
$qb->leftJoin('customer.meta', 'meta');
|
||||||
|
$searchAnd->add(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->eq('meta.name', ':metaName'),
|
||||||
|
$qb->expr()->like('meta.value', ':metaValue')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$qb->setParameter('metaName', $metaName);
|
||||||
|
$qb->setParameter('metaValue', '%' . $metaValue . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchTerm->hasSearchTerm()) {
|
||||||
|
$searchAnd->add(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->like('customer.name', ':searchTerm'),
|
||||||
|
$qb->expr()->like('customer.company', ':searchTerm')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$qb->setParameter('searchTerm', '%' . $searchTerm->getSearchTerm() . '%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($searchAnd->count() > 0) {
|
||||||
|
$qb->andWhere($searchAnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $qb;
|
return $qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function countInvoicesForQuery(InvoiceQuery $query): int
|
public function countInvoicesForQuery(InvoiceArchiveQuery $query): int
|
||||||
{
|
{
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
$qb
|
$qb
|
||||||
@@ -173,7 +231,20 @@ class InvoiceRepository extends EntityRepository
|
|||||||
return (int) $qb->getQuery()->getSingleScalarResult();
|
return (int) $qb->getQuery()->getSingleScalarResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getPaginatorForQuery(InvoiceQuery $query): PaginatorInterface
|
/**
|
||||||
|
* @param InvoiceArchiveQuery $query
|
||||||
|
* @return Invoice[]
|
||||||
|
*/
|
||||||
|
public function getInvoicesForQuery(InvoiceArchiveQuery $query): iterable
|
||||||
|
{
|
||||||
|
// this is using the paginator internally, as it will load all joined entities into the working unit
|
||||||
|
// do not "optimize" to use the query directly, as it would results in hundreds of additional lazy queries
|
||||||
|
$paginator = $this->getPaginatorForQuery($query);
|
||||||
|
|
||||||
|
return $paginator->getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPaginatorForQuery(InvoiceArchiveQuery $query): PaginatorInterface
|
||||||
{
|
{
|
||||||
$counter = $this->countInvoicesForQuery($query);
|
$counter = $this->countInvoicesForQuery($query);
|
||||||
$qb = $this->getQueryBuilderForQuery($query);
|
$qb = $this->getQueryBuilderForQuery($query);
|
||||||
@@ -181,7 +252,7 @@ class InvoiceRepository extends EntityRepository
|
|||||||
return new LoaderPaginator(new InvoiceLoader($qb->getEntityManager()), $qb, $counter);
|
return new LoaderPaginator(new InvoiceLoader($qb->getEntityManager()), $qb, $counter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPagerfantaForQuery(InvoiceQuery $query): Pagerfanta
|
public function getPagerfantaForQuery(InvoiceArchiveQuery $query): Pagerfanta
|
||||||
{
|
{
|
||||||
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
||||||
$paginator->setMaxPerPage($query->getPageSize());
|
$paginator->setMaxPerPage($query->getPageSize());
|
||||||
|
|||||||
58
src/Repository/Query/DateRangeTrait.php
Normal file
58
src/Repository/Query/DateRangeTrait.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?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\Repository\Query;
|
||||||
|
|
||||||
|
use App\Form\Model\DateRange;
|
||||||
|
|
||||||
|
trait DateRangeTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var DateRange
|
||||||
|
*/
|
||||||
|
protected $dateRange;
|
||||||
|
|
||||||
|
public function getBegin(): ?\DateTime
|
||||||
|
{
|
||||||
|
if (null === $this->dateRange) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dateRange->getBegin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBegin(\DateTime $begin): void
|
||||||
|
{
|
||||||
|
$this->dateRange->setBegin($begin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEnd(): ?\DateTime
|
||||||
|
{
|
||||||
|
if (null === $this->dateRange) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dateRange->getEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEnd(\DateTime $end): void
|
||||||
|
{
|
||||||
|
$this->dateRange->setEnd($end);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateRange(): ?DateRange
|
||||||
|
{
|
||||||
|
return $this->dateRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateRange(DateRange $dateRange): void
|
||||||
|
{
|
||||||
|
$this->dateRange = $dateRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/Repository/Query/InvoiceArchiveQuery.php
Normal file
101
src/Repository/Query/InvoiceArchiveQuery.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?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\Repository\Query;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Invoice;
|
||||||
|
use App\Form\Model\DateRange;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query for created invoices.
|
||||||
|
*/
|
||||||
|
class InvoiceArchiveQuery extends BaseQuery
|
||||||
|
{
|
||||||
|
use DateRangeTrait;
|
||||||
|
|
||||||
|
public const INVOICE_ARCHIVE_ORDER_ALLOWED = [
|
||||||
|
'date', 'customer', 'total'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter for invoice status (by default all)
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
private $status = [];
|
||||||
|
/**
|
||||||
|
* @var Customer[]
|
||||||
|
*/
|
||||||
|
private $customers = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->setDefaults([
|
||||||
|
'orderBy' => 'date',
|
||||||
|
'order' => self::ORDER_DESC,
|
||||||
|
'dateRange' => new DateRange(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addCustomer(Customer $customer): void
|
||||||
|
{
|
||||||
|
$this->customers[] = $customer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomers(array $customers): void
|
||||||
|
{
|
||||||
|
foreach ($customers as $customer) {
|
||||||
|
$this->addCustomer($customer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Customer[]
|
||||||
|
*/
|
||||||
|
public function getCustomers(): array
|
||||||
|
{
|
||||||
|
return $this->customers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasCustomers(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->customers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasStatus(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): array
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $status
|
||||||
|
*/
|
||||||
|
public function setStatus(array $status): void
|
||||||
|
{
|
||||||
|
foreach ($status as $s) {
|
||||||
|
$this->addStatus($s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addStatus(string $status): void
|
||||||
|
{
|
||||||
|
if (!\in_array($status, [Invoice::STATUS_NEW, Invoice::STATUS_PENDING, Invoice::STATUS_PAID])) {
|
||||||
|
throw new \InvalidArgumentException('Unknown invoice status given.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\in_array($status, $this->status)) {
|
||||||
|
$this->status[] = $status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ namespace App\Repository\Query;
|
|||||||
|
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find items (eg timesheets) for creating a new invoice.
|
||||||
|
*/
|
||||||
class InvoiceQuery extends TimesheetQuery
|
class InvoiceQuery extends TimesheetQuery
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use App\Form\Model\DateRange;
|
|||||||
class TimesheetQuery extends ActivityQuery implements BillableInterface
|
class TimesheetQuery extends ActivityQuery implements BillableInterface
|
||||||
{
|
{
|
||||||
use BillableTrait;
|
use BillableTrait;
|
||||||
|
use DateRangeTrait;
|
||||||
|
|
||||||
public const STATE_ALL = 1;
|
public const STATE_ALL = 1;
|
||||||
public const STATE_RUNNING = 2;
|
public const STATE_RUNNING = 2;
|
||||||
@@ -49,10 +50,6 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface
|
|||||||
* @var \DateTime|null
|
* @var \DateTime|null
|
||||||
*/
|
*/
|
||||||
private $modifiedAfter;
|
private $modifiedAfter;
|
||||||
/**
|
|
||||||
* @var DateRange
|
|
||||||
*/
|
|
||||||
protected $dateRange;
|
|
||||||
/**
|
/**
|
||||||
* @var iterable
|
* @var iterable
|
||||||
*/
|
*/
|
||||||
@@ -230,42 +227,6 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getBegin(): ?\DateTime
|
|
||||||
{
|
|
||||||
return $this->dateRange->getBegin();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setBegin(\DateTime $begin): TimesheetQuery
|
|
||||||
{
|
|
||||||
$this->dateRange->setBegin($begin);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getEnd(): ?\DateTime
|
|
||||||
{
|
|
||||||
return $this->dateRange->getEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setEnd(\DateTime $end): TimesheetQuery
|
|
||||||
{
|
|
||||||
$this->dateRange->setEnd($end);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDateRange(): DateRange
|
|
||||||
{
|
|
||||||
return $this->dateRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setDateRange(DateRange $dateRange): TimesheetQuery
|
|
||||||
{
|
|
||||||
$this->dateRange = $dateRange;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTags(bool $allowUnknown = false): iterable
|
public function getTags(bool $allowUnknown = false): iterable
|
||||||
{
|
{
|
||||||
if (empty($this->tags)) {
|
if (empty($this->tags)) {
|
||||||
|
|||||||
@@ -77,19 +77,8 @@
|
|||||||
|
|
||||||
{% macro invoice_listing(view) %}
|
{% macro invoice_listing(view) %}
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% set event = actions(app.user, 'invoice_details', {'view': view}) %}
|
||||||
{% set actions = {} %}
|
{{ widgets.page_actions(event.actions) }}
|
||||||
|
|
||||||
{% if is_granted('view_invoice') %}
|
|
||||||
{% set actions = actions|merge({'back': path('invoice')}) %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% set actions = actions|merge({'visibility': '#modal_invoices'}) %}
|
|
||||||
|
|
||||||
{% set actions = actions|merge({'help': {'url': 'invoices.html'|docu_link, 'target': '_blank'}}) %}
|
|
||||||
|
|
||||||
{% set event = trigger('actions.invoice_details', {'actions': actions, 'view': view}) %}
|
|
||||||
{{ widgets.page_actions(actions) }}
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro invoice_upload(view) %}
|
{% macro invoice_upload(view) %}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
{% block page_title %}{{ 'invoice.title'|trans }}{% endblock %}
|
{% block page_title %}{{ 'invoice.title'|trans }}{% endblock %}
|
||||||
{% block page_actions %}{{ actions.invoice_listing('index') }}{% endblock %}
|
{% block page_actions %}{{ actions.invoice_listing('index') }}{% endblock %}
|
||||||
|
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %}
|
||||||
|
|
||||||
{% block main_before %}
|
{% block main_before %}
|
||||||
{{ tables.data_table_column_modal(tableName, columns) }}
|
{{ tables.data_table_column_modal(tableName, columns) }}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
'title': {'class': 'hidden-xs text-nowrap', 'orderBy': false},
|
'title': {'class': 'hidden-xs text-nowrap', 'orderBy': false},
|
||||||
'company': {'class': 'hidden-xs hidden-sm hidden', 'orderBy': false},
|
'company': {'class': 'hidden-xs hidden-sm hidden', 'orderBy': false},
|
||||||
'vat_id': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
|
'vat_id': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
|
||||||
'vat': {'class': 'hidden-xs hidden-sm hidden-md text-nowrap', 'orderBy': false},
|
'tax_rate': {'class': 'hidden-xs hidden-sm hidden-md text-nowrap', 'orderBy': false},
|
||||||
'due_days': {'class': 'hidden-xs hidden-sm hidden-md text-nowrap', 'orderBy': false},
|
'due_days': {'class': 'hidden-xs hidden-sm hidden-md text-nowrap', 'orderBy': false},
|
||||||
'address': {'class': 'hidden', 'orderBy': false},
|
'address': {'class': 'hidden', 'orderBy': false},
|
||||||
'contact': {'class': 'hidden', 'orderBy': false},
|
'contact': {'class': 'hidden', 'orderBy': false},
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'title') }}">{{ entry.title }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'title') }}">{{ entry.title }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'company') }}">{{ entry.company }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'company') }}">{{ entry.company }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'vat_id') }}">{{ entry.vatId }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'vat_id') }}">{{ entry.vatId }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'vat') }}">{{ entry.vat }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'tax_rate') }}">{{ entry.vat }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'due_days') }}">{{ entry.dueDays }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'due_days') }}">{{ entry.dueDays }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'address') }}">{{ entry.address|nl2br }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'address') }}">{{ entry.address|nl2br }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'contact') }}">{{ entry.contact|nl2br }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'contact') }}">{{ entry.contact|nl2br }}</td>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Entity\Activity;
|
|||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\Team;
|
use App\Entity\Team;
|
||||||
|
use App\Form\Model\DateRange;
|
||||||
use App\Repository\Query\ActivityQuery;
|
use App\Repository\Query\ActivityQuery;
|
||||||
use App\Repository\Query\BaseQuery;
|
use App\Repository\Query\BaseQuery;
|
||||||
use App\Repository\Query\ProjectQuery;
|
use App\Repository\Query\ProjectQuery;
|
||||||
@@ -63,12 +64,12 @@ class BaseQueryTest extends TestCase
|
|||||||
self::assertNull($sut->getSearchTerm());
|
self::assertNull($sut->getSearchTerm());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function assertBaseQuery(BaseQuery $sut, $orderBy = 'id')
|
protected function assertBaseQuery(BaseQuery $sut, $orderBy = 'id', $order = BaseQuery::ORDER_ASC)
|
||||||
{
|
{
|
||||||
$this->assertPage($sut);
|
$this->assertPage($sut);
|
||||||
$this->assertPageSize($sut);
|
$this->assertPageSize($sut);
|
||||||
$this->assertOrderBy($sut, $orderBy);
|
$this->assertOrderBy($sut, $orderBy);
|
||||||
$this->assertOrder($sut);
|
$this->assertOrder($sut, $order);
|
||||||
$this->assertTeams($sut);
|
$this->assertTeams($sut);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,4 +276,29 @@ class BaseQueryTest extends TestCase
|
|||||||
$this->assertEquals(99, $sut->getProject());
|
$this->assertEquals(99, $sut->getProject());
|
||||||
$this->assertEquals([99], $sut->getProjects());
|
$this->assertEquals([99], $sut->getProjects());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function assertDateRangeTrait($sut)
|
||||||
|
{
|
||||||
|
self::assertNull($sut->getBegin());
|
||||||
|
self::assertNull($sut->getEnd());
|
||||||
|
|
||||||
|
$dateRange = new DateRange();
|
||||||
|
$sut->setDateRange($dateRange);
|
||||||
|
|
||||||
|
self::assertSame($dateRange, $sut->getDateRange());
|
||||||
|
self::assertNull($sut->getBegin());
|
||||||
|
self::assertNull($sut->getEnd());
|
||||||
|
|
||||||
|
$begin = new \DateTime('2013-11-23 13:45:07');
|
||||||
|
$end = new \DateTime('2014-01-01 23:45:11');
|
||||||
|
$dateRange->setBegin($begin);
|
||||||
|
$dateRange->setEnd($end);
|
||||||
|
|
||||||
|
self::assertSame($begin, $sut->getDateRange()->getBegin());
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
self::assertSame($begin, $sut->getBegin());
|
||||||
|
self::assertSame($end, $sut->getDateRange()->getEnd());
|
||||||
|
/* @phpstan-ignore-next-line */
|
||||||
|
self::assertSame($end, $sut->getEnd());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
tests/Repository/Query/InvoiceArchiveQueryTest.php
Normal file
45
tests/Repository/Query/InvoiceArchiveQueryTest.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?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\Tests\Repository\Query;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Invoice;
|
||||||
|
use App\Repository\Query\BaseQuery;
|
||||||
|
use App\Repository\Query\InvoiceArchiveQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Repository\Query\InvoiceArchiveQuery
|
||||||
|
*/
|
||||||
|
class InvoiceArchiveQueryTest extends BaseQueryTest
|
||||||
|
{
|
||||||
|
public function testQuery()
|
||||||
|
{
|
||||||
|
$sut = new InvoiceArchiveQuery();
|
||||||
|
self::assertFalse($sut->hasStatus());
|
||||||
|
$this->assertBaseQuery($sut, 'date', BaseQuery::ORDER_DESC);
|
||||||
|
$this->assertDateRangeTrait($sut);
|
||||||
|
|
||||||
|
$this->assertIsArray($sut->getCustomers());
|
||||||
|
$this->assertEmpty($sut->getCustomers());
|
||||||
|
self::assertFalse($sut->hasCustomers());
|
||||||
|
|
||||||
|
$sut->addCustomer(new Customer());
|
||||||
|
$sut->setCustomers([new Customer()]);
|
||||||
|
self::assertCount(2, $sut->getCustomers());
|
||||||
|
self::assertTrue($sut->hasCustomers());
|
||||||
|
|
||||||
|
$sut->addStatus(Invoice::STATUS_PAID);
|
||||||
|
$sut->setStatus([Invoice::STATUS_PENDING]);
|
||||||
|
self::assertTrue($sut->hasStatus());
|
||||||
|
self::assertEquals([Invoice::STATUS_PAID, Invoice::STATUS_PENDING], $sut->getStatus());
|
||||||
|
|
||||||
|
$this->assertResetByFormError(new InvoiceArchiveQuery(), 'date', BaseQuery::ORDER_DESC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ class TimesheetQueryTest extends BaseQueryTest
|
|||||||
$this->assertPageSize($sut);
|
$this->assertPageSize($sut);
|
||||||
$this->assertOrderBy($sut, 'begin');
|
$this->assertOrderBy($sut, 'begin');
|
||||||
$this->assertOrder($sut, TimesheetQuery::ORDER_DESC);
|
$this->assertOrder($sut, TimesheetQuery::ORDER_DESC);
|
||||||
|
$this->assertDateRangeTrait($sut);
|
||||||
|
|
||||||
$this->assertUser($sut);
|
$this->assertUser($sut);
|
||||||
$this->assertUsers($sut);
|
$this->assertUsers($sut);
|
||||||
|
|||||||
@@ -72,6 +72,10 @@
|
|||||||
<source>attachments</source>
|
<source>attachments</source>
|
||||||
<target>Dateien</target>
|
<target>Dateien</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="file">
|
||||||
|
<source>file</source>
|
||||||
|
<target>Datei</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="rates.empty">
|
<trans-unit id="rates.empty">
|
||||||
<source>rates.empty</source>
|
<source>rates.empty</source>
|
||||||
<target>Es wurden noch keine Gebühren hinterlegt.</target>
|
<target>Es wurden noch keine Gebühren hinterlegt.</target>
|
||||||
@@ -629,6 +633,10 @@
|
|||||||
<source>label.vat_id</source>
|
<source>label.vat_id</source>
|
||||||
<target>Umsatzsteuer-ID</target>
|
<target>Umsatzsteuer-ID</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="label.tax_rate">
|
||||||
|
<source>label.tax_rate</source>
|
||||||
|
<target>Steuersatz</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="label.contact">
|
<trans-unit id="label.contact">
|
||||||
<source>label.contact</source>
|
<source>label.contact</source>
|
||||||
<target>Kontakt</target>
|
<target>Kontakt</target>
|
||||||
|
|||||||
@@ -72,6 +72,10 @@
|
|||||||
<source>attachments</source>
|
<source>attachments</source>
|
||||||
<target>Files</target>
|
<target>Files</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="file">
|
||||||
|
<source>file</source>
|
||||||
|
<target>File</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="rates.empty">
|
<trans-unit id="rates.empty">
|
||||||
<source>rates.empty</source>
|
<source>rates.empty</source>
|
||||||
<target>No fees have been configured yet.</target>
|
<target>No fees have been configured yet.</target>
|
||||||
@@ -641,6 +645,10 @@
|
|||||||
<source>label.vat_id</source>
|
<source>label.vat_id</source>
|
||||||
<target>VAT-ID</target>
|
<target>VAT-ID</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="label.tax_rate">
|
||||||
|
<source>label.tax_rate</source>
|
||||||
|
<target>Tax rate</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="label.contact">
|
<trans-unit id="label.contact">
|
||||||
<source>label.contact</source>
|
<source>label.contact</source>
|
||||||
<target>Contact</target>
|
<target>Contact</target>
|
||||||
|
|||||||
Reference in New Issue
Block a user