search and export from invoice archive (#2408)

This commit is contained in:
Kevin Papst
2021-03-10 01:50:27 +01:00
committed by GitHub
parent c34e9f576d
commit db1344573f
22 changed files with 544 additions and 72 deletions

View File

@@ -12,8 +12,12 @@ namespace App\Controller;
use App\Configuration\SystemConfiguration;
use App\Entity\Invoice;
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\InvoiceTemplateForm;
use App\Form\Toolbar\InvoiceArchiveForm;
use App\Form\Toolbar\InvoiceToolbarForm;
use App\Form\Toolbar\InvoiceToolbarSimpleForm;
use App\Invoice\ServiceInvoice;
@@ -21,6 +25,7 @@ use App\Repository\InvoiceDocumentRepository;
use App\Repository\InvoiceRepository;
use App\Repository\InvoiceTemplateRepository;
use App\Repository\Query\BaseQuery;
use App\Repository\Query\InvoiceArchiveQuery;
use App\Repository\Query\InvoiceQuery;
use Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
@@ -236,20 +241,48 @@ final class InvoiceController extends AbstractController
$invoice = $this->invoiceRepository->find($id);
}
$query = new InvoiceQuery();
$query->setOrderBy('date');
$query = new InvoiceArchiveQuery();
$query->setPage($page);
$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);
return $this->render('invoice/listing.html.twig', [
'entries' => $invoices,
'query' => $query,
'toolbarForm' => $form->createView(),
'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"})
* @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
{
if ($template->getId() === null) {

View File

@@ -9,6 +9,7 @@
namespace App\Entity;
use App\Export\Annotation as Exporter;
use App\Invoice\InvoiceModel;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
@@ -21,10 +22,14 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ORM\UniqueConstraint(columns={"invoice_filename"})
* }
* )
* @ORM\Entity(repositoryClass="App\Repository\InvoiceRepository")
* @UniqueEntity("invoiceNumber")
* @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
{
@@ -35,6 +40,8 @@ class Invoice
/**
* @var int|null
*
* @Exporter\Expose(label="label.id", type="integer")
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
@@ -44,6 +51,8 @@ class Invoice
/**
* @var string
*
* @Exporter\Expose(label="invoice.number", type="string")
*
* @ORM\Column(name="invoice_number", type="string", length=50, nullable=false)
* @Assert\NotNull()
*/
@@ -70,6 +79,8 @@ class Invoice
/**
* @var \DateTime
*
* @Exporter\Expose(label="label.date", type="datetime")
*
* @ORM\Column(name="created_at", type="datetime", nullable=false)
* @Assert\NotNull()
*/
@@ -85,6 +96,8 @@ class Invoice
/**
* @var float
*
* @Exporter\Expose(label="label.total_rate", type="float")
*
* @ORM\Column(name="total", type="float", nullable=false)
* @Assert\NotNull()
*/
@@ -93,6 +106,8 @@ class Invoice
/**
* @var float
*
* @Exporter\Expose(label="invoice.tax", type="float")
*
* @ORM\Column(name="tax", type="float", nullable=false)
* @Assert\NotNull()
*/
@@ -101,6 +116,8 @@ class Invoice
/**
* @var string
*
* @Exporter\Expose(label="label.currency", type="string")
*
* @ORM\Column(name="currency", type="string", length=3, nullable=false)
* @Assert\NotNull()
* @Assert\Length(max=3)
@@ -110,6 +127,8 @@ class Invoice
/**
* @var int
*
* @Exporter\Expose(label="label.due_days", type="integer")
*
* @ORM\Column(name="due_days", type="integer", length=3, nullable=false)
* @Assert\NotNull()
* @Assert\Range(min = 0, max = 999)
@@ -119,6 +138,8 @@ class Invoice
/**
* @var float
*
* @Exporter\Expose(label="label.tax_rate", type="float")
*
* @ORM\Column(name="vat", type="float", nullable=false)
* @Assert\NotNull()
* @Assert\Range(min = 0.0, max = 99.99)
@@ -128,6 +149,8 @@ class Invoice
/**
* @var string
*
* @Exporter\Expose(label="label.status", type="string")
*
* @ORM\Column(name="status", type="string", length=20, nullable=false)
* @Assert\NotNull()
*/
@@ -136,6 +159,8 @@ class Invoice
/**
* @var string
*
* @Exporter\Expose(label="file", type="string")
*
* @ORM\Column(name="invoice_filename", type="string", length=150, nullable=false)
* @Assert\NotNull()
* @Assert\Length(min=1, max=150, allowEmptyString=false)

View File

@@ -9,6 +9,7 @@
namespace App\EventSubscriber\Actions;
use App\Constants;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
@@ -33,4 +34,9 @@ abstract class AbstractActionsSubscriber implements EventSubscriberInterface
{
return $this->urlGenerator->generate($route, $parameters);
}
protected function documentationLink(string $url): string
{
return Constants::HOMEPAGE . '/documentation/' . $url;
}
}

View 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);
}
}

View File

@@ -88,8 +88,6 @@ class UserSubscriber extends AbstractActionsSubscriber
$actions['trash'] = ['url' => $this->path('admin_user_delete', ['id' => $user->getId()]), 'class' => 'modal-ajax-form'];
}
$payload['actions'] = array_merge($payload['actions'], $actions);
$event->setPayload($payload);
$event->setActions($actions);
}
}

View File

@@ -67,7 +67,7 @@ class InvoiceTemplateForm extends AbstractType
'label' => 'label.due_days',
])
->add('vat', NumberType::class, [
'label' => 'label.vat',
'label' => 'label.tax_rate',
'scale' => 2,
])
->add('renderer', InvoiceRendererType::class)

View 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(),
]);
}
}

View 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;
}
}

View File

@@ -151,7 +151,7 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
// for customer
case 'cc':
$partialResult = $this->repository->getCounterForAllTime($invoiceDate, $this->model->getCustomer()) + $increaseBy;
$partialResult = $this->repository->getCounterForAllTime($this->model->getCustomer()) + $increaseBy;
break;
case 'ccy':
@@ -168,7 +168,7 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
// across all invoices
case 'c':
$partialResult = $this->repository->getCounterForAllTime($invoiceDate) + $increaseBy;
$partialResult = $this->repository->getCounterForAllTime() + $increaseBy;
break;
case 'cy':

View File

@@ -16,7 +16,7 @@ use App\Entity\User;
use App\Repository\Loader\InvoiceLoader;
use App\Repository\Paginator\LoaderPaginator;
use App\Repository\Paginator\PaginatorInterface;
use App\Repository\Query\InvoiceQuery;
use App\Repository\Query\InvoiceArchiveQuery;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta;
@@ -91,7 +91,7 @@ class InvoiceRepository extends EntityRepository
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) {
return $this->count(['customer' => $customer]);
@@ -137,7 +137,7 @@ class InvoiceRepository extends EntityRepository
$qb->setParameter('teams', $ids);
}
private function getQueryBuilderForQuery(InvoiceQuery $query): QueryBuilder
private function getQueryBuilderForQuery(InvoiceArchiveQuery $query): QueryBuilder
{
$qb = $this->getEntityManager()->createQueryBuilder();
@@ -146,21 +146,79 @@ class InvoiceRepository extends EntityRepository
->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();
switch ($orderBy) {
case 'date':
$orderBy = 'i.createdAt';
break;
case 'customer':
$orderBy = 'i.customer';
break;
case 'total':
$orderBy = 'i.total';
break;
}
$qb->addOrderBy($orderBy, $query->getOrder());
$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;
}
public function countInvoicesForQuery(InvoiceQuery $query): int
public function countInvoicesForQuery(InvoiceArchiveQuery $query): int
{
$qb = $this->getQueryBuilderForQuery($query);
$qb
@@ -173,7 +231,20 @@ class InvoiceRepository extends EntityRepository
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);
$qb = $this->getQueryBuilderForQuery($query);
@@ -181,7 +252,7 @@ class InvoiceRepository extends EntityRepository
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->setMaxPerPage($query->getPageSize());

View 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;
}
}

View 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;
}
}
}

View File

@@ -11,6 +11,9 @@ namespace App\Repository\Query;
use App\Entity\InvoiceTemplate;
/**
* Find items (eg timesheets) for creating a new invoice.
*/
class InvoiceQuery extends TimesheetQuery
{
/**

View File

@@ -20,6 +20,7 @@ use App\Form\Model\DateRange;
class TimesheetQuery extends ActivityQuery implements BillableInterface
{
use BillableTrait;
use DateRangeTrait;
public const STATE_ALL = 1;
public const STATE_RUNNING = 2;
@@ -49,10 +50,6 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface
* @var \DateTime|null
*/
private $modifiedAfter;
/**
* @var DateRange
*/
protected $dateRange;
/**
* @var iterable
*/
@@ -230,42 +227,6 @@ class TimesheetQuery extends ActivityQuery implements BillableInterface
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
{
if (empty($this->tags)) {

View File

@@ -77,19 +77,8 @@
{% macro invoice_listing(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set 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) }}
{% set event = actions(app.user, 'invoice_details', {'view': view}) %}
{{ widgets.page_actions(event.actions) }}
{% endmacro %}
{% macro invoice_upload(view) %}

View File

@@ -21,6 +21,7 @@
{% block page_title %}{{ 'invoice.title'|trans }}{% endblock %}
{% block page_actions %}{{ actions.invoice_listing('index') }}{% endblock %}
{% block page_search %}{{ toolbar.dropDownSearch(toolbarForm) }}{% endblock %}
{% block main_before %}
{{ tables.data_table_column_modal(tableName, columns) }}

View File

@@ -8,7 +8,7 @@
'title': {'class': 'hidden-xs text-nowrap', 'orderBy': false},
'company': {'class': 'hidden-xs hidden-sm hidden', '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},
'address': {'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, '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') }}">{{ 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, 'address') }}">{{ entry.address|nl2br }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'contact') }}">{{ entry.contact|nl2br }}</td>

View File

@@ -13,6 +13,7 @@ use App\Entity\Activity;
use App\Entity\Customer;
use App\Entity\Project;
use App\Entity\Team;
use App\Form\Model\DateRange;
use App\Repository\Query\ActivityQuery;
use App\Repository\Query\BaseQuery;
use App\Repository\Query\ProjectQuery;
@@ -63,12 +64,12 @@ class BaseQueryTest extends TestCase
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->assertPageSize($sut);
$this->assertOrderBy($sut, $orderBy);
$this->assertOrder($sut);
$this->assertOrder($sut, $order);
$this->assertTeams($sut);
}
@@ -275,4 +276,29 @@ class BaseQueryTest extends TestCase
$this->assertEquals(99, $sut->getProject());
$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());
}
}

View 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);
}
}

