diff --git a/backend/db/migrations/0037_outgoing_sepa_mandates.sql b/backend/db/migrations/0037_outgoing_sepa_mandates.sql new file mode 100644 index 0000000..fe993eb --- /dev/null +++ b/backend/db/migrations/0037_outgoing_sepa_mandates.sql @@ -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) +); diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json index 5279f08..9024dd0 100644 --- a/backend/db/migrations/meta/_journal.json +++ b/backend/db/migrations/meta/_journal.json @@ -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 } ] } diff --git a/backend/db/schema/contracts.ts b/backend/db/schema/contracts.ts index c9dc973..1778e4b 100644 --- a/backend/db/schema/contracts.ts +++ b/backend/db/schema/contracts.ts @@ -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"), diff --git a/backend/db/schema/createddocuments.ts b/backend/db/schema/createddocuments.ts index c98d927..e93359b 100644 --- a/backend/db/schema/createddocuments.ts +++ b/backend/db/schema/createddocuments.ts @@ -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) }) diff --git a/backend/db/schema/historyitems.ts b/backend/db/schema/historyitems.ts index 46fb6d5..26adafd 100644 --- a/backend/db/schema/historyitems.ts +++ b/backend/db/schema/historyitems.ts @@ -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( diff --git a/backend/db/schema/index.ts b/backend/db/schema/index.ts index 7f4dd1a..9dabfcc 100644 --- a/backend/db/schema/index.ts +++ b/backend/db/schema/index.ts @@ -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" diff --git a/backend/db/schema/outgoingsepamandates.ts b/backend/db/schema/outgoingsepamandates.ts new file mode 100644 index 0000000..7545bad --- /dev/null +++ b/backend/db/schema/outgoingsepamandates.ts @@ -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 diff --git a/backend/db/schema/tenants.ts b/backend/db/schema/tenants.ts index 772d8e0..2b09a01 100644 --- a/backend/db/schema/tenants.ts +++ b/backend/db/schema/tenants.ts @@ -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"), diff --git a/backend/src/routes/history.ts b/backend/src/routes/history.ts index ef57bf4..0e74798 100644 --- a/backend/src/routes/history.ts +++ b/backend/src/routes/history.ts @@ -27,6 +27,7 @@ const columnMap: Record = { customerspaces: historyitems.customerspace, customerinventoryitems: historyitems.customerinventoryitem, memberrelations: historyitems.memberrelation, + outgoingsepamandates: historyitems.outgoingsepamandate, }; const insertFieldMap: Record = { @@ -53,6 +54,7 @@ const insertFieldMap: Record = { customerspaces: "customerspace", customerinventoryitems: "customerinventoryitem", memberrelations: "memberrelation", + outgoingsepamandates: "outgoingsepamandate", } const parseId = (value: string) => { diff --git a/backend/src/routes/resources/main.ts b/backend/src/routes/resources/main.ts index 7fa9f24..ccc74fe 100644 --- a/backend/src/routes/resources/main.ts +++ b/backend/src/routes/resources/main.ts @@ -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, requireAl return { data: result } } +async function validateOutgoingSepaMandatePayload( + server: FastifyInstance, + tenantId: number, + payload: Record, + existing: Record | 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); } diff --git a/backend/src/utils/history.ts b/backend/src/utils/history.ts index a54efbb..7ea639b 100644 --- a/backend/src/utils/history.ts +++ b/backend/src/utils/history.ts @@ -34,6 +34,7 @@ const HISTORY_ENTITY_LABELS: Record = { 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] diff --git a/backend/src/utils/resource.config.ts b/backend/src/utils/resource.config.ts index da7ad93..aabdc7f 100644 --- a/backend/src/utils/resource.config.ts +++ b/backend/src/utils/resource.config.ts @@ -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 } diff --git a/frontend/components/EntityEdit.vue b/frontend/components/EntityEdit.vue index fe8150d..357dba1 100644 --- a/frontend/components/EntityEdit.vue +++ b/frontend/components/EntityEdit.vue @@ -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] diff --git a/frontend/components/MainNav.vue b/frontend/components/MainNav.vue index bb9d84c..655c6b5 100644 --- a/frontend/components/MainNav.vue +++ b/frontend/components/MainNav.vue @@ -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", diff --git a/frontend/components/columnRenderings/bankAccount.vue b/frontend/components/columnRenderings/bankAccount.vue new file mode 100644 index 0000000..ed26d57 --- /dev/null +++ b/frontend/components/columnRenderings/bankAccount.vue @@ -0,0 +1,16 @@ + + + diff --git a/frontend/components/columnRenderings/outgoingSepaMandate.vue b/frontend/components/columnRenderings/outgoingSepaMandate.vue new file mode 100644 index 0000000..4d47463 --- /dev/null +++ b/frontend/components/columnRenderings/outgoingSepaMandate.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/pages/createDocument/edit/[[id]].vue b/frontend/pages/createDocument/edit/[[id]].vue index 019bdb3..4e0e7da 100644 --- a/frontend/pages/createDocument/edit/[[id]].vue +++ b/frontend/pages/createDocument/edit/[[id]].vue @@ -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 = { + + + + + + { @@ -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",