Zahlungszwecke
This commit is contained in:
+3
-25
@@ -148,7 +148,9 @@ class CreateIncomeSurplusStatementCommand
|
||||
$rows[] = [
|
||||
'number' => (string) $invoice->invoice_number,
|
||||
'date' => $invoice->created_at?->format('d.m.Y') ?? '',
|
||||
'purpose' => $this->purpose($invoice->purposeText(), $invoice->comment),
|
||||
// Ohne die Anmerkung: dort steht, was die Kassenwart*in beim Korrigieren notiert hat,
|
||||
// und das gehört auf den Beleg, nicht in den Zweck.
|
||||
'purpose' => $invoice->purposeText(),
|
||||
'amount' => Amount::fromString($invoice->amount),
|
||||
];
|
||||
}
|
||||
@@ -165,30 +167,6 @@ class CreateIncomeSurplusStatementCommand
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Domains\Invoice\Actions\CreateInvoice;
|
||||
|
||||
use App\Enumerations\InvoiceStatus;
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Enumerations\TravelReason;
|
||||
use App\Mail\InvoiceMails\InvoiceMailsNewInvoiceMail;
|
||||
use App\Mail\InvoiceMails\InvoiceMailsSubmittedConfirmationMail;
|
||||
use App\Mail\ParticipantParticipationMails\EventSignUpSuccessfullMail;
|
||||
@@ -19,10 +21,19 @@ class CreateInvoiceCommand {
|
||||
public function execute() : CreateInvoiceResponse {
|
||||
$response = new CreateInvoiceResponse();
|
||||
|
||||
$rejection = $this->rejectTravelReason();
|
||||
if ($rejection !== null) {
|
||||
$response->message = $rejection;
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ($this->request->accountIban === 'undefined') {
|
||||
$this->request->accountIban = null;
|
||||
}
|
||||
|
||||
$travelReason = $this->travelReason();
|
||||
|
||||
$invoice = Invoice::create([
|
||||
'tenant' => currentTenant()->slug,
|
||||
'cost_unit_id' => $this->request->costUnit->id,
|
||||
@@ -30,6 +41,7 @@ class CreateInvoiceCommand {
|
||||
'status' => InvoiceStatus::INVOICE_STATUS_NEW,
|
||||
'type' => $this->request->invoiceType,
|
||||
'type_other' => $this->request->invoiceTypeExtended,
|
||||
'purpose' => $this->purpose($travelReason),
|
||||
'donation' => $this->request->isDonation,
|
||||
'user_id' => $this->request->paymentPurpose === null ? $this->request->userId : null,
|
||||
'contact_name' => $this->request->contactName,
|
||||
@@ -40,9 +52,7 @@ class CreateInvoiceCommand {
|
||||
'amount' => $this->request->totalAmount,
|
||||
'distance' => $this->request->distance,
|
||||
'travel_direction' => $this->request->travelRoute,
|
||||
'travel_reason' => $this->request->travelReason,
|
||||
'passengers' => $this->request->passengers,
|
||||
'transportation' => $this->request->transportations,
|
||||
'travel_reason' => $travelReason,
|
||||
'payment_purpose' => $this->request->paymentPurpose,
|
||||
'comment' => $this->request->notices,
|
||||
'document_filename' => $this->request->receiptFile !== null ? $this->request->receiptFile->fullPath : null,
|
||||
@@ -81,6 +91,68 @@ class CreateInvoiceCommand {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Der Zahlungsgrund, einmal beim Anlegen festgehalten.
|
||||
*
|
||||
* Danach ist er eine eigene Angabe: Die Kassenwart*in kann ihn korrigieren, und nichts schreibt ihn
|
||||
* mehr um. Bei Fahrtkosten setzt er sich aus Reisegrund und den gefahrenen Personen zusammen, sonst
|
||||
* trägt ihn "Was wurde eingekauft". Wo nichts erfasst wird -- Beitragserstattungen -- bleibt er leer.
|
||||
*
|
||||
* Steht er schon fest, wird er übernommen: Eine Abrechnungskorrektur kopiert den Beleg, und ein von
|
||||
* Hand gesetzter Grund darf dabei nicht verloren gehen.
|
||||
*/
|
||||
private function purpose(?string $travelReason) : ?string {
|
||||
if (trim((string) $this->request->purpose) !== '') {
|
||||
return $this->request->purpose;
|
||||
}
|
||||
|
||||
$purpose = Invoice::joinPurposeParts(
|
||||
$this->request->invoiceType === InvoiceType::INVOICE_TYPE_TRAVELLING
|
||||
// Der Name des Grundes, nicht sein Schlüssel: In der Belegliste soll "Materialtransport"
|
||||
// stehen, nicht "material_transport".
|
||||
? [TravelReason::text($travelReason), $this->request->travellers]
|
||||
: [$this->request->invoiceTypeExtended]
|
||||
);
|
||||
|
||||
return $purpose === '' ? null : $purpose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Was in `travel_reason` landet: der Schlüssel des gewählten Grundes -- oder, bei "Anderer Grund",
|
||||
* der Text selbst. Der Schlüssel `other` sagt für sich nichts aus, der Text alles.
|
||||
*
|
||||
* Ein unbekannter Wert ist deshalb kein Fehler, sondern genau dieser Fall: So kommen auch
|
||||
* Bestandsbelege und Kopien durch, die ihren Freitext schon mitbringen.
|
||||
*/
|
||||
private function travelReason() : ?string {
|
||||
$reason = TravelReason::find($this->request->travelReason);
|
||||
|
||||
if ($reason === null) {
|
||||
return $this->request->travelReason;
|
||||
}
|
||||
|
||||
return $reason->requires_note
|
||||
? trim((string) $this->request->travelReasonNote)
|
||||
: $reason->slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sicherheitsnetz hinter der Oberfläche: Dort geht es erst weiter, wenn die Erläuterung steht. Über
|
||||
* einen direkten Aufruf ginge das sonst vorbei, und ein "Anderer Grund" ohne Text sagt nichts aus --
|
||||
* gespeichert würde ein leerer Reisegrund.
|
||||
*/
|
||||
private function rejectTravelReason() : ?string {
|
||||
$reason = TravelReason::find($this->request->travelReason);
|
||||
|
||||
if ($reason === null || !$reason->requires_note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trim((string) $this->request->travelReasonNote) === ''
|
||||
? 'Bitte gib an, was der Grund für die Reise war.'
|
||||
: null;
|
||||
}
|
||||
|
||||
private function generateInvoiceNumber() : string {
|
||||
$lastInvoiceNumber = Invoice::query()
|
||||
->where('tenant', currentTenant()->slug)
|
||||
|
||||
@@ -16,16 +16,24 @@ class CreateInvoiceRequest {
|
||||
public ?string $invoiceTypeExtended;
|
||||
public ?string $travelRoute;
|
||||
public ?int $distance;
|
||||
public ?int $passengers;
|
||||
public ?int $transportations;
|
||||
public ?InvoiceFile $receiptFile;
|
||||
public float $totalAmount;
|
||||
public bool $isDonation;
|
||||
public ?int $userId;
|
||||
/** Der Slug eines Reisegrundes -- oder ein Freitext, wenn er von einem Bestandsbeleg stammt. */
|
||||
public ?string $travelReason;
|
||||
|
||||
/** Die Erläuterung zu "Anderer Grund"; nur bei einem Grund mit `requires_note` von Belang. */
|
||||
public ?string $travelReasonNote;
|
||||
public ?string $paymentPurpose;
|
||||
public ?string $notices;
|
||||
|
||||
/** Wer gereist ist -- Freitext aus dem Fahrtkosten-Formular, geht in den Zahlungsgrund ein. */
|
||||
public ?string $travellers;
|
||||
|
||||
/** Ein bereits feststehender Zahlungsgrund; gesetzt, gewinnt er über die Ermittlung im Command. */
|
||||
public ?string $purpose;
|
||||
|
||||
|
||||
public function __construct(
|
||||
CostUnit $costUnit,
|
||||
@@ -42,11 +50,12 @@ class CreateInvoiceRequest {
|
||||
?string $invoiceTypeExtended = null,
|
||||
?string $travelRoute = null,
|
||||
?int $distance = null,
|
||||
?int $passengers = null,
|
||||
?int $transportations,
|
||||
?string $travelReason = null,
|
||||
?string $travelReasonNote = null,
|
||||
?string $paymentPurpose = null,
|
||||
?string $notices = null,
|
||||
?string $travellers = null,
|
||||
?string $purpose = null,
|
||||
|
||||
) {
|
||||
$this->costUnit = $costUnit;
|
||||
@@ -55,8 +64,6 @@ class CreateInvoiceRequest {
|
||||
$this->invoiceTypeExtended = $invoiceTypeExtended;
|
||||
$this->travelRoute = $travelRoute;
|
||||
$this->distance = $distance;
|
||||
$this->passengers = $passengers;
|
||||
$this->transportations = $transportations;
|
||||
$this->receiptFile = $receiptFile;
|
||||
$this->contactEmail = $contactEmail;
|
||||
$this->contactPhone = $contactPhone;
|
||||
@@ -66,8 +73,11 @@ class CreateInvoiceRequest {
|
||||
$this->isDonation = $isDonation;
|
||||
$this->userId = $userId;
|
||||
$this->travelReason = $travelReason;
|
||||
$this->travelReasonNote = $travelReasonNote;
|
||||
$this->paymentPurpose = $paymentPurpose;
|
||||
$this->notices = $notices;
|
||||
$this->travellers = $travellers;
|
||||
$this->purpose = $purpose;
|
||||
|
||||
if ($accountIban === 'undefined') {
|
||||
$this->accountIban = null;
|
||||
|
||||
@@ -8,8 +8,12 @@ class CreateInvoiceResponse {
|
||||
public bool $success;
|
||||
public ?Invoice $invoice;
|
||||
|
||||
/** Warum keine Abrechnung entstanden ist -- für die Rückmeldung an die einreichende Person. */
|
||||
public ?string $message;
|
||||
|
||||
public function __construct() {
|
||||
$this->success = false;
|
||||
$this->invoice = null;
|
||||
$this->message = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +83,8 @@ class CreateInvoiceReceiptCommand {
|
||||
|
||||
$travelPartTemplate = <<<HTML
|
||||
<tr><td>Reiseweg:</td><td>%1\$s</td></tr>
|
||||
<tr><td>Grund der Reise:</td><td>%6\$s</td></tr>
|
||||
<tr><td>Grund der Reise:</td><td>%4\$s</td></tr>
|
||||
<tr><td>Gesamtlänge der Strecke:</td><td>%2\$s km x %3\$s / km</td></tr>
|
||||
<tr><td>Materialtransport:</td><td>%4\$s</td></tr>
|
||||
<tr><td>Mitfahrende im PKW:</td><td>%5\$s</td></tr>
|
||||
HTML;
|
||||
|
||||
$flatTravelPart = sprintf(
|
||||
@@ -94,8 +92,6 @@ HTML;
|
||||
$invoiceReadable['travelDirection'] ,
|
||||
$invoiceReadable['distance'],
|
||||
$invoiceReadable['distanceAllowance'],
|
||||
$invoiceReadable['transportation'],
|
||||
$invoiceReadable['passengers'],
|
||||
$invoiceReadable['travelReason'] ,
|
||||
);
|
||||
|
||||
@@ -180,7 +176,10 @@ HTML;
|
||||
$invoiceReadable['contactEmail'],
|
||||
$invoiceReadable['contactPhone'],
|
||||
$invoiceReadable['costUnitName'],
|
||||
$invoiceReadable['invoiceType'],
|
||||
// Der erfasste Zahlungsgrund, nicht der Abrechnungstyp: Der steht eine Zeile darüber schon
|
||||
// als Überschrift. Bei Fahrtkosten ist das die einzige Stelle, an der auf dem Beleg steht,
|
||||
// wer gereist ist.
|
||||
$invoiceReadable['purpose'],
|
||||
$invoiceReadable['donationText'],
|
||||
$paymentType,
|
||||
$invoiceReadable['amount'],
|
||||
|
||||
@@ -34,6 +34,17 @@ class UpdateInvoiceCommand {
|
||||
}
|
||||
|
||||
|
||||
$purpose = trim((string) $this->request->purpose);
|
||||
$purpose = $purpose === '' ? null : $purpose;
|
||||
|
||||
// Verglichen wird gegen den angezeigten Text, nicht gegen die Spalte: Das Formular ist damit
|
||||
// vorbelegt, und wer ihn unverändert abschickt, hat nichts geändert. Ein Bestandsbeleg behält so
|
||||
// seine leere Spalte und damit die Ableitung; wer das Feld leert, schaltet zurück auf automatisch.
|
||||
if (($purpose ?? '') !== $this->request->invoice->purposeText()) {
|
||||
$changes .= 'Zahlungsgrund geändert von ' . $this->request->invoice->purposeText() . ' auf ' . ($purpose ?? '--') . '.<br />';
|
||||
$this->request->invoice->purpose = $purpose;
|
||||
}
|
||||
|
||||
$this->request->invoice->comment = $this->request->comment;
|
||||
$this->request->invoice->changes = $changes;
|
||||
|
||||
|
||||
@@ -13,9 +13,11 @@ class UpdateInvoiceRequest {
|
||||
public CostUnit $costUnit;
|
||||
public Invoice $invoice;
|
||||
public Amount $amount;
|
||||
public ?string $purpose;
|
||||
|
||||
public function __construct(Invoice $invoice, ?string $comment, InvoiceType $invoiceType, CostUnit $costUnit, Amount $amount) {
|
||||
public function __construct(Invoice $invoice, ?string $comment, InvoiceType $invoiceType, CostUnit $costUnit, Amount $amount, ?string $purpose = null) {
|
||||
$this->comment = $comment;
|
||||
$this->purpose = $purpose;
|
||||
$this->invoiceType = $invoiceType;
|
||||
$this->costUnit = $costUnit;
|
||||
$this->invoice = $invoice;
|
||||
|
||||
@@ -32,25 +32,26 @@ class EditController extends CommonController{
|
||||
$receiptfile->fullPath = $invoice->document_filename;
|
||||
}
|
||||
$createInvoiceRequest = new CreateInvoiceRequest(
|
||||
$invoice->costUnit()->first(),
|
||||
$invoice->contact_name,
|
||||
$invoice->type,
|
||||
$invoice->amount,
|
||||
$receiptfile,
|
||||
$invoice->donation,
|
||||
$invoice->user_id,
|
||||
$invoice->contact_email,
|
||||
$invoice->contact_phone,
|
||||
$invoice->contact_bank_owner,
|
||||
$invoice->contact_bank_iban,
|
||||
$invoice->type_other,
|
||||
$invoice->travel_direction,
|
||||
$invoice->distance,
|
||||
$invoice->passengers,
|
||||
$invoice->transportation,
|
||||
$invoice->travel_reason,
|
||||
$invoice->payment_purpose,
|
||||
$invoice->comment,
|
||||
costUnit: $invoice->costUnit()->first(),
|
||||
contactName: $invoice->contact_name,
|
||||
invoiceType: $invoice->type,
|
||||
totalAmount: $invoice->amount,
|
||||
receiptFile: $receiptfile,
|
||||
isDonation: $invoice->donation,
|
||||
userId: $invoice->user_id,
|
||||
contactEmail: $invoice->contact_email,
|
||||
contactPhone: $invoice->contact_phone,
|
||||
accountOwner: $invoice->contact_bank_owner,
|
||||
accountIban: $invoice->contact_bank_iban,
|
||||
invoiceTypeExtended: $invoice->type_other,
|
||||
travelRoute: $invoice->travel_direction,
|
||||
distance: $invoice->distance,
|
||||
travelReason: $invoice->travel_reason,
|
||||
paymentPurpose: $invoice->payment_purpose,
|
||||
notices: $invoice->comment,
|
||||
// Die rohe Spalte, nicht purposeText(): Ein Beleg, der seinen Zahlungsgrund bisher ableitet,
|
||||
// soll das als Kopie weiter tun.
|
||||
purpose: $invoice->purpose,
|
||||
);
|
||||
|
||||
$invoiceCreationCommand = new CreateInvoiceCommand($createInvoiceRequest);
|
||||
@@ -92,7 +93,8 @@ class EditController extends CommonController{
|
||||
$modifyData['notices'],
|
||||
$invoiceType,
|
||||
$newCostUnit,
|
||||
$newAmount
|
||||
$newAmount,
|
||||
$modifyData['purpose'] ?? null
|
||||
);
|
||||
$updateInvoiceCommand = new UpdateInvoiceCommand($updateInvoiceRequest);
|
||||
$updateInvoiceCommand->execute();
|
||||
@@ -107,22 +109,22 @@ class EditController extends CommonController{
|
||||
$receiptfile->fullPath = $invoice->document_filename;
|
||||
}
|
||||
$createInvoiceRequest = new CreateInvoiceRequest(
|
||||
$invoice->costUnit()->first(),
|
||||
$invoice->contact_name,
|
||||
$invoice->type,
|
||||
$amountLeft->getAmount(),
|
||||
$receiptfile,
|
||||
$invoice->donation,
|
||||
$invoice->user_id,
|
||||
$invoice->contact_email,
|
||||
$invoice->contact_phone,
|
||||
$invoice->contact_bank_owner,
|
||||
$invoice->contact_bank_iban,
|
||||
$invoice->type_other,
|
||||
$invoice->travel_direction,
|
||||
$invoice->distance,
|
||||
$invoice->passengers,
|
||||
$invoice->transportation
|
||||
costUnit: $invoice->costUnit()->first(),
|
||||
contactName: $invoice->contact_name,
|
||||
invoiceType: $invoice->type,
|
||||
totalAmount: $amountLeft->getAmount(),
|
||||
receiptFile: $receiptfile,
|
||||
isDonation: $invoice->donation,
|
||||
userId: $invoice->user_id,
|
||||
contactEmail: $invoice->contact_email,
|
||||
contactPhone: $invoice->contact_phone,
|
||||
accountOwner: $invoice->contact_bank_owner,
|
||||
accountIban: $invoice->contact_bank_iban,
|
||||
invoiceTypeExtended: $invoice->type_other,
|
||||
travelRoute: $invoice->travel_direction,
|
||||
distance: $invoice->distance,
|
||||
travelReason: $invoice->travel_reason,
|
||||
purpose: $invoice->purpose,
|
||||
);
|
||||
|
||||
$invoiceCreationCommand = new CreateInvoiceCommand($createInvoiceRequest);
|
||||
|
||||
@@ -66,50 +66,43 @@ class SaveInvoiceController extends CommonController
|
||||
}
|
||||
|
||||
$createInvoiceRequest = new CreateInvoiceRequest(
|
||||
$costUnit,
|
||||
$request->input('name'),
|
||||
InvoiceType::INVOICE_TYPE_TRAVELLING,
|
||||
$amount,
|
||||
$uploadedFile,
|
||||
'donation' === $request->input('decision') ? true : false,
|
||||
$this->users->getCurrentUserDetails()['userId'],
|
||||
$request->input('email'),
|
||||
$request->input('telephone'),
|
||||
$request->input('accountOwner'),
|
||||
$request->input('accountIban'),
|
||||
null,
|
||||
$request->input('otherText'),
|
||||
$distance,
|
||||
$request->input('havePassengers'),
|
||||
$request->input('materialTransportation'),
|
||||
$request->input('travelReason'),
|
||||
null,
|
||||
$notices
|
||||
costUnit: $costUnit,
|
||||
contactName: $request->input('name'),
|
||||
invoiceType: InvoiceType::INVOICE_TYPE_TRAVELLING,
|
||||
totalAmount: $amount,
|
||||
receiptFile: $uploadedFile,
|
||||
isDonation: 'donation' === $request->input('decision') ? true : false,
|
||||
userId: $this->users->getCurrentUserDetails()['userId'],
|
||||
contactEmail: $request->input('email'),
|
||||
contactPhone: $request->input('telephone'),
|
||||
accountOwner: $request->input('accountOwner'),
|
||||
accountIban: $request->input('accountIban'),
|
||||
travelRoute: $request->input('otherText'),
|
||||
distance: $distance,
|
||||
travelReason: $request->input('travelReason'),
|
||||
travelReasonNote: $request->input('travelReasonNote'),
|
||||
notices: $notices,
|
||||
travellers: $request->input('travellers')
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$createInvoiceRequest = new CreateInvoiceRequest(
|
||||
$costUnit,
|
||||
$request->input('name'),
|
||||
$invoiceType,
|
||||
Amount::fromString($request->input('amount'))->getAmount(),
|
||||
$uploadedFile,
|
||||
'donation' === $request->input('decision') ? true : false,
|
||||
$this->users->getCurrentUserDetails()['userId'],
|
||||
$request->input('email'),
|
||||
$request->input('telephone'),
|
||||
$request->input('accountOwner'),
|
||||
$request->input('accountIban'),
|
||||
$request->input('otherText'),
|
||||
null,
|
||||
null,
|
||||
$request->input('havePassengers'),
|
||||
$request->input('materialTransportation'),
|
||||
null,
|
||||
$paymentPurpose,
|
||||
$notices
|
||||
costUnit: $costUnit,
|
||||
contactName: $request->input('name'),
|
||||
invoiceType: $invoiceType,
|
||||
totalAmount: Amount::fromString($request->input('amount'))->getAmount(),
|
||||
receiptFile: $uploadedFile,
|
||||
isDonation: 'donation' === $request->input('decision') ? true : false,
|
||||
userId: $this->users->getCurrentUserDetails()['userId'],
|
||||
contactEmail: $request->input('email'),
|
||||
contactPhone: $request->input('telephone'),
|
||||
accountOwner: $request->input('accountOwner'),
|
||||
accountIban: $request->input('accountIban'),
|
||||
invoiceTypeExtended: $request->input('otherText'),
|
||||
paymentPurpose: $paymentPurpose,
|
||||
notices: $notices
|
||||
);
|
||||
|
||||
break;
|
||||
@@ -128,5 +121,11 @@ class SaveInvoiceController extends CommonController
|
||||
'message' => 'Alright'
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'error',
|
||||
'message' => $response->message
|
||||
?? 'Beim Speichern ist ein Fehler aufgetreten. Bitte starte den Vorgang erneut.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,15 +37,6 @@ const props = defineProps({
|
||||
<td style="font-weight: bold">{{props.invoice.amount}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Marterialtransport</th>
|
||||
<td>{{props.invoice.transportation}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Hat Personen mitgenommen</th>
|
||||
<td>{{props.invoice.passengers}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -16,11 +16,18 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['submit', 'cancel'])
|
||||
|
||||
/**
|
||||
* Die Anmerkung heißt in der Resource `comment` und ist dort `'--'`, wenn keine gesetzt ist -- beides
|
||||
* muss hier stimmen, sonst startet das Feld leer und das Speichern löscht die vorhandene Anmerkung.
|
||||
*/
|
||||
const existingComment = props.newInvoice.comment
|
||||
|
||||
const formData = reactive({
|
||||
type_internal: props.newInvoice.internalType || '',
|
||||
cost_unit: props.newInvoice.costUnitId || '',
|
||||
amount: props.newInvoice.amountPlain || '',
|
||||
notices: props.newInvoice.comments || '',
|
||||
purpose: props.newInvoice.purpose || '',
|
||||
notices: !existingComment || existingComment === '--' ? '' : existingComment,
|
||||
})
|
||||
|
||||
const submitForm = () => {
|
||||
@@ -70,6 +77,13 @@ onMounted(async () => {
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Zahlungsgrund:</td>
|
||||
<td>
|
||||
<input type="text" v-model="formData.purpose" class="width-almost-full" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Anmerkungen:</td>
|
||||
<td>
|
||||
|
||||
@@ -100,6 +100,10 @@ const emit = defineEmits(["accept", "deny", "fix", "reopen"])
|
||||
<td v-else-if="props.data.externalPayment">Rechnungszahlung</td>
|
||||
<td v-else>Klassische Auszahlung</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Zahlungsgrund:</td>
|
||||
<td colspan="3">{{props.data.purpose}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status:</td>
|
||||
<td>{{props.data.readableStatus}}</td>
|
||||
|
||||
@@ -32,15 +32,6 @@ const props = defineProps({
|
||||
<td style="font-weight: bold">{{props.invoice.amount}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Marterialtransport</th>
|
||||
<td>{{props.invoice.transportation}}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>Hat Personen mitgenommen</th>
|
||||
<td>{{props.invoice.passengers}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -24,8 +24,6 @@ const props = defineProps({
|
||||
userTelephone: String,
|
||||
userAccountOwner: String,
|
||||
userAccountIban: String,
|
||||
havePassengers: Number,
|
||||
materialTransportation: Boolean,
|
||||
travelReason: String,
|
||||
})
|
||||
|
||||
@@ -61,8 +59,6 @@ async function sendData() {
|
||||
formData.append('accountOwner', userAccountOwner.value)
|
||||
formData.append('accountIban', userIban.value)
|
||||
formData.append('paymentPurpose', paymentPurpose.value)
|
||||
formData.append('havePassengers', props.havePassengers ? 1 : 0)
|
||||
formData.append('materialTransportation', props.materialTransportation ? 1 : 0)
|
||||
formData.append('travelReason', props.travelReason)
|
||||
|
||||
if (props.receipt) {
|
||||
|
||||
@@ -24,9 +24,9 @@ const props = defineProps({
|
||||
userTelephone: String,
|
||||
userAccountOwner: String,
|
||||
userAccountIban: String,
|
||||
havePassengers: Number,
|
||||
materialTransportation: Boolean,
|
||||
travelReason: String,
|
||||
travelReasonNote: String,
|
||||
travellers: String,
|
||||
})
|
||||
|
||||
const finalStep = ref(true)
|
||||
@@ -60,9 +60,9 @@ async function sendData() {
|
||||
formData.append('decision', decision.value)
|
||||
formData.append('accountOwner', userAccountOwner.value)
|
||||
formData.append('accountIban', userIban.value)
|
||||
formData.append('havePassengers', props.havePassengers ? 1 : 0)
|
||||
formData.append('materialTransportation', props.materialTransportation ? 1 : 0)
|
||||
formData.append('travelReason', props.travelReason)
|
||||
formData.append('travelReasonNote', props.travelReasonNote ?? '')
|
||||
formData.append('travellers', props.travellers ?? '')
|
||||
formData.append('notices', notices.value)
|
||||
|
||||
if (props.receipt) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 NumericInput from "../../../../../Views/Components/NumericInput.vue";
|
||||
@@ -19,15 +19,44 @@ const { request } = useAjax();
|
||||
const distanceAllowance = ref(null);
|
||||
const travelDirection = ref(null);
|
||||
const travelReason = ref(null);
|
||||
const travelReasonNote = ref('');
|
||||
|
||||
const travelReasonCollection = reactive({
|
||||
travelReasons: []
|
||||
});
|
||||
|
||||
const selectedTravelReason = computed(() =>
|
||||
travelReasonCollection.travelReasons.find((reason) => reason.value === travelReason.value)
|
||||
);
|
||||
|
||||
/**
|
||||
* Ein Grund ist erst vollständig, wenn die Erläuterung steht, die er verlangt: "Anderer Grund" allein
|
||||
* sagt nichts aus. Die drei anderen Gründe bremsen den Ablauf dadurch nicht.
|
||||
*/
|
||||
const travelReasonComplete = computed(() =>
|
||||
selectedTravelReason.value !== undefined
|
||||
&& (!selectedTravelReason.value.requiresNote || travelReasonNote.value.trim() !== '')
|
||||
);
|
||||
|
||||
/**
|
||||
* Wer gereist ist, steht später als Zahlungsgrund in der Belegliste, auf der EüR und auf dem Beleg. Wer
|
||||
* den Beleg einreicht, war meist selbst dabei -- deshalb der eigene Name als Vorschlag. Ohne Login
|
||||
* bleibt das Feld leer, und es hält den Ablauf auch dann nicht auf: Ein Name ist eine Hilfe, keine
|
||||
* Bedingung.
|
||||
*/
|
||||
const travellers = ref(data.userName || '');
|
||||
const have_receipt = ref('')
|
||||
const havePassengers = ref(false);
|
||||
const materialTransportation = ref(false);
|
||||
const amount = ref(0.00);
|
||||
const invoiceType = ref(null);
|
||||
const otherText = ref('');
|
||||
const receipt = ref(null)
|
||||
const finalStep = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
const response = await fetch('/api/v1/core/retrieve-travel-reasons');
|
||||
travelReasonCollection.travelReasons = await response.json();
|
||||
});
|
||||
|
||||
async function getDistanceAllowance() {
|
||||
const tempData = await request('/api/v1/cost-unit/get-distance-allowance/' + data.eventId, {
|
||||
method: "GET",
|
||||
@@ -59,14 +88,40 @@ function handleFileChange(event) {
|
||||
|
||||
<fieldset v-if="travelDirection !== null">
|
||||
<legend><span style="font-weight: bolder;">Was war der Grund für deine Reise?</span></legend>
|
||||
|
||||
<p v-for="availableReason in travelReasonCollection.travelReasons">
|
||||
<input
|
||||
name="travel-reason"
|
||||
type="radio"
|
||||
:value="availableReason.value"
|
||||
:id="'travel_reason_' + availableReason.value"
|
||||
v-model="travelReason"
|
||||
>
|
||||
<label :for="'travel_reason_' + availableReason.value">{{ availableReason.label }}</label><br />
|
||||
</p>
|
||||
|
||||
<template v-if="selectedTravelReason?.requiresNote">
|
||||
<input
|
||||
type="text"
|
||||
class="width-full"
|
||||
name="travel-reason-note"
|
||||
placeholder="z. B. Abholung der Ausrüstung aus dem Lager"
|
||||
v-model="travelReasonNote"
|
||||
/>
|
||||
</template>
|
||||
</fieldset><br /><br />
|
||||
|
||||
<fieldset v-if="travelReasonComplete">
|
||||
<legend><span style="font-weight: bolder;">Welche Personen sind gereist?</span></legend>
|
||||
<input
|
||||
type="text"
|
||||
name="travel-reason"
|
||||
v-model="travelReason"
|
||||
name="travellers"
|
||||
placeholder="z. B. Mika, Kim und Alex"
|
||||
v-model="travellers"
|
||||
/>
|
||||
</fieldset><br /><br />
|
||||
|
||||
<fieldset v-if="travelReason !== null">
|
||||
<fieldset v-if="travelReasonComplete">
|
||||
<legend><span style="font-weight: bolder;">Bist du mit dem ÖPNV gefahren oder besitzt du einen Beleg</span></legend>
|
||||
<input type="button" style="border-radius: 0; width: 100px;" @click="have_receipt='yes'" value="Ja" />
|
||||
<input type="button" style="border-radius: 0; width: 100px;" @click="getDistanceAllowance" value="Nein" />
|
||||
@@ -93,8 +148,6 @@ function handleFileChange(event) {
|
||||
invoice-type="travelling"
|
||||
:amount="amount"
|
||||
:other-text="travelDirection"
|
||||
:materialTransportation="materialTransportation"
|
||||
:havePassengers="havePassengers"
|
||||
:userName="data.userName"
|
||||
:userEmail="data.userEmail"
|
||||
:userTelephone="data.userTelephone"
|
||||
@@ -102,6 +155,8 @@ function handleFileChange(event) {
|
||||
:userAccountOwner="data.userAccountOwner"
|
||||
:receipt="receipt"
|
||||
:travelReason="travelReason"
|
||||
:travelReasonNote="travelReasonNote"
|
||||
:travellers="travellers"
|
||||
@close="finalStep = false"
|
||||
/>
|
||||
</fieldset>
|
||||
@@ -117,22 +172,6 @@ function handleFileChange(event) {
|
||||
<span style="font-weight: normal">({{ amount }} km x {{distanceAllowance.toFixed(2).replace('.', ',')}} Euro / km = <strong>{{ (amount * distanceAllowance).toFixed(2).replace('.', ',') }} Euro</strong>)</span>
|
||||
<br /><br />
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
name="havePassengers"
|
||||
v-model="havePassengers"
|
||||
id="havePassengers"
|
||||
/> <label style="margin-bottom: 20px;" for="havePassengers">Ich habe Personen mitgenommen</label>
|
||||
<br />
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
name="materialTransportation"
|
||||
v-model="materialTransportation"
|
||||
id="materialTransportation"
|
||||
/> <label style="margin-bottom: 20px;" for="materialTransportation">Ich habe Material transportiert</label>
|
||||
<br /><br />
|
||||
|
||||
<input
|
||||
v-if="amount !== null && have_receipt === 'no' && amount != '0'"
|
||||
@click="finalStep = true;"
|
||||
@@ -145,14 +184,14 @@ function handleFileChange(event) {
|
||||
invoice-type="travelling"
|
||||
:amount="amount"
|
||||
:other-text="travelDirection"
|
||||
:materialTransportation="materialTransportation"
|
||||
:havePassengers="havePassengers"
|
||||
:userName="data.userName"
|
||||
:userEmail="data.userEmail"
|
||||
:userTelephone="data.userTelephone"
|
||||
:userAccountIban="data.userAccountIban"
|
||||
:userAccountOwner="data.userAccountOwner"
|
||||
:travelReason="travelReason"
|
||||
:travelReasonNote="travelReasonNote"
|
||||
:travellers="travellers"
|
||||
@close="finalStep = false"
|
||||
/>
|
||||
|
||||
|
||||
@@ -207,15 +207,6 @@ class AcceptRefundCommand
|
||||
accountOwner: $refund->account_owner,
|
||||
accountIban: $refund->account_iban,
|
||||
|
||||
// Die folgenden vier gehören zu Reisekosten und Freitext-Typen und sind hier leer. Sie
|
||||
// müssen trotzdem stehen: `transportations` hat als einziger Parameter keinen Vorgabewert,
|
||||
// und PHP macht damit auch alle optionalen Parameter davor zu Pflichtangaben.
|
||||
invoiceTypeExtended: null,
|
||||
travelRoute: null,
|
||||
distance: null,
|
||||
passengers: null,
|
||||
transportations: null,
|
||||
|
||||
// MUSS null bleiben (nicht ''): CreateInvoiceCommand verwirft die user_id, sobald hier etwas
|
||||
// steht -- der Teili fände seine Abrechnung dann nicht unter "Meine Abrechnungen".
|
||||
paymentPurpose: null,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enumerations;
|
||||
|
||||
use App\Scopes\CommonModel;
|
||||
|
||||
/**
|
||||
* Gründe für eine Reisekostenabrechnung -- DB-gestützt (analog {@see RetentionReason}), damit die
|
||||
* Auswahl ohne Deployment pflegbar bleibt.
|
||||
*
|
||||
* Gespeichert wird an der Abrechnung der Slug, bei `other` stattdessen der Freitext: Der Slug sagt für
|
||||
* sich nichts aus, der Text alles. {@see \App\Models\Invoice::travelReasonText()} löst beides auf.
|
||||
*
|
||||
* @property string $slug
|
||||
* @property string $name
|
||||
* @property bool $requires_note
|
||||
* @property int $sort_order
|
||||
*/
|
||||
class TravelReason extends CommonModel
|
||||
{
|
||||
public const string EVENT_TRAVEL = 'event_travel';
|
||||
|
||||
public const string MATERIAL_TRANSPORT = 'material_transport';
|
||||
|
||||
public const string PURCHASE = 'purchase';
|
||||
|
||||
public const string OTHER = 'other';
|
||||
|
||||
protected $table = 'travel_reasons';
|
||||
protected $primaryKey = 'slug';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'slug',
|
||||
'name',
|
||||
'requires_note',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'requires_note' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* Ein gespeicherter Wert als lesbarer Text: der Name des Grundes, sonst der Wert selbst.
|
||||
*
|
||||
* Der ist dann der Freitext von "Anderer Grund" oder stammt aus der Zeit vor der Auswahl -- beides
|
||||
* ist bereits die Antwort auf die Frage und braucht keine Übersetzung.
|
||||
*/
|
||||
public static function text(?string $value): string
|
||||
{
|
||||
return self::find($value)?->name ?? (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionen für das Frontend. `requiresNote` steuert dort das Freitextfeld.
|
||||
*
|
||||
* @return array<int, array{value: string, label: string, requiresNote: bool}>
|
||||
*/
|
||||
public static function options(): array
|
||||
{
|
||||
return self::orderBy('sort_order')->get()
|
||||
->map(static fn (self $reason): array => [
|
||||
'value' => $reason->slug,
|
||||
'label' => $reason->name,
|
||||
'requiresNote' => $reason->requires_note,
|
||||
])
|
||||
->all();
|
||||
}
|
||||
}
|
||||
+44
-11
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Enumerations\InvoiceStatus;
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Enumerations\TravelReason;
|
||||
use App\Scopes\InstancedModel;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
@@ -14,6 +15,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property string $status
|
||||
* @property string $type
|
||||
* @property string $type_other
|
||||
* @property string $purpose
|
||||
* @property boolean $donation
|
||||
* @property string $user_id
|
||||
* @property string $contact_name
|
||||
@@ -26,8 +28,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property string $comment
|
||||
* @property string $changes
|
||||
* @property string $travel_direction
|
||||
* @property boolean $passengers
|
||||
* @property boolean $transportation
|
||||
* @property boolean $passengers Nicht mehr erfasst -- siehe Hinweis unten
|
||||
* @property boolean $transportation Nicht mehr erfasst -- siehe Hinweis unten
|
||||
* @property string $document_filename
|
||||
* @property string $approved_by
|
||||
* @property string $approved_at
|
||||
@@ -35,6 +37,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property string $denied_by
|
||||
* @property string $denied_at
|
||||
* @property string $denied_reason
|
||||
*
|
||||
* `passengers` ("Ich habe Personen mitgenommen") und `transportation` ("Ich habe Material transportiert")
|
||||
* werden seit dem Ausbau des Reisekosten-Formulars nicht mehr erfasst: Sie waren reine Ja/Nein-Angaben
|
||||
* ohne Wirkung auf den Betrag -- eine Kostenstelle führt genau eine Kilometerpauschale, es gibt also
|
||||
* keinen zweiten Satz, auf den sie umschalten könnten. Wer gereist ist, steht jetzt im `purpose`.
|
||||
* Die Spalten bleiben für die Altdaten stehen; sollen sie je wiederkommen, dann als Angaben, die in die
|
||||
* Berechnung eingehen (Mitnahmeentschädigung: eine Anzahl, kein Häkchen).
|
||||
*/
|
||||
class Invoice extends InstancedModel
|
||||
{
|
||||
@@ -45,6 +54,7 @@ class Invoice extends InstancedModel
|
||||
'status',
|
||||
'type',
|
||||
'type_other',
|
||||
'purpose',
|
||||
'donation',
|
||||
'user_id',
|
||||
'contact_name',
|
||||
@@ -94,19 +104,42 @@ class Invoice extends InstancedModel
|
||||
/**
|
||||
* Wofür der Beleg steht -- der "Zahlungsgrund" der Beleglisten und der "Zweck" der EüR-Anlage.
|
||||
*
|
||||
* `type_other` ("Was wurde eingekauft") trägt seit der Pflichtangabe zu jeder Abrechnung den Zweck.
|
||||
* Bei Fahrtkosten bleibt das Feld leer: dort schreibt der Einreiche-Flow die Strecke nach
|
||||
* `travel_direction`. Den Zweck trägt dann der Reisegrund, ergänzt um den Namen der reisenden
|
||||
* Person -- bei einer Fahrt ist "wer" Teil der Begründung, nicht bloß Kontaktangabe.
|
||||
* Erfasst wird er beim Anlegen und steht danach in der eigenen Spalte: eine Angabe, keine Ableitung.
|
||||
* Nur so lässt er sich korrigieren, ohne dass ihn der nächste Vorgang wieder überschreibt.
|
||||
*
|
||||
* Ältere Belege und Beitragserstattungen haben nichts davon gesetzt; dann bleibt der Text leer.
|
||||
* Ein "--" würde in einer Belegliste nur Platz kosten.
|
||||
* Bestandsbelege haben die Spalte leer -- für sie wird weiter abgeleitet, bei Fahrtkosten aus
|
||||
* Reisegrund und `contact_name`, weil es die Frage "wer ist gefahren" damals nicht gab. Bleibt auch
|
||||
* dabei nichts übrig, bleibt der Text leer; ein "--" würde in einer Belegliste nur Platz kosten.
|
||||
*/
|
||||
public function purposeText() : string {
|
||||
$parts = $this->type === InvoiceType::INVOICE_TYPE_TRAVELLING
|
||||
? [$this->travel_reason, $this->contact_name]
|
||||
: [$this->type_other];
|
||||
if (trim((string) $this->purpose) !== '') {
|
||||
return $this->purpose;
|
||||
}
|
||||
|
||||
return self::joinPurposeParts(
|
||||
$this->type === InvoiceType::INVOICE_TYPE_TRAVELLING
|
||||
? [$this->travelReasonText(), $this->contact_name]
|
||||
: [$this->type_other]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Reisegrund als lesbarer Text.
|
||||
*
|
||||
* Der Name des gewählten Grundes -- oder der Wert selbst, wenn er zu keinem passt. Der ist dann der
|
||||
* Freitext von "Anderer Grund" oder stammt aus der Zeit vor der Auswahl.
|
||||
*/
|
||||
public function travelReasonText() : string {
|
||||
return TravelReason::text($this->travel_reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt einen Zahlungsgrund aus seinen Teilen zusammen: leere fallen weg, damit kein " — " führt
|
||||
* oder hängt.
|
||||
*
|
||||
* @param array<int, ?string> $parts
|
||||
*/
|
||||
public static function joinPurposeParts(array $parts) : string {
|
||||
$parts = array_map(fn ($part) => trim((string) $part), $parts);
|
||||
|
||||
return implode(' — ', array_filter($parts, fn ($part) => $part !== ''));
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Enumerations\EatingHabit;
|
||||
use App\Enumerations\InvoiceType;
|
||||
use App\Enumerations\RefundReason;
|
||||
use App\Enumerations\RetentionReason;
|
||||
use App\Enumerations\TravelReason;
|
||||
use App\Enumerations\UserRole;
|
||||
use App\Models\AvailablePaymentMethod;
|
||||
use App\Models\Tenant;
|
||||
@@ -211,6 +212,11 @@ class GlobalDataProvider {
|
||||
return response()->json(RetentionReason::options());
|
||||
}
|
||||
|
||||
/** Auswahl der Reisegründe für die Reisekostenabrechnung. */
|
||||
public function getTravelReasons() : JsonResponse {
|
||||
return response()->json(TravelReason::options());
|
||||
}
|
||||
|
||||
public function getEventSettingData(Request $request) : JsonResponse {
|
||||
return response()->json(
|
||||
[
|
||||
|
||||
@@ -50,11 +50,11 @@ class InvoiceResource {
|
||||
$returnData['changes'] = $this->invoice->changes ?? '--';
|
||||
$returnData['deniedReason'] = $this->invoice->denied_reason ?? '--';
|
||||
$returnData['travelDirection'] = $this->invoice->travel_direction ?? '--';
|
||||
$returnData['travelReason'] = $this->invoice->travel_reason ?? '--';
|
||||
$returnData['travelReason'] = $this->invoice->travel_reason === null
|
||||
? '--'
|
||||
: $this->invoice->travelReasonText();
|
||||
$returnData['distance'] = $this->invoice->distance ?? '--';
|
||||
$returnData['distanceAllowance'] = new Amount($this->invoice->costUnit()->first()->distance_allowance, '')->toString();
|
||||
$returnData['passengers'] = $this->invoice->passengers ? 'Ja' : 'Nein';
|
||||
$returnData['transportation'] = $this->invoice->transportation ? 'Ja' : 'Nein';
|
||||
$returnData['travelRoute'] = $this->invoice->travel_direction;
|
||||
$returnData['costUnitId'] = $this->invoice->cost_unit_id;
|
||||
$returnData['amountPlain'] = new Amount($this->invoice->amount, '')->toString();
|
||||
|
||||
Reference in New Issue
Block a user