View File

@@ -26,6 +26,7 @@ class TimesheetQueryTest extends BaseQueryTest
$this->assertPageSize($sut);
$this->assertOrderBy($sut, 'begin');
$this->assertOrder($sut, TimesheetQuery::ORDER_DESC);
$this->assertDateRangeTrait($sut);
$this->assertUser($sut);
$this->assertUsers($sut);

View File

@@ -72,6 +72,10 @@
<source>attachments</source>
<target>Dateien</target>
</trans-unit>
<trans-unit id="file">
<source>file</source>
<target>Datei</target>
</trans-unit>
<trans-unit id="rates.empty">
<source>rates.empty</source>
<target>Es wurden noch keine Gebühren hinterlegt.</target>
@@ -629,6 +633,10 @@
<source>label.vat_id</source>
<target>Umsatzsteuer-ID</target>
</trans-unit>
<trans-unit id="label.tax_rate">
<source>label.tax_rate</source>
<target>Steuersatz</target>
</trans-unit>
<trans-unit id="label.contact">
<source>label.contact</source>
<target>Kontakt</target>

View File

@@ -72,6 +72,10 @@
<source>attachments</source>
<target>Files</target>
</trans-unit>
<trans-unit id="file">
<source>file</source>
<target>File</target>
</trans-unit>
<trans-unit id="rates.empty">
<source>rates.empty</source>
<target>No fees have been configured yet.</target>
@@ -641,6 +645,10 @@
<source>label.vat_id</source>
<target>VAT-ID</target>
</trans-unit>
<trans-unit id="label.tax_rate">
<source>label.tax_rate</source>
<target>Tax rate</target>
</trans-unit>
<trans-unit id="label.contact">
<source>label.contact</source>
<target>Contact</target>