Abrechnungsgrüne nun immer erforderlich
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ref, computed, onMounted, reactive } from 'vue'
|
||||
import {checkFilesize} from "../../../../../../resources/js/components/InvoiceUploadChecks.js";
|
||||
import RefundData from "./refund-data.vue";
|
||||
import AmountInput from "../../../../../Views/Components/AmountInput.vue";
|
||||
@@ -38,6 +38,20 @@ onMounted(async () => {
|
||||
Object.assign(invoiceTypeCollection, data);
|
||||
});
|
||||
|
||||
/**
|
||||
* Das Beispiel im Feld "Was wurde eingekauft" kommt aus `invoice_types.purchase_example` und ist damit
|
||||
* ohne Deployment pflegbar. Ein allgemeines "z. B. Material" hilft beim Ausfüllen nicht weiter, und von
|
||||
* genau diesem Text lebt später die Zweck-Spalte der EüR-Belegliste.
|
||||
*
|
||||
* Ist am Typ nichts gepflegt, greift der allgemeine Text -- ein Feld ohne jede Hilfestellung wäre
|
||||
* schlechter als ein unscharfes Beispiel.
|
||||
*/
|
||||
const purchasePlaceholder = computed(() => {
|
||||
const selected = Object.values(invoiceTypeCollection.invoiceTypes)
|
||||
.find((type) => type.slug === invoiceType.value)
|
||||
|
||||
return selected?.purchaseExample || 'z. B. Material für die Veranstaltung'
|
||||
})
|
||||
|
||||
function handleFileChange(event) {
|
||||
if (checkFilesize('receipt')) {
|
||||
@@ -66,28 +80,36 @@ function handleFileChange(event) {
|
||||
<InfoIcon :text="'INFO_INVOICE_TYPE_' + availableInvoiceType.slug" /><br />
|
||||
</p>
|
||||
|
||||
</fieldset><br /><br />
|
||||
|
||||
<label for="invoice_type_other">
|
||||
<!--
|
||||
Pflichtangabe zu jeder Rechnung, nicht nur zu "Sonstige Kosten": Erst dieser Text sagt, wofür das
|
||||
Geld ausgegeben wurde, und füllt damit die Zweck-Spalte der EüR-Belegliste.
|
||||
|
||||
Die Schritte bauen aufeinander auf -- ohne Ausgabenart gäbe es kein passendes Beispiel für den
|
||||
Einkauf, und ein Betrag ohne Zweck ließe sich später nicht mehr zuordnen.
|
||||
-->
|
||||
<template v-if="invoiceType !== null">
|
||||
<fieldset>
|
||||
<legend><span style="font-weight: bolder;">Was wurde eingekauft</span></legend>
|
||||
<input
|
||||
type="text"
|
||||
class="width-full"
|
||||
name="kostengruppe_sonstiges"
|
||||
placeholder="Sonstige"
|
||||
for="invoice_type_other"
|
||||
id="purchase_description"
|
||||
name="purchase_description"
|
||||
:placeholder="purchasePlaceholder"
|
||||
v-model="otherText"
|
||||
@focus="invoiceType = 'other'"
|
||||
/>
|
||||
</label>
|
||||
</fieldset><br /><br />
|
||||
</template>
|
||||
|
||||
</fieldset><br /><br />
|
||||
|
||||
<fieldset>
|
||||
<fieldset v-if="invoiceType !== null && otherText.trim() !== ''">
|
||||
<legend><span style="font-weight: bolder;">Wie hoch ist der Betrag</span></legend>
|
||||
<AmountInput v-model="amount" class="width-small" id="amount" name="amount" /> Euro
|
||||
<info-icon></info-icon><br /><br />
|
||||
|
||||
<input
|
||||
v-if="amount != '' && invoiceType !== null"
|
||||
v-if="amount != ''"
|
||||
class="mareike-button"
|
||||
onclick="document.getElementById('receipt').click();"
|
||||
type="button"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { ref, computed, onMounted, reactive } from 'vue'
|
||||
import {checkFilesize} from "../../../../../../resources/js/components/InvoiceUploadChecks.js";
|
||||
import RefundData from "./refund-data.vue";
|
||||
import AmountInput from "../../../../../Views/Components/AmountInput.vue";
|
||||
@@ -37,6 +37,20 @@ onMounted(async () => {
|
||||
Object.assign(invoiceTypeCollection, data);
|
||||
});
|
||||
|
||||
/**
|
||||
* Das Beispiel im Feld "Was wurde eingekauft" kommt aus `invoice_types.purchase_example` und ist damit
|
||||
* ohne Deployment pflegbar. Ein allgemeines "z. B. Material" hilft beim Ausfüllen nicht weiter, und von
|
||||
* genau diesem Text lebt später die Zweck-Spalte der EüR-Belegliste.
|
||||
*
|
||||
* Ist am Typ nichts gepflegt, greift der allgemeine Text -- ein Feld ohne jede Hilfestellung wäre
|
||||
* schlechter als ein unscharfes Beispiel.
|
||||
*/
|
||||
const purchasePlaceholder = computed(() => {
|
||||
const selected = Object.values(invoiceTypeCollection.invoiceTypes)
|
||||
.find((type) => type.slug === invoiceType.value)
|
||||
|
||||
return selected?.purchaseExample || 'z. B. Material für die Veranstaltung'
|
||||
})
|
||||
|
||||
function handleFileChange(event) {
|
||||
if (checkFilesize('receipt')) {
|
||||
@@ -65,28 +79,36 @@ function handleFileChange(event) {
|
||||
<InfoIcon :text="'INFO_INVOICE_TYPE_' + availableInvoiceType.slug" /><br />
|
||||
</p>
|
||||
|
||||
</fieldset><br /><br />
|
||||
|
||||
<label for="invoice_type_other">
|
||||
<!--
|
||||
Pflichtangabe zu jeder Abrechnung, nicht nur zu "Sonstige Kosten": Erst dieser Text sagt, wofür
|
||||
das Geld ausgegeben wurde, und füllt damit die Zweck-Spalte der EüR-Belegliste.
|
||||
|
||||
Die Schritte bauen aufeinander auf -- ohne Ausgabenart gäbe es kein passendes Beispiel für den
|
||||
Einkauf, und ein Betrag ohne Zweck ließe sich später nicht mehr zuordnen.
|
||||
-->
|
||||
<template v-if="invoiceType !== null">
|
||||
<fieldset>
|
||||
<legend><span style="font-weight: bolder;">Was wurde eingekauft</span></legend>
|
||||
<input
|
||||
type="text"
|
||||
class="width-full"
|
||||
name="kostengruppe_sonstiges"
|
||||
placeholder="Sonstige"
|
||||
for="invoice_type_other"
|
||||
id="purchase_description"
|
||||
name="purchase_description"
|
||||
:placeholder="purchasePlaceholder"
|
||||
v-model="otherText"
|
||||
@focus="invoiceType = 'other'"
|
||||
/>
|
||||
</label>
|
||||
</fieldset><br /><br />
|
||||
</template>
|
||||
|
||||
</fieldset><br /><br />
|
||||
|
||||
<fieldset>
|
||||
<fieldset v-if="invoiceType !== null && otherText.trim() !== ''">
|
||||
<legend><span style="font-weight: bolder;">Wie hoch ist der Betrag</span></legend>
|
||||
<AmountInput v-model="amount" class="width-small" id="amount" name="amount" /> Euro
|
||||
<info-icon></info-icon><br /><br />
|
||||
|
||||
<input
|
||||
v-if="amount != '' && invoiceType !== null"
|
||||
v-if="amount != ''"
|
||||
class="mareike-button"
|
||||
onclick="document.getElementById('receipt').click();"
|
||||
type="button"
|
||||
|
||||
@@ -30,6 +30,7 @@ class InvoiceType extends CommonModel {
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'name',
|
||||
'purchase_example',
|
||||
'sort_order',
|
||||
'selectable',
|
||||
'counts_as_expense',
|
||||
@@ -61,10 +62,13 @@ class InvoiceType extends CommonModel {
|
||||
* Beitragserstattung mindert bereits die Einnahmenseite, weil der abgemeldete Teili dort herausfällt.
|
||||
* Als Ausgabe gezählt, ginge derselbe Vorgang ein zweites Mal in die Bilanz.
|
||||
*
|
||||
* Der Name ist zweiter Sortierschlüssel: Die meisten Typen teilen sich `sort_order = 1`, und ohne ihn
|
||||
* stünden die Zeilen der Ausgabenrechnung bei jedem Aufruf in einer anderen Reihenfolge.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Collection<int, self>
|
||||
*/
|
||||
public static function countingAsExpense(): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return self::where('counts_as_expense', true)->orderBy('sort_order')->get();
|
||||
return self::where('counts_as_expense', true)->orderBy('sort_order')->orderBy('name')->get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,29 +121,37 @@ class ProductionDataSeeder {
|
||||
|
||||
|
||||
private function installInvoiceMetaData() {
|
||||
// `purchase_example` steht im Formular als Beispiel im Feld "Was wurde eingekauft" und ist dort
|
||||
// Pflicht. Der Text hängt an der Ausgabenart, weil ein allgemeines "z. B. Material" beim
|
||||
// Ausfüllen nicht weiterhilft; pflegen lässt er sich anschließend in der Tabelle.
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_TRAVELLING,
|
||||
'name' => 'Reisekosten'
|
||||
'name' => 'Reisekosten',
|
||||
'purchase_example' => 'z. B. Bahnfahrt Halle–Leipzig',
|
||||
]);
|
||||
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_PROGRAM,
|
||||
'name' => 'Programmkosten'
|
||||
'name' => 'Programmkosten',
|
||||
'purchase_example' => 'z. B. Bastelmaterial für den Workshop',
|
||||
]);
|
||||
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_ACCOMMODATION,
|
||||
'name' => 'Unterkunftskosten'
|
||||
'name' => 'Unterkunftskosten',
|
||||
'purchase_example' => 'z. B. zwei Nächte Jugendherberge',
|
||||
]);
|
||||
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_CATERING,
|
||||
'name' => 'Verpflegungskosten',
|
||||
'purchase_example' => 'z. B. Wocheneinkauf für das Frühstück',
|
||||
]);
|
||||
|
||||
InvoiceType::create([
|
||||
'slug' => InvoiceType::INVOICE_TYPE_OTHER,
|
||||
'name' => 'Sonstige Kosten'
|
||||
'name' => 'Sonstige Kosten',
|
||||
'purchase_example' => 'z. B. Erste-Hilfe-Set',
|
||||
]);
|
||||
|
||||
InvoiceStatus::create(['slug' => InvoiceStatus::INVOICE_STATUS_NEW]);
|
||||
|
||||
@@ -59,7 +59,10 @@ class GlobalDataProvider {
|
||||
|
||||
$invoiceTypes[] = [
|
||||
'slug' => $invoiceType->slug,
|
||||
'name' => $invoiceType->name
|
||||
'name' => $invoiceType->name,
|
||||
// Beispieltext für "Was wurde eingekauft". Ist er nicht gepflegt, setzt das Formular
|
||||
// seinen eigenen allgemeinen Text ein.
|
||||
'purchaseExample' => $invoiceType->purchase_example,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Enumerations\InvoiceStatus;
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Enumerations\UserRole;
|
||||
use App\Models\CostUnit;
|
||||
use App\Models\Invoice;
|
||||
use App\Providers\AuthCheckProvider;
|
||||
use App\Resources\CostUnitResource;
|
||||
use App\ValueObjects\Amount;
|
||||
@@ -153,8 +154,7 @@ class CostUnitRepository {
|
||||
|
||||
foreach ($costUnit->invoices()->get() as $invoice) {
|
||||
if (
|
||||
$invoice->status === InvoiceStatus::INVOICE_STATUS_DENIED ||
|
||||
$invoice->donation ||
|
||||
!self::countsAsExpense($invoice) ||
|
||||
$invoice->type !== $invoiceType->slug
|
||||
) {
|
||||
continue;
|
||||
@@ -165,6 +165,52 @@ class CostUnitRepository {
|
||||
return $amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Ausgaben einer Kostenstelle, nach Ausgabentyp gruppiert -- Summe und die Belege dahinter.
|
||||
*
|
||||
* Jeder zählende Typ steht im Ergebnis, auch ohne Beleg: Die Einnahmen-Überschuss-Rechnung zeigt
|
||||
* damit immer dieselbe Gliederung, und eine fehlende Zeile lässt sich nicht mit einer vergessenen
|
||||
* verwechseln.
|
||||
*
|
||||
* @return array<string, array{type: InvoiceType, invoices: array<int, Invoice>, sum: Amount}>
|
||||
*/
|
||||
public function groupExpensesByType(CostUnit $costUnit) : array {
|
||||
$groups = [];
|
||||
|
||||
foreach (InvoiceType::countingAsExpense() as $invoiceType) {
|
||||
$groups[$invoiceType->slug] = [
|
||||
'type' => $invoiceType,
|
||||
'invoices' => [],
|
||||
'sum' => new Amount(0, 'Euro'),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($costUnit->invoices()->orderBy('invoice_number')->get() as $invoice) {
|
||||
if (!self::countsAsExpense($invoice) || !isset($groups[$invoice->type])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$groups[$invoice->type]['invoices'][] = $invoice;
|
||||
$groups[$invoice->type]['sum']->addAmount(Amount::fromString($invoice->amount));
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ob ein Beleg als Ausgabe zählt.
|
||||
*
|
||||
* Abgelehnt und gelöscht sind keine Ausgabe, weil kein Geld fließt. Dasselbe gilt für eine gespendete
|
||||
* Auslage: Auf die Auszahlung wurde verzichtet, die Kostenstelle gibt nichts aus.
|
||||
*/
|
||||
private static function countsAsExpense(Invoice $invoice) : bool {
|
||||
return !in_array($invoice->status, [
|
||||
InvoiceStatus::INVOICE_STATUS_DENIED,
|
||||
InvoiceStatus::INVOICE_STATUS_DELETED,
|
||||
], true)
|
||||
&& !$invoice->donation;
|
||||
}
|
||||
|
||||
public function sumupEstimatedByInvoiceType(CostUnit $costUnit, InvoiceType $invoiceType) : Amount {
|
||||
$amount = new Amount(0, 'Euro');
|
||||
foreach ($costUnit->estimates()->get() as $estimate) {
|
||||
|
||||
Reference in New Issue
Block a user