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\Invoice;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Event\InvoiceDocumentsEvent;
|
||||
use App\Event\InvoiceMetaDefinitionEvent;
|
||||
use App\Event\InvoiceMetaDisplayEvent;
|
||||
use App\Export\Spreadsheet\AnnotatedObjectExporter;
|
||||
use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter;
|
||||
use App\Export\Spreadsheet\Writer\XlsxWriter;
|
||||
@@ -347,6 +350,7 @@ final class InvoiceController extends AbstractController
|
||||
'query' => $query,
|
||||
'toolbarForm' => $form->createView(),
|
||||
'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
|
||||
{
|
||||
$event = new InvoiceMetaDefinitionEvent($invoice);
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
return $this->createForm(InvoiceEditForm::class, $invoice, [
|
||||
'action' => $this->generateUrl('admin_invoice_edit', ['id' => $invoice->getId()]),
|
||||
'method' => 'POST',
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace App\Entity;
|
||||
|
||||
use App\Export\Annotation as Exporter;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
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("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_PAID = 'paid';
|
||||
@@ -49,7 +51,6 @@ class Invoice
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
private $id;
|
||||
|
||||
@@ -196,6 +197,28 @@ class Invoice
|
||||
*/
|
||||
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
|
||||
{
|
||||
return $this->id;
|
||||
@@ -399,4 +422,68 @@ class Invoice
|
||||
{
|
||||
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\Invoice\InvoiceItemInterface;
|
||||
|
||||
/**
|
||||
* Will be merged with InvoiceItemInterface in 2.0
|
||||
*/
|
||||
interface ExportItemInterface extends InvoiceItemInterface
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Form;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use App\Form\Type\DatePickerType;
|
||||
use App\Form\Type\MetaFieldsCollectionType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
@@ -48,6 +49,8 @@ class InvoiceEditForm extends AbstractType
|
||||
'label' => 'invoice.payment_date',
|
||||
'required' => false,
|
||||
]));
|
||||
|
||||
$builder->add('metaFields', MetaFieldsCollectionType::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
namespace App\Invoice;
|
||||
|
||||
/**
|
||||
* Will be removed with 2.0, just here for BC compatibility.
|
||||
*/
|
||||
interface InvoiceItemWithAmountInterface
|
||||
{
|
||||
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;
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$customer = $qb->select('PARTIAL i.{id}', 'customer')
|
||||
$qb->select('PARTIAL i.{id}', 'customer')
|
||||
->from(Invoice::class, 'i')
|
||||
->leftJoin('i.customer', 'customer')
|
||||
->getQuery()
|
||||
->execute();
|
||||
|
||||
$qb = $em->createQueryBuilder();
|
||||
$user = $qb->select('PARTIAL i.{id}', 'user')
|
||||
$qb->select('PARTIAL i.{id}', 'user')
|
||||
->from(Invoice::class, 'i')
|
||||
->leftJoin('i.user', 'user')
|
||||
->getQuery()
|
||||
->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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user