support custom fields for invoices (#3077)
This commit is contained in:
@@ -13,7 +13,10 @@ use App\Configuration\SystemConfiguration;
|
|||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\Invoice;
|
use App\Entity\Invoice;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
|
use App\Entity\MetaTableTypeInterface;
|
||||||
use App\Event\InvoiceDocumentsEvent;
|
use App\Event\InvoiceDocumentsEvent;
|
||||||
|
use App\Event\InvoiceMetaDefinitionEvent;
|
||||||
|
use App\Event\InvoiceMetaDisplayEvent;
|
||||||
use App\Export\Spreadsheet\AnnotatedObjectExporter;
|
use App\Export\Spreadsheet\AnnotatedObjectExporter;
|
||||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||||
@@ -347,6 +350,7 @@ final class InvoiceController extends AbstractController
|
|||||||
'query' => $query,
|
'query' => $query,
|
||||||
'toolbarForm' => $form->createView(),
|
'toolbarForm' => $form->createView(),
|
||||||
'download' => $invoice,
|
'download' => $invoice,
|
||||||
|
'metaColumns' => $this->findMetaColumns($query),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,8 +699,23 @@ final class InvoiceController extends AbstractController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param InvoiceArchiveQuery $query
|
||||||
|
* @return MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
private function findMetaColumns(InvoiceArchiveQuery $query): array
|
||||||
|
{
|
||||||
|
$event = new InvoiceMetaDisplayEvent($query, InvoiceMetaDisplayEvent::INVOICE);
|
||||||
|
$this->dispatcher->dispatch($event);
|
||||||
|
|
||||||
|
return $event->getFields();
|
||||||
|
}
|
||||||
|
|
||||||
private function createInvoiceEditForm(Invoice $invoice): FormInterface
|
private function createInvoiceEditForm(Invoice $invoice): FormInterface
|
||||||
{
|
{
|
||||||
|
$event = new InvoiceMetaDefinitionEvent($invoice);
|
||||||
|
$this->dispatcher->dispatch($event);
|
||||||
|
|
||||||
return $this->createForm(InvoiceEditForm::class, $invoice, [
|
return $this->createForm(InvoiceEditForm::class, $invoice, [
|
||||||
'action' => $this->generateUrl('admin_invoice_edit', ['id' => $invoice->getId()]),
|
'action' => $this->generateUrl('admin_invoice_edit', ['id' => $invoice->getId()]),
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use App\Export\Annotation as Exporter;
|
use App\Export\Annotation as Exporter;
|
||||||
use App\Invoice\InvoiceModel;
|
use App\Invoice\InvoiceModel;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use JMS\Serializer\Annotation as Serializer;
|
use JMS\Serializer\Annotation as Serializer;
|
||||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||||
@@ -34,7 +36,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||||||
* @Exporter\Expose("user", label="label.username", type="string", exp="object.getUser() === null ? null : object.getUser().getDisplayName()")
|
* @Exporter\Expose("user", label="label.username", type="string", exp="object.getUser() === null ? null : object.getUser().getDisplayName()")
|
||||||
* @Exporter\Expose("paymentDate", label="invoice.payment_date", type="date", exp="object.getPaymentDate() === null ? null : object.getPaymentDate()")
|
* @Exporter\Expose("paymentDate", label="invoice.payment_date", type="date", exp="object.getPaymentDate() === null ? null : object.getPaymentDate()")
|
||||||
*/
|
*/
|
||||||
class Invoice
|
class Invoice implements EntityWithMetaFields
|
||||||
{
|
{
|
||||||
public const STATUS_PENDING = 'pending';
|
public const STATUS_PENDING = 'pending';
|
||||||
public const STATUS_PAID = 'paid';
|
public const STATUS_PAID = 'paid';
|
||||||
@@ -49,7 +51,6 @@ class Invoice
|
|||||||
* @ORM\Column(name="id", type="integer")
|
* @ORM\Column(name="id", type="integer")
|
||||||
* @ORM\Id
|
* @ORM\Id
|
||||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||||
* @phpstan-ignore-next-line
|
|
||||||
*/
|
*/
|
||||||
private $id;
|
private $id;
|
||||||
|
|
||||||
@@ -196,6 +197,28 @@ class Invoice
|
|||||||
*/
|
*/
|
||||||
private $paymentDate;
|
private $paymentDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meta fields
|
||||||
|
*
|
||||||
|
* All visible meta (custom) fields registered with this invoice
|
||||||
|
*
|
||||||
|
* @var InvoiceMeta[]|Collection
|
||||||
|
*
|
||||||
|
* @Serializer\Expose()
|
||||||
|
* @Serializer\Groups({"Invoice"})
|
||||||
|
* @Serializer\Type(name="array<App\Entity\InvoiceMeta>")
|
||||||
|
* @Serializer\SerializedName("metaFields")
|
||||||
|
* @Serializer\Accessor(getter="getVisibleMetaFields")
|
||||||
|
*
|
||||||
|
* @ORM\OneToMany(targetEntity="App\Entity\InvoiceMeta", mappedBy="invoice", cascade={"persist"})
|
||||||
|
*/
|
||||||
|
private $meta;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->meta = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
@@ -399,4 +422,68 @@ class Invoice
|
|||||||
{
|
{
|
||||||
return $this->comment;
|
return $this->comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection|MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getMetaFields(): Collection
|
||||||
|
{
|
||||||
|
return $this->meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return MetaTableTypeInterface[]
|
||||||
|
*/
|
||||||
|
public function getVisibleMetaFields(): array
|
||||||
|
{
|
||||||
|
$all = [];
|
||||||
|
foreach ($this->meta as $meta) {
|
||||||
|
if ($meta->isVisible()) {
|
||||||
|
$all[] = $meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $all;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetaField(string $name): ?MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
foreach ($this->meta as $field) {
|
||||||
|
if (strtolower($field->getName()) === strtolower($name)) {
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
if (null === ($current = $this->getMetaField($meta->getName()))) {
|
||||||
|
$meta->setEntity($this);
|
||||||
|
$this->meta->add($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current->merge($meta);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __clone()
|
||||||
|
{
|
||||||
|
if ($this->id) {
|
||||||
|
$this->id = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentMeta = $this->meta;
|
||||||
|
$this->meta = new ArrayCollection();
|
||||||
|
/** @var InvoiceMeta $meta */
|
||||||
|
foreach ($currentMeta as $meta) {
|
||||||
|
$newMeta = clone $meta;
|
||||||
|
$newMeta->setEntity($this);
|
||||||
|
$this->setMetaField($newMeta);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/Entity/InvoiceMeta.php
Normal file
59
src/Entity/InvoiceMeta.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use JMS\Serializer\Annotation as Serializer;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ORM\Entity()
|
||||||
|
* @ORM\Table(name="kimai2_invoices_meta",
|
||||||
|
* uniqueConstraints={
|
||||||
|
* @ORM\UniqueConstraint(columns={"invoice_id", "name"})
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* @Serializer\ExclusionPolicy("all")
|
||||||
|
*/
|
||||||
|
class InvoiceMeta implements MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
use MetaTableTypeTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Invoice
|
||||||
|
*
|
||||||
|
* @ORM\ManyToOne(targetEntity="App\Entity\Invoice", inversedBy="meta")
|
||||||
|
* @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
|
||||||
|
* @Assert\NotNull()
|
||||||
|
*/
|
||||||
|
private $invoice;
|
||||||
|
|
||||||
|
public function setEntity(EntityWithMetaFields $entity): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
if (!($entity instanceof Invoice)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
sprintf('Expected instanceof Invoice, received "%s"', \get_class($entity))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$this->invoice = $entity;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Invoice|null
|
||||||
|
*/
|
||||||
|
public function getEntity(): ?EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return $this->invoice;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/Event/InvoiceMetaDefinitionEvent.php
Normal file
34
src/Event/InvoiceMetaDefinitionEvent.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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\Event;
|
||||||
|
|
||||||
|
use App\Entity\Invoice;
|
||||||
|
use Symfony\Contracts\EventDispatcher\Event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event can be used, to dynamically add meta fields to invoices
|
||||||
|
*/
|
||||||
|
final class InvoiceMetaDefinitionEvent extends Event
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Invoice
|
||||||
|
*/
|
||||||
|
private $entity;
|
||||||
|
|
||||||
|
public function __construct(Invoice $entity)
|
||||||
|
{
|
||||||
|
$this->entity = $entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEntity(): Invoice
|
||||||
|
{
|
||||||
|
return $this->entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Event/InvoiceMetaDisplayEvent.php
Normal file
27
src/Event/InvoiceMetaDisplayEvent.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?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\Event;
|
||||||
|
|
||||||
|
use App\Repository\Query\InvoiceArchiveQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically find possible meta fields for an invoice-archive query.
|
||||||
|
*
|
||||||
|
* @method InvoiceArchiveQuery getQuery()
|
||||||
|
*/
|
||||||
|
final class InvoiceMetaDisplayEvent extends AbstractMetaDisplayEvent
|
||||||
|
{
|
||||||
|
public const INVOICE = 'invoice';
|
||||||
|
|
||||||
|
public function __construct(InvoiceArchiveQuery $query, string $location)
|
||||||
|
{
|
||||||
|
parent::__construct($query, $location);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,9 @@ namespace App\Export;
|
|||||||
use App\Entity\MetaTableTypeInterface;
|
use App\Entity\MetaTableTypeInterface;
|
||||||
use App\Invoice\InvoiceItemInterface;
|
use App\Invoice\InvoiceItemInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be merged with InvoiceItemInterface in 2.0
|
||||||
|
*/
|
||||||
interface ExportItemInterface extends InvoiceItemInterface
|
interface ExportItemInterface extends InvoiceItemInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace App\Form;
|
|||||||
|
|
||||||
use App\Entity\Invoice;
|
use App\Entity\Invoice;
|
||||||
use App\Form\Type\DatePickerType;
|
use App\Form\Type\DatePickerType;
|
||||||
|
use App\Form\Type\MetaFieldsCollectionType;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||||
@@ -48,6 +49,8 @@ class InvoiceEditForm extends AbstractType
|
|||||||
'label' => 'invoice.payment_date',
|
'label' => 'invoice.payment_date',
|
||||||
'required' => false,
|
'required' => false,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
|
|
||||||
namespace App\Invoice;
|
namespace App\Invoice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be removed with 2.0, just here for BC compatibility.
|
||||||
|
*/
|
||||||
interface InvoiceItemWithAmountInterface
|
interface InvoiceItemWithAmountInterface
|
||||||
{
|
{
|
||||||
public function getAmount(): float;
|
public function getAmount(): float;
|
||||||
|
|||||||
42
src/Migrations/Version20220101204501.php
Normal file
42
src/Migrations/Version20220101204501.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 DoctrineMigrations;
|
||||||
|
|
||||||
|
use App\Doctrine\AbstractMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
|
||||||
|
final class Version20220101204501 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Creates the invoice-meta table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$table = $schema->createTable('kimai2_invoices_meta');
|
||||||
|
$table->addColumn('id', 'integer', ['autoincrement' => true, 'notnull' => true]);
|
||||||
|
$table->addColumn('invoice_id', 'integer', ['notnull' => true]);
|
||||||
|
$table->addColumn('name', 'string', ['notnull' => true, 'length' => 50]);
|
||||||
|
$table->addColumn('value', 'text', ['notnull' => false, 'length' => 65535]);
|
||||||
|
$table->addColumn('visible', 'boolean', ['notnull' => true, 'default' => false]);
|
||||||
|
$table->setPrimaryKey(['id']);
|
||||||
|
$table->addIndex(['invoice_id'], 'IDX_7EDC37D92989F1FD');
|
||||||
|
$table->addUniqueIndex(['invoice_id', 'name'], 'UNIQ_7EDC37D92989F1FD5E237E06');
|
||||||
|
$table->addForeignKeyConstraint('kimai2_invoices', ['invoice_id'], ['id'], ['onDelete' => 'CASCADE'], 'FK_7EDC37D92989F1FD');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$schema->dropTable('kimai2_invoices_meta');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,17 +39,25 @@ final class InvoiceIdLoader implements LoaderInterface
|
|||||||
$em = $this->entityManager;
|
$em = $this->entityManager;
|
||||||
|
|
||||||
$qb = $em->createQueryBuilder();
|
$qb = $em->createQueryBuilder();
|
||||||
$customer = $qb->select('PARTIAL i.{id}', 'customer')
|
$qb->select('PARTIAL i.{id}', 'customer')
|
||||||
->from(Invoice::class, 'i')
|
->from(Invoice::class, 'i')
|
||||||
->leftJoin('i.customer', 'customer')
|
->leftJoin('i.customer', 'customer')
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
$qb = $em->createQueryBuilder();
|
$qb = $em->createQueryBuilder();
|
||||||
$user = $qb->select('PARTIAL i.{id}', 'user')
|
$qb->select('PARTIAL i.{id}', 'user')
|
||||||
->from(Invoice::class, 'i')
|
->from(Invoice::class, 'i')
|
||||||
->leftJoin('i.user', 'user')
|
->leftJoin('i.user', 'user')
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
|
$qb = $em->createQueryBuilder();
|
||||||
|
$qb->select('PARTIAL i.{id}', 'meta')
|
||||||
|
->from(Invoice::class, 'i')
|
||||||
|
->leftJoin('i.meta', 'meta')
|
||||||
|
->andWhere($qb->expr()->in('i.id', $ids))
|
||||||
|
->getQuery()
|
||||||
|
->execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,13 @@
|
|||||||
'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false},
|
'user': {'class': 'hidden-xs hidden-sm text-nowrap hidden', 'orderBy': false},
|
||||||
'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
|
'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false},
|
||||||
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
|
'comment': {'class': 'hidden-xs hidden-sm', 'title': 'label.description'|trans},
|
||||||
|
} %}
|
||||||
|
{% for field in metaColumns %}
|
||||||
|
{% set columns = columns|merge({
|
||||||
|
('mf_' ~ field.name): {'title': field.label|trans, 'class': 'hidden-xs hidden-sm', 'orderBy': false}
|
||||||
|
}) %}
|
||||||
|
{% endfor %}
|
||||||
|
{% set columns = columns|merge({
|
||||||
'invoice_number': {'class': 'hidden-xs hidden-sm w-min', 'title': 'invoice.number'|trans, 'orderBy': false},
|
'invoice_number': {'class': 'hidden-xs hidden-sm w-min', 'title': 'invoice.number'|trans, 'orderBy': false},
|
||||||
'due_date': {'class': 'hidden-xs w-min', 'title': 'invoice.due_days'|trans, 'orderBy': false},
|
'due_date': {'class': 'hidden-xs w-min', 'title': 'invoice.due_days'|trans, 'orderBy': false},
|
||||||
'payment_date': {'class': 'hidden-xs hidden w-min', 'title': 'invoice.payment_date'|trans, 'orderBy': false},
|
'payment_date': {'class': 'hidden-xs hidden w-min', 'title': 'invoice.payment_date'|trans, 'orderBy': false},
|
||||||
@@ -18,7 +25,7 @@
|
|||||||
'tax': {'class': 'hidden-xs text-right w-min hidden', 'title': 'invoice.tax'|trans},
|
'tax': {'class': 'hidden-xs text-right w-min hidden', 'title': 'invoice.tax'|trans},
|
||||||
'total_rate': {'class': 'hidden-xs text-right w-min'},
|
'total_rate': {'class': 'hidden-xs text-right w-min'},
|
||||||
'actions': {'class': 'actions alwaysVisible', 'orderBy': false},
|
'actions': {'class': 'actions alwaysVisible', 'orderBy': false},
|
||||||
} %}
|
}) %}
|
||||||
|
|
||||||
{% set tableName = 'invoices' %}
|
{% set tableName = 'invoices' %}
|
||||||
|
|
||||||
@@ -42,6 +49,11 @@
|
|||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.user_avatar(entry.user) }} {{ widgets.username(entry.user) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.user_avatar(entry.user) }} {{ widgets.username(entry.user) }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.customer) }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
|
||||||
|
{% for field in metaColumns %}
|
||||||
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'mf_' ~ field.name) }}">
|
||||||
|
{{ tables.datatable_meta_column(entry, field) }}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'invoice_number') }}">{{ widgets.label(entry.invoiceNumber, 'default') }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'invoice_number') }}">{{ widgets.label(entry.invoiceNumber, 'default') }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'due_date') }}">{{ macros.invoice_due_date(entry) }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'due_date') }}">{{ macros.invoice_due_date(entry) }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'payment_date') }}">
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'payment_date') }}">
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
$this->assertIsRedirect($client);
|
$this->assertIsRedirect($client);
|
||||||
$this->assertRedirectUrl($client, '/invoice/show?id=', false);
|
$this->assertRedirectUrl($client, '/invoice/show?id=', false);
|
||||||
$client->followRedirect();
|
$client->followRedirect();
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
$this->assertDataTableRowCount($client, 'datatable_invoices', 1);
|
$this->assertDataTableRowCount($client, 'datatable_invoices', 1);
|
||||||
|
|
||||||
$em = $this->getEntityManager();
|
$em = $this->getEntityManager();
|
||||||
|
|||||||
41
tests/Entity/InvoiceMetaTest.php
Normal file
41
tests/Entity/InvoiceMetaTest.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?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\Entity;
|
||||||
|
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\EntityWithMetaFields;
|
||||||
|
use App\Entity\Invoice;
|
||||||
|
use App\Entity\InvoiceMeta;
|
||||||
|
use App\Entity\MetaTableTypeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Entity\InvoiceMeta
|
||||||
|
*/
|
||||||
|
class InvoiceMetaTest extends AbstractMetaEntityTest
|
||||||
|
{
|
||||||
|
protected function getEntity(): EntityWithMetaFields
|
||||||
|
{
|
||||||
|
return new Invoice();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMetaEntity(): MetaTableTypeInterface
|
||||||
|
{
|
||||||
|
return new InvoiceMeta();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetEntityThrowsException()
|
||||||
|
{
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('Expected instanceof Invoice, received "App\Entity\Customer"');
|
||||||
|
|
||||||
|
$sut = new InvoiceMeta();
|
||||||
|
$sut->setEntity(new Customer());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ use App\Entity\ActivityMeta;
|
|||||||
use App\Entity\Customer;
|
use App\Entity\Customer;
|
||||||
use App\Entity\CustomerMeta;
|
use App\Entity\CustomerMeta;
|
||||||
use App\Entity\Invoice;
|
use App\Entity\Invoice;
|
||||||
|
use App\Entity\InvoiceMeta;
|
||||||
use App\Entity\InvoiceTemplate;
|
use App\Entity\InvoiceTemplate;
|
||||||
use App\Entity\Project;
|
use App\Entity\Project;
|
||||||
use App\Entity\ProjectMeta;
|
use App\Entity\ProjectMeta;
|
||||||
@@ -221,4 +222,54 @@ class InvoiceTest extends TestCase
|
|||||||
|
|
||||||
return new DateNumberGenerator($repository);
|
return new DateNumberGenerator($repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testClone()
|
||||||
|
{
|
||||||
|
$sut = new Invoice();
|
||||||
|
$sut->setComment('foo kajsdhgf aksjdhfg');
|
||||||
|
$sut->setFilename('1234567890');
|
||||||
|
|
||||||
|
$meta = new InvoiceMeta();
|
||||||
|
$meta->setName('blabla');
|
||||||
|
$meta->setValue('1234567890');
|
||||||
|
$meta->setIsVisible(false);
|
||||||
|
$meta->setIsRequired(true);
|
||||||
|
$sut->setMetaField($meta);
|
||||||
|
|
||||||
|
$clone = clone $sut;
|
||||||
|
|
||||||
|
foreach ($sut->getMetaFields() as $metaField) {
|
||||||
|
$cloneMeta = $clone->getMetaField($metaField->getName());
|
||||||
|
self::assertEquals($cloneMeta->getValue(), $metaField->getValue());
|
||||||
|
}
|
||||||
|
self::assertEquals('1234567890', $clone->getInvoiceFilename());
|
||||||
|
self::assertEquals('foo kajsdhgf aksjdhfg', $clone->getComment());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMetaFields()
|
||||||
|
{
|
||||||
|
$sut = new Invoice();
|
||||||
|
$meta = new InvoiceMeta();
|
||||||
|
$meta->setName('foo')->setValue('bar')->setType('test');
|
||||||
|
self::assertInstanceOf(Invoice::class, $sut->setMetaField($meta));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test', $result->getType());
|
||||||
|
|
||||||
|
$meta2 = new InvoiceMeta();
|
||||||
|
$meta2->setName('foo')->setValue('bar')->setType('test2');
|
||||||
|
self::assertInstanceOf(Invoice::class, $sut->setMetaField($meta2));
|
||||||
|
self::assertEquals(1, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(0, $sut->getVisibleMetaFields());
|
||||||
|
|
||||||
|
$result = $sut->getMetaField('foo');
|
||||||
|
self::assertSame($result, $meta);
|
||||||
|
self::assertEquals('test2', $result->getType());
|
||||||
|
|
||||||
|
$sut->setMetaField((new InvoiceMeta())->setName('blub')->setIsVisible(true));
|
||||||
|
$sut->setMetaField((new InvoiceMeta())->setName('blab')->setIsVisible(true));
|
||||||
|
self::assertEquals(3, $sut->getMetaFields()->count());
|
||||||
|
self::assertCount(2, $sut->getVisibleMetaFields());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class InvoiceLoaderTest extends AbstractLoaderTest
|
|||||||
{
|
{
|
||||||
public function testLoadResults()
|
public function testLoadResults()
|
||||||
{
|
{
|
||||||
$em = $this->getEntityManagerMock(2);
|
$em = $this->getEntityManagerMock(3);
|
||||||
|
|
||||||
$sut = new InvoiceLoader($em);
|
$sut = new InvoiceLoader($em);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user