event = $request->event; $this->costUnits = new CostUnitRepository(); } public function execute(): CreateIncomeSurplusStatementResponse { $response = new CreateIncomeSurplusStatementResponse(); $costUnit = $this->event->costUnit()->first(); if (!$costUnit instanceof CostUnit) { $response->message = 'Der Veranstaltung ist keine Kostenstelle zugeordnet.'; return $response; } // Der Pauschalbetrag wird vor dem Resource-Aufruf gelesen: `EventResource::calculateSupportPerPerson()` // multipliziert das Amount-Objekt von `support_per_person` in place. Auf `support_flat` wirkt das // zwar nicht, aber der gesamte Zugriff auf Beträge des Models ist danach nicht mehr vertrauenswürdig. $otherIncome = $this->event->support_flat->getAmount(); $eventData = $this->event->toResource()->toArray(new Request()); $income = $this->buildIncome($eventData, $otherIncome); $expenses = $this->buildExpenses($costUnit); $result = new Amount($income['total']->getAmount() - $expenses['total']->getAmount(), 'Euro'); $html = view('pdfs.income-surplus-statement', [ 'event' => $this->event, 'createdAt' => new \DateTime()->format('d.m.Y'), 'income' => $income, 'expenses' => $expenses, 'result' => $result, 'money' => self::money(...), ])->render(); $response->success = true; $response->filename = 'EUER-' . $this->event->identifier . '.pdf'; $response->income = $income; $response->expenses = $expenses; $response->result = $result; $response->pdfContent = PdfGenerateAndDownloadProvider::fromHtml($html, 'portrait'); return $response; } /** * Die Einnahmenseite in zwei Ober-Kategorien. * * Alle Zahlen stammen aus {@see \App\Resources\EventResource} -- derselben Quelle wie die * Veranstaltungsübersicht am Bildschirm. Eine eigene Rechnung daneben würde über kurz oder lang von * der Übersicht abweichen, und dann glaubt niemand mehr einer der beiden Zahlen. * * @param array $eventData * * @return array{categories: array}>, total: Amount} */ private function buildIncome(array $eventData, float $otherIncome): array { // Beiträge aller Teilnahmearten in einer Zeile: Für die Mittelverwendung zählt, was an Beiträgen // hereingekommen ist, nicht von wem. $participationFees = new Amount(0, 'Euro'); foreach ([ ParticipationType::PARTICIPATION_TYPE_PARTICIPANT, ParticipationType::PARTICIPATION_TYPE_TEAM, ParticipationType::PARTICIPATION_TYPE_VOLUNTEER, ParticipationType::PARTICIPATION_TYPE_OTHER, ] as $participationType) { $participationFees->addAmount( new Amount((float) $eventData['participants'][$participationType]['amount']['paid']['value'], 'Euro') ); } $ownFunds = [ ['name' => 'Teilnahmebeiträge', 'amount' => $participationFees], ['name' => 'Weitere Einnahmen', 'amount' => new Amount($otherIncome, 'Euro')], [ 'name' => 'Einbehaltene Einnahmen aus Abmeldungen', 'amount' => new Amount((float) $eventData['retainedFromUnregistered']['value'], 'Euro'), ], ]; $supportRate = new Amount((float) $eventData['supportPersonValue'], 'Euro'); $funding = [ [ 'name' => 'Fördermittel (' . self::money($supportRate) . ' € p.P./Tag)', 'amount' => new Amount($eventData['supportPerson']['amount']->getAmount(), 'Euro'), ], ]; $categories = [ ['name' => 'Eigenmittel', 'entries' => $ownFunds, 'total' => self::sum($ownFunds)], ['name' => 'Förderungen', 'entries' => $funding, 'total' => self::sum($funding)], ]; return [ 'categories' => $categories, 'total' => self::sum($categories), ]; } /** * Die Ausgabenseite: eine Zeile je Ausgabentyp, dazu die Belege für die Anlage. * * @return array{groups: array}>, total: Amount} */ private function buildExpenses(CostUnit $costUnit): array { $groups = []; $total = new Amount(0, 'Euro'); foreach ($this->costUnits->groupExpensesByType($costUnit) as $group) { $rows = []; foreach ($group['invoices'] as $invoice) { $rows[] = [ 'number' => (string) $invoice->invoice_number, 'date' => $invoice->created_at?->format('d.m.Y') ?? '', 'purpose' => $this->purpose($invoice->purposeText(), $invoice->comment), 'amount' => Amount::fromString($invoice->amount), ]; } $groups[] = [ 'name' => $group['type']->name, 'sum' => $group['sum'], 'rows' => $rows, ]; $total->addAmount($group['sum']); } return ['groups' => $groups, 'total' => $total]; } /** * Wofür der Beleg steht, um die Anmerkung ergänzt. * * Den Zweck selbst bestimmt {@see \App\Models\Invoice::purposeText()} -- dieselbe Ermittlung wie in * der Beleg-Übersicht, damit ein Beleg nicht an zwei Stellen Verschiedenes über sich behauptet. Die * Anmerkung kommt nur hier dazu: Auf der Aufstellung steht der Beleg für sich, ohne die Detailansicht * daneben. * * Ältere Belege haben keinen Zweck erfasst -- dann bleibt die Anmerkung, und fehlt auch die, bleibt * die Zelle leer. Ein Platzhalter wie "--" würde in der Belegliste nur Platz kosten. */ private function purpose(?string $purpose, ?string $comment): string { $parts = []; foreach ([$purpose, $comment] as $part) { if (trim((string) $part) !== '') { $parts[] = trim((string) $part); } } return implode(' — ', $parts); } /** * Ein Betrag in deutscher Schreibweise: Punkt als Tausender-, Komma als Dezimaltrennzeichen. * * Bewusst nicht {@see Amount::getFormattedAmount()}: Die Methode ersetzt nach `number_format` jeden * Punkt durch ein Komma und macht aus 1.487,50 damit "1,487,50". Auf einer Aufstellung, in der * vierstellige Beträge die Regel sind, wäre das nicht lesbar. Der Fehler steckt im Value Object und * wirkt überall, wo Beträge angezeigt werden -- ihn dort zu beheben ist eine eigene Änderung. * * Öffentlich, weil die Vorlage sie als Callable bekommt und weil sie für sich prüfbar sein soll. */ public static function money(Amount $amount): string { return number_format(round($amount->getAmount(), 2), 2, ',', '.'); } /** * Summiert Zeilen, die je ein `amount` oder `total` tragen. * * Über ein frisches Amount-Objekt, weil `Amount::addAmount()` den Empfänger verändert -- die * Einzelbeträge sollen unangetastet bleiben, sie werden anschließend gedruckt. * * @param array> $rows */ private static function sum(array $rows): Amount { $sum = new Amount(0, 'Euro'); foreach ($rows as $row) { /** @var Amount $amount */ $amount = $row['amount'] ?? $row['total']; $sum->addAmount($amount); } return $sum; } }