Ausgehende SEPA-Mandate einführen #183

This commit is contained in:
2026-05-15 17:47:11 +02:00
parent 683d073b6e
commit 44017a768b
19 changed files with 513 additions and 8 deletions

View File

@@ -170,7 +170,7 @@ const setupQuery = () => {
Object.keys(data).forEach(key => {
if (dataType.templateColumns.find(i => i.key === key)) {
if (["customer", "contract", "plant", "contact", "project"].includes(key)) {
if (["customer", "contract", "plant", "contact", "project", "outgoingsepamandate", "bankaccount"].includes(key)) {
item.value[key] = Number(data[key])
} else {
item.value[key] = data[key]

View File

@@ -146,6 +146,11 @@ const links = computed(() => {
to: "/incomingInvoices",
icon: "i-heroicons-document-text",
} : null,
featureEnabled("outgoingsepamandates") ? {
label: "SEPA-Mandate",
to: "/standardEntity/outgoingsepamandates",
icon: "i-heroicons-identification",
} : null,
(featureEnabled("incomingInvoices") || featureEnabled("banking")) ? {
label: "Abschreibungen",
to: "/accounting/depreciation",

View File

@@ -0,0 +1,16 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
}
})
</script>
<template>
<span v-if="props.row.bankaccount">
{{ props.row.bankaccount.displayLabel || props.row.bankaccount.description || props.row.bankaccount.id }}
</span>
<span v-else>-</span>
</template>

View File

@@ -0,0 +1,26 @@
<script setup>
const props = defineProps({
row: {
type: Object,
required: true,
default: {}
},
inShow: {
type: Boolean,
default: false
}
})
</script>
<template>
<div v-if="props.row.outgoingsepamandate">
<nuxt-link
v-if="props.inShow"
:to="`/standardEntity/outgoingsepamandates/show/${props.row.outgoingsepamandate.id}`"
>
{{ props.row.outgoingsepamandate.reference }}
</nuxt-link>
<span v-else>{{ props.row.outgoingsepamandate.reference }}</span>
</div>
<span v-else>-</span>
</template>

View File

@@ -51,6 +51,7 @@ const itemInfo = ref({
dateOfPerformance: null,
paymentDays: auth.activeTenantData.standardPaymentDays,
payment_type: "transfer",
outgoingsepamandate: null,
availableInPortal: false,
customSurchargePercentage: 0,
created_by: auth.user.id,
@@ -90,6 +91,7 @@ const selectedServicecategorie = ref(null)
const customers = ref([])
const contacts = ref([])
const contracts = ref([])
const outgoingsepamandates = ref([])
const texttemplates = ref([])
const units = ref([])
const tenantUsers = ref([])
@@ -204,6 +206,7 @@ const setupData = async () => {
customers.value = await useEntities("customers").select("*", "customerNumber")
contacts.value = await useEntities("contacts").select("*")
contracts.value = await useEntities("contracts").select("*")
outgoingsepamandates.value = await useEntities("outgoingsepamandates").select("*")
texttemplates.value = await useEntities("texttemplates").select("*")
units.value = await useEntities("units").selectSpecial("*")
tenantUsers.value = (await useNuxtApp().$api(`/api/tenant/users`, {
@@ -211,6 +214,43 @@ const setupData = async () => {
})).users
}
const getMandateCustomerId = (mandate) => mandate?.customer?.id || mandate?.customer
const availableSepaMandates = computed(() => {
return outgoingsepamandates.value.filter((mandate) => {
return !mandate.archived
&& mandate.status === "Aktiv"
&& (!itemInfo.value.customer || getMandateCustomerId(mandate) === itemInfo.value.customer)
})
})
const applyDefaultSepaMandate = () => {
if (itemInfo.value.payment_type !== "direct-debit") {
itemInfo.value.outgoingsepamandate = null
return
}
const selectedContract = contracts.value.find((contract) => contract.id === itemInfo.value.contract)
const contractMandateId = selectedContract?.outgoingsepamandate?.id || selectedContract?.outgoingsepamandate
if (contractMandateId && availableSepaMandates.value.find((mandate) => mandate.id === contractMandateId)) {
itemInfo.value.outgoingsepamandate = contractMandateId
return
}
if (itemInfo.value.outgoingsepamandate && availableSepaMandates.value.find((mandate) => mandate.id === itemInfo.value.outgoingsepamandate)) {
return
}
const defaultMandate = availableSepaMandates.value.find((mandate) => mandate.defaultMandate)
|| availableSepaMandates.value[0]
itemInfo.value.outgoingsepamandate = defaultMandate?.id || null
}
watch(
() => [itemInfo.value.customer, itemInfo.value.contract, itemInfo.value.payment_type, outgoingsepamandates.value.length],
() => applyDefaultSepaMandate()
)
const loaded = ref(false)
const normalizeEntityId = (value) => {
if (value === null || typeof value === "undefined") return null
@@ -1476,6 +1516,7 @@ const saveSerialInvoice = async () => {
project: itemInfo.value.project,
paymentDays: itemInfo.value.paymentDays,
payment_type: itemInfo.value.payment_type,
outgoingsepamandate: itemInfo.value.outgoingsepamandate,
deliveryDateType: "Leistungszeitraum",
createdBy: itemInfo.value.createdBy,
created_by: itemInfo.value.created_by,
@@ -1563,6 +1604,7 @@ const saveDocument = async (state, resetup = false) => {
deliveryDateEnd: itemInfo.value.deliveryDateEnd,
paymentDays: itemInfo.value.paymentDays,
payment_type: itemInfo.value.payment_type,
outgoingsepamandate: itemInfo.value.outgoingsepamandate,
deliveryDateType: itemInfo.value.deliveryDateType,
info: {},
createdBy: itemInfo.value.createdBy,
@@ -2242,6 +2284,29 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</template>
</USelectMenu>
</UFormField>
<UFormField
v-if="itemInfo.payment_type === 'direct-debit'"
class="min-w-0 flex-1"
label="SEPA-Mandat:"
>
<USelectMenu
v-model="itemInfo.outgoingsepamandate"
value-key="id"
label-key="reference"
:items="availableSepaMandates"
:search-input="{ placeholder: 'Suche...' }"
:filter-fields="['reference', 'status']"
class="w-full"
:disabled="!itemInfo.customer"
>
<template #default>
{{ itemInfo.outgoingsepamandate ? availableSepaMandates.find(i => i.id === itemInfo.outgoingsepamandate)?.reference : "Kein Mandat ausgewählt" }}
</template>
<template #item="{ item: mandate }">
{{ mandate.reference }} - {{ mandate.bankaccount?.displayLabel || "Bankverbindung" }}
</template>
</USelectMenu>
</UFormField>
<UFormField
class="min-w-0 flex-1"
label="Individueller Aufschlag:"

View File

@@ -35,6 +35,7 @@ const defaultFeatures = {
createDocument: true,
serialInvoice: true,
incomingInvoices: true,
outgoingsepamandates: true,
costcentres: true,
branches: true,
teams: true,
@@ -82,6 +83,7 @@ const featureOptions = [
{ key: "createDocument", label: "Buchhaltung: Ausgangsbelege" },
{ key: "serialInvoice", label: "Buchhaltung: Serienvorlagen" },
{ key: "incomingInvoices", label: "Buchhaltung: Eingangsbelege" },
{ key: "outgoingsepamandates", label: "Buchhaltung: Ausgehende SEPA-Mandate" },
{ key: "costcentres", label: "Buchhaltung: Kostenstellen" },
{ key: "branches", label: "Stammdaten: Niederlassungen" },
{ key: "teams", label: "Mitarbeiter: Teams" },

View File

@@ -50,6 +50,8 @@ import {useFunctions} from "~/composables/useFunctions.js";
import signDate from "~/components/columnRenderings/signDate.vue";
import sepaDate from "~/components/columnRenderings/sepaDate.vue";
import bankAccounts from "~/components/columnRenderings/bankAccounts.vue";
import bankAccount from "~/components/columnRenderings/bankAccount.vue";
import outgoingSepaMandate from "~/components/columnRenderings/outgoingSepaMandate.vue";
// @ts-ignore
export const useDataStore = defineStore('data', () => {
@@ -173,7 +175,7 @@ export const useDataStore = defineStore('data', () => {
numberRangeHolder: "customerNumber",
historyItemHolder: "customer",
sortColumn: "customerNumber",
selectWithInformation: "*, projects(*), plants(*), contracts(*), contacts(*), createddocuments(*, statementallocations(*)), files(*), events(*), customerinventoryitems(*), customerspaces(*)",
selectWithInformation: "*, projects(*), plants(*), contracts(*), outgoingsepamandates(*, bankaccount(*)), contacts(*), createddocuments(*, statementallocations(*)), files(*), events(*), customerinventoryitems(*), customerspaces(*)",
filters: [{
name: "Archivierte ausblenden",
default: true,
@@ -457,7 +459,7 @@ export const useDataStore = defineStore('data', () => {
inputColumn: "Allgemeines"
},*/
],
showTabs: [{label: 'Informationen'},{label: 'Ansprechpartner'},{label: 'Dateien'},{label: 'Ausgangsbelege'},{label: 'Projekte'},{label: 'Objekte'},{label: 'Termine'},{label: 'Verträge'},{label: 'Kundeninventar', key: 'customerinventoryitems'},{label: 'Kundenlagerplätze', key: 'customerspaces'},{label: 'Wiki'}]
showTabs: [{label: 'Informationen'},{label: 'Ansprechpartner'},{label: 'Dateien'},{label: 'Ausgangsbelege'},{label: 'Projekte'},{label: 'Objekte'},{label: 'Termine'},{label: 'Verträge'},{label: 'Ausgehende SEPA-Mandate', key: 'outgoingsepamandates', type: 'outgoingsepamandates'},{label: 'Kundeninventar', key: 'customerinventoryitems'},{label: 'Kundenlagerplätze', key: 'customerspaces'},{label: 'Wiki'}]
},
members: {
isArchivable: true,
@@ -822,7 +824,7 @@ export const useDataStore = defineStore('data', () => {
"Allgemeines",
"Abrechnung"
],
selectWithInformation: "*, customer(*), contracttype(*), files(*)",
selectWithInformation: "*, customer(*), contracttype(*), outgoingsepamandate(*, bankaccount(*)), files(*)",
templateColumns: [
{
key: 'contractNumber',
@@ -936,6 +938,22 @@ export const useDataStore = defineStore('data', () => {
{label:'Überweisung'}
],
inputColumn: "Abrechnung"
},{
key: 'outgoingsepamandate',
label: "SEPA-Mandat",
component: outgoingSepaMandate,
inputType: "select",
selectDataType: "outgoingsepamandates",
selectOptionAttribute: "reference",
selectSearchAttributes: ["reference"],
selectDataTypeFilter: function (i, item) {
const mandateCustomer = i.customer?.id || i.customer
return !item.customer || mandateCustomer === item.customer
},
showFunction: function (item) {
return item.paymentType === "Einzug"
},
inputColumn: "Abrechnung"
},{
key: "billingInterval",
label: "Abrechnungsintervall",
@@ -1129,6 +1147,145 @@ export const useDataStore = defineStore('data', () => {
],
showTabs: [{ label: "Informationen" }]
},
outgoingsepamandates: {
isArchivable: true,
label: "Ausgehende SEPA-Mandate",
labelSingle: "Ausgehendes SEPA-Mandat",
isStandardEntity: true,
redirect: true,
numberRangeHolder: "reference",
historyItemHolder: "outgoingsepamandate",
sortColumn: "reference",
selectWithInformation: "*, customer(*), bankaccount(*)",
filters: [{
name: "Archivierte ausblenden",
default: true,
"filterFunction": function (row) {
return !row.archived
}
}],
inputColumns: [
"Allgemeines",
"Mandat"
],
templateColumns: [
{
key: "reference",
label: "Mandatsreferenz",
title: true,
required: true,
inputIsNumberRange: true,
inputType: "text",
inputColumn: "Allgemeines",
sortable: true
},
{
key: "customer",
label: "Kunde",
component: customer,
required: true,
inputType: "select",
selectDataType: "customers",
selectOptionAttribute: "name",
selectSearchAttributes: ["name", "customerNumber"],
inputColumn: "Allgemeines",
sortable: true
},
{
key: "bankaccount",
label: "Bankverbindung",
component: bankAccount,
required: true,
inputType: "select",
selectDataType: "entitybankaccounts",
selectOptionAttribute: "displayLabel",
selectSearchAttributes: ["displayLabel", "iban", "bankName"],
inputColumn: "Mandat"
},
{
key: "status",
label: "Status",
required: true,
defaultValue: "Entwurf",
inputType: "select",
selectValueAttribute: "label",
selectManualOptions: [
{ label: "Entwurf" },
{ label: "Aktiv" },
{ label: "Widerrufen" },
{ label: "Abgelaufen" }
],
inputColumn: "Allgemeines",
sortable: true
},
{
key: "mandateType",
label: "Mandatstyp",
required: true,
defaultValue: "CORE",
inputType: "select",
selectValueAttribute: "label",
selectManualOptions: [
{ label: "CORE" },
{ label: "B2B" }
],
inputColumn: "Mandat",
sortable: true
},
{
key: "sequenceType",
label: "Sequenz",
required: true,
defaultValue: "RCUR",
inputType: "select",
selectValueAttribute: "label",
selectManualOptions: [
{ label: "RCUR" },
{ label: "OOFF" },
{ label: "FRST" },
{ label: "FNAL" }
],
inputColumn: "Mandat",
sortable: true
},
{
key: "signedAt",
label: "Unterschrieben am",
inputType: "date",
inputColumn: "Mandat",
sortable: true
},
{
key: "validFrom",
label: "Gültig ab",
inputType: "date",
inputColumn: "Mandat",
sortable: true
},
{
key: "validUntil",
label: "Gültig bis",
inputType: "date",
inputColumn: "Mandat",
sortable: true
},
{
key: "defaultMandate",
label: "Standardmandat",
inputType: "bool",
defaultValue: false,
inputColumn: "Allgemeines",
sortable: true
},
{
key: "notes",
label: "Notizen",
inputType: "textarea",
inputColumn: "Allgemeines"
}
],
showTabs: [{ label: "Informationen" }, { label: "Wiki" }]
},
absencerequests: {
isArchivable: true,
label: "Abwesenheiten",