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

@@ -0,0 +1,53 @@
CREATE TABLE "outgoingsepamandates" (
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "outgoingsepamandates_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"tenant" bigint NOT NULL,
"customer" bigint NOT NULL,
"bankaccount" bigint NOT NULL,
"reference" text NOT NULL,
"status" text DEFAULT 'Entwurf' NOT NULL,
"mandate_type" text DEFAULT 'CORE' NOT NULL,
"sequence_type" text DEFAULT 'RCUR' NOT NULL,
"signed_at" timestamp with time zone,
"valid_from" timestamp with time zone,
"valid_until" timestamp with time zone,
"default_mandate" boolean DEFAULT false NOT NULL,
"notes" text,
"archived" boolean DEFAULT false NOT NULL,
"updated_at" timestamp with time zone,
"updated_by" uuid
);
--> statement-breakpoint
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_bankaccount_entitybankaccounts_id_fk" FOREIGN KEY ("bankaccount") REFERENCES "public"."entitybankaccounts"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "contracts" ADD COLUMN "outgoingsepamandate" bigint;
--> statement-breakpoint
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_outgoingsepamandate_outgoingsepamandates_id_fk" FOREIGN KEY ("outgoingsepamandate") REFERENCES "public"."outgoingsepamandates"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "createddocuments" ADD COLUMN "outgoingsepamandate" bigint;
--> statement-breakpoint
ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_outgoingsepamandate_outgoingsepamandates_id_fk" FOREIGN KEY ("outgoingsepamandate") REFERENCES "public"."outgoingsepamandates"("id") ON DELETE no action ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "historyitems" ADD COLUMN "outgoingsepamandate" bigint;
--> statement-breakpoint
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_outgoingsepamandate_outgoingsepamandates_id_fk" FOREIGN KEY ("outgoingsepamandate") REFERENCES "public"."outgoingsepamandates"("id") ON DELETE cascade ON UPDATE no action;
--> statement-breakpoint
ALTER TABLE "tenants" ALTER COLUMN "numberRanges" SET DEFAULT '{"vendors":{"prefix":"","suffix":"","nextNumber":10000},"customers":{"prefix":"","suffix":"","nextNumber":10000},"products":{"prefix":"AT-","suffix":"","nextNumber":1000},"quotes":{"prefix":"AN-","suffix":"","nextNumber":1000},"costEstimates":{"prefix":"KS-","suffix":"","nextNumber":1000},"confirmationOrders":{"prefix":"AB-","suffix":"","nextNumber":1000},"invoices":{"prefix":"RE-","suffix":"","nextNumber":1000},"deliveryNotes":{"prefix":"LS-","suffix":"","nextNumber":1000},"packingSlips":{"prefix":"PS-","suffix":"","nextNumber":1000},"spaces":{"prefix":"LP-","suffix":"","nextNumber":1000},"customerspaces":{"prefix":"KLP-","suffix":"","nextNumber":1000},"inventoryitems":{"prefix":"IA-","suffix":"","nextNumber":1000},"customerinventoryitems":{"prefix":"KIA-","suffix":"","nextNumber":1000},"projects":{"prefix":"PRJ-","suffix":"","nextNumber":1000},"costcentres":{"prefix":"KST-","suffix":"","nextNumber":1000},"outgoingsepamandates":{"prefix":"SEPA-","suffix":"","nextNumber":1000}}'::jsonb;
--> statement-breakpoint
UPDATE "tenants"
SET "numberRanges" = COALESCE("numberRanges", '{}'::jsonb) || jsonb_build_object(
'outgoingsepamandates',
COALESCE("numberRanges"->'outgoingsepamandates', '{"prefix":"SEPA-","suffix":"","nextNumber":1000}'::jsonb)
);
--> statement-breakpoint
UPDATE "tenants"
SET "features" = COALESCE("features", '{}'::jsonb) || jsonb_build_object(
'outgoingsepamandates',
COALESCE("features"->'outgoingsepamandates', 'true'::jsonb)
);

View File

