Calculation errors for refunded amounts
This commit is contained in:
@@ -51,15 +51,82 @@ const openRefundDialogSwitch = ref(false);
|
||||
// Der Erstattungsdialog. `captureMode` steuert den Weg: 'participant' schickt dem Teili einen Link, über
|
||||
// den er seine Bankverbindung selbst einträgt; 'management' heißt, sie liegt der Aktionsleitung bereits
|
||||
// vor -- dann wird die Erstattung sofort eingereicht.
|
||||
const refundForm = reactive({amount: '', reason: '', reasonNote: '', captureMode: 'participant', accountOwner: '', accountIban: ''});
|
||||
const refundForm = reactive({
|
||||
amount: '', reason: '', reasonNote: '',
|
||||
captureMode: 'participant', accountOwner: '', accountIban: '',
|
||||
retentionReason: '', retentionReasonNote: '',
|
||||
});
|
||||
const refundErrors = reactive({amount: '', reason: '', reasonNote: '', accountOwner: '', accountIban: ''});
|
||||
const refundReasons = ref([]);
|
||||
const retentionReasons = ref([]);
|
||||
const refundSaving = ref(false);
|
||||
|
||||
const selectedRefundReason = computed(
|
||||
() => refundReasons.value.find(r => r.value === refundForm.reason) ?? null
|
||||
);
|
||||
|
||||
const selectedRetentionReason = computed(
|
||||
() => retentionReasons.value.find(r => r.value === refundForm.retentionReason) ?? null
|
||||
);
|
||||
|
||||
/** Was nach der Erstattung beim Verband bleibt -- die Grundlage für den Einbehaltungsblock. */
|
||||
const retainedAmount = computed(() => {
|
||||
const paid = Number(showParticipant.value?.amountPaidValue ?? 0);
|
||||
const refunded = Number((refundForm.amount ?? '').replace(',', '.'));
|
||||
|
||||
if (!Number.isFinite(refunded)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const remaining = Math.round((paid - refunded) * 100) / 100;
|
||||
|
||||
return remaining > 0.005 ? remaining : 0;
|
||||
});
|
||||
|
||||
const hasRetention = computed(() => retainedAmount.value > 0);
|
||||
|
||||
const retainedAmountReadable = computed(
|
||||
() => retainedAmount.value.toFixed(2).replace('.', ',') + ' Euro'
|
||||
);
|
||||
|
||||
/**
|
||||
* Ob abgesendet werden kann. Der Knopf erscheint erst dann -- was noch fehlt, soll die Aktionsleitung
|
||||
* sehen, bevor sie klickt, statt danach eine Fehlermeldung zu lesen.
|
||||
*/
|
||||
const refundFormComplete = computed(() => {
|
||||
const amount = Number((refundForm.amount ?? '').replace(',', '.'));
|
||||
const paid = Number(showParticipant.value?.amountPaidValue ?? 0);
|
||||
|
||||
if (!refundForm.amount || !(amount > 0) || amount > paid + 0.005) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!refundForm.reason) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedRefundReason.value?.requiresNote && !refundForm.reasonNote.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bleibt etwas beim Verband, muss begründet sein, warum.
|
||||
if (hasRetention.value) {
|
||||
if (!refundForm.retentionReason) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedRetentionReason.value?.requiresNote && !refundForm.retentionReasonNote.trim()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (refundForm.captureMode === 'management') {
|
||||
return refundForm.accountOwner.trim() !== '' && refundForm.accountIban.trim() !== '';
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
defineEmits(['showParticipantDetails', 'markCocExisting', 'paymentComplete'])
|
||||
|
||||
function openParticipantDetails(input) {
|
||||
@@ -323,6 +390,8 @@ async function openRefundDialog(participant) {
|
||||
refundForm.captureMode = 'participant';
|
||||
refundForm.accountOwner = '';
|
||||
refundForm.accountIban = '';
|
||||
refundForm.retentionReason = '';
|
||||
refundForm.retentionReasonNote = '';
|
||||
|
||||
Object.keys(refundErrors).forEach(key => refundErrors[key] = '');
|
||||
|
||||
@@ -331,6 +400,11 @@ async function openRefundDialog(participant) {
|
||||
refundReasons.value = reasons ?? [];
|
||||
}
|
||||
|
||||
if (retentionReasons.value.length === 0) {
|
||||
const reasons = await request('/api/v1/core/retrieve-retention-reasons', {method: 'GET'});
|
||||
retentionReasons.value = reasons ?? [];
|
||||
}
|
||||
|
||||
openRefundDialogSwitch.value = true;
|
||||
}
|
||||
|
||||
@@ -384,6 +458,9 @@ async function execRefund() {
|
||||
// Leer beim Weg über den Teili -- dann verschickt der Server nur den Link.
|
||||
accountOwner: refundForm.captureMode === 'management' ? refundForm.accountOwner : '',
|
||||
accountIban: refundForm.captureMode === 'management' ? refundForm.accountIban : '',
|
||||
// Leer bei voller Erstattung -- dann gibt es nichts zu begründen.
|
||||
retentionReason: hasRetention.value ? refundForm.retentionReason : '',
|
||||
retentionReasonNote: hasRetention.value ? refundForm.retentionReasonNote : '',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -479,6 +556,15 @@ function mailToGroup(groupKey) {
|
||||
<td class="pl-amount" :id="'participant-' + participant.identifier +'-payment'" :class="participant.amount_left_value != 0 && !participant.unregistered ? 'not-paid' : ''">
|
||||
Gezahlt: <label :id="'participant-' + participant.identifier + '-paid'">{{ participant?.amountPaid.readable }}</label> /<br />
|
||||
Gesamt: <label :id="'participant-' + participant.identifier + '-expected'">{{ participant?.amountExpected.readable }}</label>
|
||||
|
||||
<!-- Warum ein Teil des Beitrags beim Verband geblieben ist. -->
|
||||
<span v-if="participant.refund?.hasRetention" class="retention-note">
|
||||
Einbehalten: {{ participant.refund.retainedAmount }}<br />
|
||||
{{ participant.refund.retentionReasonLabel }}<template
|
||||
v-if="participant.refund.retentionReasonNote"
|
||||
> – {{ participant.refund.retentionReasonNote }}</template>
|
||||
</span>
|
||||
|
||||
<br /><br />
|
||||
<span v-if="participant.amount_left_value != 0 && !participant.unregistered" :id="'participant-' + participant.identifier + '-actions'">
|
||||
<span class="link" style="font-size:10pt;" @click="paymentComplete(participant)">Zahlung buchen</span>
|
||||
@@ -655,6 +741,36 @@ function mailToGroup(groupKey) {
|
||||
<ErrorText :message="refundErrors.reasonNote" />
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Bleibt ein Teil beim Verband, muss begründet sein, warum -- ein einbehaltener Betrag ohne
|
||||
Grund ist in der Buchhaltung nicht haltbar. Bei voller Erstattung gibt es nichts zu zeigen.
|
||||
-->
|
||||
<template v-if="hasRetention">
|
||||
<p class="refund-hint">
|
||||
<strong>{{ retainedAmountReadable }}</strong> verbleiben beim Verband.
|
||||
</p>
|
||||
|
||||
<div class="refund-field">
|
||||
<label for="refund_retention_reason">Grund der Einbehaltung</label>
|
||||
<select id="refund_retention_reason" v-model="refundForm.retentionReason" class="form-input">
|
||||
<option value="">Bitte auswählen …</option>
|
||||
<option v-for="reason in retentionReasons" :key="reason.value" :value="reason.value">
|
||||
{{ reason.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedRetentionReason?.requiresNote" class="refund-field">
|
||||
<label for="refund_retention_note">Erläuterung zur Einbehaltung</label>
|
||||
<textarea
|
||||
id="refund_retention_note"
|
||||
v-model="refundForm.retentionReasonNote"
|
||||
class="form-input"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--
|
||||
Liegt die Bankverbindung schon vor, entfällt der Umweg über den Teili: die Erstattung wird
|
||||
sofort eingereicht. Er bekommt den Beleg trotzdem.
|
||||
@@ -694,7 +810,13 @@ function mailToGroup(groupKey) {
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<button class="button" :disabled="refundSaving" @click="execRefund()">
|
||||
<!-- Erscheint erst, wenn alles ausgefüllt ist; während des Speicherns gesperrt statt weg. -->
|
||||
<button
|
||||
v-if="refundFormComplete"
|
||||
class="button"
|
||||
:disabled="refundSaving"
|
||||
@click="execRefund()"
|
||||
>
|
||||
<template v-if="refundSaving">Wird gespeichert…</template>
|
||||
<template v-else-if="refundForm.captureMode === 'management'">Erstattung einreichen</template>
|
||||
<template v-else>Erstattung freigeben</template>
|
||||
@@ -749,6 +871,14 @@ function mailToGroup(groupKey) {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.retention-note {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 10pt;
|
||||
color: #ca5a0a;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.refund-hint {
|
||||
margin-bottom: 14px;
|
||||
padding: 8px 10px;
|
||||
|
||||
Reference in New Issue
Block a user