diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index 8d939b6a..1b28a36b 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -17,6 +17,7 @@ use App\Export\Spreadsheet\AnnotatedObjectExporter; use App\Export\Spreadsheet\Writer\BinaryFileResponseWriter; use App\Export\Spreadsheet\Writer\XlsxWriter; use App\Form\InvoiceDocumentUploadForm; +use App\Form\InvoicePaymentDateForm; use App\Form\InvoiceTemplateForm; use App\Form\Toolbar\InvoiceArchiveForm; use App\Form\Toolbar\InvoiceToolbarForm; @@ -228,10 +229,22 @@ final class InvoiceController extends AbstractController } /** - * @Route(path="/change-status/{id}/{status}", name="admin_invoice_status", methods={"GET"}) + * @Route(path="/change-status/{id}/{status}", name="admin_invoice_status", methods={"GET", "POST"}) */ - public function changeStatusAction(Invoice $invoice, string $status): Response + public function changeStatusAction(Invoice $invoice, string $status, Request $request): Response { + if ($status === Invoice::STATUS_PAID) { + $form = $this->createPaymentDateForm($invoice, $request); + $form->handleRequest($request); + + if (!$form->isSubmitted() || !$form->isValid()) { + return $this->render('invoice/payment_date_edit.html.twig', [ + 'invoice' => $invoice, + 'form' => $form->createView() + ]); + } + } + try { $this->service->changeInvoiceStatus($invoice, $status); $this->flashSuccess('action.update.success'); @@ -512,4 +525,17 @@ final class InvoiceController extends AbstractController 'method' => 'POST' ]); } + + private function createPaymentDateForm(Invoice $invoice, Request $request): FormInterface + { + if (null === $invoice->getPaymentDate()) { + $invoice->setPaymentDate($this->getDateTimeFactory()->createDateTime()); + } + + return $this->createForm(InvoicePaymentDateForm::class, $invoice, [ + 'action' => $request->getUri(), + 'method' => 'POST', + 'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(), + ]); + } } diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index 21115ee4..8e9fdd37 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -26,10 +26,11 @@ use Symfony\Component\Validator\Constraints as Assert; * @UniqueEntity("invoiceNumber") * @UniqueEntity("invoiceFilename") * - * @Exporter\Order({"id", "createdAt", "invoiceNumber", "status", "customer", "total", "tax", "currency", "vat", "dueDays", "dueDate", "user", "invoiceFilename"}) + * @Exporter\Order({"id", "createdAt", "invoiceNumber", "status", "customer", "subtotal", "total", "tax", "currency", "vat", "dueDays", "dueDate", "paymentDate", "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()") + * @Exporter\Expose("paymentDate", label="invoice.payment_date", type="date", exp="object.getPaymentDate() === null ? null : object.getPaymentDate()") */ class Invoice { @@ -172,6 +173,13 @@ class Invoice */ private $localized = false; + /** + * @var \DateTime + * + * @ORM\Column(name="payment_date", type="date", nullable=true) + */ + private $paymentDate; + public function getId(): ?int { return $this->id; @@ -265,6 +273,7 @@ class Invoice public function setIsNew(): Invoice { + $this->setPaymentDate(null); $this->status = self::STATUS_NEW; return $this; @@ -277,6 +286,7 @@ class Invoice public function setIsPending(): Invoice { + $this->setPaymentDate(null); $this->status = self::STATUS_PENDING; return $this; @@ -318,4 +328,25 @@ class Invoice { return $this->invoiceFilename; } + + /** + * @Exporter\Expose(label="invoice.subtotal", type="float", name="subtotal") + * @return float|null + */ + public function getSubtotal(): ?float + { + return $this->total - $this->tax; + } + + public function getPaymentDate(): ?\DateTime + { + return $this->paymentDate; + } + + public function setPaymentDate(?\DateTime $paymentDate): Invoice + { + $this->paymentDate = $paymentDate; + + return $this; + } } diff --git a/src/EventSubscriber/Actions/InvoiceSubscriber.php b/src/EventSubscriber/Actions/InvoiceSubscriber.php index b48d9909..8374b3b9 100644 --- a/src/EventSubscriber/Actions/InvoiceSubscriber.php +++ b/src/EventSubscriber/Actions/InvoiceSubscriber.php @@ -30,10 +30,10 @@ class InvoiceSubscriber extends AbstractActionsSubscriber return; } - if ($invoice->isNew()) { + if ($invoice->isNew() || $invoice->isPaid()) { $event->addAction('invoice.pending', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'pending'])]); } elseif ($invoice->isPending()) { - $event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid'])]); + $event->addAction('invoice.paid', ['url' => $this->path('admin_invoice_status', ['id' => $invoice->getId(), 'status' => 'paid']), 'class' => 'modal-ajax-form']); } $event->addAction('download', ['url' => $this->path('admin_invoice_download', ['id' => $invoice->getId()]), 'target' => '_blank']); diff --git a/src/Form/InvoicePaymentDateForm.php b/src/Form/InvoicePaymentDateForm.php new file mode 100644 index 00000000..baac96d4 --- /dev/null +++ b/src/Form/InvoicePaymentDateForm.php @@ -0,0 +1,50 @@ + $options['timezone'], + 'view_timezone' => $options['timezone'], + ]; + + $builder + ->add('paymentDate', DatePickerType::class, array_merge($dateTimeOptions, [ + 'label' => 'invoice.payment_date', + 'required' => true, + ])); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Invoice::class, + 'timezone' => date_default_timezone_get(), + 'attr' => [ + 'data-form-event' => 'kimai.invoiceUpdate' + ], + ]); + } +} diff --git a/src/Migrations/Version20210320162820.php b/src/Migrations/Version20210320162820.php new file mode 100644 index 00000000..67cf124c --- /dev/null +++ b/src/Migrations/Version20210320162820.php @@ -0,0 +1,40 @@ +getTable('kimai2_invoices'); + $invoices->addColumn('payment_date', 'date', ['default' => null, 'notnull' => false]); + } + + public function down(Schema $schema): void + { + $invoices = $schema->getTable('kimai2_invoices'); + $invoices->dropColumn('payment_date'); + } +} diff --git a/templates/invoice/listing.html.twig b/templates/invoice/listing.html.twig index 13ba6a76..4504b8ba 100644 --- a/templates/invoice/listing.html.twig +++ b/templates/invoice/listing.html.twig @@ -11,7 +11,9 @@ 'customer': {'class': 'hidden-xs hidden-sm text-nowrap', 'orderBy': false}, 'invoice_number': {'class': 'hidden-xs hidden-sm text-center w-min', 'title': 'invoice.number'|trans, 'orderBy': false}, 'due_date': {'class': 'hidden-xs text-center w-min', 'title': 'invoice.due_days'|trans, 'orderBy': false}, + 'payment_date': {'class': 'hidden-xs hidden text-center w-min', 'title': 'invoice.payment_date'|trans, 'orderBy': false}, 'status': {'class': 'text-center alwaysVisible w-min', 'orderBy': false}, + 'subtotal': {'class': 'hidden-xs text-center w-min hidden', 'title': 'invoice.subtotal'|trans, 'orderBy': false}, 'tax': {'class': 'hidden-xs text-center w-min hidden', 'title': 'invoice.tax'|trans, 'orderBy': false}, 'total_rate': {'class': 'hidden-xs text-right w-min', 'orderBy': false}, 'actions': {'class': 'actions alwaysVisible', 'orderBy': false}, @@ -40,7 +42,15 @@