diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index 46ae33ff..f57a22b2 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -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) { diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index 79b1b1ba..21115ee4 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -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) diff --git a/src/EventSubscriber/Actions/AbstractActionsSubscriber.php b/src/EventSubscriber/Actions/AbstractActionsSubscriber.php index 80e7c98b..e6298e02 100644 --- a/src/EventSubscriber/Actions/AbstractActionsSubscriber.php +++ b/src/EventSubscriber/Actions/AbstractActionsSubscriber.php @@ -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; + } } diff --git a/src/EventSubscriber/Actions/InvoiceArchiveSubscriber.php b/src/EventSubscriber/Actions/InvoiceArchiveSubscriber.php new file mode 100644 index 00000000..6983bf5a --- /dev/null +++ b/src/EventSubscriber/Actions/InvoiceArchiveSubscriber.php @@ -0,0 +1,37 @@ + ['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); + } +} diff --git a/src/EventSubscriber/Actions/UserSubscriber.php b/src/EventSubscriber/Actions/UserSubscriber.php index 23acbccc..18dbc056 100644 --- a/src/EventSubscriber/Actions/UserSubscriber.php +++ b/src/EventSubscriber/Actions/UserSubscriber.php @@ -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); } } diff --git a/src/Form/InvoiceTemplateForm.php b/src/Form/InvoiceTemplateForm.php index aba49ca3..53505c68 100644 --- a/src/Form/InvoiceTemplateForm.php +++ b/src/Form/InvoiceTemplateForm.php @@ -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) diff --git a/src/Form/Toolbar/InvoiceArchiveForm.php b/src/Form/Toolbar/InvoiceArchiveForm.php new file mode 100644 index 00000000..e2ecdf4c --- /dev/null +++ b/src/Form/Toolbar/InvoiceArchiveForm.php @@ -0,0 +1,44 @@ +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(), + ]); + } +} diff --git a/src/Form/Type/InvoiceStatusType.php b/src/Form/Type/InvoiceStatusType.php new file mode 100644 index 00000000..c5a77134 --- /dev/null +++ b/src/Form/Type/InvoiceStatusType.php @@ -0,0 +1,45 @@ +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; + } +} diff --git a/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php b/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php index c1a12f3c..417a250f 100644 --- a/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php +++ b/src/Invoice/NumberGenerator/ConfigurableNumberGenerator.php @@ -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': diff --git a/src/Repository/InvoiceRepository.php b/src/Repository/InvoiceRepository.php index b79d4c4b..ae9f256e 100644 --- a/src/Repository/InvoiceRepository.php +++ b/src/Repository/InvoiceRepository.php @@ -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()); diff --git a/src/Repository/Query/DateRangeTrait.php b/src/Repository/Query/DateRangeTrait.php new file mode 100644 index 00000000..d58b5ad6 --- /dev/null +++ b/src/Repository/Query/DateRangeTrait.php @@ -0,0 +1,58 @@ +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; + } +} diff --git a/src/Repository/Query/InvoiceArchiveQuery.php b/src/Repository/Query/InvoiceArchiveQuery.php new file mode 100644 index 00000000..376c6f8f --- /dev/null +++ b/src/Repository/Query/InvoiceArchiveQuery.php @@ -0,0 +1,101 @@ +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; + } + } +} diff --git a/src/Repository/Query/InvoiceQuery.php b/src/Repository/Query/InvoiceQuery.php index 16a0099c..b57c55e2 100644 --- a/src/Repository/Query/InvoiceQuery.php +++ b/src/Repository/Query/InvoiceQuery.php @@ -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 { /** diff --git a/src/Repository/Query/TimesheetQuery.php b/src/Repository/Query/TimesheetQuery.php index 7073f1a1..2ed4ed66 100644 --- a/src/Repository/Query/TimesheetQuery.php +++ b/src/Repository/Query/TimesheetQuery.php @@ -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)) { diff --git a/templates/invoice/actions.html.twig b/templates/invoice/actions.html.twig index a12a3a3a..da140572 100644 --- a/templates/invoice/actions.html.twig +++ b/templates/invoice/actions.html.twig @@ -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) %} diff --git a/templates/invoice/listing.html.twig b/templates/invoice/listing.html.twig index 977a778c..2ae091c6 100644 --- a/templates/invoice/listing.html.twig +++ b/templates/invoice/listing.html.twig @@ -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) }} diff --git a/templates/invoice/templates.html.twig b/templates/invoice/templates.html.twig index 4bd0e1a5..08f2daea 100644 --- a/templates/invoice/templates.html.twig +++ b/templates/invoice/templates.html.twig @@ -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 @@ {{ entry.title }} {{ entry.company }} {{ entry.vatId }} - {{ entry.vat }} + {{ entry.vat }} {{ entry.dueDays }} {{ entry.address|nl2br }} {{ entry.contact|nl2br }} diff --git a/tests/Repository/Query/BaseQueryTest.php b/tests/Repository/Query/BaseQueryTest.php index a8de6998..2d300f51 100644 --- a/tests/Repository/Query/BaseQueryTest.php +++ b/tests/Repository/Query/BaseQueryTest.php @@ -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()); + } } diff --git a/tests/Repository/Query/InvoiceArchiveQueryTest.php b/tests/Repository/Query/InvoiceArchiveQueryTest.php new file mode 100644 index 00000000..f021ffc9 --- /dev/null +++ b/tests/Repository/Query/InvoiceArchiveQueryTest.php @@ -0,0 +1,45 @@ +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); + } +} diff --git a/tests/Repository/Query/TimesheetQueryTest.php b/tests/Repository/Query/TimesheetQueryTest.php index 963b850c..858a7079 100644 --- a/tests/Repository/Query/TimesheetQueryTest.php +++ b/tests/Repository/Query/TimesheetQueryTest.php @@ -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); diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index b51d99ab..938ac9b7 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -72,6 +72,10 @@ attachments Dateien + + file + Datei + rates.empty Es wurden noch keine Gebühren hinterlegt. @@ -629,6 +633,10 @@ label.vat_id Umsatzsteuer-ID + + label.tax_rate + Steuersatz + label.contact Kontakt diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 66f19fe3..c1c57049 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -72,6 +72,10 @@ attachments Files + + file + File + rates.empty No fees have been configured yet. @@ -641,6 +645,10 @@ label.vat_id VAT-ID + + label.tax_rate + Tax rate + label.contact Contact