feat: render additional info to teamer invoice

This commit is contained in:
Björn Fromme
2025-02-28 15:15:02 +01:00
parent 901b2c7104
commit cdd0666837
14 changed files with 233 additions and 13 deletions
+6 -1
View File
@@ -158,6 +158,11 @@ services:
$options:
project_dir: '%kernel.project_dir%'
App\Service\Pdf\InvoiceApprover:
arguments:
$options:
project_dir: '%kernel.project_dir%'
App\Service\Pdf\InvoiceRenderer:
arguments:
$options:
@@ -167,4 +172,4 @@ services:
arguments:
$defaults:
from: '%default_email_from%'
to: '%default_email_to%'
to: '%default_email_to%'
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250228133252 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE assignment ADD cost_center VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE destination ADD cost_unit VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE upload ADD approved_at DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', ADD approved_by VARCHAR(255) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE upload DROP approved_at, DROP approved_by');
$this->addSql('ALTER TABLE assignment DROP cost_center');
$this->addSql('ALTER TABLE destination DROP cost_unit');
}
}
+14 -1
View File
@@ -13,6 +13,7 @@ class Hotel
private ?string $phone = null;
private ?string $type = null;
private ?string $country = null;
private ?string $costUnit = null;
public function getId(): ?int
{
@@ -121,4 +122,16 @@ class Hotel
return $this;
}
}
public function getCostUnit(): ?string
{
return $this->costUnit;
}
public function setCostUnit(?string $costUnit): static
{
$this->costUnit = $costUnit;
return $this;
}
}
+2 -1
View File
@@ -260,6 +260,7 @@ class ResponseParser
->setStreet((string) $item->strasse)
->setPhone((string) $item->telefon)
->setType((string) $item->art)
->setCostUnit($item->kostenstelle)
;
$hotels[$busProId] = $hotel;
}
@@ -281,4 +282,4 @@ class ResponseParser
return $optionsResolver->resolve($options);
}
}
}
+1
View File
@@ -95,6 +95,7 @@ class BpnImportCommand extends Command
'date_to' => $dateTo->format('Y-m-d'),
'product' => $product,
'pickups' => json_encode($datePickups),
'cost_unit' => $hotel->getCostUnit(),
'country' => $hotel->getCountry(),
'updated_at' => (new \DateTimeImmutable())->format('Y-m-d H:i:s')
], [
@@ -4,11 +4,13 @@ namespace App\Controller\Administrative\Document;
use App\Controller\Traits\ReturnUrlTrait;
use App\Entity\Upload;
use App\Entity\User;
use App\Event\DocumentConfirmedEvent;
use App\Event\DocumentRejectedEvent;
use App\Form\DocumentCheckType;
use App\Htmx\HxRedirectResponse;
use App\Model\DocumentCheckDto;
use App\Service\Pdf\InvoiceApprover;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -29,6 +31,7 @@ class CheckController extends AbstractController
#[Target('disposition')]
private readonly WorkflowInterface $workflow,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly InvoiceApprover $invoiceApprover,
private readonly LoggerInterface $logger
) {
}
@@ -114,7 +117,18 @@ class CheckController extends AbstractController
private function confirmDocument(Upload $document, string $transition, string $status, DocumentCheckDto $formData): void
{
$document->setStatus($status);
/** @var User $user */
$user = $this->getUser();
$document
->setApprovedAt(new \DateTimeImmutable())
->setApprovedBy($user->getInitials())
->setStatus($status)
;
if (Upload::TYPE_INVOICE === $document->getType()) {
$this->invoiceApprover->render($document->getDisposition());
}
$this->addFlash('success', 'Das Dokument wurde bestätigt');
$this->logger->info('Confirm document', [
'document_id' => $document->getId(),
+16
View File
@@ -43,6 +43,10 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
#[Assert\NotNull(message: 'Bitte gib die Destination an')]
private ?Destination $destination = null;
#[ORM\Column(nullable: true)]
#[Assert\NotBlank(message: 'Bitte gib den Konstenträger an')]
private ?string $costCenter = null;
#[ORM\ManyToOne]
#[Assert\NotNull(message: 'Bitte gib das Jobprofil an')]
private ?JobProfile $jobProfile = null;
@@ -194,6 +198,18 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
return $this;
}
public function getCostCenter(): ?string
{
return $this->costCenter;
}
public function setCostCenter(?string $costCenter): static
{
$this->costCenter = $costCenter;
return $this;
}
public function getJobProfile(): ?JobProfile
{
return $this->jobProfile;
+15
View File
@@ -47,6 +47,9 @@ class Destination implements TimestampableEntityInterface, SoftDeletableEntityIn
#[ORM\Column]
private ?int $hotelBusProId = null;
#[ORM\Column(nullable: true)]
private ?string $costUnit = null;
#[ORM\Column]
private array $pickups = [];
@@ -163,6 +166,18 @@ class Destination implements TimestampableEntityInterface, SoftDeletableEntityIn
return $this;
}
public function getCostUnit(): ?string
{
return $this->costUnit;
}
public function setCostUnit(?string $costUnit): static
{
$this->costUnit = $costUnit;
return $this;
}
public function getPickups(): array
{
return $this->pickups;
+30
View File
@@ -72,6 +72,12 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface
#[ORM\Column(nullable: true)]
private ?string $downloadedBy = null;
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $approvedAt = null;
#[ORM\Column(nullable: true)]
private ?string $approvedBy = null;
public function __construct()
{
$this->uuid = Uuid::v4();
@@ -284,4 +290,28 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface
return $this;
}
public function getApprovedAt(): ?\DateTimeImmutable
{
return $this->approvedAt;
}
public function setApprovedAt(?\DateTimeImmutable $approvedAt): static
{
$this->approvedAt = $approvedAt;
return $this;
}
public function getApprovedBy(): ?string
{
return $this->approvedBy;
}
public function setApprovedBy(?string $approvedBy): static
{
$this->approvedBy = $approvedBy;
return $this;
}
}
+5 -1
View File
@@ -17,6 +17,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
@@ -57,6 +58,9 @@ class AssignmentType extends AbstractType
'hx-swap' => 'innerHTML',
],
])
->add('costCenter', TextType::class, [
'label' => 'Kostenträger',
])
->add('jobProfile', EntityType::class, [
'label' => 'Jobprofil',
'class' => JobProfile::class,
@@ -204,4 +208,4 @@ class AssignmentType extends AbstractType
'anti_xss' => true,
]);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace App\Service\Pdf;
use App\Entity\Disposition;
use App\Entity\Upload;
use Symfony\Component\Filesystem\Filesystem;
class InvoiceApprover extends AbstractPdfRenderer
{
public function render(Disposition $disposition): Pdf
{
$teamer = $disposition->getTeamer();
$invoice = $disposition->getDocumentByType(Upload::TYPE_INVOICE);
$originalPdfFilename = $invoice->getFilename();
$pdf = new Pdf();
$pdf->SetAuthor($teamer->getFullName(), true);
$pdf->SetCreator($teamer->getFullName(), true);
// Load template
$filepath = sprintf('%s/uploads/invoice/%s', $this->config['project_dir'], $originalPdfFilename);
$pdf->appendPdf($filepath);
$pdf->setPage(1);
$pdf->SetFont('Arial', '', 11);
$pdf->SetTextColor(0, 128, 0);
$pdf->Text(116.5,84, utf8_decode('geprüft: '.$invoice->getApprovedBy()));
$this->createBackup($filepath);
$pdf->Output('F', $filepath);
return $pdf;
}
private function createBackup(string $source): void
{
$filesystem = new Filesystem();
if (!$filesystem->exists($source)) {
return;
}
$path = pathinfo($source, PATHINFO_DIRNAME);
$filename = pathinfo($source, PATHINFO_FILENAME);
$extension = pathinfo($source, PATHINFO_EXTENSION);
$target = sprintf('%s/%s.bak.%s', $path, $filename, $extension);
$filesystem->copy($source, $target);
}
}
+13
View File
@@ -3,6 +3,7 @@
namespace App\Service\Pdf;
use App\Entity\Disposition;
use App\Entity\Upload;
class InvoiceRenderer extends AbstractPdfRenderer
{
@@ -35,6 +36,18 @@ class InvoiceRenderer extends AbstractPdfRenderer
// Tax ID
$pdf->Text(144,62, utf8_decode($teamer->getTaxId()));
// Cost unit
if (null !== $costUnit = $assignment->getDestination()->getCostUnit()) {
$pdf->SetFont('Arial', '', 11);
$pdf->Text(116.5,74, utf8_decode('Kostenstelle: '.$costUnit));
}
// Cost center
if (null !== $costCenter = $assignment->getCostCenter()) {
$pdf->SetFont('Arial', '', 11);
$pdf->Text(116.5,79, utf8_decode('Kostenträger: '.$costCenter));
}
// Invoice number
$pdf->SetFont('Arial', '', 12);
$number = $assignment->getEffectivePeriod()->start->format('d/m/Y');
@@ -23,18 +23,19 @@
{{ form_row(form.jobProfile) }}
{{ form_row(form.jobProfileInfo) }}
{{ form_row(form.benefits) }}
{{ form_row(form.contact) }}
</div>
<div class="relative">
<div class="grid lg:grid-cols-2 gap-x-8 gap-y-4" id="pickup-form">
<div class="relative" id="pickup-form">
{% block partial_form %}
{{ form_row(form.pickup) }}
{% endblock %}
</div>
</div>
<div class="grid lg:grid-cols-2 gap-x-8 gap-y-4">
<div class="grid lg:grid-cols-2 gap-x-8 gap-y-4 items-start">
{{ form_row(form.fees) }}
{{ form_row(form.remarks) }}
<div class="grid grid-cols-1 gap-y-4">
{{ form_row(form.contact) }}
{{ form_row(form.remarks) }}
{{ form_row(form.costCenter) }}
</div>
</div>
</div>
<div class="flex items-center space-x-2">
@@ -46,4 +47,4 @@
</a>
</div>
{{ form_rest(form) }}
{{ form_end(form) }}
{{ form_end(form) }}
+19 -1
View File
@@ -11,7 +11,9 @@ use App\Entity\Embeddable\BankAccount;
use App\Entity\Embeddable\Communication;
use App\Entity\JobProfile;
use App\Entity\Teamer;
use App\Entity\Upload;
use App\Entity\User;
use App\Service\Pdf\InvoiceApprover;
use App\Service\Pdf\InvoiceRenderer;
use App\Service\Pdf\Pdf;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
@@ -64,6 +66,7 @@ class InvoiceRendererTest extends WebTestCase
->setCode('ABC123')
->setDateFrom(new \DateTimeImmutable('2023-12-01'))
->setDateTo(new \DateTimeImmutable('2023-12-08'))
->setCostUnit('FOO123')
;
$jobProfile = new JobProfile();
@@ -81,6 +84,7 @@ class InvoiceRendererTest extends WebTestCase
->setDestination($destination)
->setJobProfile($jobProfile)
->setContact($contact)
->setCostCenter('BAR456')
;
$application = new Application($assignment, $teamer);
@@ -89,6 +93,20 @@ class InvoiceRendererTest extends WebTestCase
$pdf = $renderer->render($disposition);
$this->assertInstanceOf(Pdf::class, $pdf);
$pdf->Output('F', $projectDir.'/temp/Honorarnote.pdf');
$pdf->Output('F', $projectDir.'/uploads/invoice/Honorarnote.pdf');
$invoice = new Upload();
$invoice
->setType(Upload::TYPE_INVOICE)
->setFilename('Honorarnote.pdf')
->setApprovedBy('FOO')
->setApprovedAt(new \DateTimeImmutable())
->setStatus(Upload::STATUS_PAID)
;
$disposition->addDocument($invoice);
$approver = new InvoiceApprover($translator, ['project_dir' => $projectDir]);
$pdf = $approver->render($disposition);
$this->assertInstanceOf(Pdf::class, $pdf);
}
}