feat: render additional info to teamer invoice
This commit is contained in:
@@ -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%'
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) }}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user