diff --git a/config/services.yaml b/config/services.yaml index 457e467..66433a9 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -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%' \ No newline at end of file + to: '%default_email_to%' diff --git a/migrations/Version20250228133252.php b/migrations/Version20250228133252.php new file mode 100644 index 0000000..22c7871 --- /dev/null +++ b/migrations/Version20250228133252.php @@ -0,0 +1,35 @@ +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'); + } +} diff --git a/src/BusProNet/Model/Hotel.php b/src/BusProNet/Model/Hotel.php index 3b5e087..79e131e 100644 --- a/src/BusProNet/Model/Hotel.php +++ b/src/BusProNet/Model/Hotel.php @@ -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; } -} \ No newline at end of file + + public function getCostUnit(): ?string + { + return $this->costUnit; + } + + public function setCostUnit(?string $costUnit): static + { + $this->costUnit = $costUnit; + + return $this; + } +} diff --git a/src/BusProNet/ResponseParser.php b/src/BusProNet/ResponseParser.php index 54b6840..bd98019 100644 --- a/src/BusProNet/ResponseParser.php +++ b/src/BusProNet/ResponseParser.php @@ -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); } -} \ No newline at end of file +} diff --git a/src/Command/BpnImportCommand.php b/src/Command/BpnImportCommand.php index bd42ee3..a5e503f 100644 --- a/src/Command/BpnImportCommand.php +++ b/src/Command/BpnImportCommand.php @@ -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') ], [ diff --git a/src/Controller/Administrative/Document/CheckController.php b/src/Controller/Administrative/Document/CheckController.php index aa21b8d..a5da8fa 100644 --- a/src/Controller/Administrative/Document/CheckController.php +++ b/src/Controller/Administrative/Document/CheckController.php @@ -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(), diff --git a/src/Entity/Assignment.php b/src/Entity/Assignment.php index 398516e..66733f5 100644 --- a/src/Entity/Assignment.php +++ b/src/Entity/Assignment.php @@ -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; diff --git a/src/Entity/Destination.php b/src/Entity/Destination.php index e0cab10..1dbe37e 100644 --- a/src/Entity/Destination.php +++ b/src/Entity/Destination.php @@ -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; diff --git a/src/Entity/Upload.php b/src/Entity/Upload.php index 95fd0f7..6e09a7a 100644 --- a/src/Entity/Upload.php +++ b/src/Entity/Upload.php @@ -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; + } } diff --git a/src/Form/AssignmentType.php b/src/Form/AssignmentType.php index f7a95fd..82017bb 100644 --- a/src/Form/AssignmentType.php +++ b/src/Form/AssignmentType.php @@ -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, ]); } -} \ No newline at end of file +} diff --git a/src/Service/Pdf/InvoiceApprover.php b/src/Service/Pdf/InvoiceApprover.php new file mode 100644 index 0000000..81c704c --- /dev/null +++ b/src/Service/Pdf/InvoiceApprover.php @@ -0,0 +1,54 @@ +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); + } +} diff --git a/src/Service/Pdf/InvoiceRenderer.php b/src/Service/Pdf/InvoiceRenderer.php index fb712aa..0d465ab 100644 --- a/src/Service/Pdf/InvoiceRenderer.php +++ b/src/Service/Pdf/InvoiceRenderer.php @@ -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'); diff --git a/templates/administrative/assignment/_form.html.twig b/templates/administrative/assignment/_form.html.twig index c0a695e..9874d80 100644 --- a/templates/administrative/assignment/_form.html.twig +++ b/templates/administrative/assignment/_form.html.twig @@ -23,18 +23,19 @@ {{ form_row(form.jobProfile) }} {{ form_row(form.jobProfileInfo) }} {{ form_row(form.benefits) }} - {{ form_row(form.contact) }} - -