Verbesserte EÜR-Abrechnung

This commit is contained in:
2026-10-01 13:40:51 +02:00
parent a43fd9cedf
commit bcebe2ebed
7 changed files with 252 additions and 47 deletions
@@ -202,11 +202,95 @@ class EventIncomeSurplusStatementTest extends TestCase
// Kein Geldfluss, also keine Ausgabe:
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 999.0, InvoiceStatus::INVOICE_STATUS_DENIED);
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 888.0, InvoiceStatus::INVOICE_STATUS_DELETED);
// Eine Spende ist Ausgabe ihres Typs (und zugleich Einnahme unter den Förderungen):
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 777.0, InvoiceStatus::INVOICE_STATUS_NEW, donation: true);
$this->assertEqualsWithDelta(150.0, $this->group('Programmkosten')['sum']->getAmount(), 0.001);
$this->assertEqualsWithDelta(927.0, $this->group('Programmkosten')['sum']->getAmount(), 0.001);
$this->assertEqualsWithDelta(30.0, $this->group('Verpflegungskosten')['sum']->getAmount(), 0.001);
$this->assertEqualsWithDelta(180.0, $this->statement()->expenses['total']->getAmount(), 0.001);
$this->assertEqualsWithDelta(957.0, $this->statement()->expenses['total']->getAmount(), 0.001);
}
/*
|--------------------------------------------------------------------------
| Spenden
|--------------------------------------------------------------------------
*/
public function test_without_donations_the_donation_line_is_zero(): void
{
$this->makeEvent();
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 100.0, InvoiceStatus::INVOICE_STATUS_NEW);
$this->assertEqualsWithDelta(0.0, $this->entry('Spenden')->getAmount(), 0.001);
$this->assertEqualsWithDelta(0.0, $this->statement()->donations['total']->getAmount(), 0.001);
}
public function test_a_donation_is_an_expense_of_its_type_and_funding_income(): void
{
$this->makeEvent();
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 100.0, InvoiceStatus::INVOICE_STATUS_NEW);
$resultWithoutDonation = $this->statement()->result->getAmount();
$donated = $this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 40.0, InvoiceStatus::INVOICE_STATUS_APPROVED, donation: true);
$statement = $this->statement();
$program = $this->group('Programmkosten');
$this->assertEqualsWithDelta(140.0, $program['sum']->getAmount(), 0.001);
$this->assertContains($donated->invoice_number, array_column($program['rows'], 'number'));
// Gegenbuchung unter den Förderungen (ohne Anmeldung keine Fördermittel, also nur die Spende).
$this->assertSame('Förderungen', $statement->income['categories'][1]['name']);
$this->assertEqualsWithDelta(40.0, $this->entry('Spenden')->getAmount(), 0.001);
$this->assertEqualsWithDelta(40.0, $statement->income['categories'][1]['total']->getAmount(), 0.001);
// Ausgabe und Einnahme heben sich auf.
$this->assertEqualsWithDelta($resultWithoutDonation, $statement->result->getAmount(), 0.001);
}
public function test_donations_are_grouped_like_expenses(): void
{
$this->makeEvent();
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 100.0, InvoiceStatus::INVOICE_STATUS_NEW);
$programDonation = $this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 25.0, InvoiceStatus::INVOICE_STATUS_NEW, donation: true);
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 15.0, InvoiceStatus::INVOICE_STATUS_EXPORTED, donation: true);
$this->makeInvoice(InvoiceType::INVOICE_TYPE_CATERING, 60.0, InvoiceStatus::INVOICE_STATUS_APPROVED, donation: true);
$donations = $this->statement()->donations;
// Dieselbe Gliederung wie die Ausgaben, auch Typen ohne Spende stehen darin.
$this->assertSame(
array_column($this->statement()->expenses['groups'], 'name'),
array_column($donations['groups'], 'name')
);
$this->assertEqualsWithDelta(40.0, $this->donationGroup('Programmkosten')['sum']->getAmount(), 0.001);
$this->assertEqualsWithDelta(60.0, $this->donationGroup('Verpflegungskosten')['sum']->getAmount(), 0.001);
$this->assertEqualsWithDelta(100.0, $donations['total']->getAmount(), 0.001);
$this->assertEqualsWithDelta(100.0, $this->entry('Spenden')->getAmount(), 0.001);
// Derselbe Beleg steht als Ausgabe und als Spende.
$this->assertContains($programDonation->invoice_number, array_column($this->group('Programmkosten')['rows'], 'number'));
$this->assertContains($programDonation->invoice_number, array_column($this->donationGroup('Programmkosten')['rows'], 'number'));
// Nicht gespendete Belege stehen nicht in der Spenden-Anlage.
$this->assertCount(2, $this->donationGroup('Programmkosten')['rows']);
}
public function test_donations_list_only_booked_donations(): void
{
$this->makeEvent();
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 20.0, InvoiceStatus::INVOICE_STATUS_NEW, donation: true);
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 999.0, InvoiceStatus::INVOICE_STATUS_DENIED, donation: true);
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 888.0, InvoiceStatus::INVOICE_STATUS_DELETED, donation: true);
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PARTICIPATION_REFUND, 220.0, InvoiceStatus::INVOICE_STATUS_EXPORTED, donation: true);
$donations = $this->statement()->donations;
$this->assertEqualsWithDelta(20.0, $donations['total']->getAmount(), 0.001);
$this->assertCount(1, $this->donationGroup('Programmkosten')['rows']);
$this->assertNotContains('Beitragserstattung', array_column($donations['groups'], 'name'));
}
public function test_a_refund_is_no_expense(): void
@@ -311,12 +395,34 @@ class EventIncomeSurplusStatementTest extends TestCase
public function test_the_comment_is_no_longer_part_of_the_purpose(): void
{
// Die Anmerkung trägt, was die Kassenwart*in beim Korrigieren notiert hat -- sie steht auf dem
// Beleg-PDF und hat im Zweck nichts zu suchen. Bestandsbelege ohne erfassten Zahlungsgrund haben
// hier deshalb eine leere Zelle; nachtragen lässt er sich beim Korrigieren.
// Beleg-PDF und hat im Zweck nichts zu suchen.
$this->makeEvent();
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 100.0, InvoiceStatus::INVOICE_STATUS_NEW);
$this->assertSame('', $this->group('Programmkosten')['rows'][0]['purpose']);
$this->assertStringNotContainsString('Materialkauf', $this->group('Programmkosten')['rows'][0]['purpose']);
}
public function test_an_empty_purpose_falls_back_to_the_refund_of_the_contact(): void
{
// Ältere Abrechnungen ohne jede Zweckangabe: ersatzweise, wem erstattet wurde -- als Ausgabe wie als Spende.
$this->makeEvent();
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 100.0, InvoiceStatus::INVOICE_STATUS_NEW);
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 30.0, InvoiceStatus::INVOICE_STATUS_NEW, donation: true);
$this->makeInvoice(
InvoiceType::INVOICE_TYPE_PROGRAM,
50.0,
InvoiceStatus::INVOICE_STATUS_NEW,
purpose: 'Material für den Bastelnachmittag'
);
$this->assertSame(
['Auslagenerstattung Mika Muster', 'Auslagenerstattung Mika Muster', 'Material für den Bastelnachmittag'],
array_column($this->group('Programmkosten')['rows'], 'purpose')
);
$this->assertSame(
['Auslagenerstattung Mika Muster'],
array_column($this->donationGroup('Programmkosten')['rows'], 'purpose')
);
}
/*
@@ -388,6 +494,20 @@ class EventIncomeSurplusStatementTest extends TestCase
);
}
public function test_a_donation_still_does_not_count_in_the_expense_overview(): void
{
// Die Bildschirm-Übersicht kennt keine Spenden-Einnahme -- dort bleibt die Spende außen vor.
$this->makeEvent();
$this->makeInvoice(InvoiceType::INVOICE_TYPE_PROGRAM, 777.0, InvoiceStatus::INVOICE_STATUS_NEW, donation: true);
$amount = new CostUnitRepository()->sumupByInvoiceType(
$this->costUnit->fresh(),
InvoiceType::where('slug', InvoiceType::INVOICE_TYPE_PROGRAM)->first()
);
$this->assertEqualsWithDelta(0.0, $amount->getAmount(), 0.001);
}
/*
|--------------------------------------------------------------------------
| Helfer
@@ -435,6 +555,18 @@ class EventIncomeSurplusStatementTest extends TestCase
$this->fail('Ausgaben-Gruppe "' . $name . '" fehlt.');
}
/** @return array{name: string, sum: Amount, rows: array<int, array<string, mixed>>} */
private function donationGroup(string $name): array
{
foreach ($this->statement()->donations['groups'] as $group) {
if ($group['name'] === $name) {
return $group;
}
}
$this->fail('Spenden-Gruppe "' . $name . '" fehlt.');
}
private function makeEvent(): Event
{
$fee = EventParticipationFee::create([