@@ -253,6 +253,13 @@
"when": 1778194800000, "when": 1778194800000,
"tag": "0036_allowed_contracttypes", "tag": "0036_allowed_contracttypes",
"breakpoints": true "breakpoints": true
},
{
"idx": 36,
"version": "7",
"when": 1778840100000,
"tag": "0037_outgoing_sepa_mandates",
"breakpoints": true
} }
] ]
} }

View File

@@ -13,6 +13,7 @@ import { customers } from "./customers"
import { contacts } from "./contacts" import { contacts } from "./contacts"
import { contracttypes } from "./contracttypes" import { contracttypes } from "./contracttypes"
import { authUsers } from "./auth_users" import { authUsers } from "./auth_users"
import { outgoingsepamandates } from "./outgoingsepamandates"
export const contracts = pgTable( export const contracts = pgTable(
"contracts", "contracts",
@@ -60,6 +61,9 @@ export const contracts = pgTable(
bankingOwner: text("bankingOwner"), bankingOwner: text("bankingOwner"),
sepaRef: text("sepaRef"), sepaRef: text("sepaRef"),
sepaDate: timestamp("sepaDate", { withTimezone: true }), sepaDate: timestamp("sepaDate", { withTimezone: true }),
outgoingsepamandate: bigint("outgoingsepamandate", { mode: "number" }).references(
() => outgoingsepamandates.id
),
paymentType: text("paymentType"), paymentType: text("paymentType"),
billingInterval: text("billingInterval"), billingInterval: text("billingInterval"),

View File

@@ -19,6 +19,7 @@ import { projects } from "./projects"
import { plants } from "./plants" import { plants } from "./plants"
import { authUsers } from "./auth_users" import { authUsers } from "./auth_users"
import {serialExecutions} from "./serialexecutions"; import {serialExecutions} from "./serialexecutions";
import { outgoingsepamandates } from "./outgoingsepamandates"
export const createddocuments = pgTable("createddocuments", { export const createddocuments = pgTable("createddocuments", {
id: bigint("id", { mode: "number" }) id: bigint("id", { mode: "number" })
@@ -118,6 +119,10 @@ export const createddocuments = pgTable("createddocuments", {
() => contracts.id () => contracts.id
), ),
outgoingsepamandate: bigint("outgoingsepamandate", { mode: "number" }).references(
() => outgoingsepamandates.id
),
serialexecution: uuid("serialexecution").references(() => serialExecutions.id) serialexecution: uuid("serialexecution").references(() => serialExecutions.id)
}) })

View File

@@ -36,6 +36,7 @@ import { authUsers } from "./auth_users"
import {files} from "./files"; import {files} from "./files";
import { memberrelations } from "./memberrelations"; import { memberrelations } from "./memberrelations";
import { contracts } from "./contracts"; import { contracts } from "./contracts";
import { outgoingsepamandates } from "./outgoingsepamandates";
export const historyitems = pgTable("historyitems", { export const historyitems = pgTable("historyitems", {
id: bigint("id", { mode: "number" }) id: bigint("id", { mode: "number" })
@@ -114,6 +115,11 @@ export const historyitems = pgTable("historyitems", {
memberrelation: bigint("memberrelation", { mode: "number" }).references(() => memberrelations.id), memberrelation: bigint("memberrelation", { mode: "number" }).references(() => memberrelations.id),
outgoingsepamandate: bigint("outgoingsepamandate", { mode: "number" }).references(
() => outgoingsepamandates.id,
{ onDelete: "cascade" }
),
config: jsonb("config"), config: jsonb("config"),
projecttype: bigint("projecttype", { mode: "number" }).references( projecttype: bigint("projecttype", { mode: "number" }).references(

View File

@@ -57,6 +57,7 @@ export * from "./notifications_items"
export * from "./notifications_preferences" export * from "./notifications_preferences"
export * from "./notifications_preferences_defaults" export * from "./notifications_preferences_defaults"
export * from "./ownaccounts" export * from "./ownaccounts"
export * from "./outgoingsepamandates"
export * from "./plants" export * from "./plants"
export * from "./productcategories" export * from "./productcategories"
export * from "./products" export * from "./products"

View File

@@ -0,0 +1,61 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { entitybankaccounts } from "./entitybankaccounts"
import { authUsers } from "./auth_users"
export const outgoingsepamandates = pgTable("outgoingsepamandates", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
customer: bigint("customer", { mode: "number" })
.notNull()
.references(() => customers.id),
bankaccount: bigint("bankaccount", { mode: "number" })
.notNull()
.references(() => entitybankaccounts.id),
reference: text("reference").notNull(),
status: text("status").notNull().default("Entwurf"),
mandateType: text("mandate_type").notNull().default("CORE"),
sequenceType: text("sequence_type").notNull().default("RCUR"),
signedAt: timestamp("signed_at", { withTimezone: true }),
validFrom: timestamp("valid_from", { withTimezone: true }),
validUntil: timestamp("valid_until", { withTimezone: true }),
defaultMandate: boolean("default_mandate").notNull().default(false),
notes: text("notes"),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type OutgoingSepaMandate = typeof outgoingsepamandates.$inferSelect
export type NewOutgoingSepaMandate = typeof outgoingsepamandates.$inferInsert

View File

@@ -91,6 +91,7 @@ export const tenants = pgTable(
createDocument: true, createDocument: true,
serialInvoice: true, serialInvoice: true,
incomingInvoices: true, incomingInvoices: true,
outgoingsepamandates: true,
costcentres: true, costcentres: true,
branches: true, branches: true,
teams: true, teams: true,
@@ -140,6 +141,7 @@ export const tenants = pgTable(
customerinventoryitems: { prefix: "KIA-", suffix: "", nextNumber: 1000 }, customerinventoryitems: { prefix: "KIA-", suffix: "", nextNumber: 1000 },
projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 }, projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 },
costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 }, costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 },
outgoingsepamandates: { prefix: "SEPA-", suffix: "", nextNumber: 1000 },
}), }),
accountChart: text("accountChart").notNull().default("skr03"), accountChart: text("accountChart").notNull().default("skr03"),

View File

@@ -27,6 +27,7 @@ const columnMap: Record<string, any> = {
customerspaces: historyitems.customerspace, customerspaces: historyitems.customerspace,
customerinventoryitems: historyitems.customerinventoryitem, customerinventoryitems: historyitems.customerinventoryitem,
memberrelations: historyitems.memberrelation, memberrelations: historyitems.memberrelation,
outgoingsepamandates: historyitems.outgoingsepamandate,
}; };
const insertFieldMap: Record<string, string> = { const insertFieldMap: Record<string, string> = {
@@ -53,6 +54,7 @@ const insertFieldMap: Record<string, string> = {
customerspaces: "customerspace", customerspaces: "customerspace",
customerinventoryitems: "customerinventoryitem", customerinventoryitems: "customerinventoryitem",
memberrelations: "memberrelation", memberrelations: "memberrelation",
outgoingsepamandates: "outgoingsepamandate",
} }
const parseId = (value: string) => { const parseId = (value: string) => {

View File

@@ -11,7 +11,7 @@ import {
sql, sql,
} from "drizzle-orm" } from "drizzle-orm"
import { authProfiles, costcentres } from "../../../db/schema"; import { authProfiles, costcentres, customers, entitybankaccounts } from "../../../db/schema";
import { resourceConfig } from "../../utils/resource.config"; import { resourceConfig } from "../../utils/resource.config";
import { useNextNumberRangeNumber } from "../../utils/functions"; import { useNextNumberRangeNumber } from "../../utils/functions";
import { getHistoryEntityLabel, insertHistoryItem } from "../../utils/history"; import { getHistoryEntityLabel, insertHistoryItem } from "../../utils/history";
@@ -365,6 +365,50 @@ function prepareEntityBankAccountPayload(payload: Record<string, any>, requireAl
return { data: result } return { data: result }
} }
async function validateOutgoingSepaMandatePayload(
server: FastifyInstance,
tenantId: number,
payload: Record<string, any>,
existing: Record<string, any> | null = null
) {
const customerId = Number(payload.customer ?? existing?.customer)
const bankaccountId = Number(payload.bankaccount ?? existing?.bankaccount)
if (!customerId || !bankaccountId) {
return "Kunde und Bankverbindung sind Pflichtfelder."
}
const [customer] = await server.db
.select()
.from(customers)
.where(and(eq(customers.id, customerId), eq(customers.tenant, tenantId)))
.limit(1)
if (!customer) {
return "Kunde nicht gefunden."
}
const [bankaccount] = await server.db
.select()
.from(entitybankaccounts)
.where(and(eq(entitybankaccounts.id, bankaccountId), eq(entitybankaccounts.tenant, tenantId)))
.limit(1)
if (!bankaccount) {
return "Bankverbindung nicht gefunden."
}
const assignedBankAccountIds = Array.isArray((customer.infoData as any)?.bankAccountIds)
? (customer.infoData as any).bankAccountIds.map((id: any) => Number(id))
: []
if (!assignedBankAccountIds.includes(bankaccountId)) {
return "Die Bankverbindung ist dem ausgewählten Kunden nicht zugeordnet."
}
return null
}
export default async function resourceRoutes(server: FastifyInstance) { export default async function resourceRoutes(server: FastifyInstance) {
// ------------------------------------------------------------- // -------------------------------------------------------------
@@ -796,6 +840,13 @@ export default async function resourceRoutes(server: FastifyInstance) {
} }
} }
if (resource === "outgoingsepamandates") {
const validationError = await validateOutgoingSepaMandatePayload(server, req.user.tenant_id, createData)
if (validationError) {
return reply.code(400).send({ error: validationError })
}
}
if (config.numberRangeHolder && !body[config.numberRangeHolder]) { if (config.numberRangeHolder && !body[config.numberRangeHolder]) {
const numberRangeResource = resource === "members" ? "customers" : resource const numberRangeResource = resource === "members" ? "customers" : resource
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, numberRangeResource) const result = await useNextNumberRangeNumber(server, req.user.tenant_id, numberRangeResource)
@@ -809,6 +860,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
const [created] = await server.db.insert(table).values(createData).returning() const [created] = await server.db.insert(table).values(createData).returning()
if (resource === "outgoingsepamandates" && created?.defaultMandate) {
await server.db
.update(table)
.set({ defaultMandate: false })
.where(and(
eq(table.tenant, req.user.tenant_id),
eq(table.customer, created.customer),
sql`${table.id} <> ${created.id}`
))
}
if (["products", "services", "hourrates"].includes(resource)) { if (["products", "services", "hourrates"].includes(resource)) {
await recalculateServicePricesForTenant(server, req.user.tenant_id, req.user?.user_id || null); await recalculateServicePricesForTenant(server, req.user.tenant_id, req.user?.user_id || null);
} }
@@ -917,6 +979,13 @@ export default async function resourceRoutes(server: FastifyInstance) {
} }
} }
if (resource === "outgoingsepamandates") {
const validationError = await validateOutgoingSepaMandatePayload(server, tenantId, data, oldRecord)
if (validationError) {
return reply.code(400).send({ error: validationError })
}
}
Object.keys(data).forEach((key) => { Object.keys(data).forEach((key) => {
const value = data[key] const value = data[key]
const shouldNormalize = const shouldNormalize =
@@ -934,6 +1003,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
updateWhereCond = applyResourceWhereFilters(resource, table, updateWhereCond) updateWhereCond = applyResourceWhereFilters(resource, table, updateWhereCond)
const [updated] = await server.db.update(table).set(data).where(updateWhereCond).returning() const [updated] = await server.db.update(table).set(data).where(updateWhereCond).returning()
if (resource === "outgoingsepamandates" && updated?.defaultMandate) {
await server.db
.update(table)
.set({ defaultMandate: false })
.where(and(
eq(table.tenant, tenantId),
eq(table.customer, updated.customer),
sql`${table.id} <> ${updated.id}`
))
}
if (["products", "services", "hourrates"].includes(resource)) { if (["products", "services", "hourrates"].includes(resource)) {
await recalculateServicePricesForTenant(server, tenantId, userId); await recalculateServicePricesForTenant(server, tenantId, userId);
} }

View File

@@ -34,6 +34,7 @@ const HISTORY_ENTITY_LABELS: Record<string, string> = {
files: "Dateien", files: "Dateien",
memberrelations: "Mitgliedsverhältnisse", memberrelations: "Mitgliedsverhältnisse",
teams: "Teams", teams: "Teams",
outgoingsepamandates: "Ausgehende SEPA-Mandate",
} }
export function getHistoryEntityLabel(entity: string) { export function getHistoryEntityLabel(entity: string) {
@@ -94,6 +95,7 @@ export async function insertHistoryItem(
incominginvoices: "incomingInvoice", incominginvoices: "incomingInvoice",
files: "file", files: "file",
memberrelations: "memberrelation", memberrelations: "memberrelation",
outgoingsepamandates: "outgoingsepamandate",
} }
const fkColumn = columnMap[params.entity] const fkColumn = columnMap[params.entity]

View File

@@ -25,6 +25,7 @@ import {
letterheads, letterheads,
memberrelations, memberrelations,
ownaccounts, ownaccounts,
outgoingsepamandates,
plants, plants,
productcategories, productcategories,
products, products,
@@ -53,7 +54,7 @@ export const resourceConfig = {
}, },
customers: { customers: {
searchColumns: ["name", "nameAddition", "customerNumber", "firstname", "lastname", "notes"], searchColumns: ["name", "nameAddition", "customerNumber", "firstname", "lastname", "notes"],
mtmLoad: ["contacts","projects","plants","createddocuments","contracts","customerinventoryitems","customerspaces"], mtmLoad: ["contacts","projects","plants","createddocuments","contracts","outgoingsepamandates","customerinventoryitems","customerspaces"],
table: customers, table: customers,
numberRangeHolder: "customerNumber", numberRangeHolder: "customerNumber",
}, },
@@ -77,7 +78,13 @@ export const resourceConfig = {
table: contracts, table: contracts,
searchColumns: ["name", "notes", "contractNumber", "paymentType", "billingInterval", "sepaRef", "bankingName"], searchColumns: ["name", "notes", "contractNumber", "paymentType", "billingInterval", "sepaRef", "bankingName"],
numberRangeHolder: "contractNumber", numberRangeHolder: "contractNumber",
mtoLoad: ["customer", "contracttype"], mtoLoad: ["customer", "contracttype", "outgoingsepamandate"],
},
outgoingsepamandates: {
table: outgoingsepamandates,
searchColumns: ["reference", "status", "mandateType", "sequenceType", "notes"],
numberRangeHolder: "reference",
mtoLoad: ["customer", "bankaccount"],
}, },
contracttypes: { contracttypes: {
table: contracttypes, table: contracttypes,
@@ -200,7 +207,7 @@ export const resourceConfig = {
}, },
createddocuments: { createddocuments: {
table: createddocuments, table: createddocuments,
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument"], mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument", "outgoingsepamandate"],
mtmLoad: ["statementallocations","files","createddocuments"], mtmLoad: ["statementallocations","files","createddocuments"],
mtmListLoad: ["statementallocations", "files"], mtmListLoad: ["statementallocations", "files"],
}, },
@@ -235,6 +242,10 @@ export const resourceConfig = {
table: entitybankaccounts, table: entitybankaccounts,
searchColumns: ["description"], searchColumns: ["description"],
}, },
bankaccount: {
table: entitybankaccounts,
searchColumns: ["description"],
},
serialexecutions: { serialexecutions: {
table: serialExecutions table: serialExecutions
} }

View File

@@ -170,7 +170,7 @@ const setupQuery = () => {
Object.keys(data).forEach(key => { Object.keys(data).forEach(key => {
if (dataType.templateColumns.find(i => i.key === 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]) item.value[key] = Number(data[key])
} else { } else {
item.value[key] = data[key] item.value[key] = data[key]

View File

@@ -146,6 +146,11 @@ const links = computed(() => {
to: "/incomingInvoices", to: "/incomingInvoices",
icon: "i-heroicons-document-text", icon: "i-heroicons-document-text",
} : null, } : null,
featureEnabled("outgoingsepamandates") ? {
label: "SEPA-Mandate",
to: "/standardEntity/outgoingsepamandates",
icon: "i-heroicons-identification",
} : null,
(featureEnabled("incomingInvoices") || featureEnabled("banking")) ? { (featureEnabled("incomingInvoices") || featureEnabled("banking")) ? {
label: "Abschreibungen", label: "Abschreibungen",
to: "/accounting/depreciation", 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, dateOfPerformance: null,
paymentDays: auth.activeTenantData.standardPaymentDays, paymentDays: auth.activeTenantData.standardPaymentDays,
payment_type: "transfer", payment_type: "transfer",
outgoingsepamandate: null,
availableInPortal: false, availableInPortal: false,
customSurchargePercentage: 0, customSurchargePercentage: 0,
created_by: auth.user.id, created_by: auth.user.id,
@@ -90,6 +91,7 @@ const selectedServicecategorie = ref(null)
const customers = ref([]) const customers = ref([])
const contacts = ref([]) const contacts = ref([])
const contracts = ref([]) const contracts = ref([])
const outgoingsepamandates = ref([])
const texttemplates = ref([]) const texttemplates = ref([])
const units = ref([]) const units = ref([])
const tenantUsers = ref([]) const tenantUsers = ref([])
@@ -204,6 +206,7 @@ const setupData = async () => {
customers.value = await useEntities("customers").select("*", "customerNumber") customers.value = await useEntities("customers").select("*", "customerNumber")
contacts.value = await useEntities("contacts").select("*") contacts.value = await useEntities("contacts").select("*")
contracts.value = await useEntities("contracts").select("*") contracts.value = await useEntities("contracts").select("*")
outgoingsepamandates.value = await useEntities("outgoingsepamandates").select("*")
texttemplates.value = await useEntities("texttemplates").select("*") texttemplates.value = await useEntities("texttemplates").select("*")
units.value = await useEntities("units").selectSpecial("*") units.value = await useEntities("units").selectSpecial("*")
tenantUsers.value = (await useNuxtApp().$api(`/api/tenant/users`, { tenantUsers.value = (await useNuxtApp().$api(`/api/tenant/users`, {
@@ -211,6 +214,43 @@ const setupData = async () => {
})).users })).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 loaded = ref(false)
const normalizeEntityId = (value) => { const normalizeEntityId = (value) => {
if (value === null || typeof value === "undefined") return null if (value === null || typeof value === "undefined") return null
@@ -1476,6 +1516,7 @@ const saveSerialInvoice = async () => {
project: itemInfo.value.project, project: itemInfo.value.project,
paymentDays: itemInfo.value.paymentDays, paymentDays: itemInfo.value.paymentDays,
payment_type: itemInfo.value.payment_type, payment_type: itemInfo.value.payment_type,
outgoingsepamandate: itemInfo.value.outgoingsepamandate,
deliveryDateType: "Leistungszeitraum", deliveryDateType: "Leistungszeitraum",
createdBy: itemInfo.value.createdBy, createdBy: itemInfo.value.createdBy,
created_by: itemInfo.value.created_by, created_by: itemInfo.value.created_by,
@@ -1563,6 +1604,7 @@ const saveDocument = async (state, resetup = false) => {
deliveryDateEnd: itemInfo.value.deliveryDateEnd, deliveryDateEnd: itemInfo.value.deliveryDateEnd,
paymentDays: itemInfo.value.paymentDays, paymentDays: itemInfo.value.paymentDays,
payment_type: itemInfo.value.payment_type, payment_type: itemInfo.value.payment_type,
outgoingsepamandate: itemInfo.value.outgoingsepamandate,
deliveryDateType: itemInfo.value.deliveryDateType, deliveryDateType: itemInfo.value.deliveryDateType,
info: {}, info: {},
createdBy: itemInfo.value.createdBy, createdBy: itemInfo.value.createdBy,
@@ -2242,6 +2284,29 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</template> </template>
</USelectMenu> </USelectMenu>
</UFormField> </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 <UFormField
class="min-w-0 flex-1" class="min-w-0 flex-1"
label="Individueller Aufschlag:" label="Individueller Aufschlag:"

View File

@@ -35,6 +35,7 @@ const defaultFeatures = {
createDocument: true, createDocument: true,
serialInvoice: true, serialInvoice: true,
incomingInvoices: true, incomingInvoices: true,
outgoingsepamandates: true,
costcentres: true, costcentres: true,
branches: true, branches: true,
teams: true, teams: true,
@@ -82,6 +83,7 @@ const featureOptions = [
{ key: "createDocument", label: "Buchhaltung: Ausgangsbelege" }, { key: "createDocument", label: "Buchhaltung: Ausgangsbelege" },
{ key: "serialInvoice", label: "Buchhaltung: Serienvorlagen" }, { key: "serialInvoice", label: "Buchhaltung: Serienvorlagen" },
{ key: "incomingInvoices", label: "Buchhaltung: Eingangsbelege" }, { key: "incomingInvoices", label: "Buchhaltung: Eingangsbelege" },
{ key: "outgoingsepamandates", label: "Buchhaltung: Ausgehende SEPA-Mandate" },
{ key: "costcentres", label: "Buchhaltung: Kostenstellen" }, { key: "costcentres", label: "Buchhaltung: Kostenstellen" },
{ key: "branches", label: "Stammdaten: Niederlassungen" }, { key: "branches", label: "Stammdaten: Niederlassungen" },
{ key: "teams", label: "Mitarbeiter: Teams" }, { 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 signDate from "~/components/columnRenderings/signDate.vue";
import sepaDate from "~/components/columnRenderings/sepaDate.vue"; import sepaDate from "~/components/columnRenderings/sepaDate.vue";
import bankAccounts from "~/components/columnRenderings/bankAccounts.vue"; import bankAccounts from "~/components/columnRenderings/bankAccounts.vue";
import bankAccount from "~/components/columnRenderings/bankAccount.vue";
import outgoingSepaMandate from "~/components/columnRenderings/outgoingSepaMandate.vue";
// @ts-ignore // @ts-ignore
export const useDataStore = defineStore('data', () => { export const useDataStore = defineStore('data', () => {
@@ -173,7 +175,7 @@ export const useDataStore = defineStore('data', () => {
numberRangeHolder: "customerNumber", numberRangeHolder: "customerNumber",
historyItemHolder: "customer", historyItemHolder: "customer",
sortColumn: "customerNumber", 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: [{ filters: [{
name: "Archivierte ausblenden", name: "Archivierte ausblenden",
default: true, default: true,
@@ -457,7 +459,7 @@ export const useDataStore = defineStore('data', () => {
inputColumn: "Allgemeines" 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: { members: {
isArchivable: true, isArchivable: true,
@@ -822,7 +824,7 @@ export const useDataStore = defineStore('data', () => {
"Allgemeines", "Allgemeines",
"Abrechnung" "Abrechnung"
], ],
selectWithInformation: "*, customer(*), contracttype(*), files(*)", selectWithInformation: "*, customer(*), contracttype(*), outgoingsepamandate(*, bankaccount(*)), files(*)",
templateColumns: [ templateColumns: [
{ {
key: 'contractNumber', key: 'contractNumber',
@@ -936,6 +938,22 @@ export const useDataStore = defineStore('data', () => {
{label:'Überweisung'} {label:'Überweisung'}
], ],
inputColumn: "Abrechnung" 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", key: "billingInterval",
label: "Abrechnungsintervall", label: "Abrechnungsintervall",
@@ -1129,6 +1147,145 @@ export const useDataStore = defineStore('data', () => {
], ],
showTabs: [{ label: "Informationen" }] 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: { absencerequests: {
isArchivable: true, isArchivable: true,
label: "Abwesenheiten", label: "Abwesenheiten",