Ausgehende SEPA-Mandate einführen #183
This commit is contained in:
53
backend/db/migrations/0037_outgoing_sepa_mandates.sql
Normal file
53
backend/db/migrations/0037_outgoing_sepa_mandates.sql
Normal 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)
|
||||
);
|
||||
@@ -253,6 +253,13 @@
|
||||
"when": 1778194800000,
|
||||
"tag": "0036_allowed_contracttypes",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 36,
|
||||
"version": "7",
|
||||
"when": 1778840100000,
|
||||
"tag": "0037_outgoing_sepa_mandates",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { customers } from "./customers"
|
||||
import { contacts } from "./contacts"
|
||||
import { contracttypes } from "./contracttypes"
|
||||
import { authUsers } from "./auth_users"
|
||||
import { outgoingsepamandates } from "./outgoingsepamandates"
|
||||
|
||||
export const contracts = pgTable(
|
||||
"contracts",
|
||||
@@ -60,6 +61,9 @@ export const contracts = pgTable(
|
||||
bankingOwner: text("bankingOwner"),
|
||||
sepaRef: text("sepaRef"),
|
||||
sepaDate: timestamp("sepaDate", { withTimezone: true }),
|
||||
outgoingsepamandate: bigint("outgoingsepamandate", { mode: "number" }).references(
|
||||
() => outgoingsepamandates.id
|
||||
),
|
||||
|
||||
paymentType: text("paymentType"),
|
||||
billingInterval: text("billingInterval"),
|
||||
|
||||
@@ -19,6 +19,7 @@ import { projects } from "./projects"
|
||||
import { plants } from "./plants"
|
||||
import { authUsers } from "./auth_users"
|
||||
import {serialExecutions} from "./serialexecutions";
|
||||
import { outgoingsepamandates } from "./outgoingsepamandates"
|
||||
|
||||
export const createddocuments = pgTable("createddocuments", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
@@ -118,6 +119,10 @@ export const createddocuments = pgTable("createddocuments", {
|
||||
() => contracts.id
|
||||
),
|
||||
|
||||
outgoingsepamandate: bigint("outgoingsepamandate", { mode: "number" }).references(
|
||||
() => outgoingsepamandates.id
|
||||
),
|
||||
|
||||
serialexecution: uuid("serialexecution").references(() => serialExecutions.id)
|
||||
})
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import { authUsers } from "./auth_users"
|
||||
import {files} from "./files";
|
||||
import { memberrelations } from "./memberrelations";
|
||||
import { contracts } from "./contracts";
|
||||
import { outgoingsepamandates } from "./outgoingsepamandates";
|
||||
|
||||
export const historyitems = pgTable("historyitems", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
@@ -114,6 +115,11 @@ export const historyitems = pgTable("historyitems", {
|
||||
|
||||
memberrelation: bigint("memberrelation", { mode: "number" }).references(() => memberrelations.id),
|
||||
|
||||
outgoingsepamandate: bigint("outgoingsepamandate", { mode: "number" }).references(
|
||||
() => outgoingsepamandates.id,
|
||||
{ onDelete: "cascade" }
|
||||
),
|
||||
|
||||
config: jsonb("config"),
|
||||
|
||||
projecttype: bigint("projecttype", { mode: "number" }).references(
|
||||
|
||||
@@ -57,6 +57,7 @@ export * from "./notifications_items"
|
||||
export * from "./notifications_preferences"
|
||||
export * from "./notifications_preferences_defaults"
|
||||
export * from "./ownaccounts"
|
||||
export * from "./outgoingsepamandates"
|
||||
export * from "./plants"
|
||||
export * from "./productcategories"
|
||||
export * from "./products"
|
||||
|
||||
61
backend/db/schema/outgoingsepamandates.ts
Normal file
61
backend/db/schema/outgoingsepamandates.ts
Normal 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
|
||||
@@ -91,6 +91,7 @@ export const tenants = pgTable(
|
||||
createDocument: true,
|
||||
serialInvoice: true,
|
||||
incomingInvoices: true,
|
||||
outgoingsepamandates: true,
|
||||
costcentres: true,
|
||||
branches: true,
|
||||
teams: true,
|
||||
@@ -140,6 +141,7 @@ export const tenants = pgTable(
|
||||
customerinventoryitems: { prefix: "KIA-", suffix: "", nextNumber: 1000 },
|
||||
projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 },
|
||||
costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 },
|
||||
outgoingsepamandates: { prefix: "SEPA-", suffix: "", nextNumber: 1000 },
|
||||
}),
|
||||
accountChart: text("accountChart").notNull().default("skr03"),
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ const columnMap: Record<string, any> = {
|
||||
customerspaces: historyitems.customerspace,
|
||||
customerinventoryitems: historyitems.customerinventoryitem,
|
||||
memberrelations: historyitems.memberrelation,
|
||||
outgoingsepamandates: historyitems.outgoingsepamandate,
|
||||
};
|
||||
|
||||
const insertFieldMap: Record<string, string> = {
|
||||
@@ -53,6 +54,7 @@ const insertFieldMap: Record<string, string> = {
|
||||
customerspaces: "customerspace",
|
||||
customerinventoryitems: "customerinventoryitem",
|
||||
memberrelations: "memberrelation",
|
||||
outgoingsepamandates: "outgoingsepamandate",
|
||||
}
|
||||
|
||||
const parseId = (value: string) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
sql,
|
||||
} from "drizzle-orm"
|
||||
|
||||
import { authProfiles, costcentres } from "../../../db/schema";
|
||||
import { authProfiles, costcentres, customers, entitybankaccounts } from "../../../db/schema";
|
||||
import { resourceConfig } from "../../utils/resource.config";
|
||||
import { useNextNumberRangeNumber } from "../../utils/functions";
|
||||
import { getHistoryEntityLabel, insertHistoryItem } from "../../utils/history";
|
||||
@@ -365,6 +365,50 @@ function prepareEntityBankAccountPayload(payload: Record<string, any>, requireAl
|
||||
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) {
|
||||
|
||||
// -------------------------------------------------------------
|
||||
@@ -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]) {
|
||||
const numberRangeResource = resource === "members" ? "customers" : resource
|
||||
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()
|
||||
|
||||
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)) {
|
||||
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) => {
|
||||
const value = data[key]
|
||||
const shouldNormalize =
|
||||
@@ -934,6 +1003,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
updateWhereCond = applyResourceWhereFilters(resource, table, updateWhereCond)
|
||||
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)) {
|
||||
await recalculateServicePricesForTenant(server, tenantId, userId);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ const HISTORY_ENTITY_LABELS: Record<string, string> = {
|
||||
files: "Dateien",
|
||||
memberrelations: "Mitgliedsverhältnisse",
|
||||
teams: "Teams",
|
||||
outgoingsepamandates: "Ausgehende SEPA-Mandate",
|
||||
}
|
||||
|
||||
export function getHistoryEntityLabel(entity: string) {
|
||||
@@ -94,6 +95,7 @@ export async function insertHistoryItem(
|
||||
incominginvoices: "incomingInvoice",
|
||||
files: "file",
|
||||
memberrelations: "memberrelation",
|
||||
outgoingsepamandates: "outgoingsepamandate",
|
||||
}
|
||||
|
||||
const fkColumn = columnMap[params.entity]
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
letterheads,
|
||||
memberrelations,
|
||||
ownaccounts,
|
||||
outgoingsepamandates,
|
||||
plants,
|
||||
productcategories,
|
||||
products,
|
||||
@@ -53,7 +54,7 @@ export const resourceConfig = {
|
||||
},
|
||||
customers: {
|
||||
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,
|
||||
numberRangeHolder: "customerNumber",
|
||||
},
|
||||
@@ -77,7 +78,13 @@ export const resourceConfig = {
|
||||
table: contracts,
|
||||
searchColumns: ["name", "notes", "contractNumber", "paymentType", "billingInterval", "sepaRef", "bankingName"],
|
||||
numberRangeHolder: "contractNumber",
|
||||
mtoLoad: ["customer", "contracttype"],
|
||||
mtoLoad: ["customer", "contracttype", "outgoingsepamandate"],
|
||||
},
|
||||
outgoingsepamandates: {
|
||||
table: outgoingsepamandates,
|
||||
searchColumns: ["reference", "status", "mandateType", "sequenceType", "notes"],
|
||||
numberRangeHolder: "reference",
|
||||
mtoLoad: ["customer", "bankaccount"],
|
||||
},
|
||||
contracttypes: {
|
||||
table: contracttypes,
|
||||
@@ -200,7 +207,7 @@ export const resourceConfig = {
|
||||
},
|
||||
createddocuments: {
|
||||
table: createddocuments,
|
||||
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument"],
|
||||
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument", "outgoingsepamandate"],
|
||||
mtmLoad: ["statementallocations","files","createddocuments"],
|
||||
mtmListLoad: ["statementallocations", "files"],
|
||||
},
|
||||
@@ -235,6 +242,10 @@ export const resourceConfig = {
|
||||
table: entitybankaccounts,
|
||||
searchColumns: ["description"],
|
||||
},
|
||||
bankaccount: {
|
||||
table: entitybankaccounts,
|
||||
searchColumns: ["description"],
|
||||
},
|
||||
serialexecutions: {
|
||||
table: serialExecutions
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
16
frontend/components/columnRenderings/bankAccount.vue
Normal file
16
frontend/components/columnRenderings/bankAccount.vue
Normal 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>
|
||||
26
frontend/components/columnRenderings/outgoingSepaMandate.vue
Normal file
26
frontend/components/columnRenderings/outgoingSepaMandate.vue
Normal 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>
|
||||
@@ -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:"
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user